Import Tint changes from Dawn

Changes:
  - f55d13b7544c04210550d51ec20d3f6ef81ba835 [ir] Add increment and decrement statements. by dan sinclair <dsinclair@chromium.org>
  - 0e6534e44d2c3c77ac409d787d9d5b3d449a2d0a [tint] Make Transform base class by James Price <jrprice@google.com>
  - a6d8e8137101746de775c45915b43c5b4730715e [ir][spirv-writer] Implement binary subtract by James Price <jrprice@google.com>
  - 9940c7bdccca0826cf27b2a20da5b590200c811a [tint][ir][ToProgram] Stub ToProgram() by Ben Clayton <bclayton@google.com>
  - 0df7f8bccd74f2c9fd07e5bc65f08bfabc2f3845 [ir] Update disassembly output. by dan sinclair <dsinclair@chromium.org>
  - 11bd8a012faa4a56a272f73109908c2720f2e329 [ir] Fix scope stack for loops. by dan sinclair <dsinclair@chromium.org>
  - 055de2744101630570b257d8a1583cabaa986c24 [ir][spirv-writer] Emit vector constants by James Price <jrprice@google.com>
  - 2f324c59ffbec46a5c5e3b3f21b7dace177f0907 [ir] Remove list of entry points from module by James Price <jrprice@google.com>
  - f59547fb7f1ca6d7cc3fc357020af02d94d68a8b [ir] Add optional CreateFunction parameters. by dan sinclair <dsinclair@chromium.org>
  - 69b5900c88bbc478c2a03d395b42593e833e609c [ir] Use the branch helper in the spirv tests. by dan sinclair <dsinclair@chromium.org>
  - 34c794e2e9476bd5338bc1ba548d7dc3dc59352b [tint][ir] Shuffle and refactor from_program.cc by Ben Clayton <bclayton@google.com>
  - 8d98977a30b7c00ffacad47322a1ba0f896b3ad8 [tint][ir] Make the ir::BuilderImpl PIMPL by Ben Clayton <bclayton@google.com>
  - c9dd75a0e9cd6597539c8b2d06a73636905ab266 clang-format by Ben Clayton <bclayton@google.com>
  - c9923d2ee3bc5ae541052176320e30aa3d8d6d43 [ir] Set default flow node values. by dan sinclair <dsinclair@chromium.org>
  - 809187c57968e5ee1748fb1b661e46cc9f691093 [ir] Move ir::Builder to hold module reference by dan sinclair <dsinclair@chromium.org>
  - 36aa48ce36053a10993f6eae1ad1224b7a912abf [tint][ir] Clean up tests by Ben Clayton <bclayton@google.com>
  - 8370ddddc58a6fdc5a3e9f688da909352164351b [ir][spirv-writer] Emit vector types by James Price <jrprice@google.com>
GitOrigin-RevId: f55d13b7544c04210550d51ec20d3f6ef81ba835
Change-Id: Icb27f15ec5162714de7d909637103f195a4b6194
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/133040
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 5d04077..7e85a38 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -356,6 +356,43 @@
 
 libtint_source_set("libtint_transform_src") {
   sources = [
+    "transform/transform.cc",
+    "transform/transform.h",
+  ]
+  deps = [
+    ":libtint_program_src",
+    ":libtint_utils_src",
+  ]
+}
+
+libtint_source_set("libtint_ast_transform_base_src") {
+  sources = [
+    "ast/transform/transform.cc",
+    "ast/transform/transform.h",
+  ]
+  public_deps = [ ":libtint_transform_src" ]
+  deps = [
+    ":libtint_builtins_src",
+    ":libtint_program_src",
+    ":libtint_sem_src",
+    ":libtint_type_src",
+    ":libtint_utils_src",
+  ]
+}
+
+libtint_source_set("libtint_transform_manager_src") {
+  sources = [
+    "transform/manager.cc",
+    "transform/manager.h",
+  ]
+  deps = [
+    ":libtint_ast_transform_base_src",
+    ":libtint_program_src",
+  ]
+}
+
+libtint_source_set("libtint_ast_transform_src") {
+  sources = [
     "ast/transform/add_block_attribute.cc",
     "ast/transform/add_block_attribute.h",
     "ast/transform/add_empty_entry_point.cc",
@@ -434,8 +471,6 @@
     "ast/transform/substitute_override.h",
     "ast/transform/texture_1d_to_2d.cc",
     "ast/transform/texture_1d_to_2d.h",
-    "ast/transform/transform.cc",
-    "ast/transform/transform.h",
     "ast/transform/truncate_interstage_variables.cc",
     "ast/transform/truncate_interstage_variables.h",
     "ast/transform/unshadow.cc",
@@ -456,15 +491,15 @@
     "ast/transform/while_to_loop.h",
     "ast/transform/zero_init_workgroup_memory.cc",
     "ast/transform/zero_init_workgroup_memory.h",
-    "transform/manager.cc",
-    "transform/manager.h",
   ]
+  public_deps = [ ":libtint_ast_transform_base_src" ]
   deps = [
     ":libtint_ast_src",
     ":libtint_builtins_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
   ]
@@ -860,10 +895,7 @@
     "reader/reader.h",
   ]
 
-  public_deps = [
-    ":libtint_program_src",
-    ":libtint_transform_src",
-  ]
+  public_deps = [ ":libtint_program_src" ]
 }
 
 libtint_source_set("libtint_spv_reader_src") {
@@ -892,11 +924,13 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_program_src",
     ":libtint_reader_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     "${tint_spirv_tools_dir}/:spvtools_opt",
@@ -932,11 +966,12 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_inspector_src",
     ":libtint_program_src",
     ":libtint_sem_src",
-    ":libtint_transform_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
   ]
@@ -965,12 +1000,13 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_constant_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
-    ":libtint_transform_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     ":libtint_writer_src",
@@ -1044,12 +1080,13 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_constant_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
-    ":libtint_transform_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     ":libtint_writer_src",
@@ -1066,12 +1103,13 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_constant_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
-    ":libtint_transform_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     ":libtint_writer_src",
@@ -1088,12 +1126,13 @@
 
   deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_builtins_src",
     ":libtint_constant_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
-    ":libtint_transform_src",
+    ":libtint_transform_manager_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     ":libtint_writer_src",
@@ -1121,10 +1160,10 @@
 
 libtint_source_set("libtint_ir_builder_src") {
   sources = [
-    "ir/builder_impl.cc",
-    "ir/builder_impl.h",
     "ir/from_program.cc",
     "ir/from_program.h",
+    "ir/to_program.cc",
+    "ir/to_program.h",
   ]
   deps = [
     ":libtint_ast_src",
@@ -1206,13 +1245,13 @@
 source_set("libtint") {
   public_deps = [
     ":libtint_ast_src",
+    ":libtint_ast_transform_src",
     ":libtint_constant_src",
     ":libtint_initializer_src",
     ":libtint_inspector_src",
     ":libtint_program_src",
     ":libtint_sem_src",
     ":libtint_symbols_src",
-    ":libtint_transform_src",
     ":libtint_type_src",
     ":libtint_utils_src",
     ":libtint_writer_src",
@@ -1470,8 +1509,8 @@
     ]
     deps = [
       ":libtint_ast_src",
+      ":libtint_ast_transform_src",
       ":libtint_builtins_src",
-      ":libtint_transform_src",
       ":libtint_unittests_ast_helper",
       ":libtint_utils_src",
     ]
@@ -1587,8 +1626,8 @@
       "resolver/variable_validation_test.cc",
     ]
     deps = [
+      ":libtint_ast_transform_src",
       ":libtint_builtins_src",
-      ":libtint_transform_src",
       ":libtint_utils_src",
       ":tint_unittests_ast_src",
     ]
@@ -1638,7 +1677,7 @@
     deps = [ ":libtint_builtins_src" ]
   }
 
-  tint_unittests_source_set("tint_unittests_transform_src") {
+  tint_unittests_source_set("tint_unittests_ast_transform_src") {
     sources = [
       "ast/transform/add_block_attribute_test.cc",
       "ast/transform/add_empty_entry_point_test.cc",
@@ -1697,8 +1736,9 @@
     ]
 
     deps = [
+      ":libtint_ast_transform_src",
       ":libtint_builtins_src",
-      ":libtint_transform_src",
+      ":libtint_transform_manager_src",
       ":libtint_unittests_ast_helper",
       ":libtint_utils_src",
       ":libtint_wgsl_reader_src",
@@ -2071,8 +2111,9 @@
     ]
 
     deps = [
+      ":libtint_ast_transform_src",
       ":libtint_hlsl_writer_src",
-      ":libtint_transform_src",
+      ":libtint_transform_manager_src",
       ":libtint_utils_src",
       ":tint_unittests_ast_src",
     ]
@@ -2115,9 +2156,10 @@
     ]
 
     deps = [
+      ":libtint_ast_transform_src",
       ":libtint_glsl_writer_src",
       ":libtint_symbols_src",
-      ":libtint_transform_src",
+      ":libtint_transform_manager_src",
       ":libtint_utils_src",
       ":tint_unittests_ast_src",
     ]
@@ -2151,19 +2193,20 @@
     sources = [
       "ir/binary_test.cc",
       "ir/bitcast_test.cc",
-      "ir/builder_impl_binary_test.cc",
-      "ir/builder_impl_call_test.cc",
-      "ir/builder_impl_literal_test.cc",
-      "ir/builder_impl_materialize_test.cc",
-      "ir/builder_impl_store_test.cc",
-      "ir/builder_impl_test.cc",
-      "ir/builder_impl_unary_test.cc",
-      "ir/builder_impl_var_test.cc",
       "ir/constant_test.cc",
       "ir/discard_test.cc",
+      "ir/from_program_binary_test.cc",
+      "ir/from_program_call_test.cc",
+      "ir/from_program_literal_test.cc",
+      "ir/from_program_materialize_test.cc",
+      "ir/from_program_store_test.cc",
+      "ir/from_program_test.cc",
+      "ir/from_program_unary_test.cc",
+      "ir/from_program_var_test.cc",
       "ir/module_test.cc",
       "ir/store_test.cc",
       "ir/test_helper.h",
+      "ir/to_program_roundtrip_test.cc",
       "ir/unary_test.cc",
     ]
 
@@ -2191,6 +2234,7 @@
       ":libtint_wgsl_reader_src",
       ":libtint_wgsl_writer_src",
       ":tint_unittests_ast_src",
+      ":tint_unittests_ast_transform_src",
       ":tint_unittests_builtins_src",
       ":tint_unittests_cmd_src",
       ":tint_unittests_constant_src",
@@ -2200,7 +2244,6 @@
       ":tint_unittests_resolver_src",
       ":tint_unittests_sem_src",
       ":tint_unittests_symbols_src",
-      ":tint_unittests_transform_src",
       ":tint_unittests_type_src",
       ":tint_unittests_utils_src",
       ":tint_unittests_writer_src",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index ea69d50..7d12ae2 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -452,6 +452,8 @@
   ast/transform/zero_init_workgroup_memory.h
   transform/manager.cc
   transform/manager.h
+  transform/transform.cc
+  transform/transform.h
   type/abstract_float.cc
   type/abstract_float.h
   type/abstract_int.cc
@@ -716,8 +718,6 @@
     ir/block.h
     ir/builder.cc
     ir/builder.h
-    ir/builder_impl.cc
-    ir/builder_impl.h
     ir/builtin.cc
     ir/builtin.h
     ir/call.cc
@@ -756,6 +756,8 @@
     ir/store.h
     ir/switch.cc
     ir/switch.h
+    ir/to_program.cc
+    ir/to_program.h
     ir/unary.cc
     ir/unary.h
     ir/user_call.cc
@@ -1447,16 +1449,16 @@
     list(APPEND TINT_TEST_SRCS
       ir/binary_test.cc
       ir/bitcast_test.cc
-      ir/builder_impl_binary_test.cc
-      ir/builder_impl_call_test.cc
-      ir/builder_impl_literal_test.cc
-      ir/builder_impl_materialize_test.cc
-      ir/builder_impl_store_test.cc
-      ir/builder_impl_test.cc
-      ir/builder_impl_unary_test.cc
-      ir/builder_impl_var_test.cc
       ir/constant_test.cc
       ir/discard_test.cc
+      ir/from_program_binary_test.cc
+      ir/from_program_call_test.cc
+      ir/from_program_literal_test.cc
+      ir/from_program_materialize_test.cc
+      ir/from_program_store_test.cc
+      ir/from_program_test.cc
+      ir/from_program_unary_test.cc
+      ir/from_program_var_test.cc
       ir/module_test.cc
       ir/store_test.cc
       ir/test_helper.h
@@ -1464,6 +1466,13 @@
     )
   endif()
 
+  if (${TINT_BUILD_IR} AND ${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      ir/to_program_roundtrip_test.cc
+    )
+  endif()
+
+
   if (${TINT_BUILD_FUZZERS})
     list(APPEND TINT_TEST_SRCS
       fuzzers/mersenne_twister_engine.cc
diff --git a/src/tint/ast/transform/array_length_from_uniform_test.cc b/src/tint/ast/transform/array_length_from_uniform_test.cc
index b734630..8441655 100644
--- a/src/tint/ast/transform/array_length_from_uniform_test.cc
+++ b/src/tint/ast/transform/array_length_from_uniform_test.cc
@@ -31,7 +31,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     EXPECT_FALSE(ShouldRun<ArrayLengthFromUniform>(src, data));
@@ -54,7 +54,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     EXPECT_FALSE(ShouldRun<ArrayLengthFromUniform>(src, data));
@@ -78,7 +78,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     EXPECT_TRUE(ShouldRun<ArrayLengthFromUniform>(src, data));
@@ -134,7 +134,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -183,7 +183,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -275,7 +275,7 @@
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{3u, 2u}, 3);
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{4u, 2u}, 4);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -361,7 +361,7 @@
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{3u, 2u}, 3);
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{4u, 2u}, 4);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -389,7 +389,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -454,7 +454,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 2}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
@@ -502,7 +502,7 @@
     ArrayLengthFromUniform::Config cfg({0, 30u});
     cfg.bindpoint_to_size_index.emplace(sem::BindingPoint{0, 0}, 0);
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<ArrayLengthFromUniform::Config>(std::move(cfg));
 
     auto got = Run<Unshadow, SimplifyPointers, ArrayLengthFromUniform>(src, data);
diff --git a/src/tint/ast/transform/binding_remapper_test.cc b/src/tint/ast/transform/binding_remapper_test.cc
index 296bd7c..023b919 100644
--- a/src/tint/ast/transform/binding_remapper_test.cc
+++ b/src/tint/ast/transform/binding_remapper_test.cc
@@ -26,7 +26,7 @@
 TEST_F(BindingRemapperTest, ShouldRunEmptyRemappings) {
     auto* src = R"()";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(BindingRemapper::BindingPoints{},
                                           BindingRemapper::AccessControls{});
 
@@ -36,7 +36,7 @@
 TEST_F(BindingRemapperTest, ShouldRunBindingPointRemappings) {
     auto* src = R"()";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{
             {{2, 1}, {1, 2}},
@@ -49,7 +49,7 @@
 TEST_F(BindingRemapperTest, ShouldRunAccessControlRemappings) {
     auto* src = R"()";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(BindingRemapper::BindingPoints{},
                                           BindingRemapper::AccessControls{
                                               {{2, 1}, builtin::Access::kWrite},
@@ -75,7 +75,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(BindingRemapper::BindingPoints{},
                                           BindingRemapper::AccessControls{});
     auto got = Run<BindingRemapper>(src, data);
@@ -112,7 +112,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{
             {{2, 1}, {1, 2}},  // Remap
@@ -158,7 +158,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{},
         BindingRemapper::AccessControls{
@@ -200,7 +200,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{
             {{2, 1}, {4, 5}},
@@ -254,7 +254,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{
             {{2, 1}, {1, 1}},
@@ -316,7 +316,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BindingRemapper::Remappings>(
         BindingRemapper::BindingPoints{
             {{2, 1}, {1, 1}},
diff --git a/src/tint/ast/transform/builtin_polyfill_test.cc b/src/tint/ast/transform/builtin_polyfill_test.cc
index e48ab70..668c4fc 100644
--- a/src/tint/ast/transform/builtin_polyfill_test.cc
+++ b/src/tint/ast/transform/builtin_polyfill_test.cc
@@ -41,10 +41,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // acosh
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillAcosh(Level level) {
+Transform::DataMap polyfillAcosh(Level level) {
     BuiltinPolyfill::Builtins builtins;
     builtins.acosh = level;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -172,10 +172,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // asinh
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillSinh() {
+Transform::DataMap polyfillSinh() {
     BuiltinPolyfill::Builtins builtins;
     builtins.asinh = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -253,10 +253,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // atanh
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillAtanh(Level level) {
+Transform::DataMap polyfillAtanh(Level level) {
     BuiltinPolyfill::Builtins builtins;
     builtins.atanh = level;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -384,10 +384,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // bgra8unorm
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillBgra8unorm() {
+Transform::DataMap polyfillBgra8unorm() {
     BuiltinPolyfill::Builtins builtins;
     builtins.bgra8unorm = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -505,7 +505,7 @@
     BuiltinPolyfill::Builtins builtins;
     builtins.atanh = BuiltinPolyfill::Level::kFull;
     builtins.bgra8unorm = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
 
     auto got = Run<BuiltinPolyfill>(src, std::move(data));
@@ -516,10 +516,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // bitshiftModulo
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillBitshiftModulo() {
+Transform::DataMap polyfillBitshiftModulo() {
     BuiltinPolyfill::Builtins builtins;
     builtins.bitshift_modulo = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -655,10 +655,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // clampInteger
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillClampInteger() {
+Transform::DataMap polyfillClampInteger() {
     BuiltinPolyfill::Builtins builtins;
     builtins.clamp_int = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -818,10 +818,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // conv_f32_to_iu32
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillConvF32ToIU32() {
+Transform::DataMap polyfillConvF32ToIU32() {
     BuiltinPolyfill::Builtins builtins;
     builtins.conv_f32_to_iu32 = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -969,10 +969,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // countLeadingZeros
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillCountLeadingZeros() {
+Transform::DataMap polyfillCountLeadingZeros() {
     BuiltinPolyfill::Builtins builtins;
     builtins.count_leading_zeros = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -1142,10 +1142,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // countTrailingZeros
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillCountTrailingZeros() {
+Transform::DataMap polyfillCountTrailingZeros() {
     BuiltinPolyfill::Builtins builtins;
     builtins.count_trailing_zeros = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -1315,10 +1315,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // extractBits
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillExtractBits(Level level) {
+Transform::DataMap polyfillExtractBits(Level level) {
     BuiltinPolyfill::Builtins builtins;
     builtins.extract_bits = level;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -1570,10 +1570,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // firstLeadingBit
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillFirstLeadingBit() {
+Transform::DataMap polyfillFirstLeadingBit() {
     BuiltinPolyfill::Builtins builtins;
     builtins.first_leading_bit = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -1743,10 +1743,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // firstTrailingBit
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillFirstTrailingBit() {
+Transform::DataMap polyfillFirstTrailingBit() {
     BuiltinPolyfill::Builtins builtins;
     builtins.first_trailing_bit = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -1916,10 +1916,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // insertBits
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillInsertBits(Level level) {
+Transform::DataMap polyfillInsertBits(Level level) {
     BuiltinPolyfill::Builtins builtins;
     builtins.insert_bits = level;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -2159,10 +2159,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // precise_float_mod
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillPreciseFloatMod() {
+Transform::DataMap polyfillPreciseFloatMod() {
     BuiltinPolyfill::Builtins builtins;
     builtins.precise_float_mod = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -2385,10 +2385,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // int_div_mod
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillIntDivMod() {
+Transform::DataMap polyfillIntDivMod() {
     BuiltinPolyfill::Builtins builtins;
     builtins.int_div_mod = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -3199,10 +3199,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // reflect for vec2<f32>
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillReflectVec2F32() {
+Transform::DataMap polyfillReflectVec2F32() {
     BuiltinPolyfill::Builtins builtins;
     builtins.reflect_vec2_f32 = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -3370,10 +3370,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // saturate
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillSaturate() {
+Transform::DataMap polyfillSaturate() {
     BuiltinPolyfill::Builtins builtins;
     builtins.saturate = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -3507,10 +3507,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // sign_int
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillSignInt() {
+Transform::DataMap polyfillSignInt() {
     BuiltinPolyfill::Builtins builtins;
     builtins.sign_int = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -3600,10 +3600,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // textureSampleBaseClampToEdge
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillTextureSampleBaseClampToEdge_2d_f32() {
+Transform::DataMap polyfillTextureSampleBaseClampToEdge_2d_f32() {
     BuiltinPolyfill::Builtins builtins;
     builtins.texture_sample_base_clamp_to_edge_2d_f32 = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -3671,18 +3671,18 @@
 ////////////////////////////////////////////////////////////////////////////////
 // workgroupUniformLoad
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillWorkgroupUniformLoad() {
+Transform::DataMap polyfillWorkgroupUniformLoad() {
     BuiltinPolyfill::Builtins builtins;
     builtins.workgroup_uniform_load = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
 
     return data;
 }
 
-DataMap polyfillWorkgroupUniformLoadWithDirectVariableAccess() {
-    DataMap data;
+Transform::DataMap polyfillWorkgroupUniformLoadWithDirectVariableAccess() {
+    Transform::DataMap data;
 
     BuiltinPolyfill::Builtins builtins;
     builtins.workgroup_uniform_load = true;
@@ -3893,10 +3893,10 @@
 ////////////////////////////////////////////////////////////////////////////////
 // quantizeToF16
 ////////////////////////////////////////////////////////////////////////////////
-DataMap polyfillQuantizeToF16_2d_f32() {
+Transform::DataMap polyfillQuantizeToF16_2d_f32() {
     BuiltinPolyfill::Builtins builtins;
     builtins.quantize_to_vec_f16 = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
     return data;
 }
@@ -4021,7 +4021,7 @@
     BuiltinPolyfill::Builtins builtins;
     builtins.bitshift_modulo = true;
     builtins.int_div_mod = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<BuiltinPolyfill::Config>(builtins);
 
     auto got = Run<BuiltinPolyfill>(src, std::move(data));
diff --git a/src/tint/ast/transform/canonicalize_entry_point_io_test.cc b/src/tint/ast/transform/canonicalize_entry_point_io_test.cc
index 98af3a7..454fb3b 100644
--- a/src/tint/ast/transform/canonicalize_entry_point_io_test.cc
+++ b/src/tint/ast/transform/canonicalize_entry_point_io_test.cc
@@ -48,7 +48,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -82,7 +82,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -117,7 +117,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -154,7 +154,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -189,7 +189,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -224,7 +224,7 @@
 alias myf32 = f32;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -277,7 +277,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -330,7 +330,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -384,7 +384,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -438,7 +438,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -494,7 +494,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -550,7 +550,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -579,7 +579,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -613,7 +613,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -647,7 +647,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -702,7 +702,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -757,7 +757,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -817,7 +817,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -877,7 +877,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -937,7 +937,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -997,7 +997,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1063,7 +1063,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1129,7 +1129,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1201,7 +1201,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1273,7 +1273,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1345,7 +1345,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1417,7 +1417,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1484,7 +1484,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1551,7 +1551,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1637,7 +1637,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1723,7 +1723,7 @@
 alias myf32 = f32;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1814,7 +1814,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -1905,7 +1905,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2035,7 +2035,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2165,7 +2165,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2229,7 +2229,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2293,7 +2293,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2360,7 +2360,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2427,7 +2427,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2531,7 +2531,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2635,7 +2635,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2664,7 +2664,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2696,7 +2696,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2730,7 +2730,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2764,7 +2764,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2801,7 +2801,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2853,7 +2853,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2905,7 +2905,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -2955,7 +2955,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3005,7 +3005,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3094,7 +3094,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03u);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3144,7 +3144,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl, 0x03);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3176,7 +3176,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3214,7 +3214,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3255,7 +3255,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3296,7 +3296,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3342,7 +3342,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3388,7 +3388,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3466,7 +3466,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3544,7 +3544,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3620,7 +3620,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3696,7 +3696,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kMsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3772,7 +3772,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3848,7 +3848,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl,
                                                0xFFFFFFFF, true);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
@@ -3884,7 +3884,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kSpirv);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3919,7 +3919,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kGlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
@@ -3956,7 +3956,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kGlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO>(src, data);
 
diff --git a/src/tint/ast/transform/combine_samplers_test.cc b/src/tint/ast/transform/combine_samplers_test.cc
index f658493..fa74b8a 100644
--- a/src/tint/ast/transform/combine_samplers_test.cc
+++ b/src/tint/ast/transform/combine_samplers_test.cc
@@ -28,7 +28,7 @@
     auto* src = "";
     auto* expect = "";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -55,7 +55,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -82,7 +82,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -117,7 +117,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -152,7 +152,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -179,7 +179,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     CombineSamplers::BindingMap map;
     sem::SamplerTexturePair pair;
     pair.texture_binding_point.group = 0;
@@ -214,7 +214,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     CombineSamplers::BindingMap map;
     sem::SamplerTexturePair pair;
     pair.texture_binding_point.group = 3;
@@ -262,7 +262,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -300,7 +300,7 @@
 alias Tex2d = texture_2d<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -343,7 +343,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -385,7 +385,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -428,7 +428,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -491,7 +491,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -538,7 +538,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -585,7 +585,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -624,7 +624,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -661,7 +661,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -700,7 +700,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -737,7 +737,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -776,7 +776,7 @@
     pair.sampler_binding_point.binding = placeholder.binding;
     CombineSamplers::BindingMap map;
     map[pair] = "fred";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(map, placeholder);
     auto got = Run<CombineSamplers>(src, data);
 
@@ -820,7 +820,7 @@
     CombineSamplers::BindingMap map;
     map[pair] = "barney";
     map[placeholder_pair] = "fred";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(map, placeholder);
     auto got = Run<CombineSamplers>(src, data);
 
@@ -847,7 +847,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -882,7 +882,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -916,7 +916,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -947,7 +947,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
@@ -977,7 +977,7 @@
 @internal(disable_validation__binding_point_collision) @group(0) @binding(0) var<uniform> gcoords : vec2<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CombineSamplers::BindingInfo>(CombineSamplers::BindingMap(), sem::BindingPoint());
     auto got = Run<CombineSamplers>(src, data);
 
diff --git a/src/tint/ast/transform/decompose_memory_access.cc b/src/tint/ast/transform/decompose_memory_access.cc
index f80fd56..bd48606 100644
--- a/src/tint/ast/transform/decompose_memory_access.cc
+++ b/src/tint/ast/transform/decompose_memory_access.cc
@@ -313,8 +313,8 @@
 
 /// Store describes a single storage or uniform buffer write
 struct Store {
-    const AssignmentStatement* assignment;       // The AST assignment statement
-    BufferAccess target;                         // The target for the write
+    const AssignmentStatement* assignment;  // The AST assignment statement
+    BufferAccess target;                    // The target for the write
 };
 
 }  // namespace
diff --git a/src/tint/ast/transform/direct_variable_access_test.cc b/src/tint/ast/transform/direct_variable_access_test.cc
index b735991..ecc8843 100644
--- a/src/tint/ast/transform/direct_variable_access_test.cc
+++ b/src/tint/ast/transform/direct_variable_access_test.cc
@@ -23,22 +23,22 @@
 namespace tint::ast::transform {
 namespace {
 
-/// @returns a DataMap with DirectVariableAccess::Config::transform_private enabled.
-static DataMap EnablePrivate() {
+/// @returns a Transform::DataMap with DirectVariableAccess::Config::transform_private enabled.
+static Transform::DataMap EnablePrivate() {
     DirectVariableAccess::Options opts;
     opts.transform_private = true;
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<DirectVariableAccess::Config>(opts);
     return inputs;
 }
 
-/// @returns a DataMap with DirectVariableAccess::Config::transform_function enabled.
-static DataMap EnableFunction() {
+/// @returns a Transform::DataMap with DirectVariableAccess::Config::transform_function enabled.
+static Transform::DataMap EnableFunction() {
     DirectVariableAccess::Options opts;
     opts.transform_function = true;
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<DirectVariableAccess::Config>(opts);
     return inputs;
 }
diff --git a/src/tint/ast/transform/first_index_offset.h b/src/tint/ast/transform/first_index_offset.h
index cb5e3f3..44a3a63 100644
--- a/src/tint/ast/transform/first_index_offset.h
+++ b/src/tint/ast/transform/first_index_offset.h
@@ -62,7 +62,7 @@
     /// BindingPoint is consumed by the FirstIndexOffset transform.
     /// BindingPoint specifies the binding point of the first index uniform
     /// buffer.
-    struct BindingPoint final : public utils::Castable<BindingPoint, transform::Data> {
+    struct BindingPoint final : public utils::Castable<BindingPoint, Transform::Data> {
         /// Constructor
         BindingPoint();
 
@@ -82,7 +82,7 @@
 
     /// Data is outputted by the FirstIndexOffset transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Data final : public utils::Castable<Data, transform::Data> {
+    struct Data final : public utils::Castable<Data, Transform::Data> {
         /// Constructor
         /// @param has_vtx_or_inst_index True if the shader uses vertex_index or
         /// instance_index
diff --git a/src/tint/ast/transform/first_index_offset_test.cc b/src/tint/ast/transform/first_index_offset_test.cc
index 1be6474..8dc441f 100644
--- a/src/tint/ast/transform/first_index_offset_test.cc
+++ b/src/tint/ast/transform/first_index_offset_test.cc
@@ -57,7 +57,7 @@
     auto* src = "";
     auto* expect = "";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(0, 0);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -77,7 +77,7 @@
 )";
     auto* expect = src;
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(0, 0);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -121,7 +121,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -165,7 +165,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -209,7 +209,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 7);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -253,7 +253,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 7);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -309,7 +309,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -365,7 +365,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -417,7 +417,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -469,7 +469,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -537,7 +537,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
@@ -605,7 +605,7 @@
 }
 )";
 
-    DataMap config;
+    Transform::DataMap config;
     config.Add<FirstIndexOffset::BindingPoint>(1, 2);
     auto got = Run<FirstIndexOffset>(src, std::move(config));
 
diff --git a/src/tint/ast/transform/multiplanar_external_texture_test.cc b/src/tint/ast/transform/multiplanar_external_texture_test.cc
index 25c7765..8acf254 100644
--- a/src/tint/ast/transform/multiplanar_external_texture_test.cc
+++ b/src/tint/ast/transform/multiplanar_external_texture_test.cc
@@ -23,7 +23,7 @@
 TEST_F(MultiplanarExternalTextureTest, ShouldRunEmptyModule) {
     auto* src = R"()";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
 
@@ -35,7 +35,7 @@
 alias ET = texture_external;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
 
@@ -46,7 +46,7 @@
 @group(0) @binding(0) var ext_tex : texture_external;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
 
@@ -58,7 +58,7 @@
 fn f(ext_tex : texture_external) {}
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
 
@@ -98,7 +98,7 @@
 
     auto* expect = R"(error: missing new binding points for texture_external at binding {0,1})";
 
-    DataMap data;
+    Transform::DataMap data;
     // This bindings map specifies 0,0 as the location of the texture_external,
     // which is incorrect.
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
@@ -156,7 +156,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -212,7 +212,7 @@
 @group(0) @binding(0) var ext_tex : texture_2d<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -296,7 +296,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -380,7 +380,7 @@
 @group(0) @binding(0) var s : sampler;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -475,7 +475,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -570,7 +570,7 @@
 @group(0) @binding(0) var ext_tex : texture_2d<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -670,7 +670,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -770,7 +770,7 @@
 @group(0) @binding(1) var ext_tex : texture_2d<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 1}, {{0, 2}, {0, 3}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -878,7 +878,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 1}, {{0, 4}, {0, 5}}},
         {{0, 2}, {{0, 6}, {0, 7}}},
@@ -974,7 +974,7 @@
   f(ext_tex, ext_tex_plane_1, ext_tex_params, smp);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1067,7 +1067,7 @@
 
 @group(0) @binding(1) var smp : sampler;
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1160,7 +1160,7 @@
   f(smp, ext_tex, ext_tex_plane_1, ext_tex_params);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1262,7 +1262,7 @@
   f(ext_tex, ext_tex_plane_1, ext_tex_params, smp, ext_tex2, ext_tex_plane_1_1, ext_tex_params_1);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 3}, {0, 4}}},
         {{0, 2}, {{0, 5}, {0, 6}}},
@@ -1366,7 +1366,7 @@
 
 @group(0) @binding(2) var ext_tex2 : texture_2d<f32>;
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 3}, {0, 4}}},
         {{0, 2}, {{0, 5}, {0, 6}}},
@@ -1468,7 +1468,7 @@
   f(ext_tex, ext_tex_plane_1, ext_tex_params, smp);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1569,7 +1569,7 @@
   f(ext_tex, ext_tex_plane_1, ext_tex_params, smp);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1613,7 +1613,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(
         MultiplanarExternalTexture::BindingsMap{{{0, 0}, {{0, 1}, {0, 2}}}});
     auto got = Run<MultiplanarExternalTexture>(src, data);
@@ -1708,7 +1708,7 @@
   f(ext_tex, ext_tex_plane_1, ext_tex_params, smp);
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
@@ -1804,7 +1804,7 @@
 
 alias ET = texture_external;
 )";
-    DataMap data;
+    Transform::DataMap data;
     data.Add<MultiplanarExternalTexture::NewBindingPoints>(MultiplanarExternalTexture::BindingsMap{
         {{0, 0}, {{0, 2}, {0, 3}}},
     });
diff --git a/src/tint/ast/transform/num_workgroups_from_uniform_test.cc b/src/tint/ast/transform/num_workgroups_from_uniform_test.cc
index b46f6ab..2910655 100644
--- a/src/tint/ast/transform/num_workgroups_from_uniform_test.cc
+++ b/src/tint/ast/transform/num_workgroups_from_uniform_test.cc
@@ -28,7 +28,7 @@
 TEST_F(NumWorkgroupsFromUniformTest, ShouldRunEmptyModule) {
     auto* src = R"()";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     EXPECT_FALSE(ShouldRun<NumWorkgroupsFromUniform>(src, data));
 }
@@ -40,7 +40,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     EXPECT_TRUE(ShouldRun<NumWorkgroupsFromUniform>(src, data));
 }
@@ -55,7 +55,7 @@
     auto* expect =
         "error: missing transform data for tint::ast::transform::NumWorkgroupsFromUniform";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
     EXPECT_EQ(expect, str(got));
@@ -90,7 +90,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -134,7 +134,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -178,7 +178,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -233,7 +233,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -289,7 +289,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -388,7 +388,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -429,7 +429,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<NumWorkgroupsFromUniform::Config>(sem::BindingPoint{0, 30u});
     auto got = Run<Unshadow, CanonicalizeEntryPointIO, NumWorkgroupsFromUniform>(src, data);
@@ -530,7 +530,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     // Make binding point unspecified.
     data.Add<NumWorkgroupsFromUniform::Config>(std::nullopt);
@@ -684,7 +684,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     // Make binding point unspecified.
     data.Add<NumWorkgroupsFromUniform::Config>(std::nullopt);
diff --git a/src/tint/ast/transform/packed_vec3_test.cc b/src/tint/ast/transform/packed_vec3_test.cc
index 29d9919..6ad0b0a 100644
--- a/src/tint/ast/transform/packed_vec3_test.cc
+++ b/src/tint/ast/transform/packed_vec3_test.cc
@@ -218,7 +218,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -243,7 +243,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -268,7 +268,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -293,7 +293,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -321,7 +321,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -346,7 +346,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -371,7 +371,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -409,7 +409,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -439,7 +439,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -469,7 +469,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -499,7 +499,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -537,7 +537,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -570,7 +570,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -600,7 +600,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -638,7 +638,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -668,7 +668,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -698,7 +698,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -736,7 +736,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -766,7 +766,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -796,7 +796,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -826,7 +826,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -864,7 +864,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -897,7 +897,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -927,7 +927,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -965,7 +965,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -995,7 +995,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1025,7 +1025,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1071,7 +1071,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1109,7 +1109,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1139,7 +1139,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1169,7 +1169,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1199,7 +1199,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1245,7 +1245,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1278,7 +1278,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1316,7 +1316,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1354,7 +1354,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1384,7 +1384,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1427,7 +1427,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1457,7 +1457,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1487,7 +1487,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1531,7 +1531,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1569,7 +1569,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1607,7 +1607,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1645,7 +1645,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1689,7 +1689,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1730,7 +1730,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1768,7 +1768,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1818,7 +1818,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1856,7 +1856,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1894,7 +1894,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1951,7 +1951,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2002,7 +2002,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2045,7 +2045,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2088,7 +2088,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2131,7 +2131,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2189,7 +2189,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2235,7 +2235,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2286,7 +2286,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2341,7 +2341,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2384,7 +2384,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2440,7 +2440,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2483,7 +2483,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2526,7 +2526,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2583,7 +2583,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2634,7 +2634,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2677,7 +2677,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2720,7 +2720,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2763,7 +2763,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2821,7 +2821,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2867,7 +2867,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2918,7 +2918,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2973,7 +2973,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3016,7 +3016,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3072,7 +3072,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3115,7 +3115,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3158,7 +3158,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3223,7 +3223,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3282,7 +3282,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3333,7 +3333,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3376,7 +3376,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3419,7 +3419,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3462,7 +3462,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3528,7 +3528,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3574,7 +3574,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3633,7 +3633,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3688,7 +3688,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3739,7 +3739,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3795,7 +3795,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3838,7 +3838,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3899,7 +3899,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3942,7 +3942,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3985,7 +3985,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4039,7 +4039,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4089,7 +4089,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4144,7 +4144,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4211,7 +4211,7 @@
 @group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(std::move(src), data);
 
     EXPECT_EQ(expect, str(got));
@@ -4264,7 +4264,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4317,7 +4317,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     auto& vars = got.program.AST().GlobalVariables();
@@ -4436,7 +4436,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4485,7 +4485,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4517,7 +4517,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4578,7 +4578,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4671,7 +4671,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4791,7 +4791,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4885,7 +4885,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4958,7 +4958,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5047,7 +5047,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5172,7 +5172,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5288,7 +5288,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5390,7 +5390,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5477,7 +5477,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5580,7 +5580,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5722,7 +5722,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5790,7 +5790,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5881,7 +5881,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -5978,7 +5978,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6084,7 +6084,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6190,7 +6190,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6263,7 +6263,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6336,7 +6336,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6414,7 +6414,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6482,7 +6482,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6550,7 +6550,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6647,7 +6647,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6672,7 +6672,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6697,7 +6697,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6743,7 +6743,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6795,7 +6795,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6860,7 +6860,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -6937,7 +6937,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7002,7 +7002,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7075,7 +7075,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7197,7 +7197,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7320,7 +7320,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7466,7 +7466,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7513,7 +7513,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7559,7 +7559,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7619,7 +7619,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7683,7 +7683,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7748,7 +7748,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7788,7 +7788,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7870,7 +7870,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -7944,7 +7944,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -8038,7 +8038,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -8085,7 +8085,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PackedVec3>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/pad_structs_test.cc b/src/tint/ast/transform/pad_structs_test.cc
index 89ce34c..f7dc437 100644
--- a/src/tint/ast/transform/pad_structs_test.cc
+++ b/src/tint/ast/transform/pad_structs_test.cc
@@ -28,7 +28,7 @@
     auto* src = "";
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -61,7 +61,7 @@
   let x = u.x;
 }
 )";
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -97,7 +97,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -137,7 +137,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -179,7 +179,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -219,7 +219,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -254,7 +254,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -300,7 +300,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -350,7 +350,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -396,7 +396,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -433,7 +433,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -477,7 +477,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -524,7 +524,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -561,7 +561,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -598,7 +598,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PadStructs>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/promote_side_effects_to_decl.cc b/src/tint/ast/transform/promote_side_effects_to_decl.cc
index 345caf1..19136dd 100644
--- a/src/tint/ast/transform/promote_side_effects_to_decl.cc
+++ b/src/tint/ast/transform/promote_side_effects_to_decl.cc
@@ -672,7 +672,7 @@
     tint::transform::Manager manager;
     manager.Add<SimplifySideEffectStatements>();
     manager.Add<DecomposeSideEffects>();
-    return manager.Apply(src, inputs, outputs);
+    return manager.Run(src, inputs, outputs);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/ast/transform/promote_side_effects_to_decl_test.cc b/src/tint/ast/transform/promote_side_effects_to_decl_test.cc
index 9cd2e0f..f8ce860 100644
--- a/src/tint/ast/transform/promote_side_effects_to_decl_test.cc
+++ b/src/tint/ast/transform/promote_side_effects_to_decl_test.cc
@@ -25,7 +25,7 @@
     auto* src = "";
     auto* expect = "";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -44,7 +44,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -81,7 +81,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -111,7 +111,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -142,7 +142,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -176,7 +176,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -211,7 +211,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -248,7 +248,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -278,7 +278,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -306,7 +306,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -338,7 +338,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -370,7 +370,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -401,7 +401,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -433,7 +433,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -467,7 +467,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -489,7 +489,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -532,7 +532,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -566,7 +566,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -608,7 +608,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -645,7 +645,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -679,7 +679,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -716,7 +716,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -757,7 +757,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -789,7 +789,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -820,7 +820,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -861,7 +861,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -900,7 +900,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -944,7 +944,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -998,7 +998,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1037,7 +1037,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1077,7 +1077,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1137,7 +1137,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1168,7 +1168,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1205,7 +1205,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1238,7 +1238,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1271,7 +1271,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1302,7 +1302,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1347,7 +1347,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1384,7 +1384,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1429,7 +1429,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1468,7 +1468,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1509,7 +1509,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1548,7 +1548,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1585,7 +1585,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1628,7 +1628,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1685,7 +1685,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1732,7 +1732,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1779,7 +1779,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1818,7 +1818,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1859,7 +1859,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1906,7 +1906,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1946,7 +1946,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -1984,7 +1984,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2031,7 +2031,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2079,7 +2079,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2113,7 +2113,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2146,7 +2146,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2190,7 +2190,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2232,7 +2232,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2279,7 +2279,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2342,7 +2342,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2384,7 +2384,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2427,7 +2427,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2492,7 +2492,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2517,7 +2517,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2555,7 +2555,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2593,7 +2593,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2634,7 +2634,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2680,7 +2680,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2712,7 +2712,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2747,7 +2747,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2778,7 +2778,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2805,7 +2805,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2835,7 +2835,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2866,7 +2866,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2898,7 +2898,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2933,7 +2933,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2963,7 +2963,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -2995,7 +2995,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3027,7 +3027,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3069,7 +3069,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3099,7 +3099,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3132,7 +3132,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3161,7 +3161,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3200,7 +3200,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3264,7 +3264,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3295,7 +3295,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3325,7 +3325,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3355,7 +3355,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3385,7 +3385,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3417,7 +3417,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3450,7 +3450,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3484,7 +3484,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3515,7 +3515,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3547,7 +3547,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3580,7 +3580,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3614,7 +3614,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3651,7 +3651,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3688,7 +3688,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3724,7 +3724,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3760,7 +3760,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3806,7 +3806,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3824,7 +3824,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3873,7 +3873,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3922,7 +3922,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -3970,7 +3970,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4018,7 +4018,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4079,7 +4079,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -4127,7 +4127,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<PromoteSideEffectsToDecl>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/remove_continue_in_switch_test.cc b/src/tint/ast/transform/remove_continue_in_switch_test.cc
index 84ad5cd..237ceea 100644
--- a/src/tint/ast/transform/remove_continue_in_switch_test.cc
+++ b/src/tint/ast/transform/remove_continue_in_switch_test.cc
@@ -101,7 +101,7 @@
     auto* src = "";
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -163,7 +163,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -247,7 +247,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -332,7 +332,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -423,7 +423,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -501,7 +501,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -553,7 +553,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -607,7 +607,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<RemoveContinueInSwitch>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/renamer.h b/src/tint/ast/transform/renamer.h
index fb839c2..5e7c92d 100644
--- a/src/tint/ast/transform/renamer.h
+++ b/src/tint/ast/transform/renamer.h
@@ -27,7 +27,7 @@
   public:
     /// Data is outputted by the Renamer transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Data final : public utils::Castable<Data, transform::Data> {
+    struct Data final : public utils::Castable<Data, tint::transform::Data> {
         /// Remappings is a map of old symbol name to new symbol name
         using Remappings = std::unordered_map<std::string, std::string>;
 
@@ -59,7 +59,7 @@
 
     /// Optional configuration options for the transform.
     /// If omitted, then the renamer will use Target::kAll.
-    struct Config final : public utils::Castable<Config, transform::Data> {
+    struct Config final : public utils::Castable<Config, tint::transform::Data> {
         /// Constructor
         /// @param tgt the targets to rename
         /// @param keep_unicode if false, symbols with non-ascii code-points are
diff --git a/src/tint/ast/transform/renamer_test.cc b/src/tint/ast/transform/renamer_test.cc
index fe7e845..4e97c6f 100644
--- a/src/tint/ast/transform/renamer_test.cc
+++ b/src/tint/ast/transform/renamer_test.cc
@@ -245,7 +245,7 @@
 
     auto expect = src;
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<Renamer::Config>(Renamer::Target::kMslKeywords,
                                 /* preserve_unicode */ true);
     auto got = Run<Renamer>(src, inputs);
@@ -269,7 +269,7 @@
 }
 )";
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<Renamer::Config>(Renamer::Target::kAll,
                                 /* preserve_unicode */ true);
     auto got = Run<Renamer>(src, inputs);
@@ -379,7 +379,7 @@
 }
 )";
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<Renamer::Config>(Renamer::Target::kGlslKeywords,
                                 /* preserve_unicode */ false);
     auto got = Run<Renamer>(src, inputs);
@@ -405,7 +405,7 @@
 }
 )";
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<Renamer::Config>(Renamer::Target::kHlslKeywords,
                                 /* preserve_unicode */ false);
     auto got = Run<Renamer>(src, inputs);
@@ -431,7 +431,7 @@
 }
 )";
 
-    DataMap inputs;
+    Transform::DataMap inputs;
     inputs.Add<Renamer::Config>(Renamer::Target::kMslKeywords,
                                 /* preserve_unicode */ false);
     auto got = Run<Renamer>(src, inputs);
diff --git a/src/tint/ast/transform/robustness_test.cc b/src/tint/ast/transform/robustness_test.cc
index 1200bb4..ebe7dc2 100644
--- a/src/tint/ast/transform/robustness_test.cc
+++ b/src/tint/ast/transform/robustness_test.cc
@@ -32,7 +32,7 @@
 
 namespace {
 
-DataMap Config(Robustness::Action action) {
+Transform::DataMap Config(Robustness::Action action) {
     Robustness::Config cfg;
     cfg.value_action = action;
     cfg.texture_action = action;
@@ -42,7 +42,7 @@
     cfg.storage_action = action;
     cfg.uniform_action = action;
     cfg.workgroup_action = action;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<Robustness::Config>(cfg);
     return data;
 }
diff --git a/src/tint/ast/transform/single_entry_point_test.cc b/src/tint/ast/transform/single_entry_point_test.cc
index 54c730f..6a4ebf2 100644
--- a/src/tint/ast/transform/single_entry_point_test.cc
+++ b/src/tint/ast/transform/single_entry_point_test.cc
@@ -38,7 +38,7 @@
 
     auto* expect = "error: entry point 'main' not found";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>("main");
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -57,7 +57,7 @@
 
     SingleEntryPoint::Config cfg("_");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -76,7 +76,7 @@
 
     SingleEntryPoint::Config cfg("foo");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -92,7 +92,7 @@
 
     SingleEntryPoint::Config cfg("main");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -127,7 +127,7 @@
 
     SingleEntryPoint::Config cfg("comp_main1");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -177,7 +177,7 @@
 
     SingleEntryPoint::Config cfg("comp_main1");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -233,7 +233,7 @@
 
     SingleEntryPoint::Config cfg("comp_main1");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -253,7 +253,7 @@
 
     SingleEntryPoint::Config cfg("main");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -303,7 +303,7 @@
   let local_d = c1;
 }
 )";
-        DataMap data;
+        Transform::DataMap data;
         data.Add<SingleEntryPoint::Config>(cfg);
         auto got = Run<SingleEntryPoint>(src, data);
         EXPECT_EQ(expect, str(got));
@@ -321,7 +321,7 @@
   let local_d = c2;
 }
 )";
-        DataMap data;
+        Transform::DataMap data;
         data.Add<SingleEntryPoint::Config>(cfg);
         auto got = Run<SingleEntryPoint>(src, data);
         EXPECT_EQ(expect, str(got));
@@ -337,7 +337,7 @@
   let local_d = c3;
 }
 )";
-        DataMap data;
+        Transform::DataMap data;
         data.Add<SingleEntryPoint::Config>(cfg);
         auto got = Run<SingleEntryPoint>(src, data);
         EXPECT_EQ(expect, str(got));
@@ -353,7 +353,7 @@
   let local_d = c4;
 }
 )";
-        DataMap data;
+        Transform::DataMap data;
         data.Add<SingleEntryPoint::Config>(cfg);
         auto got = Run<SingleEntryPoint>(src, data);
         EXPECT_EQ(expect, str(got));
@@ -367,7 +367,7 @@
   let local_d = 1u;
 }
 )";
-        DataMap data;
+        Transform::DataMap data;
         data.Add<SingleEntryPoint::Config>(cfg);
         auto got = Run<SingleEntryPoint>(src, data);
         EXPECT_EQ(expect, str(got));
@@ -403,7 +403,7 @@
     auto* expect = src;
 
     SingleEntryPoint::Config cfg("main");
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
     EXPECT_EQ(expect, str(got));
@@ -440,7 +440,7 @@
 )";
 
     SingleEntryPoint::Config cfg("main");
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
     EXPECT_EQ(expect, str(got));
@@ -498,7 +498,7 @@
 
     SingleEntryPoint::Config cfg("comp_main1");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -581,7 +581,7 @@
 
     SingleEntryPoint::Config cfg("comp_main1");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -604,7 +604,7 @@
 
     SingleEntryPoint::Config cfg("main");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
@@ -624,7 +624,7 @@
 
     SingleEntryPoint::Config cfg("main");
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SingleEntryPoint::Config>(cfg);
     auto got = Run<SingleEntryPoint>(src, data);
 
diff --git a/src/tint/ast/transform/substitute_override.cc b/src/tint/ast/transform/substitute_override.cc
index fed559b..f0d7717 100644
--- a/src/tint/ast/transform/substitute_override.cc
+++ b/src/tint/ast/transform/substitute_override.cc
@@ -101,20 +101,19 @@
     // Ensure that objects that are indexed with an override-expression are materialized.
     // If the object is not materialized, and the 'override' variable is turned to a 'const', the
     // resulting type of the index may change. See: crbug.com/tint/1697.
-    ctx.ReplaceAll(
-        [&](const IndexAccessorExpression* expr) -> const IndexAccessorExpression* {
-            if (auto* sem = src->Sem().Get(expr)) {
-                if (auto* access = sem->UnwrapMaterialize()->As<sem::IndexAccessorExpression>()) {
-                    if (access->Object()->UnwrapMaterialize()->Type()->HoldsAbstract() &&
-                        access->Index()->Stage() == sem::EvaluationStage::kOverride) {
-                        auto* obj = b.Call(builtin::str(builtin::Function::kTintMaterialize),
-                                           ctx.Clone(expr->object));
-                        return b.IndexAccessor(obj, ctx.Clone(expr->index));
-                    }
+    ctx.ReplaceAll([&](const IndexAccessorExpression* expr) -> const IndexAccessorExpression* {
+        if (auto* sem = src->Sem().Get(expr)) {
+            if (auto* access = sem->UnwrapMaterialize()->As<sem::IndexAccessorExpression>()) {
+                if (access->Object()->UnwrapMaterialize()->Type()->HoldsAbstract() &&
+                    access->Index()->Stage() == sem::EvaluationStage::kOverride) {
+                    auto* obj = b.Call(builtin::str(builtin::Function::kTintMaterialize),
+                                       ctx.Clone(expr->object));
+                    return b.IndexAccessor(obj, ctx.Clone(expr->index));
                 }
             }
-            return nullptr;
-        });
+        }
+        return nullptr;
+    });
 
     ctx.Clone();
     return Program(std::move(b));
diff --git a/src/tint/ast/transform/substitute_override_test.cc b/src/tint/ast/transform/substitute_override_test.cc
index 01d9597..9f3cd63 100644
--- a/src/tint/ast/transform/substitute_override_test.cc
+++ b/src/tint/ast/transform/substitute_override_test.cc
@@ -32,7 +32,7 @@
 
     auto* expect = "error: Missing override substitution data";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<SubstituteOverride>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -50,7 +50,7 @@
     auto* expect = "error: Initializer not provided for override, and override not overridden.";
 
     SubstituteOverride::Config cfg;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
 
     auto got = Run<SubstituteOverride>(src, data);
@@ -75,7 +75,7 @@
 
     SubstituteOverride::Config cfg;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
     auto got = Run<SubstituteOverride>(src, data);
 
@@ -143,7 +143,7 @@
     cfg.map.insert({OverrideId{6}, 1.0});
     cfg.map.insert({OverrideId{7}, 0.0});
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
     auto got = Run<SubstituteOverride>(src, data);
 
@@ -212,7 +212,7 @@
     cfg.map.insert({OverrideId{7}, 0.0});
     cfg.map.insert({OverrideId{5}, 13});
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
     auto got = Run<SubstituteOverride>(src, data);
 
@@ -241,7 +241,7 @@
     SubstituteOverride::Config cfg;
     cfg.map.insert({OverrideId{0}, 11.0});
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
     auto got = Run<SubstituteOverride>(src, data);
 
@@ -282,7 +282,7 @@
     SubstituteOverride::Config cfg;
     cfg.map.insert({OverrideId{0}, 0.0});
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
     auto got = Run<SubstituteOverride>(src, data);
 
diff --git a/src/tint/ast/transform/test_helper.h b/src/tint/ast/transform/test_helper.h
index 08b8e30..0ec2ec2 100644
--- a/src/tint/ast/transform/test_helper.h
+++ b/src/tint/ast/transform/test_helper.h
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "src/tint/ast/transform/transform.h"
 #include "src/tint/reader/wgsl/parser.h"
 #include "src/tint/transform/manager.h"
 #include "src/tint/writer/wgsl/generator.h"
@@ -66,11 +67,11 @@
     /// `transform`.
     /// @param transform the transform to apply
     /// @param in the input WGSL source
-    /// @param data the optional DataMap to pass to Transform::Run()
+    /// @param data the optional Transform::DataMap to pass to Transform::Run()
     /// @return the transformed output
     Output Run(std::string in,
                std::unique_ptr<transform::Transform> transform,
-               const DataMap& data = {}) {
+               const tint::transform::DataMap& data = {}) {
         std::vector<std::unique_ptr<transform::Transform>> transforms;
         transforms.emplace_back(std::move(transform));
         return Run(std::move(in), std::move(transforms), data);
@@ -79,10 +80,10 @@
     /// Transforms and returns the WGSL source `in`, transformed using
     /// a transform of type `TRANSFORM`.
     /// @param in the input WGSL source
-    /// @param data the optional DataMap to pass to Transform::Run()
+    /// @param data the optional Transform::DataMap to pass to Transform::Run()
     /// @return the transformed output
     template <typename... TRANSFORMS>
-    Output Run(std::string in, const DataMap& data = {}) {
+    Output Run(std::string in, const tint::transform::DataMap& data = {}) {
         auto file = std::make_unique<Source::File>("test", in);
         auto program = reader::wgsl::Parse(file.get());
 
@@ -95,26 +96,28 @@
     /// Transforms and returns program `program`, transformed using a transform of
     /// type `TRANSFORM`.
     /// @param program the input Program
-    /// @param data the optional DataMap to pass to Transform::Run()
+    /// @param data the optional Transform::DataMap to pass to Transform::Run()
     /// @return the transformed output
     template <typename... TRANSFORMS>
-    Output Run(Program&& program, const DataMap& data = {}) {
+    Output Run(Program&& program, const tint::transform::DataMap& data = {}) {
         if (!program.IsValid()) {
             return Output(std::move(program));
         }
 
         tint::transform::Manager manager;
+        tint::transform::DataMap outputs;
         for (auto* transform_ptr : std::initializer_list<Transform*>{new TRANSFORMS()...}) {
             manager.append(std::unique_ptr<Transform>(transform_ptr));
         }
-        return manager.Run(&program, data);
+        auto result = manager.Run(&program, data, outputs);
+        return {std::move(result), std::move(outputs)};
     }
 
     /// @param program the input program
-    /// @param data the optional DataMap to pass to Transform::Run()
+    /// @param data the optional Transform::DataMap to pass to Transform::Run()
     /// @return true if the transform should be run for the given input.
     template <typename TRANSFORM>
-    bool ShouldRun(Program&& program, const DataMap& data = {}) {
+    bool ShouldRun(Program&& program, const tint::transform::DataMap& data = {}) {
         if (!program.IsValid()) {
             ADD_FAILURE() << "ShouldRun() called with invalid program: "
                           << program.Diagnostics().str();
@@ -123,7 +126,7 @@
 
         const Transform& t = TRANSFORM();
 
-        DataMap outputs;
+        tint::transform::DataMap outputs;
         auto result = t.Apply(&program, data, outputs);
         if (!result) {
             return false;
@@ -137,10 +140,10 @@
     }
 
     /// @param in the input WGSL source
-    /// @param data the optional DataMap to pass to Transform::Run()
+    /// @param data the optional Transform::DataMap to pass to Transform::Run()
     /// @return true if the transform should be run for the given input.
     template <typename TRANSFORM>
-    bool ShouldRun(std::string in, const DataMap& data = {}) {
+    bool ShouldRun(std::string in, const tint::transform::DataMap& data = {}) {
         auto file = std::make_unique<Source::File>("test", in);
         auto program = reader::wgsl::Parse(file.get());
         return ShouldRun<TRANSFORM>(std::move(program), data);
diff --git a/src/tint/ast/transform/texture_1d_to_2d_test.cc b/src/tint/ast/transform/texture_1d_to_2d_test.cc
index 75da6fc..4189385 100644
--- a/src/tint/ast/transform/texture_1d_to_2d_test.cc
+++ b/src/tint/ast/transform/texture_1d_to_2d_test.cc
@@ -23,7 +23,7 @@
 TEST_F(Texture1DTo2DTest, EmptyModule) {
     auto* src = "";
 
-    DataMap data;
+    Transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -39,7 +39,7 @@
 @group(0) @binding(1) var s : sampler;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -65,7 +65,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -87,7 +87,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -109,7 +109,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -131,7 +131,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -153,7 +153,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -167,7 +167,7 @@
 @group(0) @binding(0) var t : texture_storage_2d<r32float, write>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -184,7 +184,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -193,7 +193,7 @@
 var<private> i : i32;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -202,7 +202,7 @@
 @group(0) @binding(0) var<uniform> m : mat2x2<f32>;
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     EXPECT_FALSE(ShouldRun<Texture1DTo2D>(src, data));
 }
 
@@ -234,7 +234,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -265,7 +265,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -290,7 +290,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<Texture1DTo2D>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/transform.cc b/src/tint/ast/transform/transform.cc
index 2270aff..bee1f39 100644
--- a/src/tint/ast/transform/transform.cc
+++ b/src/tint/ast/transform/transform.cc
@@ -28,20 +28,9 @@
 #include "src/tint/type/sampler.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Transform);
-TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::Data);
 
 namespace tint::ast::transform {
 
-Data::Data() = default;
-Data::Data(const Data&) = default;
-Data::~Data() = default;
-Data& Data::operator=(const Data&) = default;
-
-DataMap::DataMap() = default;
-DataMap::DataMap(DataMap&&) = default;
-DataMap::~DataMap() = default;
-DataMap& DataMap::operator=(DataMap&&) = default;
-
 Output::Output() = default;
 Output::Output(Program&& p) : program(std::move(p)) {}
 Transform::Transform() = default;
diff --git a/src/tint/ast/transform/transform.h b/src/tint/ast/transform/transform.h
index 82cadce..2ad3701 100644
--- a/src/tint/ast/transform/transform.h
+++ b/src/tint/ast/transform/transform.h
@@ -15,8 +15,8 @@
 #ifndef SRC_TINT_AST_TRANSFORM_TRANSFORM_H_
 #define SRC_TINT_AST_TRANSFORM_TRANSFORM_H_
 
-#include <memory>
-#include <unordered_map>
+#include "src/tint/transform/transform.h"
+
 #include <utility>
 
 #include "src/tint/program.h"
@@ -24,107 +24,6 @@
 
 namespace tint::ast::transform {
 
-/// Data is the base class for transforms that accept extra input or emit extra
-/// output information along with a Program.
-class Data : public utils::Castable<Data> {
-  public:
-    /// Constructor
-    Data();
-
-    /// Copy constructor
-    Data(const Data&);
-
-    /// Destructor
-    ~Data() override;
-
-    /// Assignment operator
-    /// @returns this Data
-    Data& operator=(const Data&);
-};
-
-/// DataMap is a map of Data unique pointers keyed by the Data's ClassID.
-class DataMap {
-  public:
-    /// Constructor
-    DataMap();
-
-    /// Move constructor
-    DataMap(DataMap&&);
-
-    /// Constructor
-    /// @param data_unique_ptrs a variadic list of additional data unique_ptrs
-    /// produced by the transform
-    template <typename... DATA>
-    explicit DataMap(DATA... data_unique_ptrs) {
-        PutAll(std::forward<DATA>(data_unique_ptrs)...);
-    }
-
-    /// Destructor
-    ~DataMap();
-
-    /// Move assignment operator
-    /// @param rhs the DataMap to move into this DataMap
-    /// @return this DataMap
-    DataMap& operator=(DataMap&& rhs);
-
-    /// Adds the data into DataMap keyed by the ClassID of type T.
-    /// @param data the data to add to the DataMap
-    template <typename T>
-    void Put(std::unique_ptr<T>&& data) {
-        static_assert(std::is_base_of<Data, T>::value, "T does not derive from Data");
-        map_[&utils::TypeInfo::Of<T>()] = std::move(data);
-    }
-
-    /// Creates the data of type `T` with the provided arguments and adds it into
-    /// DataMap keyed by the ClassID of type T.
-    /// @param args the arguments forwarded to the initializer for type T
-    template <typename T, typename... ARGS>
-    void Add(ARGS&&... args) {
-        Put(std::make_unique<T>(std::forward<ARGS>(args)...));
-    }
-
-    /// @returns a pointer to the Data placed into the DataMap with a call to
-    /// Put()
-    template <typename T>
-    T const* Get() const {
-        return const_cast<DataMap*>(this)->Get<T>();
-    }
-
-    /// @returns a pointer to the Data placed into the DataMap with a call to
-    /// Put()
-    template <typename T>
-    T* Get() {
-        auto it = map_.find(&utils::TypeInfo::Of<T>());
-        if (it == map_.end()) {
-            return nullptr;
-        }
-        return static_cast<T*>(it->second.get());
-    }
-
-    /// Add moves all the data from other into this DataMap
-    /// @param other the DataMap to move into this DataMap
-    void Add(DataMap&& other) {
-        for (auto& it : other.map_) {
-            map_.emplace(it.first, std::move(it.second));
-        }
-        other.map_.clear();
-    }
-
-  private:
-    template <typename T0>
-    void PutAll(T0&& first) {
-        Put(std::forward<T0>(first));
-    }
-
-    template <typename T0, typename... Tn>
-    void PutAll(T0&& first, Tn&&... remainder) {
-        Put(std::forward<T0>(first));
-        PutAll(std::forward<Tn>(remainder)...);
-    }
-
-    std::unordered_map<const utils::TypeInfo*, std::unique_ptr<Data>> map_;
-};
-
 /// The return type of Run()
 class Output {
   public:
@@ -147,11 +46,11 @@
     Program program;
 
     /// Extra output generated by the transforms.
-    DataMap data;
+    tint::transform::DataMap data;
 };
 
 /// Interface for Program transforms
-class Transform : public utils::Castable<Transform> {
+class Transform : public utils::Castable<Transform, tint::transform::Transform> {
   public:
     /// Constructor
     Transform();
diff --git a/src/tint/ast/transform/truncate_interstage_variables.cc b/src/tint/ast/transform/truncate_interstage_variables.cc
index 157c544..1e5bf71 100644
--- a/src/tint/ast/transform/truncate_interstage_variables.cc
+++ b/src/tint/ast/transform/truncate_interstage_variables.cc
@@ -154,16 +154,15 @@
     }
 
     // Replace return statements with new truncated shader IO struct
-    ctx.ReplaceAll(
-        [&](const ReturnStatement* return_statement) -> const ReturnStatement* {
-            auto* return_sem = sem.Get(return_statement);
-            if (auto mapping_fn_sym =
-                    entry_point_functions_to_truncate_functions.Find(return_sem->Function())) {
-                return b.Return(return_statement->source,
-                                b.Call(*mapping_fn_sym, ctx.Clone(return_statement->value)));
-            }
-            return nullptr;
-        });
+    ctx.ReplaceAll([&](const ReturnStatement* return_statement) -> const ReturnStatement* {
+        auto* return_sem = sem.Get(return_statement);
+        if (auto mapping_fn_sym =
+                entry_point_functions_to_truncate_functions.Find(return_sem->Function())) {
+            return b.Return(return_statement->source,
+                            b.Call(*mapping_fn_sym, ctx.Clone(return_statement->value)));
+        }
+        return nullptr;
+    });
 
     // Remove IO attributes from old shader IO struct which is not used as entry point output
     // anymore.
diff --git a/src/tint/ast/transform/truncate_interstage_variables_test.cc b/src/tint/ast/transform/truncate_interstage_variables_test.cc
index 14e546c..ecd691e 100644
--- a/src/tint/ast/transform/truncate_interstage_variables_test.cc
+++ b/src/tint/ast/transform/truncate_interstage_variables_test.cc
@@ -51,7 +51,7 @@
     {
         // Empty interstage_locations: truncate all interstage variables, should run
         TruncateInterstageVariables::Config cfg;
-        DataMap data;
+        Transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_TRUE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -61,7 +61,7 @@
         TruncateInterstageVariables::Config cfg;
         cfg.interstage_locations[0] = true;
         cfg.interstage_locations[2] = true;
-        DataMap data;
+        Transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_FALSE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -70,7 +70,7 @@
         // Partial interstage_locations are marked: should run
         TruncateInterstageVariables::Config cfg;
         cfg.interstage_locations[2] = true;
-        DataMap data;
+        Transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
         EXPECT_TRUE(ShouldRun<TruncateInterstageVariables>(src, data));
     }
@@ -91,7 +91,7 @@
     TruncateInterstageVariables::Config cfg;
     cfg.interstage_locations[2] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     EXPECT_FALSE(ShouldRun<TruncateInterstageVariables>(src, data));
@@ -109,7 +109,7 @@
 
     TruncateInterstageVariables::Config cfg;
     cfg.interstage_locations[0] = true;
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     data.Add<CanonicalizeEntryPointIO::Config>(CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
@@ -169,7 +169,7 @@
     // fragment has input at @location(1)
     cfg.interstage_locations[1] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -226,7 +226,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -279,7 +279,7 @@
 )";
 
         TruncateInterstageVariables::Config cfg;
-        DataMap data;
+        Transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
 
         auto got = Run<TruncateInterstageVariables>(src, data);
@@ -349,7 +349,7 @@
         cfg.interstage_locations[3] = true;
         cfg.interstage_locations[5] = true;
 
-        DataMap data;
+        Transform::DataMap data;
         data.Add<TruncateInterstageVariables::Config>(cfg);
 
         auto got = Run<TruncateInterstageVariables>(src, data);
@@ -426,7 +426,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -521,7 +521,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
@@ -586,7 +586,7 @@
     // fragment has input at @location(3)
     cfg.interstage_locations[3] = true;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<TruncateInterstageVariables::Config>(cfg);
 
     auto got = Run<TruncateInterstageVariables>(src, data);
diff --git a/src/tint/ast/transform/var_for_dynamic_index_test.cc b/src/tint/ast/transform/var_for_dynamic_index_test.cc
index 4cfb15e..34d781f 100644
--- a/src/tint/ast/transform/var_for_dynamic_index_test.cc
+++ b/src/tint/ast/transform/var_for_dynamic_index_test.cc
@@ -49,7 +49,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -73,7 +73,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -105,7 +105,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -138,7 +138,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -171,7 +171,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -204,7 +204,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -237,7 +237,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -277,7 +277,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -311,7 +311,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -364,7 +364,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -398,7 +398,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -451,7 +451,7 @@
 }
 )";
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -467,7 +467,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -483,7 +483,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -501,7 +501,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -519,7 +519,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -537,7 +537,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
@@ -553,7 +553,7 @@
 
     auto* expect = src;
 
-    DataMap data;
+    Transform::DataMap data;
     auto got = Run<VarForDynamicIndex>(src, data);
 
     EXPECT_EQ(expect, str(got));
diff --git a/src/tint/ast/transform/vertex_pulling_test.cc b/src/tint/ast/transform/vertex_pulling_test.cc
index eb46dd5..a0640e8 100644
--- a/src/tint/ast/transform/vertex_pulling_test.cc
+++ b/src/tint/ast/transform/vertex_pulling_test.cc
@@ -28,7 +28,7 @@
 
     auto* expect = "error: Vertex stage entry point not found";
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>();
     auto got = Run<VertexPulling>(src, data);
 
@@ -51,7 +51,7 @@
 
     VertexPulling::Config cfg;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -68,7 +68,7 @@
 
     VertexPulling::Config cfg;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -90,7 +90,7 @@
     VertexPulling::Config cfg;
     cfg.vertex_state = {{{15, VertexStepMode::kVertex, {{VertexFormat::kFloat32, 0, 0}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -118,7 +118,7 @@
 
     VertexPulling::Config cfg;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -154,7 +154,7 @@
     VertexPulling::Config cfg;
     cfg.vertex_state = {{{4, VertexStepMode::kVertex, {{VertexFormat::kFloat32, 0, 0}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -190,7 +190,7 @@
     VertexPulling::Config cfg;
     cfg.vertex_state = {{{4, VertexStepMode::kInstance, {{VertexFormat::kFloat32, 0, 0}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -227,7 +227,7 @@
     cfg.vertex_state = {{{4, VertexStepMode::kVertex, {{VertexFormat::kFloat32, 0, 0}}}}};
     cfg.pulling_group = 5;
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -272,7 +272,7 @@
     VertexPulling::Config cfg;
     cfg.vertex_state = {{{4, VertexStepMode::kVertex, {{VertexFormat::kFloat32, 0, 0}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -329,7 +329,7 @@
         },
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -407,7 +407,7 @@
         },
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -485,7 +485,7 @@
         },
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -560,7 +560,7 @@
         },
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -635,7 +635,7 @@
         },
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -676,7 +676,7 @@
                           VertexStepMode::kVertex,
                           {{VertexFormat::kFloat32, 0, 0}, {VertexFormat::kFloat32x4, 0, 1}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -729,7 +729,7 @@
         {16, VertexStepMode::kVertex, {{VertexFormat::kFloat32x4, 0, 2}}},
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -786,7 +786,7 @@
         {16, VertexStepMode::kVertex, {{VertexFormat::kFloat16x4, 0, 2}}},
     }};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -835,7 +835,7 @@
                           VertexStepMode::kVertex,
                           {{VertexFormat::kFloat32, 0, 0}, {VertexFormat::kFloat32x4, 0, 1}}}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, std::move(data));
 
@@ -905,7 +905,7 @@
                               {VertexFormat::kSint32x4, 64, 7},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -975,7 +975,7 @@
                               {VertexFormat::kUint32x4, 64, 7},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1069,7 +1069,7 @@
                               {VertexFormat::kFloat32x4, 64, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1167,7 +1167,7 @@
                               {VertexFormat::kFloat32x4, 64, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1237,7 +1237,7 @@
                               {VertexFormat::kSint32x4, 63, 7},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1307,7 +1307,7 @@
                               {VertexFormat::kUint32x4, 63, 7},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1401,7 +1401,7 @@
                               {VertexFormat::kFloat32x4, 63, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1499,7 +1499,7 @@
                               {VertexFormat::kFloat32x4, 63, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1577,7 +1577,7 @@
                               {VertexFormat::kSint32x3, 64, 9},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1655,7 +1655,7 @@
                               {VertexFormat::kUint32x3, 64, 9},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1757,7 +1757,7 @@
                               {VertexFormat::kFloat32x3, 64, 15},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1863,7 +1863,7 @@
                               {VertexFormat::kFloat32x3, 64, 15},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -1957,7 +1957,7 @@
                               {VertexFormat::kSint32x4, 64, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -2051,7 +2051,7 @@
                               {VertexFormat::kUint32x4, 64, 13},
                           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -2181,7 +2181,7 @@
               {VertexFormat::kFloat32x4, 64, 24}, {VertexFormat::kFloat32x4, 64, 25},
           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
@@ -2315,7 +2315,7 @@
               {VertexFormat::kFloat32x4, 64, 24}, {VertexFormat::kFloat32x4, 64, 25},
           }}}};
 
-    DataMap data;
+    Transform::DataMap data;
     data.Add<VertexPulling::Config>(cfg);
     auto got = Run<VertexPulling>(src, data);
 
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index 9f7fa36..bfdc41f 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -947,32 +947,30 @@
         /// Returns true on success, false on error (program will immediately exit)
         std::function<bool(tint::inspector::Inspector& inspector,
                            tint::transform::Manager& manager,
-                           tint::ast::transform::DataMap& inputs)>
+                           tint::transform::DataMap& inputs)>
             make;
     };
     std::vector<TransformFactory> transforms = {
         {"first_index_offset",
-         [](tint::inspector::Inspector&, tint::transform::Manager& m,
-            tint::ast::transform::DataMap& i) {
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap& i) {
              i.Add<tint::ast::transform::FirstIndexOffset::BindingPoint>(0, 0);
              m.Add<tint::ast::transform::FirstIndexOffset>();
              return true;
          }},
         {"renamer",
-         [](tint::inspector::Inspector&, tint::transform::Manager& m,
-            tint::ast::transform::DataMap&) {
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap&) {
              m.Add<tint::ast::transform::Renamer>();
              return true;
          }},
         {"robustness",
          [&](tint::inspector::Inspector&, tint::transform::Manager&,
-             tint::ast::transform::DataMap&) {  // enabled via writer option
+             tint::transform::DataMap&) {  // enabled via writer option
              options.enable_robustness = true;
              return true;
          }},
         {"substitute_override",
          [&](tint::inspector::Inspector& inspector, tint::transform::Manager& m,
-             tint::ast::transform::DataMap& i) {
+             tint::transform::DataMap& i) {
              tint::ast::transform::SubstituteOverride::Config cfg;
 
              std::unordered_map<tint::OverrideId, double> values;
@@ -1098,7 +1096,7 @@
     }
 
     tint::transform::Manager transform_manager;
-    tint::ast::transform::DataMap transform_inputs;
+    tint::transform::DataMap transform_inputs;
 
     // Renaming must always come first
     switch (options.format) {
@@ -1173,14 +1171,15 @@
         transform_inputs.Add<tint::ast::transform::SingleEntryPoint::Config>(options.ep_name);
     }
 
-    auto out = transform_manager.Run(program.get(), std::move(transform_inputs));
-    if (!out.program.IsValid()) {
-        tint::cmd::PrintWGSL(std::cerr, out.program);
-        diag_formatter.format(out.program.Diagnostics(), diag_printer.get());
+    tint::transform::DataMap outputs;
+    auto out = transform_manager.Run(program.get(), std::move(transform_inputs), outputs);
+    if (!out.IsValid()) {
+        tint::cmd::PrintWGSL(std::cerr, out);
+        diag_formatter.format(out.Diagnostics(), diag_printer.get());
         return 1;
     }
 
-    *program = std::move(out.program);
+    *program = std::move(out);
 
     bool success = false;
     switch (options.format) {
diff --git a/src/tint/fuzzers/shuffle_transform.cc b/src/tint/fuzzers/shuffle_transform.cc
index 9a075c2..6d0dfd7 100644
--- a/src/tint/fuzzers/shuffle_transform.cc
+++ b/src/tint/fuzzers/shuffle_transform.cc
@@ -24,8 +24,8 @@
 ShuffleTransform::ShuffleTransform(size_t seed) : seed_(seed) {}
 
 ast::transform::Transform::ApplyResult ShuffleTransform::Apply(const Program* src,
-                                                               const ast::transform::DataMap&,
-                                                               ast::transform::DataMap&) const {
+                                                               const transform::DataMap&,
+                                                               transform::DataMap&) const {
     ProgramBuilder b;
     CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
 
diff --git a/src/tint/fuzzers/shuffle_transform.h b/src/tint/fuzzers/shuffle_transform.h
index 6edbfd7..8a5ceeb 100644
--- a/src/tint/fuzzers/shuffle_transform.h
+++ b/src/tint/fuzzers/shuffle_transform.h
@@ -28,8 +28,8 @@
 
     /// @copydoc ast::transform::Transform::Apply
     ApplyResult Apply(const Program* program,
-                      const ast::transform::DataMap& inputs,
-                      ast::transform::DataMap& outputs) const override;
+                      const transform::DataMap& inputs,
+                      transform::DataMap& outputs) const override;
 
   private:
     size_t seed_;
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 122ab87..5595d10 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -205,10 +205,10 @@
     diagnostics_ = program.Diagnostics();
 
     auto validate_program = [&](auto& out) {
-        if (!out.program.IsValid()) {
+        if (!out.IsValid()) {
             // Transforms can produce error messages for bad input.
             // Catch ICEs and errors from non transform systems.
-            for (const auto& diag : out.program.Diagnostics()) {
+            for (const auto& diag : out.Diagnostics()) {
                 if (diag.severity > diag::Severity::Error ||
                     diag.system != diag::System::Transform) {
                     VALIDITY_ERROR(program.Diagnostics(),
@@ -219,13 +219,14 @@
             return 0;
         }
 
-        program = std::move(out.program);
+        program = std::move(out);
         RunInspector(&program);
         return 1;
     };
 
     if (transform_manager_) {
-        auto out = transform_manager_->Run(&program, *transform_inputs_);
+        transform::DataMap outputs;
+        auto out = transform_manager_->Run(&program, *transform_inputs_, outputs);
         if (!validate_program(out)) {
             return 0;
         }
@@ -248,13 +249,14 @@
         }
 
         if (!cfg.map.empty()) {
-            ast::transform::DataMap override_data;
+            transform::DataMap override_data;
             override_data.Add<ast::transform::SubstituteOverride::Config>(cfg);
 
             transform::Manager mgr;
             mgr.append(std::make_unique<ast::transform::SubstituteOverride>());
 
-            auto out = mgr.Run(&program, override_data);
+            transform::DataMap outputs;
+            auto out = mgr.Run(&program, override_data, outputs);
             if (!validate_program(out)) {
                 return 0;
             }
diff --git a/src/tint/fuzzers/tint_common_fuzzer.h b/src/tint/fuzzers/tint_common_fuzzer.h
index fdf50d5..c1cb656 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.h
+++ b/src/tint/fuzzers/tint_common_fuzzer.h
@@ -63,7 +63,7 @@
 
     /// @param tm manager for transforms to run
     /// @param inputs data for transforms to run
-    void SetTransformManager(transform::Manager* tm, ast::transform::DataMap* inputs) {
+    void SetTransformManager(transform::Manager* tm, transform::DataMap* inputs) {
         assert((!tm || inputs) && "DataMap must be !nullptr if Manager !nullptr");
         transform_manager_ = tm;
         transform_inputs_ = inputs;
@@ -121,7 +121,7 @@
     InputFormat input_;
     OutputFormat output_;
     transform::Manager* transform_manager_ = nullptr;
-    ast::transform::DataMap* transform_inputs_ = nullptr;
+    transform::DataMap* transform_inputs_ = nullptr;
     bool dump_input_ = false;
     tint::diag::List diagnostics_;
     bool enforce_validity = false;
diff --git a/src/tint/fuzzers/tint_reader_writer_fuzzer.h b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
index 94c4fc4..b5d0a95 100644
--- a/src/tint/fuzzers/tint_reader_writer_fuzzer.h
+++ b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
@@ -38,7 +38,7 @@
     /// invoked.
     /// @param tm manager for transforms to run
     /// @param inputs data for transforms to run
-    void SetTransformManager(transform::Manager* tm, ast::transform::DataMap* inputs) {
+    void SetTransformManager(transform::Manager* tm, transform::DataMap* inputs) {
         tm_set_ = true;
         CommonFuzzer::SetTransformManager(tm, inputs);
     }
diff --git a/src/tint/fuzzers/transform_builder.h b/src/tint/fuzzers/transform_builder.h
index 037568f..a8bcb6d 100644
--- a/src/tint/fuzzers/transform_builder.h
+++ b/src/tint/fuzzers/transform_builder.h
@@ -48,7 +48,7 @@
     transform::Manager* manager() { return &manager_; }
 
     /// @returns data for transforms
-    ast::transform::DataMap* data_map() { return &data_map_; }
+    transform::DataMap* data_map() { return &data_map_; }
 
     /// Adds a transform and needed data to |manager_| and |data_map_|.
     /// @tparam T - A class that inherits from ast::transform::Transform and has an
@@ -73,7 +73,7 @@
   private:
     DataBuilder builder_;
     transform::Manager manager_;
-    ast::transform::DataMap data_map_;
+    transform::DataMap data_map_;
 
     DataBuilder* builder() { return &builder_; }
 
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 7baf0cd..2968d28 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 
@@ -23,10 +24,10 @@
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, CreateAnd) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                     b.builder.Constant(2_i));
+    const auto* inst = b.And(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kAnd);
@@ -45,10 +46,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateOr) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Or(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                    b.builder.Constant(2_i));
+    const auto* inst = b.Or(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kOr);
@@ -65,10 +66,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateXor) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Xor(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                     b.builder.Constant(2_i));
+    const auto* inst = b.Xor(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kXor);
@@ -85,10 +86,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateEqual) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Equal(b.builder.ir.types.Get<type::Bool>(),
-                                       b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.Equal(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kEqual);
@@ -105,10 +106,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateNotEqual) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.NotEqual(b.builder.ir.types.Get<type::Bool>(),
-                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.NotEqual(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kNotEqual);
@@ -125,10 +126,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateLessThan) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.LessThan(b.builder.ir.types.Get<type::Bool>(),
-                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.LessThan(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kLessThan);
@@ -145,10 +146,11 @@
 }
 
 TEST_F(IR_InstructionTest, CreateGreaterThan) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.GreaterThan(b.builder.ir.types.Get<type::Bool>(),
-                                             b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst =
+        b.GreaterThan(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kGreaterThan);
@@ -165,10 +167,11 @@
 }
 
 TEST_F(IR_InstructionTest, CreateLessThanEqual) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.LessThanEqual(b.builder.ir.types.Get<type::Bool>(),
-                                               b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst =
+        b.LessThanEqual(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kLessThanEqual);
@@ -185,10 +188,11 @@
 }
 
 TEST_F(IR_InstructionTest, CreateGreaterThanEqual) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.GreaterThanEqual(b.builder.ir.types.Get<type::Bool>(),
-                                                  b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst =
+        b.GreaterThanEqual(b.ir.types.Get<type::Bool>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kGreaterThanEqual);
@@ -205,9 +209,9 @@
 }
 
 TEST_F(IR_InstructionTest, CreateNot) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Not(b.builder.ir.types.Get<type::Bool>(), b.builder.Constant(true));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Not(b.ir.types.Get<type::Bool>(), b.Constant(true));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kEqual);
@@ -224,10 +228,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateShiftLeft) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.ShiftLeft(b.builder.ir.types.Get<type::I32>(),
-                                           b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.ShiftLeft(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kShiftLeft);
@@ -244,10 +248,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateShiftRight) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.ShiftRight(b.builder.ir.types.Get<type::I32>(),
-                                            b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.ShiftRight(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kShiftRight);
@@ -264,10 +268,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateAdd) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Add(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                     b.builder.Constant(2_i));
+    const auto* inst = b.Add(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kAdd);
@@ -284,10 +288,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateSubtract) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Subtract(b.builder.ir.types.Get<type::I32>(),
-                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.Subtract(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kSubtract);
@@ -304,10 +308,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateMultiply) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Multiply(b.builder.ir.types.Get<type::I32>(),
-                                          b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.Multiply(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kMultiply);
@@ -324,10 +328,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateDivide) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Divide(b.builder.ir.types.Get<type::I32>(),
-                                        b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.Divide(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kDivide);
@@ -344,10 +348,10 @@
 }
 
 TEST_F(IR_InstructionTest, CreateModulo) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Modulo(b.builder.ir.types.Get<type::I32>(),
-                                        b.builder.Constant(4_i), b.builder.Constant(2_i));
+    const auto* inst = b.Modulo(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     ASSERT_TRUE(inst->Is<Binary>());
     EXPECT_EQ(inst->kind, Binary::Kind::kModulo);
@@ -364,9 +368,9 @@
 }
 
 TEST_F(IR_InstructionTest, Binary_Usage) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i),
-                                     b.builder.Constant(2_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.And(b.ir.types.Get<type::I32>(), b.Constant(4_i), b.Constant(2_i));
 
     EXPECT_EQ(inst->kind, Binary::Kind::kAnd);
 
@@ -380,9 +384,10 @@
 }
 
 TEST_F(IR_InstructionTest, Binary_Usage_DuplicateValue) {
-    auto& b = CreateEmptyBuilder();
-    auto val = b.builder.Constant(4_i);
-    const auto* inst = b.builder.And(b.builder.ir.types.Get<type::I32>(), val, val);
+    Module mod;
+    Builder b{mod};
+    auto val = b.Constant(4_i);
+    const auto* inst = b.And(b.ir.types.Get<type::I32>(), val, val);
 
     EXPECT_EQ(inst->kind, Binary::Kind::kAnd);
     ASSERT_EQ(inst->LHS(), inst->RHS());
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index dfe7461..e78b734 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
+#include "src/tint/ir/constant.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 
@@ -23,9 +25,9 @@
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, Bitcast) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Bitcast(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<ir::Bitcast>());
     ASSERT_NE(inst->Type(), nullptr);
@@ -38,9 +40,9 @@
 }
 
 TEST_F(IR_InstructionTest, Bitcast_Usage) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Bitcast(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Bitcast(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_EQ(inst->args.Length(), 1u);
     ASSERT_NE(inst->args[0], nullptr);
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index fe350cd..d62bb43 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -20,9 +20,7 @@
 
 namespace tint::ir {
 
-Builder::Builder() {}
-
-Builder::Builder(Module&& mod) : ir(std::move(mod)) {}
+Builder::Builder(Module& mod) : ir(mod) {}
 
 Builder::~Builder() = default;
 
@@ -49,8 +47,13 @@
     return ir.flow_nodes.Create<FunctionTerminator>();
 }
 
-Function* Builder::CreateFunction() {
-    auto* ir_func = ir.flow_nodes.Create<Function>();
+Function* Builder::CreateFunction(Symbol name,
+                                  type::Type* return_type,
+                                  Function::PipelineStage stage,
+                                  std::optional<std::array<uint32_t, 3>> wg_size) {
+    TINT_ASSERT(IR, return_type);
+
+    auto* ir_func = ir.flow_nodes.Create<Function>(name, return_type, stage, wg_size);
     ir_func->start_target = CreateBlock();
     ir_func->end_target = CreateFunctionTerminator();
 
@@ -60,8 +63,10 @@
     return ir_func;
 }
 
-If* Builder::CreateIf() {
-    auto* ir_if = ir.flow_nodes.Create<If>();
+If* Builder::CreateIf(Value* condition) {
+    TINT_ASSERT(IR, condition);
+
+    auto* ir_if = ir.flow_nodes.Create<If>(condition);
     ir_if->true_.target = CreateBlock();
     ir_if->false_.target = CreateBlock();
     ir_if->merge.target = CreateBlock();
@@ -85,8 +90,8 @@
     return ir_loop;
 }
 
-Switch* Builder::CreateSwitch() {
-    auto* ir_switch = ir.flow_nodes.Create<Switch>();
+Switch* Builder::CreateSwitch(Value* condition) {
+    auto* ir_switch = ir.flow_nodes.Create<Switch>(condition);
     ir_switch->merge.target = CreateBlock();
     return ir_switch;
 }
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 87a256b..b4a2a81 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -50,10 +50,8 @@
 class Builder {
   public:
     /// Constructor
-    Builder();
-    /// Constructor
     /// @param mod the ir::Module to wrap with this builder
-    explicit Builder(Module&& mod);
+    explicit Builder(Module& mod);
     /// Destructor
     ~Builder();
 
@@ -67,20 +65,29 @@
     FunctionTerminator* CreateFunctionTerminator();
 
     /// Creates a function flow node
+    /// @param name the function name
+    /// @param return_type the function return type
+    /// @param stage the function stage
+    /// @param wg_size the workgroup_size
     /// @returns the flow node
-    Function* CreateFunction();
+    Function* CreateFunction(Symbol name,
+                             type::Type* return_type,
+                             Function::PipelineStage stage = Function::PipelineStage::kUndefined,
+                             std::optional<std::array<uint32_t, 3>> wg_size = {});
 
     /// Creates an if flow node
+    /// @param condition the if condition
     /// @returns the flow node
-    If* CreateIf();
+    If* CreateIf(Value* condition);
 
     /// Creates a loop flow node
     /// @returns the flow node
     Loop* CreateLoop();
 
     /// Creates a switch flow node
+    /// @param condition the switch condition
     /// @returns the flow node
-    Switch* CreateSwitch();
+    Switch* CreateSwitch(Value* condition);
 
     /// Creates a case flow node for the given case branch.
     /// @param s the switch to create the case into
@@ -92,7 +99,7 @@
     /// @param from the block to branch from
     /// @param to the node to branch too
     /// @param args arguments to the branch
-    void Branch(Block* from, FlowNode* to, utils::VectorRef<Value*> args);
+    void Branch(Block* from, FlowNode* to, utils::VectorRef<Value*> args = {});
 
     /// Creates a constant::Value
     /// @param args the arguments
@@ -363,7 +370,7 @@
     ir::Block* CreateRootBlockIfNeeded();
 
     /// The IR module.
-    Module ir;
+    Module& ir;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
deleted file mode 100644
index bf6a0e7..0000000
--- a/src/tint/ir/builder_impl.cc
+++ /dev/null
@@ -1,1145 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/builder_impl.h"
-
-#include <iostream>
-
-#include "src/tint/ast/alias.h"
-#include "src/tint/ast/assignment_statement.h"
-#include "src/tint/ast/binary_expression.h"
-#include "src/tint/ast/bitcast_expression.h"
-#include "src/tint/ast/block_statement.h"
-#include "src/tint/ast/bool_literal_expression.h"
-#include "src/tint/ast/break_if_statement.h"
-#include "src/tint/ast/break_statement.h"
-#include "src/tint/ast/call_expression.h"
-#include "src/tint/ast/call_statement.h"
-#include "src/tint/ast/compound_assignment_statement.h"
-#include "src/tint/ast/const.h"
-#include "src/tint/ast/const_assert.h"
-#include "src/tint/ast/continue_statement.h"
-#include "src/tint/ast/discard_statement.h"
-#include "src/tint/ast/float_literal_expression.h"
-#include "src/tint/ast/for_loop_statement.h"
-#include "src/tint/ast/function.h"
-#include "src/tint/ast/id_attribute.h"
-#include "src/tint/ast/identifier.h"
-#include "src/tint/ast/identifier_expression.h"
-#include "src/tint/ast/if_statement.h"
-#include "src/tint/ast/int_literal_expression.h"
-#include "src/tint/ast/invariant_attribute.h"
-#include "src/tint/ast/let.h"
-#include "src/tint/ast/literal_expression.h"
-#include "src/tint/ast/loop_statement.h"
-#include "src/tint/ast/override.h"
-#include "src/tint/ast/return_statement.h"
-#include "src/tint/ast/statement.h"
-#include "src/tint/ast/struct.h"
-#include "src/tint/ast/struct_member_align_attribute.h"
-#include "src/tint/ast/struct_member_size_attribute.h"
-#include "src/tint/ast/switch_statement.h"
-#include "src/tint/ast/templated_identifier.h"
-#include "src/tint/ast/unary_op_expression.h"
-#include "src/tint/ast/var.h"
-#include "src/tint/ast/variable_decl_statement.h"
-#include "src/tint/ast/while_statement.h"
-#include "src/tint/ir/function.h"
-#include "src/tint/ir/if.h"
-#include "src/tint/ir/loop.h"
-#include "src/tint/ir/module.h"
-#include "src/tint/ir/store.h"
-#include "src/tint/ir/switch.h"
-#include "src/tint/ir/value.h"
-#include "src/tint/program.h"
-#include "src/tint/sem/builtin.h"
-#include "src/tint/sem/call.h"
-#include "src/tint/sem/function.h"
-#include "src/tint/sem/materialize.h"
-#include "src/tint/sem/module.h"
-#include "src/tint/sem/switch_statement.h"
-#include "src/tint/sem/value_constructor.h"
-#include "src/tint/sem/value_conversion.h"
-#include "src/tint/sem/value_expression.h"
-#include "src/tint/sem/variable.h"
-#include "src/tint/switch.h"
-#include "src/tint/type/void.h"
-#include "src/tint/utils/defer.h"
-#include "src/tint/utils/scoped_assignment.h"
-
-namespace tint::ir {
-namespace {
-
-using ResultType = utils::Result<Module>;
-
-class FlowStackScope {
-  public:
-    FlowStackScope(BuilderImpl* impl, FlowNode* node) : impl_(impl) {
-        impl_->flow_stack.Push(node);
-    }
-
-    ~FlowStackScope() { impl_->flow_stack.Pop(); }
-
-  private:
-    BuilderImpl* impl_;
-};
-
-bool IsBranched(const Block* b) {
-    return b->branch.target != nullptr;
-}
-
-bool IsConnected(const FlowNode* b) {
-    // Function is always connected as it's the start.
-    if (b->Is<ir::Function>()) {
-        return true;
-    }
-
-    for (auto* parent : b->inbound_branches) {
-        if (IsConnected(parent)) {
-            return true;
-        }
-    }
-    // Getting here means all the incoming branches are disconnected.
-    return false;
-}
-
-}  // namespace
-
-BuilderImpl::BuilderImpl(const Program* program)
-    : program_(program),
-      clone_ctx_{
-          type::CloneContext{{&program->Symbols()}, {&builder.ir.symbols, &builder.ir.types}},
-          {&builder.ir.constants}} {}
-
-BuilderImpl::~BuilderImpl() = default;
-
-void BuilderImpl::add_error(const Source& s, const std::string& err) {
-    diagnostics_.add_error(tint::diag::System::IR, err, s);
-}
-
-void BuilderImpl::BranchTo(FlowNode* node, utils::VectorRef<Value*> args) {
-    TINT_ASSERT(IR, current_flow_block);
-    TINT_ASSERT(IR, !IsBranched(current_flow_block));
-
-    builder.Branch(current_flow_block, node, args);
-    current_flow_block = nullptr;
-}
-
-void BuilderImpl::BranchToIfNeeded(FlowNode* node) {
-    if (!current_flow_block || IsBranched(current_flow_block)) {
-        return;
-    }
-    BranchTo(node);
-}
-
-FlowNode* BuilderImpl::FindEnclosingControl(ControlFlags flags) {
-    for (auto it = flow_stack.rbegin(); it != flow_stack.rend(); ++it) {
-        if ((*it)->Is<Loop>()) {
-            return *it;
-        }
-        if (flags == ControlFlags::kExcludeSwitch) {
-            continue;
-        }
-        if ((*it)->Is<Switch>()) {
-            return *it;
-        }
-    }
-    return nullptr;
-}
-
-Symbol BuilderImpl::CloneSymbol(Symbol sym) const {
-    return clone_ctx_.type_ctx.dst.st->Register(sym.Name());
-}
-
-ResultType BuilderImpl::Build() {
-    auto* sem = program_->Sem().Module();
-
-    for (auto* decl : sem->DependencyOrderedDeclarations()) {
-        tint::Switch(
-            decl,  //
-            [&](const ast::Struct*) {
-                // Will be encoded into the `type::Struct` when used. We will then hoist all
-                // used structs up to module scope when converting IR.
-            },
-            [&](const ast::Alias*) {
-                // Folded away and doesn't appear in the IR.
-            },
-            [&](const ast::Variable* var) {
-                // Setup the current flow node to be the root block for the module. The builder will
-                // handle creating it if it doesn't exist already.
-                TINT_SCOPED_ASSIGNMENT(current_flow_block, builder.CreateRootBlockIfNeeded());
-                EmitVariable(var);
-            },
-            [&](const ast::Function* func) { EmitFunction(func); },
-            // [&](const ast::Enable*) {
-            // TODO(dsinclair): Implement? I think these need to be passed along so further stages
-            // know what is enabled.
-            // },
-            [&](const ast::ConstAssert*) {
-                // Evaluated by the resolver, drop from the IR.
-            },
-            [&](Default) {
-                add_error(decl->source, "unknown type: " + std::string(decl->TypeInfo().name));
-            });
-    }
-    if (!diagnostics_.empty()) {
-        return utils::Failure;
-    }
-
-    return ResultType{std::move(builder.ir)};
-}
-
-void BuilderImpl::EmitFunction(const ast::Function* ast_func) {
-    // The flow stack should have been emptied when the previous function finished building.
-    TINT_ASSERT(IR, flow_stack.IsEmpty());
-
-    auto* ir_func = builder.CreateFunction();
-    ir_func->name = CloneSymbol(ast_func->name->symbol);
-    current_function_ = ir_func;
-    builder.ir.functions.Push(ir_func);
-
-    ast_to_flow_[ast_func] = ir_func;
-
-    const auto* sem = program_->Sem().Get(ast_func);
-    if (ast_func->IsEntryPoint()) {
-        builder.ir.entry_points.Push(ir_func);
-
-        switch (ast_func->PipelineStage()) {
-            case ast::PipelineStage::kVertex:
-                ir_func->pipeline_stage = Function::PipelineStage::kVertex;
-                break;
-            case ast::PipelineStage::kFragment:
-                ir_func->pipeline_stage = Function::PipelineStage::kFragment;
-                break;
-            case ast::PipelineStage::kCompute: {
-                ir_func->pipeline_stage = Function::PipelineStage::kCompute;
-
-                auto wg_size = sem->WorkgroupSize();
-                ir_func->workgroup_size = {
-                    wg_size[0].value(),
-                    wg_size[1].value_or(1),
-                    wg_size[2].value_or(1),
-                };
-                break;
-            }
-            default: {
-                TINT_ICE(IR, diagnostics_) << "Invalid pipeline stage";
-                return;
-            }
-        }
-
-        for (auto* attr : ast_func->return_type_attributes) {
-            tint::Switch(
-                attr,  //
-                [&](const ast::LocationAttribute*) {
-                    ir_func->return_attributes.Push(Function::ReturnAttribute::kLocation);
-                },
-                [&](const ast::InvariantAttribute*) {
-                    ir_func->return_attributes.Push(Function::ReturnAttribute::kInvariant);
-                },
-                [&](const ast::BuiltinAttribute* b) {
-                    if (auto* ident_sem =
-                            program_->Sem()
-                                .Get(b)
-                                ->As<sem::BuiltinEnumExpression<builtin::BuiltinValue>>()) {
-                        switch (ident_sem->Value()) {
-                            case builtin::BuiltinValue::kPosition:
-                                ir_func->return_attributes.Push(
-                                    Function::ReturnAttribute::kPosition);
-                                break;
-                            case builtin::BuiltinValue::kFragDepth:
-                                ir_func->return_attributes.Push(
-                                    Function::ReturnAttribute::kFragDepth);
-                                break;
-                            case builtin::BuiltinValue::kSampleMask:
-                                ir_func->return_attributes.Push(
-                                    Function::ReturnAttribute::kSampleMask);
-                                break;
-                            default:
-                                TINT_ICE(IR, diagnostics_)
-                                    << "Unknown builtin value in return attributes "
-                                    << ident_sem->Value();
-                                return;
-                        }
-                    } else {
-                        TINT_ICE(IR, diagnostics_) << "Builtin attribute sem invalid";
-                        return;
-                    }
-                });
-        }
-    }
-    ir_func->return_type = sem->ReturnType()->Clone(clone_ctx_.type_ctx);
-    ir_func->return_location = sem->ReturnLocation();
-
-    {
-        FlowStackScope scope(this, ir_func);
-
-        current_flow_block = ir_func->start_target;
-        EmitBlock(ast_func->body);
-
-        // TODO(dsinclair): Store return type and attributes
-        // TODO(dsinclair): Store parameters
-
-        // If the branch target has already been set then a `return` was called. Only set in the
-        // case where `return` wasn't called.
-        BranchToIfNeeded(current_function_->end_target);
-    }
-
-    TINT_ASSERT(IR, flow_stack.IsEmpty());
-    current_flow_block = nullptr;
-    current_function_ = nullptr;
-}
-
-void BuilderImpl::EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
-    for (auto* s : stmts) {
-        EmitStatement(s);
-
-        // If the current flow block has a branch target then the rest of the statements in this
-        // block are dead code. Skip them.
-        if (!current_flow_block || IsBranched(current_flow_block)) {
-            break;
-        }
-    }
-}
-
-void BuilderImpl::EmitStatement(const ast::Statement* stmt) {
-    tint::Switch(
-        stmt,  //
-        [&](const ast::AssignmentStatement* a) { EmitAssignment(a); },
-        [&](const ast::BlockStatement* b) { EmitBlock(b); },
-        [&](const ast::BreakStatement* b) { EmitBreak(b); },
-        [&](const ast::BreakIfStatement* b) { EmitBreakIf(b); },
-        [&](const ast::CallStatement* c) { EmitCall(c); },
-        [&](const ast::CompoundAssignmentStatement* c) { EmitCompoundAssignment(c); },
-        [&](const ast::ContinueStatement* c) { EmitContinue(c); },
-        [&](const ast::DiscardStatement* d) { EmitDiscard(d); },
-        [&](const ast::IfStatement* i) { EmitIf(i); },
-        [&](const ast::LoopStatement* l) { EmitLoop(l); },
-        [&](const ast::ForLoopStatement* l) { EmitForLoop(l); },
-        [&](const ast::WhileStatement* l) { EmitWhile(l); },
-        [&](const ast::ReturnStatement* r) { EmitReturn(r); },
-        [&](const ast::SwitchStatement* s) { EmitSwitch(s); },
-        [&](const ast::VariableDeclStatement* v) { EmitVariable(v->variable); },
-        [&](const ast::ConstAssert*) {
-            // Not emitted
-        },
-        [&](Default) {
-            add_error(stmt->source,
-                      "unknown statement type: " + std::string(stmt->TypeInfo().name));
-        });
-}
-
-void BuilderImpl::EmitAssignment(const ast::AssignmentStatement* stmt) {
-    auto lhs = EmitExpression(stmt->lhs);
-    if (!lhs) {
-        return;
-    }
-
-    auto rhs = EmitExpression(stmt->rhs);
-    if (!rhs) {
-        return;
-    }
-    auto store = builder.Store(lhs.Get(), rhs.Get());
-    current_flow_block->instructions.Push(store);
-}
-
-void BuilderImpl::EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt) {
-    auto lhs = EmitExpression(stmt->lhs);
-    if (!lhs) {
-        return;
-    }
-
-    auto rhs = EmitExpression(stmt->rhs);
-    if (!rhs) {
-        return;
-    }
-
-    auto* ty = lhs.Get()->Type();
-    Binary* inst = nullptr;
-    switch (stmt->op) {
-        case ast::BinaryOp::kAnd:
-            inst = builder.And(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kOr:
-            inst = builder.Or(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kXor:
-            inst = builder.Xor(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kShiftLeft:
-            inst = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kShiftRight:
-            inst = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kAdd:
-            inst = builder.Add(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kSubtract:
-            inst = builder.Subtract(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kMultiply:
-            inst = builder.Multiply(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kDivide:
-            inst = builder.Divide(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kModulo:
-            inst = builder.Modulo(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kLessThanEqual:
-        case ast::BinaryOp::kGreaterThanEqual:
-        case ast::BinaryOp::kGreaterThan:
-        case ast::BinaryOp::kLessThan:
-        case ast::BinaryOp::kNotEqual:
-        case ast::BinaryOp::kEqual:
-        case ast::BinaryOp::kLogicalAnd:
-        case ast::BinaryOp::kLogicalOr:
-            TINT_ICE(IR, diagnostics_) << "invalid compound assignment";
-            return;
-        case ast::BinaryOp::kNone:
-            TINT_ICE(IR, diagnostics_) << "missing binary operand type";
-            return;
-    }
-    current_flow_block->instructions.Push(inst);
-
-    auto store = builder.Store(lhs.Get(), inst);
-    current_flow_block->instructions.Push(store);
-}
-
-void BuilderImpl::EmitBlock(const ast::BlockStatement* block) {
-    scopes_.Push();
-    TINT_DEFER(scopes_.Pop());
-
-    // Note, this doesn't need to emit a Block as the current block flow node should be sufficient
-    // as the blocks all get flattened. Each flow control node will inject the basic blocks it
-    // requires.
-    EmitStatements(block->statements);
-}
-
-void BuilderImpl::EmitIf(const ast::IfStatement* stmt) {
-    auto* if_node = builder.CreateIf();
-
-    // Emit the if condition into the end of the preceding block
-    auto reg = EmitExpression(stmt->condition);
-    if (!reg) {
-        return;
-    }
-    if_node->condition = reg.Get();
-
-    BranchTo(if_node);
-
-    ast_to_flow_[stmt] = if_node;
-
-    {
-        FlowStackScope scope(this, if_node);
-
-        current_flow_block = if_node->true_.target->As<Block>();
-        EmitBlock(stmt->body);
-
-        // If the true branch did not execute control flow, then go to the merge target
-        BranchToIfNeeded(if_node->merge.target);
-
-        current_flow_block = if_node->false_.target->As<Block>();
-        if (stmt->else_statement) {
-            EmitStatement(stmt->else_statement);
-        }
-
-        // If the false branch did not execute control flow, then go to the merge target
-        BranchToIfNeeded(if_node->merge.target);
-    }
-    current_flow_block = nullptr;
-
-    // If both branches went somewhere, then they both returned, continued or broke. So, there is no
-    // need for the if merge-block and there is nothing to branch to the merge block anyway.
-    if (IsConnected(if_node->merge.target)) {
-        current_flow_block = if_node->merge.target->As<Block>();
-    }
-}
-
-void BuilderImpl::EmitLoop(const ast::LoopStatement* stmt) {
-    auto* loop_node = builder.CreateLoop();
-
-    BranchTo(loop_node);
-
-    ast_to_flow_[stmt] = loop_node;
-
-    {
-        FlowStackScope scope(this, loop_node);
-
-        current_flow_block = loop_node->start.target->As<Block>();
-        EmitBlock(stmt->body);
-
-        // The current block didn't `break`, `return` or `continue`, go to the continuing block.
-        BranchToIfNeeded(loop_node->continuing.target);
-
-        current_flow_block = loop_node->continuing.target->As<Block>();
-        if (stmt->continuing) {
-            EmitBlock(stmt->continuing);
-        }
-
-        // Branch back to the start node if the continue target didn't branch out already
-        BranchToIfNeeded(loop_node->start.target);
-    }
-
-    // The loop merge can get disconnected if the loop returns directly, or the continuing target
-    // branches, eventually, to the merge, but nothing branched to the continuing target.
-    current_flow_block = loop_node->merge.target->As<Block>();
-    if (!IsConnected(loop_node->merge.target)) {
-        current_flow_block = nullptr;
-    }
-}
-
-void BuilderImpl::EmitWhile(const ast::WhileStatement* stmt) {
-    auto* loop_node = builder.CreateLoop();
-    // Continue is always empty, just go back to the start
-    TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
-    builder.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
-                   utils::Empty);
-
-    BranchTo(loop_node);
-
-    ast_to_flow_[stmt] = loop_node;
-
-    {
-        FlowStackScope scope(this, loop_node);
-
-        current_flow_block = loop_node->start.target->As<Block>();
-
-        // Emit the while condition into the start target of the loop
-        auto reg = EmitExpression(stmt->condition);
-        if (!reg) {
-            return;
-        }
-
-        // Create an `if (cond) {} else {break;}` control flow
-        auto* if_node = builder.CreateIf();
-        TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
-        builder.Branch(if_node->true_.target->As<Block>(), if_node->merge.target, utils::Empty);
-
-        TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
-        builder.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target, utils::Empty);
-        if_node->condition = reg.Get();
-
-        BranchTo(if_node);
-
-        current_flow_block = if_node->merge.target->As<Block>();
-        EmitBlock(stmt->body);
-
-        BranchToIfNeeded(loop_node->continuing.target);
-    }
-    // The while loop always has a path to the merge target as the break statement comes before
-    // anything inside the loop.
-    current_flow_block = loop_node->merge.target->As<Block>();
-}
-
-void BuilderImpl::EmitForLoop(const ast::ForLoopStatement* stmt) {
-    auto* loop_node = builder.CreateLoop();
-    TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
-    builder.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
-                   utils::Empty);
-
-    // Make sure the initializer ends up in a contained scope
-    scopes_.Push();
-    TINT_DEFER(scopes_.Pop());
-
-    if (stmt->initializer) {
-        // Emit the for initializer before branching to the loop
-        EmitStatement(stmt->initializer);
-    }
-
-    BranchTo(loop_node);
-
-    ast_to_flow_[stmt] = loop_node;
-
-    {
-        FlowStackScope scope(this, loop_node);
-
-        current_flow_block = loop_node->start.target->As<Block>();
-
-        if (stmt->condition) {
-            // Emit the condition into the target target of the loop
-            auto reg = EmitExpression(stmt->condition);
-            if (!reg) {
-                return;
-            }
-
-            // Create an `if (cond) {} else {break;}` control flow
-            auto* if_node = builder.CreateIf();
-            TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
-            builder.Branch(if_node->true_.target->As<Block>(), if_node->merge.target, utils::Empty);
-
-            TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
-            builder.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target,
-                           utils::Empty);
-            if_node->condition = reg.Get();
-
-            BranchTo(if_node);
-            current_flow_block = if_node->merge.target->As<Block>();
-        }
-
-        EmitBlock(stmt->body);
-        BranchToIfNeeded(loop_node->continuing.target);
-
-        if (stmt->continuing) {
-            current_flow_block = loop_node->continuing.target->As<Block>();
-            EmitStatement(stmt->continuing);
-        }
-    }
-
-    // The while loop always has a path to the merge target as the break statement comes before
-    // anything inside the loop.
-    current_flow_block = loop_node->merge.target->As<Block>();
-}
-
-void BuilderImpl::EmitSwitch(const ast::SwitchStatement* stmt) {
-    auto* switch_node = builder.CreateSwitch();
-
-    // Emit the condition into the preceding block
-    auto reg = EmitExpression(stmt->condition);
-    if (!reg) {
-        return;
-    }
-    switch_node->condition = reg.Get();
-
-    BranchTo(switch_node);
-
-    ast_to_flow_[stmt] = switch_node;
-
-    {
-        FlowStackScope scope(this, switch_node);
-
-        const auto* sem = program_->Sem().Get(stmt);
-        for (const auto* c : sem->Cases()) {
-            utils::Vector<Switch::CaseSelector, 4> selectors;
-            for (const auto* selector : c->Selectors()) {
-                if (selector->IsDefault()) {
-                    selectors.Push({nullptr});
-                } else {
-                    selectors.Push({builder.Constant(selector->Value()->Clone(clone_ctx_))});
-                }
-            }
-
-            current_flow_block = builder.CreateCase(switch_node, selectors);
-            EmitBlock(c->Body()->Declaration());
-
-            BranchToIfNeeded(switch_node->merge.target);
-        }
-    }
-    current_flow_block = nullptr;
-
-    if (IsConnected(switch_node->merge.target)) {
-        current_flow_block = switch_node->merge.target->As<Block>();
-    }
-}
-
-void BuilderImpl::EmitReturn(const ast::ReturnStatement* stmt) {
-    utils::Vector<Value*, 1> ret_value;
-    if (stmt->value) {
-        auto ret = EmitExpression(stmt->value);
-        if (!ret) {
-            return;
-        }
-        ret_value.Push(ret.Get());
-    }
-
-    BranchTo(current_function_->end_target, std::move(ret_value));
-}
-
-void BuilderImpl::EmitBreak(const ast::BreakStatement*) {
-    auto* current_control = FindEnclosingControl(ControlFlags::kNone);
-    TINT_ASSERT(IR, current_control);
-
-    if (auto* c = current_control->As<Loop>()) {
-        BranchTo(c->merge.target);
-    } else if (auto* s = current_control->As<Switch>()) {
-        BranchTo(s->merge.target);
-    } else {
-        TINT_UNREACHABLE(IR, diagnostics_);
-    }
-}
-
-void BuilderImpl::EmitContinue(const ast::ContinueStatement*) {
-    auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
-    TINT_ASSERT(IR, current_control);
-
-    if (auto* c = current_control->As<Loop>()) {
-        BranchTo(c->continuing.target);
-    } else {
-        TINT_UNREACHABLE(IR, diagnostics_);
-    }
-}
-
-// Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
-// code has to continue as before it just predicates writes. If WGSL grows some kind of terminating
-// discard that would probably make sense as a FlowNode but would then require figuring out the
-// multi-level exit that is triggered.
-void BuilderImpl::EmitDiscard(const ast::DiscardStatement*) {
-    auto* inst = builder.Discard();
-    current_flow_block->instructions.Push(inst);
-}
-
-void BuilderImpl::EmitBreakIf(const ast::BreakIfStatement* stmt) {
-    auto* if_node = builder.CreateIf();
-
-    // Emit the break-if condition into the end of the preceding block
-    auto reg = EmitExpression(stmt->condition);
-    if (!reg) {
-        return;
-    }
-    if_node->condition = reg.Get();
-
-    BranchTo(if_node);
-
-    ast_to_flow_[stmt] = if_node;
-
-    auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
-    TINT_ASSERT(IR, current_control);
-    TINT_ASSERT(IR, current_control->Is<Loop>());
-
-    auto* loop = current_control->As<Loop>();
-
-    current_flow_block = if_node->true_.target->As<Block>();
-    BranchTo(loop->merge.target);
-
-    current_flow_block = if_node->false_.target->As<Block>();
-    BranchTo(if_node->merge.target);
-
-    current_flow_block = if_node->merge.target->As<Block>();
-
-    // The `break-if` has to be the last item in the continuing block. The false branch of the
-    // `break-if` will always take us back to the start of the loop.
-    BranchTo(loop->start.target);
-}
-
-utils::Result<Value*> BuilderImpl::EmitExpression(const ast::Expression* expr) {
-    // If this is a value that has been const-eval'd return the result.
-    if (auto* sem = program_->Sem().Get(expr)->As<sem::ValueExpression>()) {
-        if (auto* v = sem->ConstantValue()) {
-            if (auto* cv = v->Clone(clone_ctx_)) {
-                return builder.Constant(cv);
-            }
-        }
-    }
-
-    return tint::Switch(
-        expr,
-        // [&](const ast::IndexAccessorExpression* a) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
-        [&](const ast::BitcastExpression* b) { return EmitBitcast(b); },
-        [&](const ast::CallExpression* c) { return EmitCall(c); },
-        [&](const ast::IdentifierExpression* i) {
-            auto* v = scopes_.Get(i->identifier->symbol);
-            return utils::Result<Value*>{v};
-        },
-        [&](const ast::LiteralExpression* l) { return EmitLiteral(l); },
-        // [&](const ast::MemberAccessorExpression* m) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::PhonyExpression*) {
-        // TODO(dsinclair): Implement. The call may have side effects so has to be made.
-        // },
-        [&](const ast::UnaryOpExpression* u) { return EmitUnary(u); },
-        [&](Default) {
-            add_error(expr->source,
-                      "unknown expression type: " + std::string(expr->TypeInfo().name));
-            return utils::Failure;
-        });
-}
-
-void BuilderImpl::EmitVariable(const ast::Variable* var) {
-    auto* sem = program_->Sem().Get(var);
-
-    return tint::Switch(  //
-        var,
-        [&](const ast::Var* v) {
-            auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
-            auto* val = builder.Declare(ty, sem->AddressSpace(), sem->Access());
-            current_flow_block->instructions.Push(val);
-
-            if (v->initializer) {
-                auto init = EmitExpression(v->initializer);
-                if (!init) {
-                    return;
-                }
-                val->initializer = init.Get();
-            }
-            // Store the declaration so we can get the instruction to store too
-            scopes_.Set(v->name->symbol, val);
-
-            // Record the original name of the var
-            builder.ir.SetName(val, v->name->symbol.Name());
-        },
-        [&](const ast::Let* l) {
-            // A `let` doesn't exist as a standalone item in the IR, it's just the result of the
-            // initializer.
-            auto init = EmitExpression(l->initializer);
-            if (!init) {
-                return;
-            }
-
-            // Store the results of the initialization
-            scopes_.Set(l->name->symbol, init.Get());
-
-            // Record the original name of the let
-            builder.ir.SetName(init.Get(), l->name->symbol.Name());
-        },
-        [&](const ast::Override*) {
-            add_error(var->source,
-                      "found an `Override` variable. The SubstituteOverrides "
-                      "transform must be run before converting to IR");
-        },
-        [&](const ast::Const*) {
-            // Skip. This should be handled by const-eval already, so the const will be a
-            // `constant::` value at the usage sites. Can just ignore the `const` variable as it
-            // should never be used.
-            //
-            // TODO(dsinclair): Probably want to store the const variable somewhere and then in
-            // identifier expression log an error if we ever see a const identifier. Add this when
-            // identifiers and variables are supported.
-        },
-        [&](Default) {
-            add_error(var->source, "unknown variable: " + std::string(var->TypeInfo().name));
-        });
-}
-
-utils::Result<Value*> BuilderImpl::EmitUnary(const ast::UnaryOpExpression* expr) {
-    auto val = EmitExpression(expr->expr);
-    if (!val) {
-        return utils::Failure;
-    }
-
-    auto* sem = program_->Sem().Get(expr);
-    auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
-
-    Instruction* inst = nullptr;
-    switch (expr->op) {
-        case ast::UnaryOp::kAddressOf:
-            inst = builder.AddressOf(ty, val.Get());
-            break;
-        case ast::UnaryOp::kComplement:
-            inst = builder.Complement(ty, val.Get());
-            break;
-        case ast::UnaryOp::kIndirection:
-            inst = builder.Indirection(ty, val.Get());
-            break;
-        case ast::UnaryOp::kNegation:
-            inst = builder.Negation(ty, val.Get());
-            break;
-        case ast::UnaryOp::kNot:
-            inst = builder.Not(ty, val.Get());
-            break;
-    }
-
-    current_flow_block->instructions.Push(inst);
-    return inst;
-}
-
-// A short-circut needs special treatment. The short-circuit is decomposed into the relevant if
-// statements and declarations.
-utils::Result<Value*> BuilderImpl::EmitShortCircuit(const ast::BinaryExpression* expr) {
-    switch (expr->op) {
-        case ast::BinaryOp::kLogicalAnd:
-        case ast::BinaryOp::kLogicalOr:
-            break;
-        default:
-            TINT_ICE(IR, diagnostics_) << "invalid operation type for short-circut decomposition";
-            return utils::Failure;
-    }
-
-    // Evaluate the LHS of the short-circuit
-    auto lhs = EmitExpression(expr->lhs);
-    if (!lhs) {
-        return utils::Failure;
-    }
-
-    // Generate a variable to store the short-circut into
-    auto* ty = builder.ir.types.Get<type::Bool>();
-    auto* result_var =
-        builder.Declare(ty, builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
-    current_flow_block->instructions.Push(result_var);
-
-    auto* lhs_store = builder.Store(result_var, lhs.Get());
-    current_flow_block->instructions.Push(lhs_store);
-
-    auto* if_node = builder.CreateIf();
-    if_node->condition = lhs.Get();
-    BranchTo(if_node);
-
-    utils::Result<Value*> rhs;
-    {
-        FlowStackScope scope(this, if_node);
-
-        // If this is an `&&` then we only evaluate the RHS expression in the true block.
-        // If this is an `||` then we only evaluate the RHS expression in the false block.
-        if (expr->op == ast::BinaryOp::kLogicalAnd) {
-            current_flow_block = if_node->true_.target->As<Block>();
-        } else {
-            current_flow_block = if_node->false_.target->As<Block>();
-        }
-
-        rhs = EmitExpression(expr->rhs);
-        if (!rhs) {
-            return utils::Failure;
-        }
-        auto* rhs_store = builder.Store(result_var, rhs.Get());
-        current_flow_block->instructions.Push(rhs_store);
-
-        BranchTo(if_node->merge.target);
-    }
-    current_flow_block = if_node->merge.target->As<Block>();
-
-    return result_var;
-}
-
-utils::Result<Value*> BuilderImpl::EmitBinary(const ast::BinaryExpression* expr) {
-    if (expr->op == ast::BinaryOp::kLogicalAnd || expr->op == ast::BinaryOp::kLogicalOr) {
-        return EmitShortCircuit(expr);
-    }
-
-    auto lhs = EmitExpression(expr->lhs);
-    if (!lhs) {
-        return utils::Failure;
-    }
-
-    auto rhs = EmitExpression(expr->rhs);
-    if (!rhs) {
-        return utils::Failure;
-    }
-
-    auto* sem = program_->Sem().Get(expr);
-    auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
-
-    Binary* inst = nullptr;
-    switch (expr->op) {
-        case ast::BinaryOp::kAnd:
-            inst = builder.And(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kOr:
-            inst = builder.Or(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kXor:
-            inst = builder.Xor(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kEqual:
-            inst = builder.Equal(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kNotEqual:
-            inst = builder.NotEqual(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kLessThan:
-            inst = builder.LessThan(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kGreaterThan:
-            inst = builder.GreaterThan(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kLessThanEqual:
-            inst = builder.LessThanEqual(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kGreaterThanEqual:
-            inst = builder.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kShiftLeft:
-            inst = builder.ShiftLeft(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kShiftRight:
-            inst = builder.ShiftRight(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kAdd:
-            inst = builder.Add(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kSubtract:
-            inst = builder.Subtract(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kMultiply:
-            inst = builder.Multiply(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kDivide:
-            inst = builder.Divide(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kModulo:
-            inst = builder.Modulo(ty, lhs.Get(), rhs.Get());
-            break;
-        case ast::BinaryOp::kLogicalAnd:
-        case ast::BinaryOp::kLogicalOr:
-            TINT_ICE(IR, diagnostics_) << "short circuit op should have already been handled";
-            return utils::Failure;
-        case ast::BinaryOp::kNone:
-            TINT_ICE(IR, diagnostics_) << "missing binary operand type";
-            return utils::Failure;
-    }
-
-    current_flow_block->instructions.Push(inst);
-    return inst;
-}
-
-utils::Result<Value*> BuilderImpl::EmitBitcast(const ast::BitcastExpression* expr) {
-    auto val = EmitExpression(expr->expr);
-    if (!val) {
-        return utils::Failure;
-    }
-
-    auto* sem = program_->Sem().Get(expr);
-    auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
-    auto* inst = builder.Bitcast(ty, val.Get());
-
-    current_flow_block->instructions.Push(inst);
-    return inst;
-}
-
-void BuilderImpl::EmitCall(const ast::CallStatement* stmt) {
-    (void)EmitCall(stmt->expr);
-}
-
-utils::Result<Value*> BuilderImpl::EmitCall(const ast::CallExpression* expr) {
-    // If this is a materialized semantic node, just use the constant value.
-    if (auto* mat = program_->Sem().Get(expr)) {
-        if (mat->ConstantValue()) {
-            auto* cv = mat->ConstantValue()->Clone(clone_ctx_);
-            if (!cv) {
-                add_error(expr->source, "failed to get constant value for call " +
-                                            std::string(expr->TypeInfo().name));
-                return utils::Failure;
-            }
-            return builder.Constant(cv);
-        }
-    }
-
-    utils::Vector<Value*, 8> args;
-    args.Reserve(expr->args.Length());
-
-    // Emit the arguments
-    for (const auto* arg : expr->args) {
-        auto value = EmitExpression(arg);
-        if (!value) {
-            add_error(arg->source, "failed to convert arguments");
-            return utils::Failure;
-        }
-        args.Push(value.Get());
-    }
-
-    auto* sem = program_->Sem().Get<sem::Call>(expr);
-    if (!sem) {
-        add_error(expr->source, "failed to get semantic information for call " +
-                                    std::string(expr->TypeInfo().name));
-        return utils::Failure;
-    }
-
-    auto* ty = sem->Target()->ReturnType()->Clone(clone_ctx_.type_ctx);
-
-    Instruction* inst = nullptr;
-
-    // If this is a builtin function, emit the specific builtin value
-    if (auto* b = sem->Target()->As<sem::Builtin>()) {
-        inst = builder.Builtin(ty, b->Type(), args);
-    } else if (sem->Target()->As<sem::ValueConstructor>()) {
-        inst = builder.Construct(ty, std::move(args));
-    } else if (auto* conv = sem->Target()->As<sem::ValueConversion>()) {
-        auto* from = conv->Source()->Clone(clone_ctx_.type_ctx);
-        inst = builder.Convert(ty, from, std::move(args));
-    } else if (expr->target->identifier->Is<ast::TemplatedIdentifier>()) {
-        TINT_UNIMPLEMENTED(IR, diagnostics_) << "missing templated ident support";
-        return utils::Failure;
-    } else {
-        // Not a builtin and not a templated call, so this is a user function.
-        auto name = CloneSymbol(expr->target->identifier->symbol);
-        inst = builder.UserCall(ty, name, std::move(args));
-    }
-    if (inst == nullptr) {
-        return utils::Failure;
-    }
-    current_flow_block->instructions.Push(inst);
-    return inst;
-}
-
-utils::Result<Value*> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
-    auto* sem = program_->Sem().Get(lit);
-    if (!sem) {
-        add_error(lit->source, "failed to get semantic information for node " +
-                                   std::string(lit->TypeInfo().name));
-        return utils::Failure;
-    }
-
-    auto* cv = sem->ConstantValue()->Clone(clone_ctx_);
-    if (!cv) {
-        add_error(lit->source,
-                  "failed to get constant value for node " + std::string(lit->TypeInfo().name));
-        return utils::Failure;
-    }
-    return builder.Constant(cv);
-}
-
-void BuilderImpl::EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
-    for (auto* attr : attrs) {
-        EmitAttribute(attr);
-    }
-}
-
-void BuilderImpl::EmitAttribute(const ast::Attribute* attr) {
-    tint::Switch(  //
-        attr,
-        // [&](const ast::WorkgroupAttribute* wg) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::StageAttribute* s) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::BindingAttribute* b) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::GroupAttribute* g) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::LocationAttribute* l) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::BuiltinAttribute* b) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::InterpolateAttribute* i) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::InvariantAttribute* i) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::MustUseAttribute* i) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](const ast::IdAttribute*) {
-            add_error(attr->source,
-                      "found an `Id` attribute. The SubstituteOverrides transform "
-                      "must be run before converting to IR");
-        },
-        [&](const ast::StructMemberSizeAttribute*) {
-            TINT_ICE(IR, diagnostics_)
-                << "StructMemberSizeAttribute encountered during IR conversion";
-        },
-        [&](const ast::StructMemberAlignAttribute*) {
-            TINT_ICE(IR, diagnostics_)
-                << "StructMemberAlignAttribute encountered during IR conversion";
-        },
-        // [&](const ast::StrideAttribute* s) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::InternalAttribute *i) {
-        // TODO(dsinclair): Implement
-        // },
-        [&](Default) {
-            add_error(attr->source, "unknown attribute: " + std::string(attr->TypeInfo().name));
-        });
-}
-
-}  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
deleted file mode 100644
index f6bed12..0000000
--- a/src/tint/ir/builder_impl.h
+++ /dev/null
@@ -1,243 +0,0 @@
-// Copyright 2022 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_IR_BUILDER_IMPL_H_
-#define SRC_TINT_IR_BUILDER_IMPL_H_
-
-#include <string>
-#include <unordered_map>
-#include <utility>
-
-#include "src/tint/ast/type.h"
-#include "src/tint/constant/clone_context.h"
-#include "src/tint/diagnostic/diagnostic.h"
-#include "src/tint/ir/builder.h"
-#include "src/tint/ir/flow_node.h"
-#include "src/tint/ir/module.h"
-#include "src/tint/ir/value.h"
-#include "src/tint/scope_stack.h"
-#include "src/tint/utils/result.h"
-
-// Forward Declarations
-namespace tint {
-class Program;
-}  // namespace tint
-namespace tint::ast {
-class Attribute;
-class AssignmentStatement;
-class BinaryExpression;
-class BitcastExpression;
-class BlockStatement;
-class BreakIfStatement;
-class BreakStatement;
-class CallExpression;
-class CallStatement;
-class CompoundAssignmentStatement;
-class ContinueStatement;
-class DiscardStatement;
-class Expression;
-class ForLoopStatement;
-class Function;
-class IfStatement;
-class LoopStatement;
-class LiteralExpression;
-class Node;
-class ReturnStatement;
-class Statement;
-class SwitchStatement;
-class UnaryOpExpression;
-class WhileStatement;
-class Variable;
-}  // namespace tint::ast
-namespace tint::sem {
-class Builtin;
-}  // namespace tint::sem
-
-namespace tint::ir {
-
-/// Builds an ir::Module from a given ast::Program
-class BuilderImpl {
-  public:
-    /// Constructor
-    /// @param program the program to create from
-    explicit BuilderImpl(const Program* program);
-    /// Destructor
-    ~BuilderImpl();
-
-    /// Builds an ir::Module from the given Program
-    /// @returns true on success, false otherwise
-    utils::Result<Module> Build();
-
-    /// @returns the diagnostics
-    diag::List Diagnostics() const { return diagnostics_; }
-
-    /// Emits a function to the IR.
-    /// @param func the function to emit
-    void EmitFunction(const ast::Function* func);
-
-    /// Emits a set of statements to the IR.
-    /// @param stmts the statements to emit
-    void EmitStatements(utils::VectorRef<const ast::Statement*> stmts);
-
-    /// Emits a statement to the IR
-    /// @param stmt the statment to emit
-    void EmitStatement(const ast::Statement* stmt);
-
-    /// Emits a block statement to the IR.
-    /// @param block the block to emit
-    void EmitBlock(const ast::BlockStatement* block);
-
-    /// Emits an if control node to the IR.
-    /// @param stmt the if statement
-    void EmitIf(const ast::IfStatement* stmt);
-
-    /// Emits a return node to the IR.
-    /// @param stmt the return AST statement
-    void EmitReturn(const ast::ReturnStatement* stmt);
-
-    /// Emits a loop control node to the IR.
-    /// @param stmt the loop statement
-    void EmitLoop(const ast::LoopStatement* stmt);
-
-    /// Emits a loop control node to the IR.
-    /// @param stmt the while statement
-    void EmitWhile(const ast::WhileStatement* stmt);
-
-    /// Emits a loop control node to the IR.
-    /// @param stmt the for loop statement
-    void EmitForLoop(const ast::ForLoopStatement* stmt);
-
-    /// Emits a switch statement
-    /// @param stmt the switch statement
-    void EmitSwitch(const ast::SwitchStatement* stmt);
-
-    /// Emits a break statement
-    /// @param stmt the break statement
-    void EmitBreak(const ast::BreakStatement* stmt);
-
-    /// Emits a continue statement
-    /// @param stmt the continue statement
-    void EmitContinue(const ast::ContinueStatement* stmt);
-
-    /// Emits a discard statement
-    void EmitDiscard(const ast::DiscardStatement*);
-
-    /// Emits a break-if statement
-    /// @param stmt the break-if statement
-    void EmitBreakIf(const ast::BreakIfStatement* stmt);
-
-    /// Emits an assignment statement
-    /// @param stmt the statement
-    void EmitAssignment(const ast::AssignmentStatement* stmt);
-
-    /// Emits a compound assignment statement
-    /// @param stmt the statement
-    void EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt);
-
-    /// Emits an expression
-    /// @param expr the expression to emit
-    /// @returns true if successful, false otherwise
-    utils::Result<Value*> EmitExpression(const ast::Expression* expr);
-
-    /// Emits a variable
-    /// @param var the variable to emit
-    void EmitVariable(const ast::Variable* var);
-
-    /// Emits a Unary expression
-    /// @param expr the unary expression
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitUnary(const ast::UnaryOpExpression* expr);
-
-    /// Emits a short-circult binary expression
-    /// @param expr the binary expression
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitShortCircuit(const ast::BinaryExpression* expr);
-
-    /// Emits a binary expression
-    /// @param expr the binary expression
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitBinary(const ast::BinaryExpression* expr);
-
-    /// Emits a bitcast expression
-    /// @param expr the bitcast expression
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitBitcast(const ast::BitcastExpression* expr);
-
-    /// Emits a call expression
-    /// @param stmt the call statement
-    void EmitCall(const ast::CallStatement* stmt);
-
-    /// Emits a call expression
-    /// @param expr the call expression
-    /// @returns the value storing the result if successful, utils::Failure otherwise
-    utils::Result<Value*> EmitCall(const ast::CallExpression* expr);
-
-    /// Emits a literal expression
-    /// @param lit the literal to emit
-    /// @returns true if successful, false otherwise
-    utils::Result<Value*> EmitLiteral(const ast::LiteralExpression* lit);
-
-    /// Emits a set of attributes
-    /// @param attrs the attributes to emit
-    void EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs);
-
-    /// Emits an attribute
-    /// @param attr the attribute to emit
-    void EmitAttribute(const ast::Attribute* attr);
-
-    /// Retrieve the IR Flow node for a given AST node.
-    /// @param n the node to lookup
-    /// @returns the FlowNode for the given ast::Node or nullptr if it doesn't exist.
-    const ir::FlowNode* FlowNodeForAstNode(const ast::Node* n) const {
-        if (ast_to_flow_.count(n) == 0) {
-            return nullptr;
-        }
-        return ast_to_flow_.at(n);
-    }
-
-    /// The stack of flow control blocks.
-    utils::Vector<FlowNode*, 8> flow_stack;
-
-    /// The IR builder being used by the impl.
-    Builder builder;
-
-    /// The current flow block for expressions
-    Block* current_flow_block = nullptr;
-
-  private:
-    enum class ControlFlags { kNone, kExcludeSwitch };
-
-    void BranchTo(ir::FlowNode* node, utils::VectorRef<Value*> args = {});
-    void BranchToIfNeeded(ir::FlowNode* node);
-
-    FlowNode* FindEnclosingControl(ControlFlags flags);
-
-    void add_error(const Source& s, const std::string& err);
-
-    Symbol CloneSymbol(Symbol sym) const;
-
-    const Program* program_ = nullptr;
-    Function* current_function_ = nullptr;
-    ScopeStack<Symbol, Value*> scopes_;
-    constant::CloneContext clone_ctx_;
-    diag::List diagnostics_;
-
-    /// Map from ast nodes to flow nodes, used to retrieve the flow node for a given AST node.
-    /// Used for testing purposes.
-    std::unordered_map<const ast::Node*, const FlowNode*> ast_to_flow_;
-};
-
-}  // namespace tint::ir
-
-#endif  // SRC_TINT_IR_BUILDER_IMPL_H_
diff --git a/src/tint/ir/builder_impl_binary_test.cc b/src/tint/ir/builder_impl_binary_test.cc
deleted file mode 100644
index 3be8405..0000000
--- a/src/tint/ir/builder_impl_binary_test.cc
+++ /dev/null
@@ -1,696 +0,0 @@
-// Copyright 2023 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/test_helper.h"
-
-#include "gmock/gmock.h"
-#include "src/tint/ast/case_selector.h"
-#include "src/tint/ast/int_literal_expression.h"
-#include "src/tint/constant/scalar.h"
-
-namespace tint::ir {
-namespace {
-
-using namespace tint::number_suffixes;  // NOLINT
-
-using IR_BuilderImplTest = TestHelper;
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Add(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = add %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAdd) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kAdd);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = add %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Subtract) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Sub(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = sub %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundSubtract) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kSubtract);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = sub %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Multiply) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Mul(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = mul %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundMultiply) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kMultiply);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = mul %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Div) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Div(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = div %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundDiv) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kDivide);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = div %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Modulo) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Mod(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = mod %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundModulo) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kModulo);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = mod %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_And) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = And(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = and %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAnd) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
-    auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kAnd);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, bool, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, bool, read_write> = and %v1:ref<private, bool, read_write>, false
-  store %v1:ref<private, bool, read_write>, %2:ref<private, bool, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Or) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Or(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = or %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundOr) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
-    auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kOr);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, bool, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, bool, read_write> = or %v1:ref<private, bool, read_write>, false
-  store %v1:ref<private, bool, read_write>, %2:ref<private, bool, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Xor) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Xor(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = xor %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundXor) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kXor);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = xor %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalAnd) {
-    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
-    auto* expr = LogicalAnd(Call("my_func"), false);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func my_func():bool
-  %fn2 = block
-  ret true
-func_end
-
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn4 = block
-  %1:bool = call my_func
-  %tint_symbol:bool = var function, read_write
-  store %tint_symbol:bool, %1:bool
-  branch %fn5
-
-  %fn5 = if %1:bool [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    %fn6 = block
-    store %tint_symbol:bool, false
-    branch %fn8
-
-  # if merge
-  %fn8 = block
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalOr) {
-    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
-    auto* expr = LogicalOr(Call("my_func"), true);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func my_func():bool
-  %fn2 = block
-  ret true
-func_end
-
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn4 = block
-  %1:bool = call my_func
-  %tint_symbol:bool = var function, read_write
-  store %tint_symbol:bool, %1:bool
-  branch %fn5
-
-  %fn5 = if %1:bool [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    # false branch
-    %fn7 = block
-    store %tint_symbol:bool, true
-    branch %fn8
-
-  # if merge
-  %fn8 = block
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Equal) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Equal(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = eq %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_NotEqual) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = NotEqual(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = neq %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThan) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = LessThan(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = lt %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThan) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = GreaterThan(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = gt %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThanEqual) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = LessThanEqual(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = lte %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThanEqual) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = GreaterThanEqual(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:bool = gte %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftLeft) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Shl(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = shiftl %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftLeft) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftLeft);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = shiftl %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftRight) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
-    auto* expr = Shr(Call("my_func"), 4_u);
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = shiftr %1:u32, 4u
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftRight) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
-    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftRight);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, u32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %2:ref<private, u32, read_write> = shiftr %v1:ref<private, u32, read_write>, 1u
-  store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound) {
-    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
-
-    auto* expr = LogicalAnd(LessThan(Call("my_func"), 2_f),
-                            GreaterThan(2.5_f, Div(Call("my_func"), Mul(2.3_f, Call("my_func")))));
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func my_func():f32
-  %fn2 = block
-  ret 0.0f
-func_end
-
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn4 = block
-  %1:f32 = call my_func
-  %2:bool = lt %1:f32, 2.0f
-  %tint_symbol:bool = var function, read_write
-  store %tint_symbol:bool, %2:bool
-  branch %fn5
-
-  %fn5 = if %2:bool [t: %fn6, f: %fn7, m: %fn8]
-    # true branch
-    %fn6 = block
-    %4:f32 = call my_func
-    %5:f32 = call my_func
-    %6:f32 = mul 2.29999995231628417969f, %5:f32
-    %7:f32 = div %4:f32, %6:f32
-    %8:bool = gt 2.5f, %7:f32
-    store %tint_symbol:bool, %8:bool
-    branch %fn8
-
-  # if merge
-  %fn8 = block
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound_WithConstEval) {
-    Func("my_func", utils::Vector{Param("p", ty.bool_())}, ty.bool_(), utils::Vector{Return(true)});
-    auto* expr = Call("my_func", LogicalAnd(LessThan(2.4_f, 2_f),
-                                            GreaterThan(2.5_f, Div(10_f, Mul(2.3_f, 9.4_f)))));
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func my_func():bool
-  %fn2 = block
-  ret true
-func_end
-
-%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn4 = block
-  %tint_symbol:bool = call my_func, false
-  ret
-func_end
-
-)");
-}
-
-}  // namespace
-}  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl_call_test.cc b/src/tint/ir/builder_impl_call_test.cc
deleted file mode 100644
index 0fb070e..0000000
--- a/src/tint/ir/builder_impl_call_test.cc
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2023 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/test_helper.h"
-
-#include "gmock/gmock.h"
-#include "src/tint/ast/case_selector.h"
-#include "src/tint/ast/int_literal_expression.h"
-#include "src/tint/constant/scalar.h"
-
-namespace tint::ir {
-namespace {
-
-using namespace tint::number_suffixes;  // NOLINT
-
-using IR_BuilderImplTest = TestHelper;
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Bitcast) {
-    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
-
-    auto* expr = Bitcast<f32>(Call("my_func"));
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:f32 = call my_func
-%2:f32 = bitcast %1:f32
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitStatement_Discard) {
-    auto* expr = Discard();
-    Func("test_function", {}, ty.void_(), expr,
-         utils::Vector{
-             create<ast::StageAttribute>(ast::PipelineStage::kFragment),
-         });
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    b.EmitStatement(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(discard
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
-    Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
-
-    auto* stmt = CallStmt(Call("my_func", Mul(2_a, 3_a)));
-    WrapInFunction(stmt);
-
-    auto& b = CreateBuilder();
-
-    InjectFlowBlock();
-    b.EmitStatement(stmt);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:void = call my_func, 6.0f
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Convert) {
-    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
-    auto* expr = Call(ty.f32(), i);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-    ASSERT_TRUE(r);
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%i:ref<private, i32, read_write> = var private, read_write, 1i
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %tint_symbol:f32 = convert i32, %i:ref<private, i32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_ConstructEmpty) {
-    auto* expr = vec3(ty.f32());
-    GlobalVar("i", builtin::AddressSpace::kPrivate, expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-    ASSERT_TRUE(r);
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%i:ref<private, vec3<f32>, read_write> = var private, read_write, vec3<f32> 0.0f
-
-
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Construct) {
-    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
-    auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-    ASSERT_TRUE(r);
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%i:ref<private, f32, read_write> = var private, read_write, 1.0f
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %tint_symbol:vec3<f32> = construct 2.0f, 3.0f, %i:ref<private, f32, read_write>
-  ret
-func_end
-
-)");
-}
-
-}  // namespace
-}  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl_unary_test.cc b/src/tint/ir/builder_impl_unary_test.cc
deleted file mode 100644
index 2323acb..0000000
--- a/src/tint/ir/builder_impl_unary_test.cc
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright 2023 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "src/tint/ir/test_helper.h"
-
-#include "gmock/gmock.h"
-#include "src/tint/ast/case_selector.h"
-#include "src/tint/ast/int_literal_expression.h"
-#include "src/tint/constant/scalar.h"
-
-namespace tint::ir {
-namespace {
-
-using namespace tint::number_suffixes;  // NOLINT
-
-using IR_BuilderImplTest = TestHelper;
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Not) {
-    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(false)});
-    auto* expr = Not(Call("my_func"));
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:bool = call my_func
-%2:bool = eq %1:bool, false
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Complement) {
-    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(1_u)});
-    auto* expr = Complement(Call("my_func"));
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:u32 = call my_func
-%2:u32 = complement %1:u32
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Negation) {
-    Func("my_func", utils::Empty, ty.i32(), utils::Vector{Return(1_i)});
-    auto* expr = Negation(Call("my_func"));
-    WrapInFunction(expr);
-
-    auto& b = CreateBuilder();
-    InjectFlowBlock();
-    auto r = b.EmitExpression(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
-    ASSERT_TRUE(r);
-
-    Disassembler d(b.builder.ir);
-    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
-    EXPECT_EQ(d.AsString(), R"(%1:i32 = call my_func
-%2:i32 = negation %1:i32
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_AddressOf) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
-
-    auto* expr = Decl(Let("v2", AddressOf("v1")));
-    WrapInFunction(expr);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, i32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %v2:ptr<private, i32, read_write> = addr_of %v1:ref<private, i32, read_write>
-  ret
-func_end
-
-)");
-}
-
-TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Indirection) {
-    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
-    utils::Vector stmts = {
-        Decl(Let("v3", AddressOf("v1"))),
-        Decl(Let("v2", Deref("v3"))),
-    };
-    WrapInFunction(stmts);
-
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
-
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%v1:ref<private, i32, read_write> = var private, read_write
-
-
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  %v3:ptr<private, i32, read_write> = addr_of %v1:ref<private, i32, read_write>
-  %v2:i32 = indirection %v3:ptr<private, i32, read_write>
-  ret
-func_end
-
-)");
-}
-
-}  // namespace
-}  // namespace tint::ir
diff --git a/src/tint/ir/constant_test.cc b/src/tint/ir/constant_test.cc
index c550763..7005751 100644
--- a/src/tint/ir/constant_test.cc
+++ b/src/tint/ir/constant_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/ir/value.h"
 
@@ -23,11 +24,12 @@
 using IR_ConstantTest = TestHelper;
 
 TEST_F(IR_ConstantTest, f32) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     utils::StringStream str;
 
-    auto* c = b.builder.Constant(1.2_f);
+    auto* c = b.Constant(1.2_f);
     EXPECT_EQ(1.2_f, c->value->As<constant::Scalar<f32>>()->ValueAs<f32>());
 
     EXPECT_TRUE(c->value->Is<constant::Scalar<f32>>());
@@ -38,11 +40,12 @@
 }
 
 TEST_F(IR_ConstantTest, f16) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     utils::StringStream str;
 
-    auto* c = b.builder.Constant(1.1_h);
+    auto* c = b.Constant(1.1_h);
     EXPECT_EQ(1.1_h, c->value->As<constant::Scalar<f16>>()->ValueAs<f16>());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -53,11 +56,12 @@
 }
 
 TEST_F(IR_ConstantTest, i32) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     utils::StringStream str;
 
-    auto* c = b.builder.Constant(1_i);
+    auto* c = b.Constant(1_i);
     EXPECT_EQ(1_i, c->value->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -68,11 +72,12 @@
 }
 
 TEST_F(IR_ConstantTest, u32) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     utils::StringStream str;
 
-    auto* c = b.builder.Constant(2_u);
+    auto* c = b.Constant(2_u);
     EXPECT_EQ(2_u, c->value->As<constant::Scalar<u32>>()->ValueAs<u32>());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
@@ -83,18 +88,19 @@
 }
 
 TEST_F(IR_ConstantTest, bool) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     {
         utils::StringStream str;
 
-        auto* c = b.builder.Constant(false);
+        auto* c = b.Constant(false);
         EXPECT_FALSE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
     }
 
     {
         utils::StringStream str;
-        auto c = b.builder.Constant(true);
+        auto c = b.Constant(true);
         EXPECT_TRUE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
         EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 688fc41..d4ee3a7 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -138,13 +138,14 @@
 
                 out_ << "]";
             }
-            out_ << std::endl;
+            out_ << " {" << std::endl;
 
             {
                 ScopedIndent func_indent(indent_size_);
                 ScopedStopNode scope(stop_nodes_, f->end_target);
                 Walk(f->start_target);
             }
+            out_ << "} ";
             Walk(f->end_target);
         },
         [&](const ir::Block* b) {
@@ -153,16 +154,23 @@
                 return;
             }
 
-            Indent() << "%fn" << IdOf(b) << " = block" << std::endl;
-            EmitBlockInstructions(b);
+            Indent() << "%fn" << IdOf(b) << " = block {" << std::endl;
+            {
+                ScopedIndent si(indent_size_);
+                EmitBlockInstructions(b);
+            }
+            Indent() << "}";
 
+            std::string suffix = "";
             if (b->branch.target->Is<FunctionTerminator>()) {
-                Indent() << "ret";
+                out_ << " -> %func_end";
+                suffix = "return";
             } else if (b->branch.target->Is<RootTerminator>()) {
                 // Nothing to do
             } else {
-                Indent() << "branch "
-                         << "%fn" << IdOf(b->branch.target);
+                out_ << " -> "
+                     << "%fn" << IdOf(b->branch.target);
+                suffix = "branch";
             }
             if (!b->branch.args.IsEmpty()) {
                 out_ << " ";
@@ -173,6 +181,9 @@
                     EmitValue(v);
                 }
             }
+            if (!suffix.empty()) {
+                out_ << " # " << suffix;
+            }
             out_ << std::endl;
 
             if (!b->branch.target->Is<FunctionTerminator>()) {
@@ -294,7 +305,7 @@
         },
         [&](const ir::FunctionTerminator*) {
             TINT_ASSERT(IR, in_function_);
-            Indent() << "func_end" << std::endl << std::endl;
+            Indent() << "%func_end" << std::endl << std::endl;
         },
         [&](const ir::RootTerminator*) {
             TINT_ASSERT(IR, !in_function_);
diff --git a/src/tint/ir/discard_test.cc b/src/tint/ir/discard_test.cc
index b1bf8c2..054727f 100644
--- a/src/tint/ir/discard_test.cc
+++ b/src/tint/ir/discard_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 
@@ -21,9 +22,10 @@
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, Discard) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    const auto* inst = b.builder.Discard();
+    const auto* inst = b.Discard();
     ASSERT_TRUE(inst->Is<ir::Discard>());
 }
 
diff --git a/src/tint/ir/from_program.cc b/src/tint/ir/from_program.cc
index d093103..f7729f3 100644
--- a/src/tint/ir/from_program.cc
+++ b/src/tint/ir/from_program.cc
@@ -14,20 +14,1221 @@
 
 #include "src/tint/ir/from_program.h"
 
-#include "src/tint/ir/builder_impl.h"
+#include <iostream>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/bitcast_expression.h"
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/bool_literal_expression.h"
+#include "src/tint/ast/break_if_statement.h"
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/compound_assignment_statement.h"
+#include "src/tint/ast/const.h"
+#include "src/tint/ast/const_assert.h"
+#include "src/tint/ast/continue_statement.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/enable.h"
+#include "src/tint/ast/float_literal_expression.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/function.h"
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/identifier.h"
+#include "src/tint/ast/identifier_expression.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/increment_decrement_statement.h"
+#include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/ast/invariant_attribute.h"
+#include "src/tint/ast/let.h"
+#include "src/tint/ast/literal_expression.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/override.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/statement.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/struct_member_align_attribute.h"
+#include "src/tint/ast/struct_member_size_attribute.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/templated_identifier.h"
+#include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/ast/var.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/ast/while_statement.h"
+#include "src/tint/ir/builder.h"
+#include "src/tint/ir/function.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/loop.h"
+#include "src/tint/ir/module.h"
+#include "src/tint/ir/store.h"
+#include "src/tint/ir/switch.h"
+#include "src/tint/ir/value.h"
 #include "src/tint/program.h"
+#include "src/tint/scope_stack.h"
+#include "src/tint/sem/builtin.h"
+#include "src/tint/sem/call.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/materialize.h"
+#include "src/tint/sem/module.h"
+#include "src/tint/sem/switch_statement.h"
+#include "src/tint/sem/value_constructor.h"
+#include "src/tint/sem/value_conversion.h"
+#include "src/tint/sem/value_expression.h"
+#include "src/tint/sem/variable.h"
+#include "src/tint/switch.h"
+#include "src/tint/type/void.h"
+#include "src/tint/utils/defer.h"
+#include "src/tint/utils/result.h"
+#include "src/tint/utils/scoped_assignment.h"
+
+using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::ir {
 
+namespace {
+
+using ResultType = utils::Result<Module, diag::List>;
+
+bool IsBranched(const Block* b) {
+    return b->branch.target != nullptr;
+}
+
+bool IsConnected(const FlowNode* b) {
+    // Function is always connected as it's the start.
+    if (b->Is<ir::Function>()) {
+        return true;
+    }
+
+    for (auto* parent : b->inbound_branches) {
+        if (IsConnected(parent)) {
+            return true;
+        }
+    }
+    // Getting here means all the incoming branches are disconnected.
+    return false;
+}
+
+/// Impl is the private-implementation of FromProgram().
+class Impl {
+  public:
+    /// Constructor
+    /// @param program the program to convert to IR
+    explicit Impl(const Program* program) : program_(program) {}
+
+    /// Builds an IR module from the program passed to the constructor.
+    /// @return the IR module or an error.
+    ResultType Build() { return EmitModule(); }
+
+  private:
+    enum class ControlFlags { kNone, kExcludeSwitch };
+
+    // The input Program
+    const Program* program_ = nullptr;
+
+    /// The IR module being built
+    Module mod;
+
+    /// The IR builder being used by the impl.
+    Builder builder_{mod};
+
+    // The clone context used to clone data from #program_
+    constant::CloneContext clone_ctx_{
+        /* type_ctx */ type::CloneContext{
+            /* src */ {&program_->Symbols()},
+            /* dst */ {&builder_.ir.symbols, &builder_.ir.types},
+        },
+        /* dst */ {&builder_.ir.constants},
+    };
+
+    /// The stack of flow control blocks.
+    utils::Vector<FlowNode*, 8> flow_stack_;
+
+    /// The current flow block for expressions.
+    Block* current_flow_block_ = nullptr;
+
+    /// The current function being processed.
+    Function* current_function_ = nullptr;
+
+    /// The current stack of scopes being processed.
+    ScopeStack<Symbol, Value*> scopes_;
+
+    /// The diagnostic that have been raised.
+    diag::List diagnostics_;
+
+    /// Map from ast nodes to flow nodes, used to retrieve the flow node for a given AST node.
+    /// Used for testing purposes.
+    std::unordered_map<const ast::Node*, const FlowNode*> ast_to_flow_;
+
+    class FlowStackScope {
+      public:
+        FlowStackScope(Impl* impl, FlowNode* node) : impl_(impl) { impl_->flow_stack_.Push(node); }
+
+        ~FlowStackScope() { impl_->flow_stack_.Pop(); }
+
+      private:
+        Impl* impl_;
+    };
+
+    void add_error(const Source& s, const std::string& err) {
+        diagnostics_.add_error(tint::diag::System::IR, err, s);
+    }
+
+    void BranchTo(FlowNode* node, utils::VectorRef<Value*> args = {}) {
+        TINT_ASSERT(IR, current_flow_block_);
+        TINT_ASSERT(IR, !IsBranched(current_flow_block_));
+
+        builder_.Branch(current_flow_block_, node, args);
+        current_flow_block_ = nullptr;
+    }
+
+    void BranchToIfNeeded(FlowNode* node) {
+        if (!current_flow_block_ || IsBranched(current_flow_block_)) {
+            return;
+        }
+        BranchTo(node);
+    }
+
+    FlowNode* FindEnclosingControl(ControlFlags flags) {
+        for (auto it = flow_stack_.rbegin(); it != flow_stack_.rend(); ++it) {
+            if ((*it)->Is<Loop>()) {
+                return *it;
+            }
+            if (flags == ControlFlags::kExcludeSwitch) {
+                continue;
+            }
+            if ((*it)->Is<Switch>()) {
+                return *it;
+            }
+        }
+        return nullptr;
+    }
+
+    Symbol CloneSymbol(Symbol sym) const {
+        return clone_ctx_.type_ctx.dst.st->Register(sym.Name());
+    }
+
+    ResultType EmitModule() {
+        auto* sem = program_->Sem().Module();
+
+        for (auto* decl : sem->DependencyOrderedDeclarations()) {
+            tint::Switch(
+                decl,  //
+                [&](const ast::Struct*) {
+                    // Will be encoded into the `type::Struct` when used. We will then hoist all
+                    // used structs up to module scope when converting IR.
+                },
+                [&](const ast::Alias*) {
+                    // Folded away and doesn't appear in the IR.
+                },
+                [&](const ast::Variable* var) {
+                    // Setup the current flow node to be the root block for the module. The builder
+                    // will handle creating it if it doesn't exist already.
+                    TINT_SCOPED_ASSIGNMENT(current_flow_block_, builder_.CreateRootBlockIfNeeded());
+                    EmitVariable(var);
+                },
+                [&](const ast::Function* func) { EmitFunction(func); },
+                [&](const ast::Enable*) {
+                    // TODO(dsinclair): Implement? I think these need to be passed along so further
+                    // stages know what is enabled.
+                },
+                [&](const ast::ConstAssert*) {
+                    // Evaluated by the resolver, drop from the IR.
+                },
+                [&](Default) {
+                    add_error(decl->source, "unknown type: " + std::string(decl->TypeInfo().name));
+                });
+        }
+
+        if (diagnostics_.contains_errors()) {
+            return ResultType(std::move(diagnostics_));
+        }
+
+        return ResultType{std::move(mod)};
+    }
+
+    void EmitFunction(const ast::Function* ast_func) {
+        // The flow stack should have been emptied when the previous function finished building.
+        TINT_ASSERT(IR, flow_stack_.IsEmpty());
+
+        const auto* sem = program_->Sem().Get(ast_func);
+
+        auto* ir_func = builder_.CreateFunction(CloneSymbol(ast_func->name->symbol),
+                                                sem->ReturnType()->Clone(clone_ctx_.type_ctx));
+        current_function_ = ir_func;
+        builder_.ir.functions.Push(ir_func);
+
+        ast_to_flow_[ast_func] = ir_func;
+
+        if (ast_func->IsEntryPoint()) {
+            switch (ast_func->PipelineStage()) {
+                case ast::PipelineStage::kVertex:
+                    ir_func->pipeline_stage = Function::PipelineStage::kVertex;
+                    break;
+                case ast::PipelineStage::kFragment:
+                    ir_func->pipeline_stage = Function::PipelineStage::kFragment;
+                    break;
+                case ast::PipelineStage::kCompute: {
+                    ir_func->pipeline_stage = Function::PipelineStage::kCompute;
+
+                    auto wg_size = sem->WorkgroupSize();
+                    ir_func->workgroup_size = {
+                        wg_size[0].value(),
+                        wg_size[1].value_or(1),
+                        wg_size[2].value_or(1),
+                    };
+                    break;
+                }
+                default: {
+                    TINT_ICE(IR, diagnostics_) << "Invalid pipeline stage";
+                    return;
+                }
+            }
+
+            for (auto* attr : ast_func->return_type_attributes) {
+                tint::Switch(
+                    attr,  //
+                    [&](const ast::LocationAttribute*) {
+                        ir_func->return_attributes.Push(Function::ReturnAttribute::kLocation);
+                    },
+                    [&](const ast::InvariantAttribute*) {
+                        ir_func->return_attributes.Push(Function::ReturnAttribute::kInvariant);
+                    },
+                    [&](const ast::BuiltinAttribute* b) {
+                        if (auto* ident_sem =
+                                program_->Sem()
+                                    .Get(b)
+                                    ->As<sem::BuiltinEnumExpression<builtin::BuiltinValue>>()) {
+                            switch (ident_sem->Value()) {
+                                case builtin::BuiltinValue::kPosition:
+                                    ir_func->return_attributes.Push(
+                                        Function::ReturnAttribute::kPosition);
+                                    break;
+                                case builtin::BuiltinValue::kFragDepth:
+                                    ir_func->return_attributes.Push(
+                                        Function::ReturnAttribute::kFragDepth);
+                                    break;
+                                case builtin::BuiltinValue::kSampleMask:
+                                    ir_func->return_attributes.Push(
+                                        Function::ReturnAttribute::kSampleMask);
+                                    break;
+                                default:
+                                    TINT_ICE(IR, diagnostics_)
+                                        << "Unknown builtin value in return attributes "
+                                        << ident_sem->Value();
+                                    return;
+                            }
+                        } else {
+                            TINT_ICE(IR, diagnostics_) << "Builtin attribute sem invalid";
+                            return;
+                        }
+                    });
+            }
+        }
+        ir_func->return_location = sem->ReturnLocation();
+
+        {
+            FlowStackScope scope(this, ir_func);
+
+            current_flow_block_ = ir_func->start_target;
+            EmitBlock(ast_func->body);
+
+            // TODO(dsinclair): Store return type and attributes
+            // TODO(dsinclair): Store parameters
+
+            // If the branch target has already been set then a `return` was called. Only set in the
+            // case where `return` wasn't called.
+            BranchToIfNeeded(current_function_->end_target);
+        }
+
+        TINT_ASSERT(IR, flow_stack_.IsEmpty());
+        current_flow_block_ = nullptr;
+        current_function_ = nullptr;
+    }
+
+    void EmitStatements(utils::VectorRef<const ast::Statement*> stmts) {
+        for (auto* s : stmts) {
+            EmitStatement(s);
+
+            // If the current flow block has a branch target then the rest of the statements in this
+            // block are dead code. Skip them.
+            if (!current_flow_block_ || IsBranched(current_flow_block_)) {
+                break;
+            }
+        }
+    }
+
+    void EmitStatement(const ast::Statement* stmt) {
+        tint::Switch(
+            stmt,  //
+            [&](const ast::AssignmentStatement* a) { EmitAssignment(a); },
+            [&](const ast::BlockStatement* b) { EmitBlock(b); },
+            [&](const ast::BreakStatement* b) { EmitBreak(b); },
+            [&](const ast::BreakIfStatement* b) { EmitBreakIf(b); },
+            [&](const ast::CallStatement* c) { EmitCall(c); },
+            [&](const ast::CompoundAssignmentStatement* c) { EmitCompoundAssignment(c); },
+            [&](const ast::ContinueStatement* c) { EmitContinue(c); },
+            [&](const ast::DiscardStatement* d) { EmitDiscard(d); },
+            [&](const ast::IfStatement* i) { EmitIf(i); },
+            [&](const ast::LoopStatement* l) { EmitLoop(l); },
+            [&](const ast::ForLoopStatement* l) { EmitForLoop(l); },
+            [&](const ast::WhileStatement* l) { EmitWhile(l); },
+            [&](const ast::ReturnStatement* r) { EmitReturn(r); },
+            [&](const ast::SwitchStatement* s) { EmitSwitch(s); },
+            [&](const ast::VariableDeclStatement* v) { EmitVariable(v->variable); },
+            [&](const ast::IncrementDecrementStatement* i) { EmitIncrementDecrement(i); },
+            [&](const ast::ConstAssert*) {
+                // Not emitted
+            },
+            [&](Default) {
+                add_error(stmt->source,
+                          "unknown statement type: " + std::string(stmt->TypeInfo().name));
+            });
+    }
+
+    void EmitAssignment(const ast::AssignmentStatement* stmt) {
+        auto lhs = EmitExpression(stmt->lhs);
+        if (!lhs) {
+            return;
+        }
+
+        auto rhs = EmitExpression(stmt->rhs);
+        if (!rhs) {
+            return;
+        }
+        auto store = builder_.Store(lhs.Get(), rhs.Get());
+        current_flow_block_->instructions.Push(store);
+    }
+
+    void EmitIncrementDecrement(const ast::IncrementDecrementStatement* stmt) {
+        auto lhs = EmitExpression(stmt->lhs);
+        if (!lhs) {
+            return;
+        }
+
+        auto* ty = lhs.Get()->Type();
+        auto* rhs = ty->UnwrapRef()->is_signed_integer_scalar() ? builder_.Constant(1_i)
+                                                                : builder_.Constant(1_u);
+
+        Binary* inst = nullptr;
+        if (stmt->increment) {
+            inst = builder_.Add(ty, lhs.Get(), rhs);
+        } else {
+            inst = builder_.Subtract(ty, lhs.Get(), rhs);
+        }
+        current_flow_block_->instructions.Push(inst);
+
+        auto store = builder_.Store(lhs.Get(), inst);
+        current_flow_block_->instructions.Push(store);
+    }
+
+    void EmitCompoundAssignment(const ast::CompoundAssignmentStatement* stmt) {
+        auto lhs = EmitExpression(stmt->lhs);
+        if (!lhs) {
+            return;
+        }
+
+        auto rhs = EmitExpression(stmt->rhs);
+        if (!rhs) {
+            return;
+        }
+        auto* ty = lhs.Get()->Type();
+        Binary* inst = nullptr;
+        switch (stmt->op) {
+            case ast::BinaryOp::kAnd:
+                inst = builder_.And(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kOr:
+                inst = builder_.Or(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kXor:
+                inst = builder_.Xor(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kShiftLeft:
+                inst = builder_.ShiftLeft(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kShiftRight:
+                inst = builder_.ShiftRight(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kAdd:
+                inst = builder_.Add(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kSubtract:
+                inst = builder_.Subtract(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kMultiply:
+                inst = builder_.Multiply(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kDivide:
+                inst = builder_.Divide(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kModulo:
+                inst = builder_.Modulo(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kLessThanEqual:
+            case ast::BinaryOp::kGreaterThanEqual:
+            case ast::BinaryOp::kGreaterThan:
+            case ast::BinaryOp::kLessThan:
+            case ast::BinaryOp::kNotEqual:
+            case ast::BinaryOp::kEqual:
+            case ast::BinaryOp::kLogicalAnd:
+            case ast::BinaryOp::kLogicalOr:
+                TINT_ICE(IR, diagnostics_) << "invalid compound assignment";
+                return;
+            case ast::BinaryOp::kNone:
+                TINT_ICE(IR, diagnostics_) << "missing binary operand type";
+                return;
+        }
+        current_flow_block_->instructions.Push(inst);
+
+        auto store = builder_.Store(lhs.Get(), inst);
+        current_flow_block_->instructions.Push(store);
+    }
+
+    void EmitBlock(const ast::BlockStatement* block) {
+        scopes_.Push();
+        TINT_DEFER(scopes_.Pop());
+
+        // Note, this doesn't need to emit a Block as the current block flow node should be
+        // sufficient as the blocks all get flattened. Each flow control node will inject the basic
+        // blocks it requires.
+        EmitStatements(block->statements);
+    }
+
+    void EmitIf(const ast::IfStatement* stmt) {
+        // Emit the if condition into the end of the preceding block
+        auto reg = EmitExpression(stmt->condition);
+        if (!reg) {
+            return;
+        }
+        auto* if_node = builder_.CreateIf(reg.Get());
+
+        BranchTo(if_node);
+
+        ast_to_flow_[stmt] = if_node;
+
+        {
+            FlowStackScope scope(this, if_node);
+
+            current_flow_block_ = if_node->true_.target->As<Block>();
+            EmitBlock(stmt->body);
+
+            // If the true branch did not execute control flow, then go to the merge target
+            BranchToIfNeeded(if_node->merge.target);
+
+            current_flow_block_ = if_node->false_.target->As<Block>();
+            if (stmt->else_statement) {
+                EmitStatement(stmt->else_statement);
+            }
+
+            // If the false branch did not execute control flow, then go to the merge target
+            BranchToIfNeeded(if_node->merge.target);
+        }
+        current_flow_block_ = nullptr;
+
+        // If both branches went somewhere, then they both returned, continued or broke. So, there
+        // is no need for the if merge-block and there is nothing to branch to the merge block
+        // anyway.
+        if (IsConnected(if_node->merge.target)) {
+            current_flow_block_ = if_node->merge.target->As<Block>();
+        }
+    }
+
+    void EmitLoop(const ast::LoopStatement* stmt) {
+        auto* loop_node = builder_.CreateLoop();
+
+        BranchTo(loop_node);
+
+        ast_to_flow_[stmt] = loop_node;
+
+        {
+            FlowStackScope scope(this, loop_node);
+
+            current_flow_block_ = loop_node->start.target->As<Block>();
+
+            // The loop doesn't use EmitBlock because it needs the scope stack to not get popped
+            // until after the continuing block.
+            scopes_.Push();
+            TINT_DEFER(scopes_.Pop());
+            EmitStatements(stmt->body->statements);
+
+            // The current block didn't `break`, `return` or `continue`, go to the continuing block.
+            BranchToIfNeeded(loop_node->continuing.target);
+
+            current_flow_block_ = loop_node->continuing.target->As<Block>();
+            if (stmt->continuing) {
+                EmitBlock(stmt->continuing);
+            }
+
+            // Branch back to the start node if the continue target didn't branch out already
+            BranchToIfNeeded(loop_node->start.target);
+        }
+
+        // The loop merge can get disconnected if the loop returns directly, or the continuing
+        // target branches, eventually, to the merge, but nothing branched to the continuing target.
+        current_flow_block_ = loop_node->merge.target->As<Block>();
+        if (!IsConnected(loop_node->merge.target)) {
+            current_flow_block_ = nullptr;
+        }
+    }
+
+    void EmitWhile(const ast::WhileStatement* stmt) {
+        auto* loop_node = builder_.CreateLoop();
+        // Continue is always empty, just go back to the start
+        TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
+        builder_.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
+                        utils::Empty);
+
+        BranchTo(loop_node);
+
+        ast_to_flow_[stmt] = loop_node;
+
+        {
+            FlowStackScope scope(this, loop_node);
+
+            current_flow_block_ = loop_node->start.target->As<Block>();
+
+            // Emit the while condition into the start target of the loop
+            auto reg = EmitExpression(stmt->condition);
+            if (!reg) {
+                return;
+            }
+
+            // Create an `if (cond) {} else {break;}` control flow
+            auto* if_node = builder_.CreateIf(reg.Get());
+            TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
+            builder_.Branch(if_node->true_.target->As<Block>(), if_node->merge.target,
+                            utils::Empty);
+
+            TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
+            builder_.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target,
+                            utils::Empty);
+
+            BranchTo(if_node);
+
+            current_flow_block_ = if_node->merge.target->As<Block>();
+            EmitBlock(stmt->body);
+
+            BranchToIfNeeded(loop_node->continuing.target);
+        }
+        // The while loop always has a path to the merge target as the break statement comes before
+        // anything inside the loop.
+        current_flow_block_ = loop_node->merge.target->As<Block>();
+    }
+
+    void EmitForLoop(const ast::ForLoopStatement* stmt) {
+        auto* loop_node = builder_.CreateLoop();
+        TINT_ASSERT(IR, loop_node->continuing.target->Is<Block>());
+        builder_.Branch(loop_node->continuing.target->As<Block>(), loop_node->start.target,
+                        utils::Empty);
+
+        // Make sure the initializer ends up in a contained scope
+        scopes_.Push();
+        TINT_DEFER(scopes_.Pop());
+
+        if (stmt->initializer) {
+            // Emit the for initializer before branching to the loop
+            EmitStatement(stmt->initializer);
+        }
+
+        BranchTo(loop_node);
+
+        ast_to_flow_[stmt] = loop_node;
+
+        {
+            FlowStackScope scope(this, loop_node);
+
+            current_flow_block_ = loop_node->start.target->As<Block>();
+
+            if (stmt->condition) {
+                // Emit the condition into the target target of the loop
+                auto reg = EmitExpression(stmt->condition);
+                if (!reg) {
+                    return;
+                }
+
+                // Create an `if (cond) {} else {break;}` control flow
+                auto* if_node = builder_.CreateIf(reg.Get());
+                TINT_ASSERT(IR, if_node->true_.target->Is<Block>());
+                builder_.Branch(if_node->true_.target->As<Block>(), if_node->merge.target,
+                                utils::Empty);
+
+                TINT_ASSERT(IR, if_node->false_.target->Is<Block>());
+                builder_.Branch(if_node->false_.target->As<Block>(), loop_node->merge.target,
+                                utils::Empty);
+
+                BranchTo(if_node);
+                current_flow_block_ = if_node->merge.target->As<Block>();
+            }
+
+            EmitBlock(stmt->body);
+            BranchToIfNeeded(loop_node->continuing.target);
+
+            if (stmt->continuing) {
+                current_flow_block_ = loop_node->continuing.target->As<Block>();
+                EmitStatement(stmt->continuing);
+            }
+        }
+
+        // The while loop always has a path to the merge target as the break statement comes before
+        // anything inside the loop.
+        current_flow_block_ = loop_node->merge.target->As<Block>();
+    }
+
+    void EmitSwitch(const ast::SwitchStatement* stmt) {
+        // Emit the condition into the preceding block
+        auto reg = EmitExpression(stmt->condition);
+        if (!reg) {
+            return;
+        }
+        auto* switch_node = builder_.CreateSwitch(reg.Get());
+
+        BranchTo(switch_node);
+
+        ast_to_flow_[stmt] = switch_node;
+
+        {
+            FlowStackScope scope(this, switch_node);
+
+            const auto* sem = program_->Sem().Get(stmt);
+            for (const auto* c : sem->Cases()) {
+                utils::Vector<Switch::CaseSelector, 4> selectors;
+                for (const auto* selector : c->Selectors()) {
+                    if (selector->IsDefault()) {
+                        selectors.Push({nullptr});
+                    } else {
+                        selectors.Push({builder_.Constant(selector->Value()->Clone(clone_ctx_))});
+                    }
+                }
+
+                current_flow_block_ = builder_.CreateCase(switch_node, selectors);
+                EmitBlock(c->Body()->Declaration());
+
+                BranchToIfNeeded(switch_node->merge.target);
+            }
+        }
+        current_flow_block_ = nullptr;
+
+        if (IsConnected(switch_node->merge.target)) {
+            current_flow_block_ = switch_node->merge.target->As<Block>();
+        }
+    }
+
+    void EmitReturn(const ast::ReturnStatement* stmt) {
+        utils::Vector<Value*, 1> ret_value;
+        if (stmt->value) {
+            auto ret = EmitExpression(stmt->value);
+            if (!ret) {
+                return;
+            }
+            ret_value.Push(ret.Get());
+        }
+
+        BranchTo(current_function_->end_target, std::move(ret_value));
+    }
+
+    void EmitBreak(const ast::BreakStatement*) {
+        auto* current_control = FindEnclosingControl(ControlFlags::kNone);
+        TINT_ASSERT(IR, current_control);
+
+        if (auto* c = current_control->As<Loop>()) {
+            BranchTo(c->merge.target);
+        } else if (auto* s = current_control->As<Switch>()) {
+            BranchTo(s->merge.target);
+        } else {
+            TINT_UNREACHABLE(IR, diagnostics_);
+        }
+    }
+
+    void EmitContinue(const ast::ContinueStatement*) {
+        auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
+        TINT_ASSERT(IR, current_control);
+
+        if (auto* c = current_control->As<Loop>()) {
+            BranchTo(c->continuing.target);
+        } else {
+            TINT_UNREACHABLE(IR, diagnostics_);
+        }
+    }
+
+    // Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
+    // code has to continue as before it just predicates writes. If WGSL grows some kind of
+    // terminating discard that would probably make sense as a FlowNode but would then require
+    // figuring out the multi-level exit that is triggered.
+    void EmitDiscard(const ast::DiscardStatement*) {
+        auto* inst = builder_.Discard();
+        current_flow_block_->instructions.Push(inst);
+    }
+
+    void EmitBreakIf(const ast::BreakIfStatement* stmt) {
+        // Emit the break-if condition into the end of the preceding block
+        auto reg = EmitExpression(stmt->condition);
+        if (!reg) {
+            return;
+        }
+        auto* if_node = builder_.CreateIf(reg.Get());
+
+        BranchTo(if_node);
+
+        ast_to_flow_[stmt] = if_node;
+
+        auto* current_control = FindEnclosingControl(ControlFlags::kExcludeSwitch);
+        TINT_ASSERT(IR, current_control);
+        TINT_ASSERT(IR, current_control->Is<Loop>());
+
+        auto* loop = current_control->As<Loop>();
+
+        current_flow_block_ = if_node->true_.target->As<Block>();
+        BranchTo(loop->merge.target);
+
+        current_flow_block_ = if_node->false_.target->As<Block>();
+        BranchTo(if_node->merge.target);
+
+        current_flow_block_ = if_node->merge.target->As<Block>();
+
+        // The `break-if` has to be the last item in the continuing block. The false branch of the
+        // `break-if` will always take us back to the start of the loop.
+        BranchTo(loop->start.target);
+    }
+
+    utils::Result<Value*> EmitExpression(const ast::Expression* expr) {
+        // If this is a value that has been const-eval'd return the result.
+        if (auto* sem = program_->Sem().Get(expr)->As<sem::ValueExpression>()) {
+            if (auto* v = sem->ConstantValue()) {
+                if (auto* cv = v->Clone(clone_ctx_)) {
+                    return builder_.Constant(cv);
+                }
+            }
+        }
+
+        return tint::Switch(
+            expr,
+            // [&](const ast::IndexAccessorExpression* a) {
+            // TODO(dsinclair): Implement
+            // },
+            [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
+            [&](const ast::BitcastExpression* b) { return EmitBitcast(b); },
+            [&](const ast::CallExpression* c) { return EmitCall(c); },
+            [&](const ast::IdentifierExpression* i) -> utils::Result<Value*> {
+                auto* v = scopes_.Get(i->identifier->symbol);
+                if (TINT_UNLIKELY(!v)) {
+                    add_error(expr->source,
+                              "unable to find identifier " + i->identifier->symbol.Name());
+                    return utils::Failure;
+                }
+                return {v};
+            },
+            [&](const ast::LiteralExpression* l) { return EmitLiteral(l); },
+            // [&](const ast::MemberAccessorExpression* m) {
+            // TODO(dsinclair): Implement
+            // },
+            // [&](const ast::PhonyExpression*) {
+            // TODO(dsinclair): Implement. The call may have side effects so has to be made.
+            // },
+            [&](const ast::UnaryOpExpression* u) { return EmitUnary(u); },
+            [&](Default) {
+                add_error(expr->source,
+                          "unknown expression type: " + std::string(expr->TypeInfo().name));
+                return utils::Failure;
+            });
+    }
+
+    void EmitVariable(const ast::Variable* var) {
+        auto* sem = program_->Sem().Get(var);
+
+        return tint::Switch(  //
+            var,
+            [&](const ast::Var* v) {
+                auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+                auto* val = builder_.Declare(ty, sem->AddressSpace(), sem->Access());
+                current_flow_block_->instructions.Push(val);
+
+                if (v->initializer) {
+                    auto init = EmitExpression(v->initializer);
+                    if (!init) {
+                        return;
+                    }
+                    val->initializer = init.Get();
+                }
+                // Store the declaration so we can get the instruction to store too
+                scopes_.Set(v->name->symbol, val);
+
+                // Record the original name of the var
+                builder_.ir.SetName(val, v->name->symbol.Name());
+            },
+            [&](const ast::Let* l) {
+                // A `let` doesn't exist as a standalone item in the IR, it's just the result of the
+                // initializer.
+                auto init = EmitExpression(l->initializer);
+                if (!init) {
+                    return;
+                }
+
+                // Store the results of the initialization
+                scopes_.Set(l->name->symbol, init.Get());
+
+                // Record the original name of the let
+                builder_.ir.SetName(init.Get(), l->name->symbol.Name());
+            },
+            [&](const ast::Override*) {
+                add_error(var->source,
+                          "found an `Override` variable. The SubstituteOverrides "
+                          "transform must be run before converting to IR");
+            },
+            [&](const ast::Const*) {
+                // Skip. This should be handled by const-eval already, so the const will be a
+                // `constant::` value at the usage sites. Can just ignore the `const` variable as it
+                // should never be used.
+                //
+                // TODO(dsinclair): Probably want to store the const variable somewhere and then in
+                // identifier expression log an error if we ever see a const identifier. Add this
+                // when identifiers and variables are supported.
+            },
+            [&](Default) {
+                add_error(var->source, "unknown variable: " + std::string(var->TypeInfo().name));
+            });
+    }
+
+    utils::Result<Value*> EmitUnary(const ast::UnaryOpExpression* expr) {
+        auto val = EmitExpression(expr->expr);
+        if (!val) {
+            return utils::Failure;
+        }
+
+        auto* sem = program_->Sem().Get(expr);
+        auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+
+        Instruction* inst = nullptr;
+        switch (expr->op) {
+            case ast::UnaryOp::kAddressOf:
+                inst = builder_.AddressOf(ty, val.Get());
+                break;
+            case ast::UnaryOp::kComplement:
+                inst = builder_.Complement(ty, val.Get());
+                break;
+            case ast::UnaryOp::kIndirection:
+                inst = builder_.Indirection(ty, val.Get());
+                break;
+            case ast::UnaryOp::kNegation:
+                inst = builder_.Negation(ty, val.Get());
+                break;
+            case ast::UnaryOp::kNot:
+                inst = builder_.Not(ty, val.Get());
+                break;
+        }
+
+        current_flow_block_->instructions.Push(inst);
+        return inst;
+    }
+
+    // A short-circut needs special treatment. The short-circuit is decomposed into the relevant if
+    // statements and declarations.
+    utils::Result<Value*> EmitShortCircuit(const ast::BinaryExpression* expr) {
+        switch (expr->op) {
+            case ast::BinaryOp::kLogicalAnd:
+            case ast::BinaryOp::kLogicalOr:
+                break;
+            default:
+                TINT_ICE(IR, diagnostics_)
+                    << "invalid operation type for short-circut decomposition";
+                return utils::Failure;
+        }
+
+        // Evaluate the LHS of the short-circuit
+        auto lhs = EmitExpression(expr->lhs);
+        if (!lhs) {
+            return utils::Failure;
+        }
+
+        // Generate a variable to store the short-circut into
+        auto* ty = builder_.ir.types.Get<type::Bool>();
+        auto* result_var =
+            builder_.Declare(ty, builtin::AddressSpace::kFunction, builtin::Access::kReadWrite);
+        current_flow_block_->instructions.Push(result_var);
+
+        auto* lhs_store = builder_.Store(result_var, lhs.Get());
+        current_flow_block_->instructions.Push(lhs_store);
+
+        auto* if_node = builder_.CreateIf(lhs.Get());
+        BranchTo(if_node);
+
+        utils::Result<Value*> rhs;
+        {
+            FlowStackScope scope(this, if_node);
+
+            // If this is an `&&` then we only evaluate the RHS expression in the true block.
+            // If this is an `||` then we only evaluate the RHS expression in the false block.
+            if (expr->op == ast::BinaryOp::kLogicalAnd) {
+                current_flow_block_ = if_node->true_.target->As<Block>();
+            } else {
+                current_flow_block_ = if_node->false_.target->As<Block>();
+            }
+
+            rhs = EmitExpression(expr->rhs);
+            if (!rhs) {
+                return utils::Failure;
+            }
+            auto* rhs_store = builder_.Store(result_var, rhs.Get());
+            current_flow_block_->instructions.Push(rhs_store);
+
+            BranchTo(if_node->merge.target);
+        }
+        current_flow_block_ = if_node->merge.target->As<Block>();
+
+        return result_var;
+    }
+
+    utils::Result<Value*> EmitBinary(const ast::BinaryExpression* expr) {
+        if (expr->op == ast::BinaryOp::kLogicalAnd || expr->op == ast::BinaryOp::kLogicalOr) {
+            return EmitShortCircuit(expr);
+        }
+
+        auto lhs = EmitExpression(expr->lhs);
+        if (!lhs) {
+            return utils::Failure;
+        }
+
+        auto rhs = EmitExpression(expr->rhs);
+        if (!rhs) {
+            return utils::Failure;
+        }
+
+        auto* sem = program_->Sem().Get(expr);
+        auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+
+        Binary* inst = nullptr;
+        switch (expr->op) {
+            case ast::BinaryOp::kAnd:
+                inst = builder_.And(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kOr:
+                inst = builder_.Or(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kXor:
+                inst = builder_.Xor(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kEqual:
+                inst = builder_.Equal(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kNotEqual:
+                inst = builder_.NotEqual(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kLessThan:
+                inst = builder_.LessThan(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kGreaterThan:
+                inst = builder_.GreaterThan(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kLessThanEqual:
+                inst = builder_.LessThanEqual(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kGreaterThanEqual:
+                inst = builder_.GreaterThanEqual(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kShiftLeft:
+                inst = builder_.ShiftLeft(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kShiftRight:
+                inst = builder_.ShiftRight(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kAdd:
+                inst = builder_.Add(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kSubtract:
+                inst = builder_.Subtract(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kMultiply:
+                inst = builder_.Multiply(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kDivide:
+                inst = builder_.Divide(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kModulo:
+                inst = builder_.Modulo(ty, lhs.Get(), rhs.Get());
+                break;
+            case ast::BinaryOp::kLogicalAnd:
+            case ast::BinaryOp::kLogicalOr:
+                TINT_ICE(IR, diagnostics_) << "short circuit op should have already been handled";
+                return utils::Failure;
+            case ast::BinaryOp::kNone:
+                TINT_ICE(IR, diagnostics_) << "missing binary operand type";
+                return utils::Failure;
+        }
+
+        current_flow_block_->instructions.Push(inst);
+        return inst;
+    }
+
+    utils::Result<Value*> EmitBitcast(const ast::BitcastExpression* expr) {
+        auto val = EmitExpression(expr->expr);
+        if (!val) {
+            return utils::Failure;
+        }
+
+        auto* sem = program_->Sem().Get(expr);
+        auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+        auto* inst = builder_.Bitcast(ty, val.Get());
+
+        current_flow_block_->instructions.Push(inst);
+        return inst;
+    }
+
+    void EmitCall(const ast::CallStatement* stmt) { (void)EmitCall(stmt->expr); }
+
+    utils::Result<Value*> EmitCall(const ast::CallExpression* expr) {
+        // If this is a materialized semantic node, just use the constant value.
+        if (auto* mat = program_->Sem().Get(expr)) {
+            if (mat->ConstantValue()) {
+                auto* cv = mat->ConstantValue()->Clone(clone_ctx_);
+                if (!cv) {
+                    add_error(expr->source, "failed to get constant value for call " +
+                                                std::string(expr->TypeInfo().name));
+                    return utils::Failure;
+                }
+                return builder_.Constant(cv);
+            }
+        }
+
+        utils::Vector<Value*, 8> args;
+        args.Reserve(expr->args.Length());
+
+        // Emit the arguments
+        for (const auto* arg : expr->args) {
+            auto value = EmitExpression(arg);
+            if (!value) {
+                add_error(arg->source, "failed to convert arguments");
+                return utils::Failure;
+            }
+            args.Push(value.Get());
+        }
+
+        auto* sem = program_->Sem().Get<sem::Call>(expr);
+        if (!sem) {
+            add_error(expr->source, "failed to get semantic information for call " +
+                                        std::string(expr->TypeInfo().name));
+            return utils::Failure;
+        }
+
+        auto* ty = sem->Target()->ReturnType()->Clone(clone_ctx_.type_ctx);
+
+        Instruction* inst = nullptr;
+
+        // If this is a builtin function, emit the specific builtin value
+        if (auto* b = sem->Target()->As<sem::Builtin>()) {
+            inst = builder_.Builtin(ty, b->Type(), args);
+        } else if (sem->Target()->As<sem::ValueConstructor>()) {
+            inst = builder_.Construct(ty, std::move(args));
+        } else if (auto* conv = sem->Target()->As<sem::ValueConversion>()) {
+            auto* from = conv->Source()->Clone(clone_ctx_.type_ctx);
+            inst = builder_.Convert(ty, from, std::move(args));
+        } else if (expr->target->identifier->Is<ast::TemplatedIdentifier>()) {
+            TINT_UNIMPLEMENTED(IR, diagnostics_) << "missing templated ident support";
+            return utils::Failure;
+        } else {
+            // Not a builtin and not a templated call, so this is a user function.
+            auto name = CloneSymbol(expr->target->identifier->symbol);
+            inst = builder_.UserCall(ty, name, std::move(args));
+        }
+        if (inst == nullptr) {
+            return utils::Failure;
+        }
+        current_flow_block_->instructions.Push(inst);
+        return inst;
+    }
+
+    utils::Result<Value*> EmitLiteral(const ast::LiteralExpression* lit) {
+        auto* sem = program_->Sem().Get(lit);
+        if (!sem) {
+            add_error(lit->source, "failed to get semantic information for node " +
+                                       std::string(lit->TypeInfo().name));
+            return utils::Failure;
+        }
+
+        auto* cv = sem->ConstantValue()->Clone(clone_ctx_);
+        if (!cv) {
+            add_error(lit->source,
+                      "failed to get constant value for node " + std::string(lit->TypeInfo().name));
+            return utils::Failure;
+        }
+        return builder_.Constant(cv);
+    }
+
+    //    void EmitAttributes(utils::VectorRef<const ast::Attribute*> attrs) {
+    //        for (auto* attr : attrs) {
+    //            EmitAttribute(attr);
+    //        }
+    //    }
+    //
+    //    void EmitAttribute(const ast::Attribute* attr) {
+    //        tint::Switch(  //
+    //            attr,
+    //            [&](const ast::WorkgroupAttribute* wg) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::StageAttribute* s) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::BindingAttribute* b) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::GroupAttribute* g) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::LocationAttribute* l) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::BuiltinAttribute* b) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::InterpolateAttribute* i) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::InvariantAttribute* i) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::MustUseAttribute* i) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::IdAttribute*) {
+    //                add_error(attr->source,
+    //                          "found an `Id` attribute. The SubstituteOverrides transform "
+    //                          "must be run before converting to IR");
+    //            },
+    //            [&](const ast::StructMemberSizeAttribute*) {
+    //                TINT_ICE(IR, diagnostics_)
+    //                    << "StructMemberSizeAttribute encountered during IR conversion";
+    //            },
+    //            [&](const ast::StructMemberAlignAttribute*) {
+    //                TINT_ICE(IR, diagnostics_)
+    //                    << "StructMemberAlignAttribute encountered during IR conversion";
+    //            },
+    //            [&](const ast::StrideAttribute* s) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](const ast::InternalAttribute *i) {
+    //              // TODO(dsinclair): Implement
+    //            },
+    //            [&](Default) {
+    //                add_error(attr->source, "unknown attribute: " +
+    //                std::string(attr->TypeInfo().name));
+    //            });
+    //    }
+};
+
+}  // namespace
+
 utils::Result<Module, std::string> FromProgram(const Program* program) {
     if (!program->IsValid()) {
         return std::string("input program is not valid");
     }
 
-    BuilderImpl b(program);
+    Impl b(program);
     auto r = b.Build();
     if (!r) {
-        return b.Diagnostics().str();
+        return r.Failure().str();
     }
 
     return r.Move();
diff --git a/src/tint/ir/from_program_binary_test.cc b/src/tint/ir/from_program_binary_test.cc
new file mode 100644
index 0000000..33266cb
--- /dev/null
+++ b/src/tint/ir/from_program_binary_test.cc
@@ -0,0 +1,808 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/test_helper.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/ast/case_selector.h"
+#include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/constant/scalar.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_BuilderImplTest = TestHelper;
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Add(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = add %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Increment) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = Increment("v1");
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = add %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAdd) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kAdd);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = add %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Subtract) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Sub(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = sub %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Decrement) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
+    auto* expr = Decrement("v1");
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, i32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, i32, read_write> = sub %v1:ref<private, i32, read_write>, 1i
+    store %v1:ref<private, i32, read_write>, %2:ref<private, i32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundSubtract) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kSubtract);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = sub %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Multiply) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Mul(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = mul %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundMultiply) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kMultiply);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = mul %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Div) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Div(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = div %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundDiv) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kDivide);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = div %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Modulo) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Mod(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = mod %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundModulo) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kModulo);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = mod %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_And) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = And(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = and %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundAnd) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
+    auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kAnd);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, bool, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, bool, read_write> = and %v1:ref<private, bool, read_write>, false
+    store %v1:ref<private, bool, read_write>, %2:ref<private, bool, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Or) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Or(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = or %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundOr) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.bool_());
+    auto* expr = CompoundAssign("v1", false, ast::BinaryOp::kOr);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, bool, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, bool, read_write> = or %v1:ref<private, bool, read_write>, false
+    store %v1:ref<private, bool, read_write>, %2:ref<private, bool, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Xor) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Xor(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = xor %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundXor) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kXor);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = xor %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalAnd) {
+    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = LogicalAnd(Call("my_func"), false);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
+  %fn2 = block {
+  } -> %func_end true # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:bool = call my_func
+    %tint_symbol:bool = var function, read_write
+    store %tint_symbol:bool, %1:bool
+  } -> %fn5 # branch
+
+  %fn5 = if %1:bool [t: %fn6, f: %fn7, m: %fn8]
+    # true branch
+    %fn6 = block {
+      store %tint_symbol:bool, false
+    } -> %fn8 # branch
+
+  # if merge
+  %fn8 = block {
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LogicalOr) {
+    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = LogicalOr(Call("my_func"), true);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
+  %fn2 = block {
+  } -> %func_end true # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:bool = call my_func
+    %tint_symbol:bool = var function, read_write
+    store %tint_symbol:bool, %1:bool
+  } -> %fn5 # branch
+
+  %fn5 = if %1:bool [t: %fn6, f: %fn7, m: %fn8]
+    # true branch
+    # false branch
+    %fn7 = block {
+      store %tint_symbol:bool, true
+    } -> %fn8 # branch
+
+  # if merge
+  %fn8 = block {
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Equal) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Equal(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = eq %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_NotEqual) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = NotEqual(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = neq %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThan) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = LessThan(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = lt %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThan) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = GreaterThan(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = gt %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_LessThanEqual) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = LessThanEqual(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = lte %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_GreaterThanEqual) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = GreaterThanEqual(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:bool = gte %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftLeft) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Shl(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = shiftl %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftLeft) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftLeft);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = shiftl %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_ShiftRight) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
+    auto* expr = Shr(Call("my_func"), 4_u);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 0u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = shiftr %1:u32, 4u
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_CompoundShiftRight) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.u32());
+    auto* expr = CompoundAssign("v1", 1_u, ast::BinaryOp::kShiftRight);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, u32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %2:ref<private, u32, read_write> = shiftr %v1:ref<private, u32, read_write>, 1u
+    store %v1:ref<private, u32, read_write>, %2:ref<private, u32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound) {
+    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
+
+    auto* expr = LogicalAnd(LessThan(Call("my_func"), 2_f),
+                            GreaterThan(2.5_f, Div(Call("my_func"), Mul(2.3_f, Call("my_func")))));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 {
+  %fn2 = block {
+  } -> %func_end 0.0f # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:f32 = call my_func
+    %2:bool = lt %1:f32, 2.0f
+    %tint_symbol:bool = var function, read_write
+    store %tint_symbol:bool, %2:bool
+  } -> %fn5 # branch
+
+  %fn5 = if %2:bool [t: %fn6, f: %fn7, m: %fn8]
+    # true branch
+    %fn6 = block {
+      %4:f32 = call my_func
+      %5:f32 = call my_func
+      %6:f32 = mul 2.29999995231628417969f, %5:f32
+      %7:f32 = div %4:f32, %6:f32
+      %8:bool = gt 2.5f, %7:f32
+      store %tint_symbol:bool, %8:bool
+    } -> %fn8 # branch
+
+  # if merge
+  %fn8 = block {
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Compound_WithConstEval) {
+    Func("my_func", utils::Vector{Param("p", ty.bool_())}, ty.bool_(), utils::Vector{Return(true)});
+    auto* expr = Call("my_func", LogicalAnd(LessThan(2.4_f, 2_f),
+                                            GreaterThan(2.5_f, Div(10_f, Mul(2.3_f, 9.4_f)))));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
+  %fn2 = block {
+  } -> %func_end true # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %tint_symbol:bool = call my_func, false
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/from_program_call_test.cc b/src/tint/ir/from_program_call_test.cc
new file mode 100644
index 0000000..ae217d1
--- /dev/null
+++ b/src/tint/ir/from_program_call_test.cc
@@ -0,0 +1,154 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/test_helper.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/ast/case_selector.h"
+#include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/constant/scalar.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_BuilderImplTest = TestHelper;
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Bitcast) {
+    Func("my_func", utils::Empty, ty.f32(), utils::Vector{Return(0_f)});
+
+    auto* expr = Bitcast<f32>(Call("my_func"));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():f32 {
+  %fn2 = block {
+  } -> %func_end 0.0f # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:f32 = call my_func
+    %tint_symbol:f32 = bitcast %1:f32
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitStatement_Discard) {
+    auto* expr = Discard();
+    Func("test_function", {}, ty.void_(), expr,
+         utils::Vector{
+             create<ast::StageAttribute>(ast::PipelineStage::kFragment),
+         });
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():void [@fragment] {
+  %fn2 = block {
+    discard
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
+    Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
+
+    auto* stmt = CallStmt(Call("my_func", Mul(2_a, 3_a)));
+    WrapInFunction(stmt);
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():void {
+  %fn2 = block {
+  } -> %func_end # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:void = call my_func, 6.0f
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Convert) {
+    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
+    auto* expr = Call(ty.f32(), i);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %i:ref<private, i32, read_write> = var private, read_write, 1i
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %tint_symbol:f32 = convert i32, %i:ref<private, i32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_ConstructEmpty) {
+    auto* expr = vec3(ty.f32());
+    GlobalVar("i", builtin::AddressSpace::kPrivate, expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %i:ref<private, vec3<f32>, read_write> = var private, read_write, vec3<f32> 0.0f
+}
+
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Construct) {
+    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
+    auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %i:ref<private, f32, read_write> = var private, read_write, 1.0f
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %tint_symbol:vec3<f32> = construct 2.0f, 3.0f, %i:ref<private, f32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl_literal_test.cc b/src/tint/ir/from_program_literal_test.cc
similarity index 61%
rename from src/tint/ir/builder_impl_literal_test.cc
rename to src/tint/ir/from_program_literal_test.cc
index 090f3f8..de5e81e 100644
--- a/src/tint/ir/builder_impl_literal_test.cc
+++ b/src/tint/ir/from_program_literal_test.cc
@@ -18,10 +18,26 @@
 #include "src/tint/ast/case_selector.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/constant/scalar.h"
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/constant.h"
+#include "src/tint/ir/var.h"
 
 namespace tint::ir {
 namespace {
 
+Value* GlobalVarInitializer(const Module& m) {
+    if (m.root_block->instructions.Length() == 0u) {
+        ADD_FAILURE() << "m.root_block has no instruction";
+        return nullptr;
+    }
+    auto* var = m.root_block->instructions[0]->As<ir::Var>();
+    if (!var) {
+        ADD_FAILURE() << "m.root_block.instructions[0] was not a var";
+        return nullptr;
+    }
+    return var->initializer;
+}
+
 using namespace tint::number_suffixes;  // NOLINT
 
 using IR_BuilderImplTest = TestHelper;
@@ -30,12 +46,12 @@
     auto* expr = Expr(true);
     GlobalVar("a", ty.bool_(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<bool>>());
     EXPECT_TRUE(val->As<constant::Scalar<bool>>()->ValueAs<bool>());
 }
@@ -44,12 +60,12 @@
     auto* expr = Expr(false);
     GlobalVar("a", ty.bool_(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<bool>>());
     EXPECT_FALSE(val->As<constant::Scalar<bool>>()->ValueAs<bool>());
 }
@@ -58,12 +74,12 @@
     auto* expr = Expr(1.2_f);
     GlobalVar("a", ty.f32(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<f32>>());
     EXPECT_EQ(1.2_f, val->As<constant::Scalar<f32>>()->ValueAs<f32>());
 }
@@ -73,12 +89,12 @@
     auto* expr = Expr(1.2_h);
     GlobalVar("a", ty.f16(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<f16>>());
     EXPECT_EQ(1.2_h, val->As<constant::Scalar<f16>>()->ValueAs<f32>());
 }
@@ -87,12 +103,12 @@
     auto* expr = Expr(-2_i);
     GlobalVar("a", ty.i32(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<i32>>());
     EXPECT_EQ(-2_i, val->As<constant::Scalar<i32>>()->ValueAs<f32>());
 }
@@ -101,12 +117,12 @@
     auto* expr = Expr(2_u);
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
 
-    auto& b = CreateBuilder();
-    auto r = b.EmitLiteral(expr);
-    ASSERT_THAT(b.Diagnostics(), testing::IsEmpty());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_TRUE(r.Get()->Is<Constant>());
-    auto* val = r.Get()->As<Constant>()->value;
+    auto* init = GlobalVarInitializer(m.Get());
+    ASSERT_TRUE(Is<Constant>(init));
+    auto* val = init->As<Constant>()->value;
     EXPECT_TRUE(val->Is<constant::Scalar<u32>>());
     EXPECT_EQ(2_u, val->As<constant::Scalar<u32>>()->ValueAs<f32>());
 }
diff --git a/src/tint/ir/builder_impl_materialize_test.cc b/src/tint/ir/from_program_materialize_test.cc
similarity index 84%
rename from src/tint/ir/builder_impl_materialize_test.cc
rename to src/tint/ir/from_program_materialize_test.cc
index 6b4ae84..ba293a7 100644
--- a/src/tint/ir/builder_impl_materialize_test.cc
+++ b/src/tint/ir/from_program_materialize_test.cc
@@ -31,14 +31,13 @@
 
     Func("test_function", {}, ty.f32(), expr, utils::Empty);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func test_function():f32
-  %fn2 = block
-  ret 2.0f
-func_end
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func test_function():f32 {
+  %fn2 = block {
+  } -> %func_end 2.0f # return
+} %func_end
 
 )");
 }
diff --git a/src/tint/ir/builder_impl_store_test.cc b/src/tint/ir/from_program_store_test.cc
similarity index 80%
rename from src/tint/ir/builder_impl_store_test.cc
rename to src/tint/ir/from_program_store_test.cc
index 82fbc0a..4e89dbe 100644
--- a/src/tint/ir/builder_impl_store_test.cc
+++ b/src/tint/ir/from_program_store_test.cc
@@ -32,20 +32,19 @@
     auto* expr = Assign("a", 4_u);
     WrapInFunction(expr);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%a:ref<private, u32, read_write> = var private, read_write
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %a:ref<private, u32, read_write> = var private, read_write
+}
 
 
-
-%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn3 = block
-  store %a:ref<private, u32, read_write>, 4u
-  ret
-func_end
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    store %a:ref<private, u32, read_write>, 4u
+  } -> %func_end # return
+} %func_end
 
 )");
 }
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/from_program_test.cc
similarity index 67%
rename from src/tint/ir/builder_impl_test.cc
rename to src/tint/ir/from_program_test.cc
index 36eb3b6..234b6cc 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/from_program_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Tint Authors.
+// 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.
@@ -18,34 +18,63 @@
 #include "src/tint/ast/case_selector.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/constant/scalar.h"
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/function_terminator.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/loop.h"
+#include "src/tint/ir/switch.h"
 
 namespace tint::ir {
 namespace {
 
+/// Looks for the flow node with the given type T.
+/// If no flow node is found, then nullptr is returned.
+/// If multiple flow nodes are found with the type T, then an error is raised and the first is
+/// returned.
+template <typename T>
+const T* FindSingleFlowNode(const Module& mod) {
+    const T* found = nullptr;
+    size_t count = 0;
+    for (auto* node : mod.flow_nodes.Objects()) {
+        if (auto* as = node->As<T>()) {
+            count++;
+            if (!found) {
+                found = as;
+            }
+        }
+    }
+    if (count > 1) {
+        ADD_FAILURE() << "FindSingleFlowNode() found " << count << " nodes of type "
+                      << utils::TypeInfo::Of<T>().name;
+    }
+    return found;
+}
+
 using namespace tint::number_suffixes;  // NOLINT
 
 using IR_BuilderImplTest = TestHelper;
 
 TEST_F(IR_BuilderImplTest, Func) {
     Func("f", utils::Empty, ty.void_(), utils::Empty);
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
 
-    ASSERT_EQ(0u, m.entry_points.Length());
-    ASSERT_EQ(1u, m.functions.Length());
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* f = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+
+    auto* f = m->functions[0];
     ASSERT_NE(f->start_target, nullptr);
     ASSERT_NE(f->end_target, nullptr);
 
     EXPECT_EQ(1u, f->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, f->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = func f():void
-  %fn2 = block
-  ret
-func_end
+    EXPECT_EQ(m->functions[0]->pipeline_stage, Function::PipelineStage::kUndefined);
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func f():void {
+  %fn2 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -53,33 +82,27 @@
 TEST_F(IR_BuilderImplTest, EntryPoint) {
     Func("f", utils::Empty, ty.void_(), utils::Empty,
          utils::Vector{Stage(ast::PipelineStage::kFragment)});
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
 
-    ASSERT_EQ(1u, m.entry_points.Length());
-    EXPECT_EQ(m.functions[0], m.entry_points[0]);
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(m->functions[0]->pipeline_stage, Function::PipelineStage::kFragment);
 }
 
 TEST_F(IR_BuilderImplTest, IfStatement) {
     auto* ast_if = If(true, Block(), Else(Block()));
     WrapInFunction(ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    EXPECT_TRUE(ir_if->Is<ir::If>());
-
-    auto* flow = ir_if->As<ir::If>();
+    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(flow->true_.target, nullptr);
     ASSERT_NE(flow->false_.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
@@ -88,24 +111,24 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
     # true branch
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     # false branch
-    %fn5 = block
-    branch %fn6
+    %fn5 = block {
+    } -> %fn6 # branch
 
   # if merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -114,21 +137,16 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    EXPECT_TRUE(ir_if->Is<ir::If>());
-
-    auto* flow = ir_if->As<ir::If>();
+    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(flow->true_.target, nullptr);
     ASSERT_NE(flow->false_.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
@@ -137,23 +155,23 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
     # true branch
-    %fn4 = block
-    ret
+    %fn4 = block {
+    } -> %func_end # return
     # false branch
-    %fn5 = block
-    branch %fn6
+    %fn5 = block {
+    } -> %fn6 # branch
 
   # if merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -162,21 +180,16 @@
     auto* ast_if = If(true, Block(), Else(Block(Return())));
     WrapInFunction(ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    EXPECT_TRUE(ir_if->Is<ir::If>());
-
-    auto* flow = ir_if->As<ir::If>();
+    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(flow->true_.target, nullptr);
     ASSERT_NE(flow->false_.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
@@ -185,23 +198,23 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
     # true branch
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     # false branch
-    %fn5 = block
-    ret
+    %fn5 = block {
+    } -> %func_end # return
   # if merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -210,21 +223,16 @@
     auto* ast_if = If(true, Block(Return()), Else(Block(Return())));
     WrapInFunction(ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    EXPECT_TRUE(ir_if->Is<ir::If>());
-
-    auto* flow = ir_if->As<ir::If>();
+    auto* flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(flow->true_.target, nullptr);
     ASSERT_NE(flow->false_.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(1u, flow->true_.target->inbound_branches.Length());
@@ -233,19 +241,19 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = if true [t: %fn4, f: %fn5]
     # true branch
-    %fn4 = block
-    ret
+    %fn4 = block {
+    } -> %func_end # return
     # false branch
-    %fn5 = block
-    ret
-func_end
+    %fn5 = block {
+    } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -255,55 +263,47 @@
     auto* ast_if = If(true, Block(ast_loop));
     WrapInFunction(ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    EXPECT_TRUE(ir_if->Is<ir::If>());
-
-    auto* if_flow = ir_if->As<ir::If>();
+    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(if_flow->true_.target, nullptr);
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
+    ASSERT_NE(loop_flow, nullptr);
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = if true [t: %fn4, f: %fn5, m: %fn6]
     # true branch
-    %fn4 = block
-    branch %fn7
+    %fn4 = block {
+    } -> %fn7 # branch
 
     %fn7 = loop [s: %fn8, m: %fn9]
       # loop start
-      %fn8 = block
-      branch %fn9
+      %fn8 = block {
+      } -> %fn9 # branch
 
     # loop merge
-    %fn9 = block
-    branch %fn6
+    %fn9 = block {
+    } -> %fn6 # branch
 
     # false branch
-    %fn5 = block
-    branch %fn6
+    %fn5 = block {
+    } -> %fn6 # branch
 
   # if merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -312,21 +312,16 @@
     auto* ast_loop = Loop(Block(Break()));
     WrapInFunction(ast_loop);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* flow = ir_loop->As<ir::Loop>();
+    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(flow->start.target, nullptr);
     ASSERT_NE(flow->continuing.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
@@ -335,20 +330,20 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, m: %fn5]
     # loop start
-    %fn4 = block
-    branch %fn5
+    %fn4 = block {
+    } -> %fn5 # branch
 
   # loop merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -358,30 +353,21 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    ASSERT_TRUE(ir_if->Is<ir::If>());
-
-    auto* if_flow = ir_if->As<ir::If>();
+    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(if_flow->true_.target, nullptr);
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -394,37 +380,37 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
     # loop start
-    %fn4 = block
-    branch %fn7
+    %fn4 = block {
+    } -> %fn7 # branch
 
     %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
       # true branch
-      %fn8 = block
-      branch %fn6
+      %fn8 = block {
+      } -> %fn6 # branch
 
       # false branch
-      %fn9 = block
-      branch %fn10
+      %fn9 = block {
+      } -> %fn10 # branch
 
     # if merge
-    %fn10 = block
-    branch %fn5
+    %fn10 = block {
+    } -> %fn5 # branch
 
     # loop continuing
-    %fn5 = block
-    branch %fn4
+    %fn5 = block {
+    } -> %fn4 # branch
 
   # loop merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -434,30 +420,21 @@
     auto* ast_loop = Loop(Block(), Block(ast_break_if));
     WrapInFunction(ast_loop);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_break_if = FlowNodeForAstNode(ast_break_if);
-    ASSERT_NE(ir_break_if, nullptr);
-    ASSERT_TRUE(ir_break_if->Is<ir::If>());
-
-    auto* break_if_flow = ir_break_if->As<ir::If>();
+    auto* break_if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(break_if_flow->true_.target, nullptr);
     ASSERT_NE(break_if_flow->false_.target, nullptr);
     ASSERT_NE(break_if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -470,37 +447,81 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
     # loop start
-    %fn4 = block
-    branch %fn5
+    %fn4 = block {
+    } -> %fn5 # branch
 
     # loop continuing
-    %fn5 = block
-    branch %fn7
+    %fn5 = block {
+    } -> %fn7 # branch
 
     %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
       # true branch
-      %fn8 = block
-      branch %fn6
+      %fn8 = block {
+      } -> %fn6 # branch
 
       # false branch
-      %fn9 = block
-      branch %fn10
+      %fn9 = block {
+      } -> %fn10 # branch
 
     # if merge
-    %fn10 = block
-    branch %fn4
+    %fn10 = block {
+    } -> %fn4 # branch
 
   # loop merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Loop_Continuing_Body_Scope) {
+    auto* a = Decl(Let("a", Expr(true)));
+    auto* ast_break_if = BreakIf("a");
+    auto* ast_loop = Loop(Block(a), Block(ast_break_if));
+    WrapInFunction(ast_loop);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
+
+  %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
+    # loop start
+    %fn4 = block {
+    } -> %fn5 # branch
+
+    # loop continuing
+    %fn5 = block {
+    } -> %fn7 # branch
+
+    %fn7 = if true [t: %fn8, f: %fn9, m: %fn10]
+      # true branch
+      %fn8 = block {
+      } -> %fn6 # branch
+
+      # false branch
+      %fn9 = block {
+      } -> %fn10 # branch
+
+    # if merge
+    %fn10 = block {
+    } -> %fn4 # branch
+
+  # loop merge
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -510,30 +531,21 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    ASSERT_TRUE(ir_if->Is<ir::If>());
-
-    auto* if_flow = ir_if->As<ir::If>();
+    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(if_flow->true_.target, nullptr);
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -546,33 +558,33 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, c: %fn5]
     # loop start
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
       # true branch
-      %fn7 = block
-      ret
+      %fn7 = block {
+      } -> %func_end # return
       # false branch
-      %fn8 = block
-      branch %fn9
+      %fn8 = block {
+      } -> %fn9 # branch
 
     # if merge
-    %fn9 = block
-    branch %fn5
+    %fn9 = block {
+    } -> %fn5 # branch
 
     # loop continuing
-    %fn5 = block
-    branch %fn4
+    %fn5 = block {
+    } -> %fn4 # branch
 
-func_end
+} %func_end
 
 )");
 }
@@ -581,21 +593,16 @@
     auto* ast_loop = Loop(Block(Return(), Continue()));
     WrapInFunction(ast_loop, If(true, Block(Return())));
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -604,16 +611,16 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4]
     # loop start
-    %fn4 = block
-    ret
-func_end
+    %fn4 = block {
+    } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -631,33 +638,21 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(Block(ast_loop, ast_if));
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    EXPECT_EQ(ir_if, nullptr);
-
-    auto* ir_break_if = FlowNodeForAstNode(ast_break_if);
-    ASSERT_NE(ir_break_if, nullptr);
-    EXPECT_TRUE(ir_break_if->Is<ir::If>());
-
-    auto* break_if_flow = ir_break_if->As<ir::If>();
+    auto* break_if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(break_if_flow->true_.target, nullptr);
     ASSERT_NE(break_if_flow->false_.target, nullptr);
     ASSERT_NE(break_if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -667,16 +662,16 @@
     // This is 1 because only the loop branch happens. The subsequent if return is dead code.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4]
     # loop start
-    %fn4 = block
-    ret
-func_end
+    %fn4 = block {
+    } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -686,30 +681,21 @@
     auto* ast_loop = Loop(Block(ast_if, Continue()));
     WrapInFunction(ast_loop);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop = FlowNodeForAstNode(ast_loop);
-    ASSERT_NE(ir_loop, nullptr);
-    EXPECT_TRUE(ir_loop->Is<ir::Loop>());
-
-    auto* loop_flow = ir_loop->As<ir::Loop>();
+    auto* loop_flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(loop_flow->start.target, nullptr);
     ASSERT_NE(loop_flow->continuing.target, nullptr);
     ASSERT_NE(loop_flow->merge.target, nullptr);
 
-    auto* ir_if = FlowNodeForAstNode(ast_if);
-    ASSERT_NE(ir_if, nullptr);
-    ASSERT_TRUE(ir_if->Is<ir::If>());
-
-    auto* if_flow = ir_if->As<ir::If>();
+    auto* if_flow = FindSingleFlowNode<ir::If>(m.Get());
     ASSERT_NE(if_flow->true_.target, nullptr);
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow->start.target->inbound_branches.Length());
@@ -722,29 +708,29 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, m: %fn5]
     # loop start
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     %fn6 = if true [t: %fn7, f: %fn8]
       # true branch
-      %fn7 = block
-      branch %fn5
+      %fn7 = block {
+      } -> %fn5 # branch
 
       # false branch
-      %fn8 = block
-      branch %fn5
+      %fn8 = block {
+      } -> %fn5 # branch
 
   # loop merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -763,76 +749,68 @@
 
     WrapInFunction(ast_loop_a);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_loop_a = FlowNodeForAstNode(ast_loop_a);
-    ASSERT_NE(ir_loop_a, nullptr);
-    EXPECT_TRUE(ir_loop_a->Is<ir::Loop>());
-    auto* loop_flow_a = ir_loop_a->As<ir::Loop>();
+    ASSERT_EQ(1u, m->functions.Length());
+
+    auto block_exit = [&](const ir::FlowNode* node) -> const ir::FlowNode* {
+        if (auto* block = As<ir::Block>(node)) {
+            return block->branch.target;
+        }
+        return nullptr;
+    };
+
+    auto* loop_flow_a = As<ir::Loop>(m->functions[0]->start_target->branch.target);
+    ASSERT_NE(loop_flow_a, nullptr);
     ASSERT_NE(loop_flow_a->start.target, nullptr);
     ASSERT_NE(loop_flow_a->continuing.target, nullptr);
     ASSERT_NE(loop_flow_a->merge.target, nullptr);
 
-    auto* ir_loop_b = FlowNodeForAstNode(ast_loop_b);
-    ASSERT_NE(ir_loop_b, nullptr);
-    EXPECT_TRUE(ir_loop_b->Is<ir::Loop>());
-    auto* loop_flow_b = ir_loop_b->As<ir::Loop>();
+    auto* loop_flow_b = As<ir::Loop>(block_exit(loop_flow_a->start.target));
+    ASSERT_NE(loop_flow_b, nullptr);
     ASSERT_NE(loop_flow_b->start.target, nullptr);
     ASSERT_NE(loop_flow_b->continuing.target, nullptr);
     ASSERT_NE(loop_flow_b->merge.target, nullptr);
 
-    auto* ir_loop_c = FlowNodeForAstNode(ast_loop_c);
-    ASSERT_NE(ir_loop_c, nullptr);
-    EXPECT_TRUE(ir_loop_c->Is<ir::Loop>());
-    auto* loop_flow_c = ir_loop_c->As<ir::Loop>();
-    ASSERT_NE(loop_flow_c->start.target, nullptr);
-    ASSERT_NE(loop_flow_c->continuing.target, nullptr);
-    ASSERT_NE(loop_flow_c->merge.target, nullptr);
-
-    auto* ir_loop_d = FlowNodeForAstNode(ast_loop_d);
-    ASSERT_NE(ir_loop_d, nullptr);
-    EXPECT_TRUE(ir_loop_d->Is<ir::Loop>());
-    auto* loop_flow_d = ir_loop_d->As<ir::Loop>();
-    ASSERT_NE(loop_flow_d->start.target, nullptr);
-    ASSERT_NE(loop_flow_d->continuing.target, nullptr);
-    ASSERT_NE(loop_flow_d->merge.target, nullptr);
-
-    auto* ir_if_a = FlowNodeForAstNode(ast_if_a);
-    ASSERT_NE(ir_if_a, nullptr);
-    EXPECT_TRUE(ir_if_a->Is<ir::If>());
-    auto* if_flow_a = ir_if_a->As<ir::If>();
+    auto* if_flow_a = As<ir::If>(block_exit(loop_flow_b->start.target));
+    ASSERT_NE(if_flow_a, nullptr);
     ASSERT_NE(if_flow_a->true_.target, nullptr);
     ASSERT_NE(if_flow_a->false_.target, nullptr);
     ASSERT_NE(if_flow_a->merge.target, nullptr);
 
-    auto* ir_if_b = FlowNodeForAstNode(ast_if_b);
-    ASSERT_NE(ir_if_b, nullptr);
-    EXPECT_TRUE(ir_if_b->Is<ir::If>());
-    auto* if_flow_b = ir_if_b->As<ir::If>();
+    auto* if_flow_b = As<ir::If>(block_exit(if_flow_a->merge.target));
+    ASSERT_NE(if_flow_b, nullptr);
     ASSERT_NE(if_flow_b->true_.target, nullptr);
     ASSERT_NE(if_flow_b->false_.target, nullptr);
     ASSERT_NE(if_flow_b->merge.target, nullptr);
 
-    auto* ir_if_c = FlowNodeForAstNode(ast_if_c);
-    ASSERT_NE(ir_if_c, nullptr);
-    EXPECT_TRUE(ir_if_c->Is<ir::If>());
-    auto* if_flow_c = ir_if_c->As<ir::If>();
+    auto* loop_flow_c = As<ir::Loop>(block_exit(loop_flow_b->continuing.target));
+    ASSERT_NE(loop_flow_c, nullptr);
+    ASSERT_NE(loop_flow_c->start.target, nullptr);
+    ASSERT_NE(loop_flow_c->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_c->merge.target, nullptr);
+
+    auto* loop_flow_d = As<ir::Loop>(block_exit(loop_flow_c->merge.target));
+    ASSERT_NE(loop_flow_d, nullptr);
+    ASSERT_NE(loop_flow_d->start.target, nullptr);
+    ASSERT_NE(loop_flow_d->continuing.target, nullptr);
+    ASSERT_NE(loop_flow_d->merge.target, nullptr);
+
+    auto* if_flow_c = As<ir::If>(block_exit(loop_flow_d->continuing.target));
+    ASSERT_NE(if_flow_c, nullptr);
     ASSERT_NE(if_flow_c->true_.target, nullptr);
     ASSERT_NE(if_flow_c->false_.target, nullptr);
     ASSERT_NE(if_flow_c->merge.target, nullptr);
 
-    auto* ir_if_d = FlowNodeForAstNode(ast_if_d);
-    ASSERT_NE(ir_if_d, nullptr);
-    EXPECT_TRUE(ir_if_d->Is<ir::If>());
-    auto* if_flow_d = ir_if_d->As<ir::If>();
+    auto* if_flow_d = As<ir::If>(block_exit(loop_flow_b->merge.target));
+    ASSERT_NE(if_flow_d, nullptr);
     ASSERT_NE(if_flow_d->true_.target, nullptr);
     ASSERT_NE(if_flow_d->false_.target, nullptr);
     ASSERT_NE(if_flow_d->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, loop_flow_a->inbound_branches.Length());
     EXPECT_EQ(2u, loop_flow_a->start.target->inbound_branches.Length());
@@ -869,111 +847,111 @@
     EXPECT_EQ(1u, func->start_target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
     # loop start
-    %fn4 = block
-    branch %fn7
+    %fn4 = block {
+    } -> %fn7 # branch
 
     %fn7 = loop [s: %fn8, c: %fn9, m: %fn10]
       # loop start
-      %fn8 = block
-      branch %fn11
+      %fn8 = block {
+      } -> %fn11 # branch
 
       %fn11 = if true [t: %fn12, f: %fn13, m: %fn14]
         # true branch
-        %fn12 = block
-        branch %fn10
+        %fn12 = block {
+        } -> %fn10 # branch
 
         # false branch
-        %fn13 = block
-        branch %fn14
+        %fn13 = block {
+        } -> %fn14 # branch
 
       # if merge
-      %fn14 = block
-      branch %fn15
+      %fn14 = block {
+      } -> %fn15 # branch
 
       %fn15 = if true [t: %fn16, f: %fn17, m: %fn18]
         # true branch
-        %fn16 = block
-        branch %fn9
+        %fn16 = block {
+        } -> %fn9 # branch
 
         # false branch
-        %fn17 = block
-        branch %fn18
+        %fn17 = block {
+        } -> %fn18 # branch
 
       # if merge
-      %fn18 = block
-      branch %fn9
+      %fn18 = block {
+      } -> %fn9 # branch
 
       # loop continuing
-      %fn9 = block
-      branch %fn19
+      %fn9 = block {
+      } -> %fn19 # branch
 
       %fn19 = loop [s: %fn20, m: %fn21]
         # loop start
-        %fn20 = block
-        branch %fn21
+        %fn20 = block {
+        } -> %fn21 # branch
 
       # loop merge
-      %fn21 = block
-      branch %fn22
+      %fn21 = block {
+      } -> %fn22 # branch
 
       %fn22 = loop [s: %fn23, c: %fn24, m: %fn25]
         # loop start
-        %fn23 = block
-        branch %fn24
+        %fn23 = block {
+        } -> %fn24 # branch
 
         # loop continuing
-        %fn24 = block
-        branch %fn26
+        %fn24 = block {
+        } -> %fn26 # branch
 
         %fn26 = if true [t: %fn27, f: %fn28, m: %fn29]
           # true branch
-          %fn27 = block
-          branch %fn25
+          %fn27 = block {
+          } -> %fn25 # branch
 
           # false branch
-          %fn28 = block
-          branch %fn29
+          %fn28 = block {
+          } -> %fn29 # branch
 
         # if merge
-        %fn29 = block
-        branch %fn23
+        %fn29 = block {
+        } -> %fn23 # branch
 
       # loop merge
-      %fn25 = block
-      branch %fn8
+      %fn25 = block {
+      } -> %fn8 # branch
 
     # loop merge
-    %fn10 = block
-    branch %fn30
+    %fn10 = block {
+    } -> %fn30 # branch
 
     %fn30 = if true [t: %fn31, f: %fn32, m: %fn33]
       # true branch
-      %fn31 = block
-      branch %fn6
+      %fn31 = block {
+      } -> %fn6 # branch
 
       # false branch
-      %fn32 = block
-      branch %fn33
+      %fn32 = block {
+      } -> %fn33 # branch
 
     # if merge
-    %fn33 = block
-    branch %fn5
+    %fn33 = block {
+    } -> %fn5 # branch
 
     # loop continuing
-    %fn5 = block
-    branch %fn4
+    %fn5 = block {
+    } -> %fn4 # branch
 
   # loop merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -982,15 +960,10 @@
     auto* ast_while = While(false, Block());
     WrapInFunction(ast_while);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_while = FlowNodeForAstNode(ast_while);
-    ASSERT_NE(ir_while, nullptr);
-    ASSERT_TRUE(ir_while->Is<ir::Loop>());
-
-    auto* flow = ir_while->As<ir::Loop>();
+    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(flow->start.target, nullptr);
     ASSERT_NE(flow->continuing.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
@@ -1002,8 +975,8 @@
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
@@ -1014,37 +987,37 @@
     EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, c: %fn5, m: %fn6]
     # loop start
-    %fn4 = block
-    branch %fn7
+    %fn4 = block {
+    } -> %fn7 # branch
 
     %fn7 = if false [t: %fn8, f: %fn9, m: %fn10]
       # true branch
-      %fn8 = block
-      branch %fn10
+      %fn8 = block {
+      } -> %fn10 # branch
 
       # false branch
-      %fn9 = block
-      branch %fn6
+      %fn9 = block {
+      } -> %fn6 # branch
 
     # if merge
-    %fn10 = block
-    branch %fn5
+    %fn10 = block {
+    } -> %fn5 # branch
 
     # loop continuing
-    %fn5 = block
-    branch %fn4
+    %fn5 = block {
+    } -> %fn4 # branch
 
   # loop merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1053,15 +1026,10 @@
     auto* ast_while = While(true, Block(Return()));
     WrapInFunction(ast_while);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_while = FlowNodeForAstNode(ast_while);
-    ASSERT_NE(ir_while, nullptr);
-    ASSERT_TRUE(ir_while->Is<ir::Loop>());
-
-    auto* flow = ir_while->As<ir::Loop>();
+    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(flow->start.target, nullptr);
     ASSERT_NE(flow->continuing.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
@@ -1073,8 +1041,8 @@
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
@@ -1085,32 +1053,32 @@
     EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, m: %fn5]
     # loop start
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     %fn6 = if true [t: %fn7, f: %fn8, m: %fn9]
       # true branch
-      %fn7 = block
-      branch %fn9
+      %fn7 = block {
+      } -> %fn9 # branch
 
       # false branch
-      %fn8 = block
-      branch %fn5
+      %fn8 = block {
+      } -> %fn5 # branch
 
     # if merge
-    %fn9 = block
-    ret
+    %fn9 = block {
+    } -> %func_end # return
   # loop merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1132,15 +1100,10 @@
     auto* ast_for = For(Decl(Var("i", ty.i32())), LessThan("i", 10_a), Increment("i"), Block());
     WrapInFunction(ast_for);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_for = FlowNodeForAstNode(ast_for);
-    ASSERT_NE(ir_for, nullptr);
-    ASSERT_TRUE(ir_for->Is<ir::Loop>());
-
-    auto* flow = ir_for->As<ir::Loop>();
+    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(flow->start.target, nullptr);
     ASSERT_NE(flow->continuing.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
@@ -1152,8 +1115,8 @@
     ASSERT_NE(if_flow->false_.target, nullptr);
     ASSERT_NE(if_flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
     EXPECT_EQ(1u, flow->inbound_branches.Length());
@@ -1164,28 +1127,23 @@
     EXPECT_EQ(1u, if_flow->false_.target->inbound_branches.Length());
     EXPECT_EQ(1u, if_flow->merge.target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m), R"()");
+    EXPECT_EQ(Disassemble(m.Get()), R"()");
 }
 
 TEST_F(IR_BuilderImplTest, For_NoInitCondOrContinuing) {
     auto* ast_for = For(nullptr, nullptr, nullptr, Block(Break()));
     WrapInFunction(ast_for);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_for = FlowNodeForAstNode(ast_for);
-    ASSERT_NE(ir_for, nullptr);
-    ASSERT_TRUE(ir_for->Is<ir::Loop>());
-
-    auto* flow = ir_for->As<ir::Loop>();
+    auto* flow = FindSingleFlowNode<ir::Loop>(m.Get());
     ASSERT_NE(flow->start.target, nullptr);
     ASSERT_NE(flow->continuing.target, nullptr);
     ASSERT_NE(flow->merge.target, nullptr);
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     EXPECT_EQ(1u, flow->inbound_branches.Length());
     EXPECT_EQ(2u, flow->start.target->inbound_branches.Length());
@@ -1193,20 +1151,20 @@
     EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = loop [s: %fn4, m: %fn5]
     # loop start
-    %fn4 = block
-    branch %fn5
+    %fn4 = block {
+    } -> %fn5 # branch
 
   # loop merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1218,20 +1176,15 @@
 
     WrapInFunction(ast_switch);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_switch = FlowNodeForAstNode(ast_switch);
-    ASSERT_NE(ir_switch, nullptr);
-    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
-
-    auto* flow = ir_switch->As<ir::Switch>();
+    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
     ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(3u, flow->cases.Length());
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
     ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
@@ -1253,28 +1206,28 @@
     EXPECT_EQ(3u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = switch 1i [c: (0i, %fn4), c: (1i, %fn5), c: (default, %fn6), m: %fn7]
     # case 0i
-    %fn4 = block
-    branch %fn7
+    %fn4 = block {
+    } -> %fn7 # branch
 
     # case 1i
-    %fn5 = block
-    branch %fn7
+    %fn5 = block {
+    } -> %fn7 # branch
 
     # case default
-    %fn6 = block
-    branch %fn7
+    %fn6 = block {
+    } -> %fn7 # branch
 
   # switch merge
-  %fn7 = block
-  ret
-func_end
+  %fn7 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1287,20 +1240,15 @@
 
     WrapInFunction(ast_switch);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_switch = FlowNodeForAstNode(ast_switch);
-    ASSERT_NE(ir_switch, nullptr);
-    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
-
-    auto* flow = ir_switch->As<ir::Switch>();
+    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
     ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(1u, flow->cases.Length());
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     ASSERT_EQ(3u, flow->cases[0].selectors.Length());
     ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
@@ -1318,20 +1266,20 @@
     EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = switch 1i [c: (0i 1i default, %fn4), m: %fn5]
     # case 0i 1i default
-    %fn4 = block
-    branch %fn5
+    %fn4 = block {
+    } -> %fn5 # branch
 
   # switch merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1340,20 +1288,15 @@
     auto* ast_switch = Switch(1_i, utils::Vector{DefaultCase(Block())});
     WrapInFunction(ast_switch);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_switch = FlowNodeForAstNode(ast_switch);
-    ASSERT_NE(ir_switch, nullptr);
-    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
-
-    auto* flow = ir_switch->As<ir::Switch>();
+    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
     ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(1u, flow->cases.Length());
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
     EXPECT_TRUE(flow->cases[0].selectors[0].IsDefault());
@@ -1363,20 +1306,20 @@
     EXPECT_EQ(1u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = switch 1i [c: (default, %fn4), m: %fn5]
     # case default
-    %fn4 = block
-    branch %fn5
+    %fn4 = block {
+    } -> %fn5 # branch
 
   # switch merge
-  %fn5 = block
-  ret
-func_end
+  %fn5 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1387,20 +1330,15 @@
                                                  DefaultCase(Block())});
     WrapInFunction(ast_switch);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    auto* ir_switch = FlowNodeForAstNode(ast_switch);
-    ASSERT_NE(ir_switch, nullptr);
-    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
-
-    auto* flow = ir_switch->As<ir::Switch>();
+    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
     ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(2u, flow->cases.Length());
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
     ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
@@ -1417,24 +1355,24 @@
     // This is 1 because the if is dead-code eliminated and the return doesn't happen.
     EXPECT_EQ(1u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = switch 1i [c: (0i, %fn4), c: (default, %fn5), m: %fn6]
     # case 0i
-    %fn4 = block
-    branch %fn6
+    %fn4 = block {
+    } -> %fn6 # branch
 
     # case default
-    %fn5 = block
-    branch %fn6
+    %fn5 = block {
+    } -> %fn6 # branch
 
   # switch merge
-  %fn6 = block
-  ret
-func_end
+  %fn6 = block {
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -1446,22 +1384,17 @@
     auto* ast_if = If(true, Block(Return()));
     WrapInFunction(ast_switch, ast_if);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    ASSERT_EQ(FlowNodeForAstNode(ast_if), nullptr);
+    ASSERT_EQ(FindSingleFlowNode<ir::If>(m.Get()), nullptr);
 
-    auto* ir_switch = FlowNodeForAstNode(ast_switch);
-    ASSERT_NE(ir_switch, nullptr);
-    ASSERT_TRUE(ir_switch->Is<ir::Switch>());
-
-    auto* flow = ir_switch->As<ir::Switch>();
+    auto* flow = FindSingleFlowNode<ir::Switch>(m.Get());
     ASSERT_NE(flow->merge.target, nullptr);
     ASSERT_EQ(2u, flow->cases.Length());
 
-    ASSERT_EQ(1u, m.functions.Length());
-    auto* func = m.functions[0];
+    ASSERT_EQ(1u, m->functions.Length());
+    auto* func = m->functions[0];
 
     ASSERT_EQ(1u, flow->cases[0].selectors.Length());
     ASSERT_TRUE(flow->cases[0].selectors[0].val->value->Is<constant::Scalar<tint::i32>>());
@@ -1477,19 +1410,19 @@
     EXPECT_EQ(0u, flow->merge.target->inbound_branches.Length());
     EXPECT_EQ(2u, func->end_target->inbound_branches.Length());
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  branch %fn3
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+  } -> %fn3 # branch
 
   %fn3 = switch 1i [c: (0i, %fn4), c: (default, %fn5)]
     # case 0i
-    %fn4 = block
-    ret
+    %fn4 = block {
+    } -> %func_end # return
     # case default
-    %fn5 = block
-    ret
-func_end
+    %fn5 = block {
+    } -> %func_end # return
+} %func_end
 
 )");
 }
diff --git a/src/tint/ir/from_program_unary_test.cc b/src/tint/ir/from_program_unary_test.cc
new file mode 100644
index 0000000..b2ba2d7
--- /dev/null
+++ b/src/tint/ir/from_program_unary_test.cc
@@ -0,0 +1,148 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/test_helper.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/ast/case_selector.h"
+#include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/constant/scalar.h"
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+using IR_BuilderImplTest = TestHelper;
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Not) {
+    Func("my_func", utils::Empty, ty.bool_(), utils::Vector{Return(false)});
+    auto* expr = Not(Call("my_func"));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():bool {
+  %fn2 = block {
+  } -> %func_end false # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:bool = call my_func
+    %tint_symbol:bool = eq %1:bool, false
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Complement) {
+    Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(1_u)});
+    auto* expr = Complement(Call("my_func"));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():u32 {
+  %fn2 = block {
+  } -> %func_end 1u # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:u32 = call my_func
+    %tint_symbol:u32 = complement %1:u32
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Negation) {
+    Func("my_func", utils::Empty, ty.i32(), utils::Vector{Return(1_i)});
+    auto* expr = Negation(Call("my_func"));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = func my_func():i32 {
+  %fn2 = block {
+  } -> %func_end 1i # return
+} %func_end
+
+%fn3 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn4 = block {
+    %1:i32 = call my_func
+    %tint_symbol:i32 = negation %1:i32
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Unary_AddressOf) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
+
+    auto* expr = Decl(Let("v2", AddressOf("v1")));
+    WrapInFunction(expr);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, i32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %v2:ptr<private, i32, read_write> = addr_of %v1:ref<private, i32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Unary_Indirection) {
+    GlobalVar("v1", builtin::AddressSpace::kPrivate, ty.i32());
+    utils::Vector stmts = {
+        Decl(Let("v3", AddressOf("v1"))),
+        Decl(Let("v2", Deref("v3"))),
+    };
+    WrapInFunction(stmts);
+
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
+
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %v1:ref<private, i32, read_write> = var private, read_write
+}
+
+
+%fn2 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn3 = block {
+    %v3:ptr<private, i32, read_write> = addr_of %v1:ref<private, i32, read_write>
+    %v2:i32 = indirection %v3:ptr<private, i32, read_write>
+  } -> %func_end # return
+} %func_end
+
+)");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/builder_impl_var_test.cc b/src/tint/ir/from_program_var_test.cc
similarity index 66%
rename from src/tint/ir/builder_impl_var_test.cc
rename to src/tint/ir/from_program_var_test.cc
index a3f4d27..b5379ac 100644
--- a/src/tint/ir/builder_impl_var_test.cc
+++ b/src/tint/ir/from_program_var_test.cc
@@ -29,13 +29,12 @@
 TEST_F(IR_BuilderImplTest, Emit_GlobalVar_NoInit) {
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%a:ref<private, u32, read_write> = var private, read_write
-
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %a:ref<private, u32, read_write> = var private, read_write
+}
 
 
 )");
@@ -45,13 +44,12 @@
     auto* expr = Expr(2_u);
     GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m), R"(%fn1 = block
-%a:ref<private, u32, read_write> = var private, read_write, 2u
-
+    EXPECT_EQ(Disassemble(m.Get()), R"(%fn1 = block {
+  %a:ref<private, u32, read_write> = var private, read_write, 2u
+}
 
 
 )");
@@ -61,16 +59,15 @@
     auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction);
     WrapInFunction(a);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  %a:ref<function, u32, read_write> = var function, read_write
-  ret
-func_end
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+    %a:ref<function, u32, read_write> = var function, read_write
+  } -> %func_end # return
+} %func_end
 
 )");
 }
@@ -80,16 +77,15 @@
     auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction, expr);
     WrapInFunction(a);
 
-    auto r = Build();
-    ASSERT_TRUE(r) << Error();
-    auto m = r.Move();
+    auto m = Build();
+    ASSERT_TRUE(m) << (!m ? m.Failure() : "");
 
-    EXPECT_EQ(Disassemble(m),
-              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)]
-  %fn2 = block
-  %a:ref<function, u32, read_write> = var function, read_write, 2u
-  ret
-func_end
+    EXPECT_EQ(Disassemble(m.Get()),
+              R"(%fn1 = func test_function():void [@compute @workgroup_size(1, 1, 1)] {
+  %fn2 = block {
+    %a:ref<function, u32, read_write> = var function, read_write, 2u
+  } -> %func_end # return
+} %func_end
 
 )");
 }
diff --git a/src/tint/ir/function.cc b/src/tint/ir/function.cc
index a03812e..2a5f8c3 100644
--- a/src/tint/ir/function.cc
+++ b/src/tint/ir/function.cc
@@ -18,7 +18,11 @@
 
 namespace tint::ir {
 
-Function::Function() : Base() {}
+Function::Function(Symbol n,
+                   type::Type* rt,
+                   PipelineStage stage,
+                   std::optional<std::array<uint32_t, 3>> wg_size)
+    : Base(), name(n), pipeline_stage(stage), workgroup_size(wg_size), return_type(rt) {}
 
 Function::~Function() = default;
 
diff --git a/src/tint/ir/function.h b/src/tint/ir/function.h
index e3d5529..95487b9 100644
--- a/src/tint/ir/function.h
+++ b/src/tint/ir/function.h
@@ -62,7 +62,14 @@
     };
 
     /// Constructor
-    Function();
+    /// @param n the function name
+    /// @param rt the function return type
+    /// @param stage the function stage
+    /// @param wg_size the workgroup_size
+    Function(Symbol n,
+             type::Type* rt,
+             PipelineStage stage = PipelineStage::kUndefined,
+             std::optional<std::array<uint32_t, 3>> wg_size = {});
     ~Function() override;
 
     /// The function name
diff --git a/src/tint/ir/if.cc b/src/tint/ir/if.cc
index 22a9078..b59d87f 100644
--- a/src/tint/ir/if.cc
+++ b/src/tint/ir/if.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ir {
 
-If::If() : Base() {}
+If::If(Value* cond) : Base(), condition(cond) {}
 
 If::~If() = default;
 
diff --git a/src/tint/ir/if.h b/src/tint/ir/if.h
index 255b165..aadc5c9 100644
--- a/src/tint/ir/if.h
+++ b/src/tint/ir/if.h
@@ -30,7 +30,8 @@
 class If : public utils::Castable<If, FlowNode> {
   public:
     /// Constructor
-    If();
+    /// @param cond the if condition
+    explicit If(Value* cond);
     ~If() override;
 
     /// The true branch block
diff --git a/src/tint/ir/module.h b/src/tint/ir/module.h
index 0f89d86..a7929d2 100644
--- a/src/tint/ir/module.h
+++ b/src/tint/ir/module.h
@@ -75,8 +75,6 @@
 
     /// List of functions in the program
     utils::Vector<Function*, 8> functions;
-    /// List of indexes into the functions list for the entry points
-    utils::Vector<Function*, 8> entry_points;
 
     /// The block containing module level declarations, if any exist.
     Block* root_block = nullptr;
diff --git a/src/tint/ir/module_test.cc b/src/tint/ir/module_test.cc
index 15d5cb1..c9b1150 100644
--- a/src/tint/ir/module_test.cc
+++ b/src/tint/ir/module_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ir/module.h"
 #include "src/tint/ir/test_helper.h"
+#include "src/tint/ir/var.h"
 
 namespace tint::ir {
 namespace {
diff --git a/src/tint/ir/store_test.cc b/src/tint/ir/store_test.cc
index 0005c80..902ca95 100644
--- a/src/tint/ir/store_test.cc
+++ b/src/tint/ir/store_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 
@@ -23,12 +24,13 @@
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, CreateStore) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     // TODO(dsinclair): This is wrong, but we don't have anything correct to store too at the
     // moment.
-    auto* to = b.builder.Discard();
-    const auto* inst = b.builder.Store(to, b.builder.Constant(4_i));
+    auto* to = b.Discard();
+    const auto* inst = b.Store(to, b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Store>());
     ASSERT_EQ(inst->to, to);
@@ -40,10 +42,11 @@
 }
 
 TEST_F(IR_InstructionTest, Store_Usage) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
-    auto* to = b.builder.Discard();
-    const auto* inst = b.builder.Store(to, b.builder.Constant(4_i));
+    auto* to = b.Discard();
+    const auto* inst = b.Store(to, b.Constant(4_i));
 
     ASSERT_NE(inst->to, nullptr);
     ASSERT_EQ(inst->to->Usage().Length(), 1u);
diff --git a/src/tint/ir/switch.cc b/src/tint/ir/switch.cc
index 9ad6d30..ad6a145 100644
--- a/src/tint/ir/switch.cc
+++ b/src/tint/ir/switch.cc
@@ -18,7 +18,7 @@
 
 namespace tint::ir {
 
-Switch::Switch() : Base() {}
+Switch::Switch(Value* cond) : Base(), condition(cond) {}
 
 Switch::~Switch() = default;
 
diff --git a/src/tint/ir/switch.h b/src/tint/ir/switch.h
index 2ff927e..6be7b62 100644
--- a/src/tint/ir/switch.h
+++ b/src/tint/ir/switch.h
@@ -44,7 +44,8 @@
     };
 
     /// Constructor
-    Switch();
+    /// @param cond the condition
+    explicit Switch(Value* cond);
     ~Switch() override;
 
     /// The switch merge target
diff --git a/src/tint/ir/test_helper.h b/src/tint/ir/test_helper.h
index d8568c2..d9055ce 100644
--- a/src/tint/ir/test_helper.h
+++ b/src/tint/ir/test_helper.h
@@ -20,8 +20,8 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "src/tint/ir/builder_impl.h"
 #include "src/tint/ir/disassembler.h"
+#include "src/tint/ir/from_program.h"
 #include "src/tint/number.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/utils/string_stream.h"
@@ -36,84 +36,28 @@
 
     ~TestHelperBase() override = default;
 
-    /// Builds and returns a BuilderImpl from the program.
-    /// @note The builder is only created once. Multiple calls to Build() will
-    /// return the same builder without rebuilding.
-    /// @return the builder
-    BuilderImpl& CreateBuilder() {
-        SetResolveOnBuild(true);
-
-        if (gen_) {
-            return *gen_;
-        }
-        diag::Formatter formatter;
-
-        program_ = std::make_unique<Program>(std::move(*this));
-        [&]() { ASSERT_TRUE(program_->IsValid()) << formatter.format(program_->Diagnostics()); }();
-        gen_ = std::make_unique<BuilderImpl>(program_.get());
-        return *gen_;
-    }
-
-    /// Injects a flow block into the builder
-    /// @returns the injected block
-    ir::Block* InjectFlowBlock() {
-        auto* block = gen_->builder.CreateBlock();
-        gen_->current_flow_block = block;
-        return block;
-    }
-
-    /// Creates a BuilderImpl without an originating program. This is used for testing the
-    /// expressions which don't require the full builder implementation. The current flow block
-    /// is initialized with an empty block.
-    /// @returns the BuilderImpl for testing.
-    BuilderImpl& CreateEmptyBuilder() {
-        program_ = std::make_unique<Program>();
-        gen_ = std::make_unique<BuilderImpl>(program_.get());
-        gen_->current_flow_block = gen_->builder.CreateBlock();
-        return *gen_;
-    }
-
     /// Build the module, cleaning up the program before returning.
     /// @returns the generated module
-    utils::Result<Module> Build() {
-        auto& b = CreateBuilder();
-        auto m = b.Build();
+    utils::Result<Module, std::string> Build() {
+        SetResolveOnBuild(true);
 
-        // Store the error away in case we need it
-        error_ = b.Diagnostics().str();
+        auto program = std::make_unique<Program>(std::move(*this));
+        [&]() {
+            diag::Formatter formatter;
+            ASSERT_TRUE(program->IsValid()) << formatter.format(program->Diagnostics());
+        }();
 
-        // Explicitly remove program to guard against pointers back to ast. Note, this does mean the
-        // BuilderImpl is pointing to an invalid program. We keep the BuilderImpl around because we
-        // need to be able to map from ast pointers to flow nodes in tests.
-        program_ = nullptr;
-        return m;
-    }
-
-    /// @param node the ast node to lookup
-    /// @returns the IR flow node for the given ast node.
-    const ir::FlowNode* FlowNodeForAstNode(const ast::Node* node) const {
-        return gen_->FlowNodeForAstNode(node);
+        return FromProgram(program.get());
     }
 
     /// @param mod the module
     /// @returns the disassembly string of the module
-    std::string Disassemble(Module& mod) const {
+    std::string Disassemble(const Module& mod) const {
         Disassembler d(mod);
         return d.Disassemble();
     }
-
-    /// @returns the error generated during build, if any
-    std::string Error() const { return error_; }
-
-  private:
-    std::unique_ptr<BuilderImpl> gen_;
-
-    /// The program built with a call to Build()
-    std::unique_ptr<Program> program_;
-
-    /// Error generated when calling `Build`
-    std::string error_;
 };
+
 using TestHelper = TestHelperBase<testing::Test>;
 
 template <typename T>
diff --git a/src/tint/ir/to_program.cc b/src/tint/ir/to_program.cc
new file mode 100644
index 0000000..8a2dca9
--- /dev/null
+++ b/src/tint/ir/to_program.cc
@@ -0,0 +1,300 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/to_program.h"
+
+#include <utility>
+
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/call.h"
+#include "src/tint/ir/constant.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/instruction.h"
+#include "src/tint/ir/module.h"
+#include "src/tint/ir/store.h"
+#include "src/tint/ir/user_call.h"
+#include "src/tint/ir/var.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/switch.h"
+#include "src/tint/type/atomic.h"
+#include "src/tint/type/depth_multisampled_texture.h"
+#include "src/tint/type/depth_texture.h"
+#include "src/tint/type/multisampled_texture.h"
+#include "src/tint/type/pointer.h"
+#include "src/tint/type/reference.h"
+#include "src/tint/type/sampler.h"
+#include "src/tint/type/texture.h"
+#include "src/tint/utils/hashmap.h"
+#include "src/tint/utils/predicates.h"
+#include "src/tint/utils/transform.h"
+#include "src/tint/utils/vector.h"
+
+namespace tint::ir {
+
+namespace {
+
+class State {
+  public:
+    explicit State(const Module& m) : mod(m) {}
+
+    Program Run() {
+        // TODO(crbug.com/tint/1902): Emit root block
+        // TODO(crbug.com/tint/1902): Emit user-declared types
+        for (auto* fn : mod.functions) {
+            Fn(fn);
+        }
+        return Program{std::move(b)};
+    }
+
+  private:
+    const Module& mod;
+    ProgramBuilder b;
+    utils::Hashmap<const Value*, Symbol, 32> value_names_;
+
+    void Fn(const Function* fn) {
+        auto name = Sym(fn->name);
+        // TODO(crbug.com/tint/1915): Properly implement this when we've fleshed out Function
+        utils::Vector<const ast::Parameter*, 1> params{};
+        ast::Type ret_ty;
+        auto* body = Block(fn->start_target);
+        utils::Vector<const ast::Attribute*, 1> attrs{};
+        utils::Vector<const ast::Attribute*, 1> ret_attrs{};
+        b.Func(name, std::move(params), ret_ty, body, std::move(attrs), std::move(ret_attrs));
+    }
+
+    const ast::BlockStatement* Block(const ir::Block* block) {
+        // TODO(crbug.com/tint/1902): Check if the block is dead
+        utils::Vector<const ast::Statement*, decltype(ir::Block::instructions)::static_length>
+            stmts;
+        for (auto* inst : block->instructions) {
+            auto* stmt = Stmt(inst);
+            if (!stmt) {
+                return nullptr;
+            }
+            stmts.Push(stmt);
+        }
+        return b.Block(std::move(stmts));
+    }
+
+    const ast::Statement* FlowNode(const ir::FlowNode* node) {
+        // TODO(crbug.com/tint/1902): Check the node is connected
+        return Switch(
+            node,  //
+            [&](const ir::If* i) {
+                auto* cond = Expr(i->condition);
+                auto* t = Branch(i->true_);
+                if (auto* f = Branch(i->false_)) {
+                    return b.If(cond, t, b.Else(f));
+                }
+                // TODO(crbug.com/tint/1902): Emit merge block
+                return b.If(cond, t);
+            },
+            [&](Default) {
+                TINT_UNIMPLEMENTED(IR, b.Diagnostics())
+                    << "unhandled case in Switch(): " << node->TypeInfo().name;
+                return nullptr;
+            });
+    }
+
+    const ast::BlockStatement* Branch(const ir::Branch& branch) {
+        auto* stmt = FlowNode(branch.target);
+        if (!stmt) {
+            return nullptr;
+        }
+        if (auto* block = stmt->As<ast::BlockStatement>()) {
+            return block;
+        }
+        return b.Block(stmt);
+    }
+
+    const ast::Statement* Stmt(const ir::Instruction* inst) {
+        return Switch(
+            inst,                                            //
+            [&](const ir::Call* i) { return CallStmt(i); },  //
+            [&](const ir::Var* i) { return Var(i); },        //
+            [&](const ir::Store* i) { return Store(i); },
+            [&](Default) {
+                TINT_UNIMPLEMENTED(IR, b.Diagnostics())
+                    << "unhandled case in Switch(): " << inst->TypeInfo().name;
+                return nullptr;
+            });
+    }
+
+    const ast::CallStatement* CallStmt(const ir::Call* call) {
+        auto* expr = Call(call);
+        if (!expr) {
+            return nullptr;
+        }
+        return b.CallStmt(expr);
+    }
+
+    const ast::VariableDeclStatement* Var(const ir::Var* var) {
+        Symbol name = NameOf(var);
+        auto ty = Type(var->Type());
+        const ast::Expression* init = nullptr;
+        if (var->initializer) {
+            init = Expr(var->initializer);
+            if (!init) {
+                return nullptr;
+            }
+        }
+        switch (var->address_space) {
+            case builtin::AddressSpace::kFunction:
+                return b.Decl(b.Var(name, ty, init));
+            case builtin::AddressSpace::kStorage:
+                return b.Decl(b.Var(name, ty, init, var->access, var->address_space));
+            default:
+                return b.Decl(b.Var(name, ty, init, var->address_space));
+        }
+    }
+
+    const ast::AssignmentStatement* Store(const ir::Store* store) {
+        auto* expr = Expr(store->from);
+        return b.Assign(NameOf(store->to), expr);
+    }
+
+    const ast::CallExpression* Call(const ir::Call* call) {
+        auto args = utils::Transform(call->args, [&](const ir::Value* arg) { return Expr(arg); });
+        if (args.Any(utils::IsNull)) {
+            return nullptr;
+        }
+        return Switch(
+            call,  //
+            [&](const ir::UserCall* c) { return b.Call(Sym(c->name), std::move(args)); },
+            [&](Default) {
+                TINT_UNIMPLEMENTED(IR, b.Diagnostics())
+                    << "unhandled case in Switch(): " << call->TypeInfo().name;
+                return nullptr;
+            });
+    }
+
+    const ast::Expression* Expr(const ir::Value* val) {
+        return Switch(
+            val,  //
+            [&](const ir::Constant* c) { return ConstExpr(c); },
+            [&](Default) {
+                TINT_UNIMPLEMENTED(IR, b.Diagnostics())
+                    << "unhandled case in Switch(): " << val->TypeInfo().name;
+                return nullptr;
+            });
+    }
+
+    const ast::Expression* ConstExpr(const ir::Constant* c) {
+        return Switch(
+            c->Type(),  //
+            [&](const type::I32*) { return b.Expr(c->value->ValueAs<i32>()); },
+            [&](const type::U32*) { return b.Expr(c->value->ValueAs<u32>()); },
+            [&](const type::F32*) { return b.Expr(c->value->ValueAs<f32>()); },
+            [&](const type::F16*) { return b.Expr(c->value->ValueAs<f16>()); },
+            [&](const type::Bool*) { return b.Expr(c->value->ValueAs<bool>()); },
+            [&](Default) {
+                TINT_UNIMPLEMENTED(IR, b.Diagnostics())
+                    << "unhandled case in Switch(): " << c->TypeInfo().name;
+                return nullptr;
+            });
+    }
+
+    const ast::Type Type(const type::Type* ty) {
+        return Switch(
+            ty,                                              //
+            [&](const type::Void*) { return ast::Type{}; },  //
+            [&](const type::I32*) { return b.ty.i32(); },    //
+            [&](const type::U32*) { return b.ty.u32(); },    //
+            [&](const type::F16*) { return b.ty.f16(); },    //
+            [&](const type::F32*) { return b.ty.f32(); },    //
+            [&](const type::Bool*) { return b.ty.bool_(); },
+            [&](const type::Matrix* m) {
+                auto el = Type(m->type());
+                return b.ty.mat(el, m->columns(), m->rows());
+            },
+            [&](const type::Vector* v) {
+                auto el = Type(v->type());
+                if (v->Packed()) {
+                    TINT_ASSERT(IR, v->Width() == 3u);
+                    return b.ty(builtin::Builtin::kPackedVec3, el);
+                } else {
+                    return b.ty.vec(el, v->Width());
+                }
+            },
+            [&](const type::Array* a) {
+                auto el = Type(a->ElemType());
+                utils::Vector<const ast::Attribute*, 1> attrs;
+                if (!a->IsStrideImplicit()) {
+                    attrs.Push(b.Stride(a->Stride()));
+                }
+                if (a->Count()->Is<type::RuntimeArrayCount>()) {
+                    return b.ty.array(el, std::move(attrs));
+                }
+                auto count = a->ConstantCount();
+                if (TINT_UNLIKELY(!count)) {
+                    TINT_ICE(IR, b.Diagnostics()) << type::Array::kErrExpectedConstantCount;
+                    return b.ty.array(el, u32(1), std::move(attrs));
+                }
+                return b.ty.array(el, u32(count.value()), std::move(attrs));
+            },
+            [&](const type::Struct* s) { return b.ty(s->Name().NameView()); },
+            [&](const type::Atomic* a) { return b.ty.atomic(Type(a->Type())); },
+            [&](const type::DepthTexture* t) { return b.ty.depth_texture(t->dim()); },
+            [&](const type::DepthMultisampledTexture* t) {
+                return b.ty.depth_multisampled_texture(t->dim());
+            },
+            [&](const type::ExternalTexture*) { return b.ty.external_texture(); },
+            [&](const type::MultisampledTexture* t) {
+                return b.ty.multisampled_texture(t->dim(), Type(t->type()));
+            },
+            [&](const type::SampledTexture* t) {
+                return b.ty.sampled_texture(t->dim(), Type(t->type()));
+            },
+            [&](const type::StorageTexture* t) {
+                return b.ty.storage_texture(t->dim(), t->texel_format(), t->access());
+            },
+            [&](const type::Sampler* s) { return b.ty.sampler(s->kind()); },
+            [&](const type::Pointer* p) {
+                // Note: type::Pointer always has an inferred access, but WGSL only allows an
+                // explicit access in the 'storage' address space.
+                auto address_space = p->AddressSpace();
+                auto access = address_space == builtin::AddressSpace::kStorage
+                                  ? p->Access()
+                                  : builtin::Access::kUndefined;
+                return b.ty.pointer(Type(p->StoreType()), address_space, access);
+            },
+            [&](const type::Reference* r) { return Type(r->StoreType()); },
+            [&](Default) {
+                TINT_UNREACHABLE(IR, b.Diagnostics()) << "unhandled type: " << ty->TypeInfo().name;
+                return ast::Type{};
+            });
+    }
+
+    Symbol NameOf(const Value* value) {
+        TINT_ASSERT(IR, value);
+        return value_names_.GetOrCreate(value, [&] {
+            if (auto sym = mod.NameOf(value)) {
+                return b.Symbols().New(sym.Name());
+            }
+            return b.Symbols().New("v" + std::to_string(value_names_.Count()));
+        });
+    }
+
+    Symbol Sym(const Symbol& s) { return b.Symbols().Register(s.NameView()); }
+
+    // void Err(std::string str) { b.Diagnostics().add_error(diag::System::IR, std::move(str)); }
+};
+
+}  // namespace
+
+Program ToProgram(const Module& i) {
+    return State{i}.Run();
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/to_program.h b/src/tint/ir/to_program.h
new file mode 100644
index 0000000..d4cf2c6
--- /dev/null
+++ b/src/tint/ir/to_program.h
@@ -0,0 +1,34 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_IR_TO_PROGRAM_H_
+#define SRC_TINT_IR_TO_PROGRAM_H_
+
+#include "src/tint/program.h"
+
+namespace tint::ir {
+class Module;
+}
+
+namespace tint::ir {
+
+/// Builds a tint::Program from an ir::Module
+/// @param module the IR module
+/// @return the tint::Program.
+/// @note Check the returned Program::Diagnostics() for any errors.
+Program ToProgram(const Module& module);
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_TO_PROGRAM_H_
diff --git a/src/tint/ir/to_program_roundtrip_test.cc b/src/tint/ir/to_program_roundtrip_test.cc
new file mode 100644
index 0000000..11aff46
--- /dev/null
+++ b/src/tint/ir/to_program_roundtrip_test.cc
@@ -0,0 +1,79 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ir/from_program.h"
+#include "src/tint/ir/test_helper.h"
+#include "src/tint/ir/to_program.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/utils/string.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+#if !TINT_BUILD_WGSL_READER || !TINT_BUILD_WGSL_WRITER
+#error "to_program_roundtrip_test.cc requires both the WGSL reader and writer to be enabled"
+#endif
+
+namespace tint::ir {
+namespace {
+
+using namespace tint::number_suffixes;  // NOLINT
+
+class IRToProgramRoundtripTest : public TestHelper {
+  public:
+    void Test(std::string_view input_wgsl, std::string_view expected_wgsl) {
+        auto input = utils::TrimSpace(input_wgsl);
+        Source::File file("test.wgsl", std::string(input));
+        auto input_program = reader::wgsl::Parse(&file);
+        ASSERT_TRUE(input_program.IsValid()) << input_program.Diagnostics().str();
+
+        auto ir_module = FromProgram(&input_program);
+        ASSERT_TRUE(ir_module);
+
+        auto output_program = ToProgram(ir_module.Get());
+        ASSERT_TRUE(output_program.IsValid()) << output_program.Diagnostics().str();
+
+        auto output = writer::wgsl::Generate(&output_program, {});
+        ASSERT_TRUE(output.success) << output.error;
+
+        auto expected = expected_wgsl.empty() ? input : utils::TrimSpace(expected_wgsl);
+        auto got = utils::TrimSpace(output.wgsl);
+        if (expected != got) {
+            tint::ir::Disassembler d{ir_module.Get()};
+            EXPECT_EQ(expected, got) << "IR:" << std::endl << d.Disassemble();
+        }
+    }
+
+    void Test(std::string_view wgsl) { Test(wgsl, wgsl); }
+};
+
+TEST_F(IRToProgramRoundtripTest, EmptyModule) {
+    Test("");
+}
+
+TEST_F(IRToProgramRoundtripTest, EmptySingleFunction) {
+    Test(R"(
+fn f() {
+}
+)");
+}
+
+TEST_F(IRToProgramRoundtripTest, FunctionScopeVar_i32_InitLiteral) {
+    Test(R"(
+fn f() {
+  var i : i32 = 42i;
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::ir
diff --git a/src/tint/ir/unary_test.cc b/src/tint/ir/unary_test.cc
index 86a3993..392a75e 100644
--- a/src/tint/ir/unary_test.cc
+++ b/src/tint/ir/unary_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/ir/builder.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 
@@ -23,14 +24,14 @@
 using IR_InstructionTest = TestHelper;
 
 TEST_F(IR_InstructionTest, CreateAddressOf) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     // TODO(dsinclair): This would be better as an identifier, but works for now.
-    const auto* inst =
-        b.builder.AddressOf(b.builder.ir.types.Get<type::Pointer>(
-                                b.builder.ir.types.Get<type::I32>(),
-                                builtin::AddressSpace::kPrivate, builtin::Access::kReadWrite),
-                            b.builder.Constant(4_i));
+    const auto* inst = b.AddressOf(
+        b.ir.types.Get<type::Pointer>(b.ir.types.Get<type::I32>(), builtin::AddressSpace::kPrivate,
+                                      builtin::Access::kReadWrite),
+        b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->kind, Unary::Kind::kAddressOf);
@@ -44,9 +45,9 @@
 }
 
 TEST_F(IR_InstructionTest, CreateComplement) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Complement(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Complement(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->kind, Unary::Kind::kComplement);
@@ -58,11 +59,11 @@
 }
 
 TEST_F(IR_InstructionTest, CreateIndirection) {
-    auto& b = CreateEmptyBuilder();
+    Module mod;
+    Builder b{mod};
 
     // TODO(dsinclair): This would be better as an identifier, but works for now.
-    const auto* inst =
-        b.builder.Indirection(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    const auto* inst = b.Indirection(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->kind, Unary::Kind::kIndirection);
@@ -74,9 +75,9 @@
 }
 
 TEST_F(IR_InstructionTest, CreateNegation) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     ASSERT_TRUE(inst->Is<Unary>());
     EXPECT_EQ(inst->kind, Unary::Kind::kNegation);
@@ -88,9 +89,9 @@
 }
 
 TEST_F(IR_InstructionTest, Unary_Usage) {
-    auto& b = CreateEmptyBuilder();
-    const auto* inst =
-        b.builder.Negation(b.builder.ir.types.Get<type::I32>(), b.builder.Constant(4_i));
+    Module mod;
+    Builder b{mod};
+    const auto* inst = b.Negation(b.ir.types.Get<type::I32>(), b.Constant(4_i));
 
     EXPECT_EQ(inst->kind, Unary::Kind::kNegation);
 
diff --git a/src/tint/reader/spirv/parser.cc b/src/tint/reader/spirv/parser.cc
index bc0f258..1699d2c 100644
--- a/src/tint/reader/spirv/parser.cc
+++ b/src/tint/reader/spirv/parser.cc
@@ -57,13 +57,14 @@
     }
 
     transform::Manager manager;
+    transform::DataMap outputs;
     manager.Add<ast::transform::Unshadow>();
     manager.Add<ast::transform::SimplifyPointers>();
     manager.Add<ast::transform::DecomposeStridedMatrix>();
     manager.Add<ast::transform::DecomposeStridedArray>();
     manager.Add<ast::transform::RemoveUnreachableStatements>();
     manager.Add<ast::transform::SpirvAtomic>();
-    return manager.Run(&program).program;
+    return manager.Run(&program, {}, outputs);
 }
 
 }  // namespace tint::reader::spirv
diff --git a/src/tint/transform/manager.cc b/src/tint/transform/manager.cc
index b2e6c80..eff3a0c 100644
--- a/src/tint/transform/manager.cc
+++ b/src/tint/transform/manager.cc
@@ -14,6 +14,9 @@
 
 #include "src/tint/transform/manager.h"
 
+#include "src/tint/ast/transform/transform.h"
+#include "src/tint/program_builder.h"
+
 /// If set to 1 then the transform::Manager will dump the WGSL of the program
 /// before and after each transform. Helpful for debugging bad output.
 #define TINT_PRINT_PROGRAM_FOR_EACH_TRANSFORM 0
@@ -25,16 +28,14 @@
 #define TINT_IF_PRINT_PROGRAM(x)
 #endif  // TINT_PRINT_PROGRAM_FOR_EACH_TRANSFORM
 
-TINT_INSTANTIATE_TYPEINFO(tint::transform::Manager);
-
 namespace tint::transform {
 
 Manager::Manager() = default;
 Manager::~Manager() = default;
 
-ast::transform::Transform::ApplyResult Manager::Apply(const Program* program,
-                                                      const ast::transform::DataMap& inputs,
-                                                      ast::transform::DataMap& outputs) const {
+Program Manager::Run(const Program* program,
+                     const transform::DataMap& inputs,
+                     transform::DataMap& outputs) const {
 #if TINT_PRINT_PROGRAM_FOR_EACH_TRANSFORM
     auto print_program = [&](const char* msg, const Transform* transform) {
         auto wgsl = Program::printer(program);
@@ -56,25 +57,38 @@
     TINT_IF_PRINT_PROGRAM(print_program("Input of", this));
 
     for (const auto& transform : transforms_) {
-        if (auto result = transform->Apply(program, inputs, outputs)) {
-            output.emplace(std::move(result.value()));
-            program = &output.value();
+        if (auto* ast_transform = transform->As<ast::transform::Transform>()) {
+            if (auto result = ast_transform->Apply(program, inputs, outputs)) {
+                output.emplace(std::move(result.value()));
+                program = &output.value();
 
-            if (!program->IsValid()) {
-                TINT_IF_PRINT_PROGRAM(print_program("Invalid output of", transform.get()));
-                break;
+                if (!program->IsValid()) {
+                    TINT_IF_PRINT_PROGRAM(print_program("Invalid output of", transform.get()));
+                    break;
+                }
+
+                TINT_IF_PRINT_PROGRAM(print_program("Output of", transform.get()));
+            } else {
+                TINT_IF_PRINT_PROGRAM(std::cout << "Skipped " << transform->TypeInfo().name
+                                                << std::endl);
             }
-
-            TINT_IF_PRINT_PROGRAM(print_program("Output of", transform.get()));
         } else {
-            TINT_IF_PRINT_PROGRAM(std::cout << "Skipped " << transform->TypeInfo().name
-                                            << std::endl);
+            ProgramBuilder b;
+            TINT_ICE(Transform, b.Diagnostics()) << "unhandled transform type";
+            return Program(std::move(b));
         }
     }
 
     TINT_IF_PRINT_PROGRAM(print_program("Final output of", this));
 
-    return output;
+    if (!output) {
+        ProgramBuilder b;
+        CloneContext ctx{&b, program, /* auto_clone_symbols */ true};
+        ctx.Clone();
+        output = Program(std::move(b));
+    }
+
+    return std::move(output.value());
 }
 
 }  // namespace tint::transform
diff --git a/src/tint/transform/manager.h b/src/tint/transform/manager.h
index c7b9286..2df5785 100644
--- a/src/tint/transform/manager.h
+++ b/src/tint/transform/manager.h
@@ -19,7 +19,7 @@
 #include <utility>
 #include <vector>
 
-#include "src/tint/ast/transform/transform.h"
+#include "src/tint/transform/transform.h"
 
 namespace tint::transform {
 
@@ -27,11 +27,11 @@
 /// The inner transforms will execute in the appended order.
 /// If any inner transform fails the manager will return immediately and
 /// the error can be retrieved with the Output's diagnostics.
-class Manager final : public tint::utils::Castable<Manager, ast::transform::Transform> {
+class Manager {
   public:
     /// Constructor
     Manager();
-    ~Manager() override;
+    ~Manager();
 
     /// Add pass to the manager
     /// @param transform the transform to append
@@ -47,10 +47,12 @@
         transforms_.emplace_back(std::make_unique<T>(std::forward<ARGS>(args)...));
     }
 
-    /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
-                      const ast::transform::DataMap& inputs,
-                      ast::transform::DataMap& outputs) const override;
+    /// Runs the transforms on @p program, returning the transformed clone of @p program.
+    /// @param program the source program to transform
+    /// @param inputs optional extra transform-specific input data
+    /// @param outputs optional extra transform-specific output data
+    /// @returns the transformed program
+    Program Run(const Program* program, const DataMap& inputs, DataMap& outputs) const;
 
   private:
     std::vector<std::unique_ptr<Transform>> transforms_;
diff --git a/src/tint/transform/transform.cc b/src/tint/transform/transform.cc
new file mode 100644
index 0000000..657f9be
--- /dev/null
+++ b/src/tint/transform/transform.cc
@@ -0,0 +1,37 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/transform/transform.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::transform::Transform);
+TINT_INSTANTIATE_TYPEINFO(tint::transform::Data);
+
+namespace tint::transform {
+
+Data::Data() = default;
+Data::Data(const Data&) = default;
+Data::~Data() = default;
+Data& Data::operator=(const Data&) = default;
+
+DataMap::DataMap() = default;
+DataMap::DataMap(DataMap&&) = default;
+DataMap::~DataMap() = default;
+DataMap& DataMap::operator=(DataMap&&) = default;
+
+Transform::Transform() = default;
+Transform::~Transform() = default;
+
+}  // namespace tint::transform
diff --git a/src/tint/transform/transform.h b/src/tint/transform/transform.h
new file mode 100644
index 0000000..1f8a3f5
--- /dev/null
+++ b/src/tint/transform/transform.h
@@ -0,0 +1,141 @@
+// 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_TRANSFORM_TRANSFORM_H_
+#define SRC_TINT_TRANSFORM_TRANSFORM_H_
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/program.h"
+#include "src/tint/utils/castable.h"
+
+namespace tint::transform {
+
+/// Data is the base class for transforms that accept extra input or emit extra output information.
+class Data : public utils::Castable<Data> {
+  public:
+    /// Constructor
+    Data();
+
+    /// Copy constructor
+    Data(const Data&);
+
+    /// Destructor
+    ~Data() override;
+
+    /// Assignment operator
+    /// @returns this Data
+    Data& operator=(const Data&);
+};
+
+/// DataMap is a map of Data unique pointers keyed by the Data's ClassID.
+class DataMap {
+  public:
+    /// Constructor
+    DataMap();
+
+    /// Move constructor
+    DataMap(DataMap&&);
+
+    /// Constructor
+    /// @param data_unique_ptrs a variadic list of additional data unique_ptrs produced by the
+    /// transform
+    template <typename... DATA>
+    explicit DataMap(DATA... data_unique_ptrs) {
+        PutAll(std::forward<DATA>(data_unique_ptrs)...);
+    }
+
+    /// Destructor
+    ~DataMap();
+
+    /// Move assignment operator
+    /// @param rhs the DataMap to move into this DataMap
+    /// @return this DataMap
+    DataMap& operator=(DataMap&& rhs);
+
+    /// Adds the data into DataMap keyed by the ClassID of type T.
+    /// @param data the data to add to the DataMap
+    template <typename T>
+    void Put(std::unique_ptr<T>&& data) {
+        static_assert(std::is_base_of<Data, T>::value, "T does not derive from Data");
+        map_[&utils::TypeInfo::Of<T>()] = std::move(data);
+    }
+
+    /// Creates the data of type `T` with the provided arguments and adds it into DataMap keyed by
+    /// the ClassID of type T.
+    /// @param args the arguments forwarded to the initializer for type T
+    template <typename T, typename... ARGS>
+    void Add(ARGS&&... args) {
+        Put(std::make_unique<T>(std::forward<ARGS>(args)...));
+    }
+
+    /// @returns a pointer to the Data placed into the DataMap with a call to Put()
+    template <typename T>
+    T const* Get() const {
+        return const_cast<DataMap*>(this)->Get<T>();
+    }
+
+    /// @returns a pointer to the Data placed into the DataMap with a call to Put()
+    template <typename T>
+    T* Get() {
+        auto it = map_.find(&utils::TypeInfo::Of<T>());
+        if (it == map_.end()) {
+            return nullptr;
+        }
+        return static_cast<T*>(it->second.get());
+    }
+
+    /// Add moves all the data from other into this DataMap
+    /// @param other the DataMap to move into this DataMap
+    void Add(DataMap&& other) {
+        for (auto& it : other.map_) {
+            map_.emplace(it.first, std::move(it.second));
+        }
+        other.map_.clear();
+    }
+
+  private:
+    template <typename T0>
+    void PutAll(T0&& first) {
+        Put(std::forward<T0>(first));
+    }
+
+    template <typename T0, typename... Tn>
+    void PutAll(T0&& first, Tn&&... remainder) {
+        Put(std::forward<T0>(first));
+        PutAll(std::forward<Tn>(remainder)...);
+    }
+
+    std::unordered_map<const utils::TypeInfo*, std::unique_ptr<Data>> map_;
+};
+
+/// Interface for transforms.
+class Transform : public utils::Castable<Transform> {
+  public:
+    /// @copydoc tint::transform::Data
+    using Data = tint::transform::Data;
+    /// @copydoc tint::transform::DataMap
+    using DataMap = tint::transform::DataMap;
+
+    /// Constructor
+    Transform();
+    /// Destructor
+    ~Transform() override;
+};
+
+}  // namespace tint::transform
+
+#endif  // SRC_TINT_TRANSFORM_TRANSFORM_H_
diff --git a/src/tint/writer/flatten_bindings.cc b/src/tint/writer/flatten_bindings.cc
index 3aa659d..fe262d6 100644
--- a/src/tint/writer/flatten_bindings.cc
+++ b/src/tint/writer/flatten_bindings.cc
@@ -63,16 +63,15 @@
     }
 
     // Run the binding remapper transform.
-    tint::ast::transform::Output transform_output;
     if (!binding_points.empty()) {
         tint::transform::Manager manager;
-        tint::ast::transform::DataMap inputs;
+        tint::transform::DataMap inputs;
+        tint::transform::DataMap outputs;
         inputs.Add<tint::ast::transform::BindingRemapper::Remappings>(
             std::move(binding_points), tint::ast::transform::BindingRemapper::AccessControls{},
             /* mayCollide */ true);
         manager.Add<tint::ast::transform::BindingRemapper>();
-        transform_output = manager.Run(program, inputs);
-        return std::move(transform_output.program);
+        return manager.Run(program, inputs, outputs);
     }
 
     return {};
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index d3d8130..a15f6a9 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -152,7 +152,7 @@
                          const Options& options,
                          const std::string& entry_point) {
     transform::Manager manager;
-    ast::transform::DataMap data;
+    transform::DataMap data;
 
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
@@ -246,10 +246,9 @@
     data.Add<ast::transform::CanonicalizeEntryPointIO::Config>(
         ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kGlsl);
 
-    auto out = manager.Run(in, data);
-
     SanitizedResult result;
-    result.program = std::move(out.program);
+    transform::DataMap outputs;
+    result.program = manager.Run(in, data, outputs);
     return result;
 }
 
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index c9ebddd..95d2022 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -167,7 +167,7 @@
 
 SanitizedResult Sanitize(const Program* in, const Options& options) {
     transform::Manager manager;
-    ast::transform::DataMap data;
+    transform::DataMap data;
 
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
@@ -305,11 +305,10 @@
         ast::transform::CanonicalizeEntryPointIO::ShaderStyle::kHlsl);
     data.Add<ast::transform::NumWorkgroupsFromUniform::Config>(options.root_constant_binding_point);
 
-    auto out = manager.Run(in, data);
-
     SanitizedResult result;
-    result.program = std::move(out.program);
-    if (auto* res = out.data.Get<ast::transform::ArrayLengthFromUniform::Result>()) {
+    transform::DataMap outputs;
+    result.program = manager.Run(in, data, outputs);
+    if (auto* res = outputs.Get<ast::transform::ArrayLengthFromUniform::Result>()) {
         result.used_array_length_from_uniform_indices = std::move(res->used_size_indices);
     }
     return result;
diff --git a/src/tint/writer/hlsl/test_helper.h b/src/tint/writer/hlsl/test_helper.h
index e645346..2f8e886 100644
--- a/src/tint/writer/hlsl/test_helper.h
+++ b/src/tint/writer/hlsl/test_helper.h
@@ -87,16 +87,15 @@
         }();
 
         transform::Manager transform_manager;
-        ast::transform::DataMap transform_data;
+        transform::DataMap transform_data;
+        transform::DataMap outputs;
         transform_data.Add<ast::transform::Renamer::Config>(
             ast::transform::Renamer::Target::kHlslKeywords,
             /* preserve_unicode */ true);
         transform_manager.Add<tint::ast::transform::Renamer>();
-        auto result = transform_manager.Run(&sanitized_result.program, transform_data);
-        [&]() {
-            ASSERT_TRUE(result.program.IsValid()) << formatter.format(result.program.Diagnostics());
-        }();
-        *program = std::move(result.program);
+        auto result = transform_manager.Run(&sanitized_result.program, transform_data, outputs);
+        [&]() { ASSERT_TRUE(result.IsValid()) << formatter.format(result.Diagnostics()); }();
+        *program = std::move(result);
         gen_ = std::make_unique<GeneratorImpl>(program.get());
         return *gen_;
     }
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 23c39f3..55b9f8d 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -167,7 +167,7 @@
 
 SanitizedResult Sanitize(const Program* in, const Options& options) {
     transform::Manager manager;
-    ast::transform::DataMap data;
+    transform::DataMap data;
 
     manager.Add<ast::transform::DisableUniformityAnalysis>();
 
@@ -257,14 +257,13 @@
     manager.Add<ast::transform::PackedVec3>();
     manager.Add<ast::transform::ModuleScopeVarToEntryPointParam>();
 
-    auto out = manager.Run(in, data);
-
     SanitizedResult result;
-    result.program = std::move(out.program);
+    transform::DataMap outputs;
+    result.program = manager.Run(in, data, outputs);
     if (!result.program.IsValid()) {
         return result;
     }
-    if (auto* res = out.data.Get<ast::transform::ArrayLengthFromUniform::Result>()) {
+    if (auto* res = outputs.Get<ast::transform::ArrayLengthFromUniform::Result>()) {
         result.used_array_length_from_uniform_indices = std::move(res->used_size_indices);
     }
     result.needs_storage_buffer_sizes = !result.used_array_length_from_uniform_indices.empty();
diff --git a/src/tint/writer/spirv/generator_impl.cc b/src/tint/writer/spirv/generator_impl.cc
index 9df66ef..1b22710 100644
--- a/src/tint/writer/spirv/generator_impl.cc
+++ b/src/tint/writer/spirv/generator_impl.cc
@@ -49,7 +49,7 @@
 
 SanitizedResult Sanitize(const Program* in, const Options& options) {
     transform::Manager manager;
-    ast::transform::DataMap data;
+    transform::DataMap data;
 
     if (options.clamp_frag_depth) {
         manager.Add<tint::ast::transform::ClampFragDepth>();
@@ -166,7 +166,8 @@
             options.emit_vertex_point_size));
 
     SanitizedResult result;
-    result.program = std::move(manager.Run(in, data).program);
+    transform::DataMap outputs;
+    result.program = manager.Run(in, data, outputs);
     return result;
 }
 
diff --git a/src/tint/writer/spirv/generator_impl_binary_test.cc b/src/tint/writer/spirv/generator_impl_binary_test.cc
index d5cd2bb..4bf6b81 100644
--- a/src/tint/writer/spirv/generator_impl_binary_test.cc
+++ b/src/tint/writer/spirv/generator_impl_binary_test.cc
@@ -20,13 +20,11 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_I32) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("foo");
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
-    func->start_target->instructions.Push(CreateBinary(
-        ir::Binary::Kind::kAdd, ir.types.Get<type::I32>(), Constant(1_i), Constant(2_i)));
+    func->start_target->instructions.Push(
+        b.Add(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i)));
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -44,13 +42,11 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_U32) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("foo");
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
-    func->start_target->instructions.Push(CreateBinary(
-        ir::Binary::Kind::kAdd, ir.types.Get<type::U32>(), Constant(1_u), Constant(2_u)));
+    func->start_target->instructions.Push(
+        b.Add(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u)));
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -68,13 +64,11 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Binary_Add_F32) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("foo");
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
-    func->start_target->instructions.Push(CreateBinary(
-        ir::Binary::Kind::kAdd, ir.types.Get<type::F32>(), Constant(1_f), Constant(2_f)));
+    func->start_target->instructions.Push(
+        b.Add(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f)));
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -91,17 +85,12 @@
 )");
 }
 
-TEST_F(SpvGeneratorImplTest, Binary_Add_Chain) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("foo");
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+TEST_F(SpvGeneratorImplTest, Binary_Sub_I32) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
-    auto* a = CreateBinary(ir::Binary::Kind::kAdd, ir.types.Get<type::I32>(), Constant(1_i),
-                           Constant(2_i));
-    func->start_target->instructions.Push(a);
     func->start_target->instructions.Push(
-        CreateBinary(ir::Binary::Kind::kAdd, ir.types.Get<type::I32>(), a, a));
+        b.Subtract(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i)));
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -112,7 +101,148 @@
 %8 = OpConstant %6 2
 %1 = OpFunction %2 None %3
 %4 = OpLabel
-%5 = OpIAdd %6 %7 %8
+%5 = OpISub %6 %7 %8
+OpReturn
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Binary_Sub_U32) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
+
+    func->start_target->instructions.Push(
+        b.Subtract(mod.types.Get<type::U32>(), b.Constant(1_u), b.Constant(2_u)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%6 = OpTypeInt 32 0
+%7 = OpConstant %6 1
+%8 = OpConstant %6 2
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+%5 = OpISub %6 %7 %8
+OpReturn
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Binary_Sub_F32) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
+
+    func->start_target->instructions.Push(
+        b.Subtract(mod.types.Get<type::F32>(), b.Constant(1_f), b.Constant(2_f)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%6 = OpTypeFloat 32
+%7 = OpConstant %6 1
+%8 = OpConstant %6 2
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+%5 = OpFSub %6 %7 %8
+OpReturn
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Binary_Sub_Vec2i) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
+
+    auto* lhs = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u),
+        utils::Vector{b.Constant(42_i)->value, b.Constant(-1_i)->value}, false, false);
+    auto* rhs = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u),
+        utils::Vector{b.Constant(0_i)->value, b.Constant(-43_i)->value}, false, false);
+    func->start_target->instructions.Push(
+        b.Subtract(mod.types.Get<type::Vector>(mod.types.Get<type::I32>(), 2u), b.Constant(lhs),
+                   b.Constant(rhs)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%7 = OpTypeInt 32 1
+%6 = OpTypeVector %7 2
+%9 = OpConstant %7 42
+%10 = OpConstant %7 -1
+%8 = OpConstantComposite %6 %9 %10
+%12 = OpConstant %7 0
+%13 = OpConstant %7 -43
+%11 = OpConstantComposite %6 %12 %13
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+%5 = OpISub %6 %8 %11
+OpReturn
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Binary_Sub_Vec4f) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
+
+    auto* lhs = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u),
+        utils::Vector{b.Constant(42_f)->value, b.Constant(-1_f)->value, b.Constant(0_f)->value,
+                      b.Constant(1.25_f)->value},
+        false, false);
+    auto* rhs = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u),
+        utils::Vector{b.Constant(0_f)->value, b.Constant(1.25_f)->value, b.Constant(-42_f)->value,
+                      b.Constant(1_f)->value},
+        false, false);
+    func->start_target->instructions.Push(
+        b.Subtract(mod.types.Get<type::Vector>(mod.types.Get<type::F32>(), 4u), b.Constant(lhs),
+                   b.Constant(rhs)));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%7 = OpTypeFloat 32
+%6 = OpTypeVector %7 4
+%9 = OpConstant %7 42
+%10 = OpConstant %7 -1
+%11 = OpConstant %7 0
+%12 = OpConstant %7 1.25
+%8 = OpConstantComposite %6 %9 %10 %11 %12
+%14 = OpConstant %7 -42
+%15 = OpConstant %7 1
+%13 = OpConstantComposite %6 %11 %12 %14 %15
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+%5 = OpFSub %6 %8 %13
+OpReturn
+OpFunctionEnd
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Binary_Chain) {
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
+
+    auto* a = b.Subtract(mod.types.Get<type::I32>(), b.Constant(1_i), b.Constant(2_i));
+    func->start_target->instructions.Push(a);
+    func->start_target->instructions.Push(b.Add(mod.types.Get<type::I32>(), a, a));
+
+    generator_.EmitFunction(func);
+    EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
+%2 = OpTypeVoid
+%3 = OpTypeFunction %2
+%6 = OpTypeInt 32 1
+%7 = OpConstant %6 1
+%8 = OpConstant %6 2
+%1 = OpFunction %2 None %3
+%4 = OpLabel
+%5 = OpISub %6 %7 %8
 %9 = OpIAdd %6 %5 %5
 OpReturn
 OpFunctionEnd
diff --git a/src/tint/writer/spirv/generator_impl_constant_test.cc b/src/tint/writer/spirv/generator_impl_constant_test.cc
index c8d7a10..1e27e89 100644
--- a/src/tint/writer/spirv/generator_impl_constant_test.cc
+++ b/src/tint/writer/spirv/generator_impl_constant_test.cc
@@ -17,9 +17,9 @@
 namespace tint::writer::spirv {
 namespace {
 
-TEST_F(SpvGeneratorImplTest, Type_Bool) {
-    generator_.Constant(Constant(true));
-    generator_.Constant(Constant(false));
+TEST_F(SpvGeneratorImplTest, Constant_Bool) {
+    generator_.Constant(b.Constant(true));
+    generator_.Constant(b.Constant(false));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeBool
 %1 = OpConstantTrue %2
 %3 = OpConstantFalse %2
@@ -27,8 +27,8 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_I32) {
-    generator_.Constant(Constant(i32(42)));
-    generator_.Constant(Constant(i32(-1)));
+    generator_.Constant(b.Constant(i32(42)));
+    generator_.Constant(b.Constant(i32(-1)));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeInt 32 1
 %1 = OpConstant %2 42
 %3 = OpConstant %2 -1
@@ -36,8 +36,8 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_U32) {
-    generator_.Constant(Constant(u32(42)));
-    generator_.Constant(Constant(u32(4000000000)));
+    generator_.Constant(b.Constant(u32(42)));
+    generator_.Constant(b.Constant(u32(4000000000)));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeInt 32 0
 %1 = OpConstant %2 42
 %3 = OpConstant %2 4000000000
@@ -45,8 +45,8 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_F32) {
-    generator_.Constant(Constant(f32(42)));
-    generator_.Constant(Constant(f32(-1)));
+    generator_.Constant(b.Constant(f32(42)));
+    generator_.Constant(b.Constant(f32(-1)));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeFloat 32
 %1 = OpConstant %2 42
 %3 = OpConstant %2 -1
@@ -54,19 +54,102 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Constant_F16) {
-    generator_.Constant(Constant(f16(42)));
-    generator_.Constant(Constant(f16(-1)));
+    generator_.Constant(b.Constant(f16(42)));
+    generator_.Constant(b.Constant(f16(-1)));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeFloat 16
 %1 = OpConstant %2 0x1.5p+5
 %3 = OpConstant %2 -0x1p+0
 )");
 }
 
+TEST_F(SpvGeneratorImplTest, Constant_Vec4Bool) {
+    auto* t = b.Constant(true);
+    auto* f = b.Constant(false);
+    auto* v = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(mod.types.Get<type::Bool>(), 4u),
+        utils::Vector{t->value, f->value, f->value, t->value}, false, true);
+    generator_.Constant(b.Constant(v));
+    EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeBool
+%2 = OpTypeVector %3 4
+%4 = OpConstantTrue %3
+%5 = OpConstantFalse %3
+%1 = OpConstantComposite %2 %4 %5 %5 %4
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Constant_Vec2i) {
+    auto* i = mod.types.Get<type::I32>();
+    auto* i_42 = b.Constant(i32(42));
+    auto* i_n1 = b.Constant(i32(-1));
+    auto* v = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(i, 2u), utils::Vector{i_42->value, i_n1->value}, false, false);
+    generator_.Constant(b.Constant(v));
+    EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeInt 32 1
+%2 = OpTypeVector %3 2
+%4 = OpConstant %3 42
+%5 = OpConstant %3 -1
+%1 = OpConstantComposite %2 %4 %5
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Constant_Vec3u) {
+    auto* u = mod.types.Get<type::U32>();
+    auto* u_42 = b.Constant(u32(42));
+    auto* u_0 = b.Constant(u32(0));
+    auto* u_4b = b.Constant(u32(4000000000));
+    auto* v = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(u, 3u), utils::Vector{u_42->value, u_0->value, u_4b->value},
+        false, true);
+    generator_.Constant(b.Constant(v));
+    EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeInt 32 0
+%2 = OpTypeVector %3 3
+%4 = OpConstant %3 42
+%5 = OpConstant %3 0
+%6 = OpConstant %3 4000000000
+%1 = OpConstantComposite %2 %4 %5 %6
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Constant_Vec4f) {
+    auto* f = mod.types.Get<type::F32>();
+    auto* f_42 = b.Constant(f32(42));
+    auto* f_0 = b.Constant(f32(0));
+    auto* f_q = b.Constant(f32(0.25));
+    auto* f_n1 = b.Constant(f32(-1));
+    auto* v = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(f, 4u),
+        utils::Vector{f_42->value, f_0->value, f_q->value, f_n1->value}, false, true);
+    generator_.Constant(b.Constant(v));
+    EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeFloat 32
+%2 = OpTypeVector %3 4
+%4 = OpConstant %3 42
+%5 = OpConstant %3 0
+%6 = OpConstant %3 0.25
+%7 = OpConstant %3 -1
+%1 = OpConstantComposite %2 %4 %5 %6 %7
+)");
+}
+
+TEST_F(SpvGeneratorImplTest, Constant_Vec2h) {
+    auto* h = mod.types.Get<type::F16>();
+    auto* h_42 = b.Constant(f16(42));
+    auto* h_q = b.Constant(f16(0.25));
+    auto* v = mod.constants.Create<constant::Composite>(
+        mod.types.Get<type::Vector>(h, 2u), utils::Vector{h_42->value, h_q->value}, false, false);
+    generator_.Constant(b.Constant(v));
+    EXPECT_EQ(DumpTypes(), R"(%3 = OpTypeFloat 16
+%2 = OpTypeVector %3 2
+%4 = OpConstant %3 0x1.5p+5
+%5 = OpConstant %3 0x1p-2
+%1 = OpConstantComposite %2 %4 %5
+)");
+}
+
 // Test that we do not emit the same constant more than once.
 TEST_F(SpvGeneratorImplTest, Constant_Deduplicate) {
-    generator_.Constant(Constant(i32(42)));
-    generator_.Constant(Constant(i32(42)));
-    generator_.Constant(Constant(i32(42)));
+    generator_.Constant(b.Constant(i32(42)));
+    generator_.Constant(b.Constant(i32(42)));
+    generator_.Constant(b.Constant(i32(42)));
     EXPECT_EQ(DumpTypes(), R"(%2 = OpTypeInt 32 1
 %1 = OpConstant %2 42
 )");
diff --git a/src/tint/writer/spirv/generator_impl_function_test.cc b/src/tint/writer/spirv/generator_impl_function_test.cc
index a1ce3cc..b8faebb 100644
--- a/src/tint/writer/spirv/generator_impl_function_test.cc
+++ b/src/tint/writer/spirv/generator_impl_function_test.cc
@@ -18,10 +18,8 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, Function_Empty) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("foo");
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpName %1 "foo"
@@ -36,9 +34,8 @@
 
 // Test that we do not emit the same function type more than once.
 TEST_F(SpvGeneratorImplTest, Function_DeduplicateType) {
-    auto* func = CreateFunction();
-    func->return_type = ir.types.Get<type::Void>();
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("foo"), mod.types.Get<type::Void>());
+    b.Branch(func->start_target, func->end_target);
 
     generator_.EmitFunction(func);
     generator_.EmitFunction(func);
@@ -49,12 +46,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Compute) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("main");
-    func->return_type = ir.types.Get<type::Void>();
-    func->pipeline_stage = ir::Function::PipelineStage::kCompute;
-    func->workgroup_size = {32, 4, 1};
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
+                                  ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
+    b.Branch(func->start_target, func->end_target);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint GLCompute %1 "main"
@@ -70,11 +64,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Fragment) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("main");
-    func->return_type = ir.types.Get<type::Void>();
-    func->pipeline_stage = ir::Function::PipelineStage::kFragment;
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
+                                  ir::Function::PipelineStage::kFragment);
+    b.Branch(func->start_target, func->end_target);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint Fragment %1 "main"
@@ -90,11 +82,9 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Vertex) {
-    auto* func = CreateFunction();
-    func->name = ir.symbols.Register("main");
-    func->return_type = ir.types.Get<type::Void>();
-    func->pipeline_stage = ir::Function::PipelineStage::kVertex;
-    func->start_target->branch.target = func->end_target;
+    auto* func = b.CreateFunction(mod.symbols.Register("main"), mod.types.Get<type::Void>(),
+                                  ir::Function::PipelineStage::kVertex);
+    b.Branch(func->start_target, func->end_target);
 
     generator_.EmitFunction(func);
     EXPECT_EQ(DumpModule(generator_.Module()), R"(OpEntryPoint Vertex %1 "main"
@@ -109,25 +99,17 @@
 }
 
 TEST_F(SpvGeneratorImplTest, Function_EntryPoint_Multiple) {
-    auto* f1 = CreateFunction();
-    f1->name = ir.symbols.Register("main1");
-    f1->return_type = ir.types.Get<type::Void>();
-    f1->pipeline_stage = ir::Function::PipelineStage::kCompute;
-    f1->workgroup_size = {32, 4, 1};
-    f1->start_target->branch.target = f1->end_target;
+    auto* f1 = b.CreateFunction(mod.symbols.Register("main1"), mod.types.Get<type::Void>(),
+                                ir::Function::PipelineStage::kCompute, {{32, 4, 1}});
+    b.Branch(f1->start_target, f1->end_target);
 
-    auto* f2 = CreateFunction();
-    f2->name = ir.symbols.Register("main2");
-    f2->return_type = ir.types.Get<type::Void>();
-    f2->pipeline_stage = ir::Function::PipelineStage::kCompute;
-    f2->workgroup_size = {8, 2, 16};
-    f2->start_target->branch.target = f2->end_target;
+    auto* f2 = b.CreateFunction(mod.symbols.Register("main2"), mod.types.Get<type::Void>(),
+                                ir::Function::PipelineStage::kCompute, {{8, 2, 16}});
+    b.Branch(f2->start_target, f2->end_target);
 
-    auto* f3 = CreateFunction();
-    f3->name = ir.symbols.Register("main3");
-    f3->return_type = ir.types.Get<type::Void>();
-    f3->pipeline_stage = ir::Function::PipelineStage::kFragment;
-    f3->start_target->branch.target = f3->end_target;
+    auto* f3 = b.CreateFunction(mod.symbols.Register("main3"), mod.types.Get<type::Void>(),
+                                ir::Function::PipelineStage::kFragment);
+    b.Branch(f3->start_target, f3->end_target);
 
     generator_.EmitFunction(f1);
     generator_.EmitFunction(f2);
diff --git a/src/tint/writer/spirv/generator_impl_ir.cc b/src/tint/writer/spirv/generator_impl_ir.cc
index 202ccbe..5d8c2f0 100644
--- a/src/tint/writer/spirv/generator_impl_ir.cc
+++ b/src/tint/writer/spirv/generator_impl_ir.cc
@@ -26,6 +26,7 @@
 #include "src/tint/type/i32.h"
 #include "src/tint/type/type.h"
 #include "src/tint/type/u32.h"
+#include "src/tint/type/vector.h"
 #include "src/tint/type/void.h"
 #include "src/tint/writer/spirv/module.h"
 
@@ -67,31 +68,41 @@
 }
 
 uint32_t GeneratorImplIr::Constant(const ir::Constant* constant) {
+    return Constant(constant->value);
+}
+
+uint32_t GeneratorImplIr::Constant(const constant::Value* constant) {
     return constants_.GetOrCreate(constant, [&]() {
         auto id = module_.NextId();
         auto* ty = constant->Type();
-        auto* value = constant->value;
         Switch(
             ty,  //
             [&](const type::Bool*) {
                 module_.PushType(
-                    value->ValueAs<bool>() ? spv::Op::OpConstantTrue : spv::Op::OpConstantFalse,
+                    constant->ValueAs<bool>() ? spv::Op::OpConstantTrue : spv::Op::OpConstantFalse,
                     {Type(ty), id});
             },
             [&](const type::I32*) {
-                module_.PushType(spv::Op::OpConstant, {Type(ty), id, value->ValueAs<u32>()});
+                module_.PushType(spv::Op::OpConstant, {Type(ty), id, constant->ValueAs<u32>()});
             },
             [&](const type::U32*) {
                 module_.PushType(spv::Op::OpConstant,
-                                 {Type(ty), id, U32Operand(value->ValueAs<i32>())});
+                                 {Type(ty), id, U32Operand(constant->ValueAs<i32>())});
             },
             [&](const type::F32*) {
-                module_.PushType(spv::Op::OpConstant, {Type(ty), id, value->ValueAs<f32>()});
+                module_.PushType(spv::Op::OpConstant, {Type(ty), id, constant->ValueAs<f32>()});
             },
             [&](const type::F16*) {
                 module_.PushType(
                     spv::Op::OpConstant,
-                    {Type(ty), id, U32Operand(value->ValueAs<f16>().BitsRepresentation())});
+                    {Type(ty), id, U32Operand(constant->ValueAs<f16>().BitsRepresentation())});
+            },
+            [&](const type::Vector* vec) {
+                OperandList operands = {Type(ty), id};
+                for (uint32_t i = 0; i < vec->Width(); i++) {
+                    operands.push_back(Constant(constant->Index(i)));
+                }
+                module_.PushType(spv::Op::OpConstantComposite, operands);
             },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_) << "unhandled constant type: " << ty->FriendlyName();
@@ -119,6 +130,9 @@
             [&](const type::F16*) {
                 module_.PushType(spv::Op::OpTypeFloat, {id, 16u});
             },
+            [&](const type::Vector* vec) {
+                module_.PushType(spv::Op::OpTypeVector, {id, Type(vec->type()), vec->Width()});
+            },
             [&](Default) {
                 TINT_ICE(Writer, diagnostics_) << "unhandled type: " << ty->FriendlyName();
             });
@@ -257,6 +271,10 @@
             op = binary->Type()->is_integer_scalar_or_vector() ? spv::Op::OpIAdd : spv::Op::OpFAdd;
             break;
         }
+        case ir::Binary::Kind::kSubtract: {
+            op = binary->Type()->is_integer_scalar_or_vector() ? spv::Op::OpISub : spv::Op::OpFSub;
+            break;
+        }
         default: {
             TINT_ICE(Writer, diagnostics_)
                 << "unimplemented binary instruction: " << static_cast<uint32_t>(binary->kind);
diff --git a/src/tint/writer/spirv/generator_impl_ir.h b/src/tint/writer/spirv/generator_impl_ir.h
index 61870d4..e885edb 100644
--- a/src/tint/writer/spirv/generator_impl_ir.h
+++ b/src/tint/writer/spirv/generator_impl_ir.h
@@ -95,6 +95,11 @@
     uint32_t EmitBinary(const ir::Binary* binary);
 
   private:
+    /// Get the result ID of the constant `constant`, emitting its instruction if necessary.
+    /// @param constant the constant to get the ID for
+    /// @returns the result ID of the constant
+    uint32_t Constant(const constant::Value* constant);
+
     const ir::Module* ir_;
     spirv::Module module_;
     BinaryWriter writer_;
@@ -125,22 +130,22 @@
         }
     };
 
-    /// ConstantHasher provides a hash function for an ir::Constant pointer, hashing the value
+    /// ConstantHasher provides a hash function for a constant::Value pointer, hashing the value
     /// instead of the pointer itself.
     struct ConstantHasher {
-        /// @param c the ir::Constant pointer to create a hash for
+        /// @param c the constant::Value pointer to create a hash for
         /// @return the hash value
-        inline std::size_t operator()(const ir::Constant* c) const { return c->value->Hash(); }
+        inline std::size_t operator()(const constant::Value* c) const { return c->Hash(); }
     };
 
-    /// ConstantEquals provides an equality function for two ir::Constant pointers, comparing their
-    /// values instead of the pointers.
+    /// ConstantEquals provides an equality function for two constant::Value pointers, comparing
+    /// their values instead of the pointers.
     struct ConstantEquals {
-        /// @param a the first ir::Constant pointer to compare
-        /// @param b the second ir::Constant pointer to compare
+        /// @param a the first constant::Value pointer to compare
+        /// @param b the second constant::Value pointer to compare
         /// @return the hash value
-        inline bool operator()(const ir::Constant* a, const ir::Constant* b) const {
-            return a->value->Equal(b->value);
+        inline bool operator()(const constant::Value* a, const constant::Value* b) const {
+            return a->Equal(b);
         }
     };
 
@@ -151,7 +156,7 @@
     utils::Hashmap<FunctionType, uint32_t, 8, FunctionType::Hasher> function_types_;
 
     /// The map of constants to their result IDs.
-    utils::Hashmap<const ir::Constant*, uint32_t, 16, ConstantHasher, ConstantEquals> constants_;
+    utils::Hashmap<const constant::Value*, uint32_t, 16, ConstantHasher, ConstantEquals> constants_;
 
     /// The map of instructions to their result IDs.
     utils::Hashmap<const ir::Instruction*, uint32_t, 8> instructions_;
diff --git a/src/tint/writer/spirv/generator_impl_type_test.cc b/src/tint/writer/spirv/generator_impl_type_test.cc
index c8a2e8e..c86d363 100644
--- a/src/tint/writer/spirv/generator_impl_type_test.cc
+++ b/src/tint/writer/spirv/generator_impl_type_test.cc
@@ -25,48 +25,93 @@
 namespace {
 
 TEST_F(SpvGeneratorImplTest, Type_Void) {
-    auto id = generator_.Type(ir.types.Get<type::Void>());
+    auto id = generator_.Type(mod.types.Get<type::Void>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeVoid\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_Bool) {
-    auto id = generator_.Type(ir.types.Get<type::Bool>());
+    auto id = generator_.Type(mod.types.Get<type::Bool>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeBool\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_I32) {
-    auto id = generator_.Type(ir.types.Get<type::I32>());
+    auto id = generator_.Type(mod.types.Get<type::I32>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeInt 32 1\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_U32) {
-    auto id = generator_.Type(ir.types.Get<type::U32>());
+    auto id = generator_.Type(mod.types.Get<type::U32>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeInt 32 0\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_F32) {
-    auto id = generator_.Type(ir.types.Get<type::F32>());
+    auto id = generator_.Type(mod.types.Get<type::F32>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeFloat 32\n");
 }
 
 TEST_F(SpvGeneratorImplTest, Type_F16) {
-    auto id = generator_.Type(ir.types.Get<type::F16>());
+    auto id = generator_.Type(mod.types.Get<type::F16>());
     EXPECT_EQ(id, 1u);
     EXPECT_EQ(DumpTypes(), "%1 = OpTypeFloat 16\n");
 }
 
-// Test that we do can emit multiple types.
+TEST_F(SpvGeneratorImplTest, Type_Vec2i) {
+    auto* vec = b.ir.types.Get<type::Vector>(b.ir.types.Get<type::I32>(), 2u);
+    auto id = generator_.Type(vec);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%2 = OpTypeInt 32 1\n"
+              "%1 = OpTypeVector %2 2\n");
+}
+
+TEST_F(SpvGeneratorImplTest, Type_Vec3u) {
+    auto* vec = b.ir.types.Get<type::Vector>(b.ir.types.Get<type::U32>(), 3u);
+    auto id = generator_.Type(vec);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%2 = OpTypeInt 32 0\n"
+              "%1 = OpTypeVector %2 3\n");
+}
+
+TEST_F(SpvGeneratorImplTest, Type_Vec4f) {
+    auto* vec = b.ir.types.Get<type::Vector>(b.ir.types.Get<type::F32>(), 4u);
+    auto id = generator_.Type(vec);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%2 = OpTypeFloat 32\n"
+              "%1 = OpTypeVector %2 4\n");
+}
+
+TEST_F(SpvGeneratorImplTest, Type_Vec4h) {
+    auto* vec = b.ir.types.Get<type::Vector>(b.ir.types.Get<type::F16>(), 2u);
+    auto id = generator_.Type(vec);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%2 = OpTypeFloat 16\n"
+              "%1 = OpTypeVector %2 2\n");
+}
+
+TEST_F(SpvGeneratorImplTest, Type_Vec4Bool) {
+    auto* vec = b.ir.types.Get<type::Vector>(b.ir.types.Get<type::Bool>(), 4u);
+    auto id = generator_.Type(vec);
+    EXPECT_EQ(id, 1u);
+    EXPECT_EQ(DumpTypes(),
+              "%2 = OpTypeBool\n"
+              "%1 = OpTypeVector %2 4\n");
+}
+
+// Test that we can emit multiple types.
 // Includes types with the same opcode but different parameters.
 TEST_F(SpvGeneratorImplTest, Type_Multiple) {
-    EXPECT_EQ(generator_.Type(ir.types.Get<type::I32>()), 1u);
-    EXPECT_EQ(generator_.Type(ir.types.Get<type::U32>()), 2u);
-    EXPECT_EQ(generator_.Type(ir.types.Get<type::F32>()), 3u);
-    EXPECT_EQ(generator_.Type(ir.types.Get<type::F16>()), 4u);
+    EXPECT_EQ(generator_.Type(mod.types.Get<type::I32>()), 1u);
+    EXPECT_EQ(generator_.Type(mod.types.Get<type::U32>()), 2u);
+    EXPECT_EQ(generator_.Type(mod.types.Get<type::F32>()), 3u);
+    EXPECT_EQ(generator_.Type(mod.types.Get<type::F16>()), 4u);
     EXPECT_EQ(DumpTypes(), R"(%1 = OpTypeInt 32 1
 %2 = OpTypeInt 32 0
 %3 = OpTypeFloat 32
@@ -76,7 +121,7 @@
 
 // Test that we do not emit the same type more than once.
 TEST_F(SpvGeneratorImplTest, Type_Deduplicate) {
-    auto* i32 = ir.types.Get<type::I32>();
+    auto* i32 = mod.types.Get<type::I32>();
     EXPECT_EQ(generator_.Type(i32), 1u);
     EXPECT_EQ(generator_.Type(i32), 1u);
     EXPECT_EQ(generator_.Type(i32), 1u);
diff --git a/src/tint/writer/spirv/test_helper_ir.h b/src/tint/writer/spirv/test_helper_ir.h
index 6de6f48..3574645 100644
--- a/src/tint/writer/spirv/test_helper_ir.h
+++ b/src/tint/writer/spirv/test_helper_ir.h
@@ -26,9 +26,14 @@
 
 /// Base helper class for testing the SPIR-V generator implementation.
 template <typename BASE>
-class SpvGeneratorTestHelperBase : public ir::Builder, public BASE {
+class SpvGeneratorTestHelperBase : public BASE {
   public:
-    SpvGeneratorTestHelperBase() : generator_(&ir, false) {}
+    SpvGeneratorTestHelperBase() : generator_(&mod, false) {}
+
+    /// The test module
+    ir::Module mod;
+    /// The test builder
+    ir::Builder b{mod};
 
   protected:
     /// The SPIR-V generator.