src/transform: Reimplement tests in WGSL

Easier to read and write, and ensures that the tests exercise valid AST instead of synthetic structures that can never exist.

Change-Id: I5d361ef96383c71943a424f5765952f21d740042
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/36422
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 01684a9..547b74a 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -831,6 +831,7 @@
     "src/transform/bound_array_accessors_test.cc",
     "src/transform/emit_vertex_point_size_test.cc",
     "src/transform/first_index_offset_test.cc",
+    "src/transform/test_helper.h",
     "src/transform/vertex_pulling_test.cc",
     "src/type_determiner_test.cc",
     "src/validator/validator_control_block_test.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1a7c6a4..adc89af 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -465,10 +465,6 @@
     scope_stack_test.cc
     symbol_table_test.cc
     symbol_test.cc
-    transform/emit_vertex_point_size_test.cc
-    transform/bound_array_accessors_test.cc
-    transform/first_index_offset_test.cc
-    transform/vertex_pulling_test.cc
     type_determiner_test.cc
     validator/validator_control_block_test.cc
     validator/validator_function_test.cc
@@ -656,6 +652,16 @@
     )
   endif()
 
+  if(${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      transform/bound_array_accessors_test.cc
+      transform/emit_vertex_point_size_test.cc
+      transform/first_index_offset_test.cc
+      transform/test_helper.h
+      transform/vertex_pulling_test.cc
+    )
+  endif()
+
   if(${TINT_BUILD_MSL_WRITER})
     list(APPEND TINT_TEST_SRCS
       writer/msl/generator_impl_alias_type_test.cc
diff --git a/src/transform/bound_array_accessors_test.cc b/src/transform/bound_array_accessors_test.cc
index a3c148f..e7d70ce 100644
--- a/src/transform/bound_array_accessors_test.cc
+++ b/src/transform/bound_array_accessors_test.cc
@@ -14,918 +14,400 @@
 
 #include "src/transform/bound_array_accessors.h"
 
-#include <memory>
-#include <utility>
-
-#include "gtest/gtest.h"
-#include "src/ast/array_accessor_expression.h"
-#include "src/ast/binary_expression.h"
-#include "src/ast/block_statement.h"
-#include "src/ast/builder.h"
-#include "src/ast/call_expression.h"
-#include "src/ast/function.h"
-#include "src/ast/identifier_expression.h"
-#include "src/ast/module.h"
-#include "src/ast/scalar_constructor_expression.h"
-#include "src/ast/sint_literal.h"
-#include "src/ast/storage_class.h"
-#include "src/ast/type/array_type.h"
-#include "src/ast/type/f32_type.h"
-#include "src/ast/type/i32_type.h"
-#include "src/ast/type/matrix_type.h"
-#include "src/ast/type/pointer_type.h"
-#include "src/ast/type/u32_type.h"
-#include "src/ast/type/vector_type.h"
-#include "src/ast/type/void_type.h"
-#include "src/ast/type_constructor_expression.h"
-#include "src/ast/uint_literal.h"
-#include "src/ast/variable.h"
-#include "src/ast/variable_decl_statement.h"
-#include "src/diagnostic/formatter.h"
-#include "src/transform/manager.h"
-#include "src/type_determiner.h"
+#include "src/transform/test_helper.h"
 
 namespace tint {
 namespace transform {
 namespace {
 
-template <typename T = ast::Expression>
-T* FindVariable(ast::Module* mod, std::string name) {
-  if (auto* func = mod->FindFunctionBySymbol(mod->RegisterSymbol("func"))) {
-    for (auto* stmt : *func->body()) {
-      if (auto* decl = stmt->As<ast::VariableDeclStatement>()) {
-        if (auto* var = decl->variable()) {
-          if (var->name() == name) {
-            return As<T>(var->constructor());
-          }
-        }
-      }
-    }
-  }
-  return nullptr;
-}
-
-class BoundArrayAccessorsTest : public testing::Test {
- public:
-  ast::Module Transform(ast::Module in) {
-    TypeDeterminer td(&in);
-    if (!td.Determine()) {
-      error = "Type determination failed: " + td.error();
-      return {};
-    }
-
-    Manager manager;
-    manager.append(std::make_unique<BoundArrayAccessors>());
-    auto result = manager.Run(&in);
-
-    if (result.diagnostics.contains_errors()) {
-      error = "manager().Run() errored:\n" +
-              diag::Formatter().format(result.diagnostics);
-      return {};
-    }
-
-    return std::move(result.module);
-  }
-
-  std::string error;
-};
-
-struct ModuleBuilder : public ast::BuilderWithModule {
-  ast::Module Module() {
-    Build();
-    mod->AddFunction(Func("func", ast::VariableList{}, ty.void_, statements,
-                          ast::FunctionDecorationList{}));
-    return std::move(*mod);
-  }
-
- protected:
-  virtual void Build() = 0;
-  void OnVariableBuilt(ast::Variable* var) override {
-    statements.emplace_back(create<ast::VariableDeclStatement>(var));
-  }
-  ast::StatementList statements;
-};
+using BoundArrayAccessorsTest = TransformTest;
 
 TEST_F(BoundArrayAccessorsTest, Ptrs_Clamp) {
-  // var a : array<f32, 3>;
-  // const c : u32 =  1;
-  // const b : ptr<function, f32> = a[c]
-  //
-  //   -> const b : ptr<function, i32> = a[min(u32(c), 2)]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array<f32, 3>());
-      Const("c", ast::StorageClass::kFunction, ty.u32);
-      Const("b", ast::StorageClass::kFunction,
-            ty.pointer<f32>(ast::StorageClass::kFunction),
-            IndexAccessor("a", "c"), {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
+const c : u32 = 1u;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  const b : ptr<function, f32> = a[c];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
+const c : u32 = 1u;
 
-  ASSERT_TRUE(b->idx_expr()->Is<ast::CallExpression>());
+fn f() -> void {
+  const b : ptr<function, f32> = a[min(u32(c), 2u)];
+}
+)";
 
-  auto* idx = b->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_EQ(idx->params().size(), 2u);
-
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  ASSERT_TRUE(tc->values()[0]->Is<ast::IdentifierExpression>());
-  ASSERT_EQ(tc->values()[0]->As<ast::IdentifierExpression>()->name(), "c");
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  auto* scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Array_Idx_Nested_Scalar) {
-  // var a : array<f32, 3>;
-  // var b : array<f32, 5>;
-  // var i : u32;
-  // var c : f32 = a[b[i]];
-  //
-  // -> var c : f32 = a[min(u32(b[min(u32(i), 4)]), 2)];
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array<f32, 3>());
-      Var("b", ast::StorageClass::kFunction, ty.array<f32, 5>());
-      Var("i", ast::StorageClass::kFunction, ty.u32);
-      Const("c", ast::StorageClass::kFunction, ty.f32,
-            IndexAccessor("a", IndexAccessor("b", "i")), {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
+var b : array<f32, 5>;
+var i : u32;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var c : f32 = a[ b[i] ];
+}
+)";
 
-  auto* c = FindVariable<ast::ArrayAccessorExpression>(&module, "c");
-  ASSERT_NE(c, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
+var b : array<f32, 5>;
+var i : u32;
 
-  ASSERT_TRUE(c->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(c->idx_expr()->Is<ast::CallExpression>());
+fn f() -> void {
+  var c : f32 = a[min(u32(b[min(u32(i), 4u)]), 2u)];
+}
+)";
 
-  auto* idx = c->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_EQ(idx->params().size(), 2u);
-
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-
-  auto* sub = tc->values()[0];
-  ASSERT_TRUE(sub->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(sub->As<ast::ArrayAccessorExpression>()
-                  ->idx_expr()
-                  ->Is<ast::CallExpression>());
-
-  auto* sub_idx = sub->As<ast::ArrayAccessorExpression>()
-                      ->idx_expr()
-                      ->As<ast::CallExpression>();
-  ASSERT_TRUE(sub_idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(sub_idx->func()->As<ast::IdentifierExpression>()->name(), "min");
-
-  ASSERT_TRUE(sub_idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(sub_idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  tc = sub_idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  ASSERT_TRUE(tc->values()[0]->Is<ast::IdentifierExpression>());
-  ASSERT_EQ(tc->values()[0]->As<ast::IdentifierExpression>()->name(), "i");
-
-  ASSERT_TRUE(sub_idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(sub_idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  auto* scalar = sub_idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 4u);
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(c->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(c->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Array_Idx_Scalar) {
-  // var a : array<f32, 3>
-  // var b : f32 = a[1];
-  //
-  // -> var b : f32 = a[1];
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array(ty.f32, 3));
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", 1u),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[1];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Array_Idx_Expr) {
-  // var a : array<f32, 3>
-  // var c : u32;
-  // var b : f32 = a[c + 2 - 3]
-  //
-  // -> var b : f32 = a[min(u32(c + 2 - 3), 2)]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array<f32, 3>());
-      Var("c", ast::StorageClass::kFunction, ty.u32);
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor("a", Add("c", Sub(2u, 3u))), {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
+var c : u32;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[c + 2 - 3];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
+var c : u32;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::CallExpression>());
+fn f() -> void {
+  var b : f32 = a[min(u32(((c + 2) - 3)), 2u)];
+}
+)";
 
-  auto* idx = b->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_EQ(idx->params().size(), 2u);
-
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  auto* add = tc->values()[0]->As<ast::BinaryExpression>();
-  ASSERT_NE(add, nullptr);
-  ASSERT_EQ(add->op(), ast::BinaryOp::kAdd);
-  auto* add_lhs = add->lhs()->As<ast::IdentifierExpression>();
-  ASSERT_NE(add_lhs, nullptr);
-  ASSERT_EQ(add_lhs->name(), "c");
-  auto* add_rhs = add->rhs()->As<ast::BinaryExpression>();
-  ASSERT_NE(add_rhs, nullptr);
-  ASSERT_TRUE(add_rhs->lhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->lhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            2u);
-  ASSERT_TRUE(add_rhs->rhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->rhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            3u);
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  auto* scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Array_Idx_Negative) {
-  // var a : array<f32, 3>
-  // var b : f32 = a[-1]
-  //
-  // -> var b : f32 = a[0]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array<f32, 3>());
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", -1),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[-1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[0];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 0);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::I32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Array_Idx_OutOfBounds) {
-  // var a : array<f32, 3>
-  // var b : f32 = a[3]
-  //
-  // -> var b : f32 = a[2]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.array<f32, 3>());
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", 3u),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : array<f32, 3>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[3];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : array<f32, 3>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[2];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Vector_Idx_Scalar) {
-  // var a : vec3<f32>
-  // var b : f32 = a[1];
-  //
-  // -> var b : f32 = a[1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.vec3<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", 1u),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : vec3<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : vec3<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[1];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Vector_Idx_Expr) {
-  // var a : vec3<f32>
-  // var c : u32;
-  // var b : f32 = a[c + 2 - 3]
-  //
-  // -> var b : f32 = a[min(u32(c + 2 - 3), 2)]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.vec3<f32>());
-      Var("c", ast::StorageClass::kFunction, ty.u32);
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor("a", Add("c", Sub(2u, 3u))), {});
-    }
-  };
+  auto* src = R"(
+var a : vec3<f32>;
+var c : u32;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[c + 2 - 3];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : vec3<f32>;
+var c : u32;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::CallExpression>());
+fn f() -> void {
+  var b : f32 = a[min(u32(((c + 2) - 3)), 2u)];
+}
+)";
 
-  auto* idx = b->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_EQ(idx->params().size(), 2u);
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  auto* add = tc->values()[0]->As<ast::BinaryExpression>();
-  ASSERT_NE(add, nullptr);
-  auto* add_lhs = add->lhs()->As<ast::IdentifierExpression>();
-  ASSERT_NE(add_lhs, nullptr);
-  ASSERT_EQ(add_lhs->name(), "c");
-  auto* add_rhs = add->rhs()->As<ast::BinaryExpression>();
-  ASSERT_NE(add_rhs, nullptr);
-  ASSERT_TRUE(add_rhs->lhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->lhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            2u);
-  ASSERT_TRUE(add_rhs->rhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->rhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            3u);
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  auto* scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Vector_Idx_Negative) {
-  // var a : vec3<f32>
-  // var b : f32 = a[-1]
-  //
-  // -> var b : f32 = a[0]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.vec3<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", -1),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : vec3<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[-1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : vec3<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[0];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 0);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::I32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Vector_Idx_OutOfBounds) {
-  // var a : vec3<f32>
-  // var b : f32 = a[3]
-  //
-  // -> var b : f32 = a[2]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.vec3<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32, IndexAccessor("a", 3u),
-          {});
-    }
-  };
+  auto* src = R"(
+var a : vec3<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[3];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : vec3<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
+fn f() -> void {
+  var b : f32 = a[2];
+}
+)";
 
-  auto* scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_Scalar) {
-  // var a : mat3x2<f32>
-  // var b : f32 = a[2][1];
-  //
-  // -> var b : f32 = a[2][1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", 2u), 1u), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[2][1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[2][1];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::U32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_Expr_Column) {
-  // var a : mat3x2<f32>
-  // var c : u32;
-  // var b : f32 = a[c + 2 - 3][1]
-  //
-  // -> var b : f32 = a[min(u32(c + 2 - 3), 2)][1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("c", ast::StorageClass::kFunction, ty.u32);
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", Add("c", Sub(2u, 3u))), 1u), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
+var c : u32;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[c + 2 - 3][1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
+var c : u32;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[min(u32(((c + 2) - 3)), 2u)][1];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::CallExpression>());
-  auto* idx = ary->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
-
-  ASSERT_EQ(idx->params().size(), 2u);
-
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  auto* add = tc->values()[0]->As<ast::BinaryExpression>();
-  ASSERT_NE(add, nullptr);
-  auto* add_lhs = add->lhs()->As<ast::IdentifierExpression>();
-  ASSERT_NE(add_lhs, nullptr);
-  ASSERT_EQ(add_lhs->name(), "c");
-  auto* add_rhs = add->rhs()->As<ast::BinaryExpression>();
-  ASSERT_NE(add_rhs, nullptr);
-  ASSERT_TRUE(add_rhs->lhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->lhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            2u);
-  ASSERT_TRUE(add_rhs->rhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->rhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            3u);
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  auto* scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::U32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_Expr_Row) {
-  // var a : mat3x2<f32>
-  // var c : u32;
-  // var b : f32 = a[1][c + 2 - 3]
-  //
-  // -> var b : f32 = a[1][min(u32(c + 2 - 3), 1)]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("c", ast::StorageClass::kFunction, ty.u32);
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", 1u), Add("c", Sub(2u, 3u))), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
+var c : u32;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[1][c + 2 - 3];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
+var c : u32;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[1][min(u32(((c + 2) - 3)), 1u)];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::CallExpression>());
-  auto* idx = b->idx_expr()->As<ast::CallExpression>();
-  ASSERT_TRUE(idx->func()->Is<ast::IdentifierExpression>());
-  EXPECT_EQ(idx->func()->As<ast::IdentifierExpression>()->name(), "min");
-
-  ASSERT_EQ(idx->params().size(), 2u);
-
-  ASSERT_TRUE(idx->params()[0]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[0]->Is<ast::TypeConstructorExpression>());
-  auto* tc = idx->params()[0]->As<ast::TypeConstructorExpression>();
-  EXPECT_TRUE(tc->type()->Is<ast::type::U32>());
-  ASSERT_EQ(tc->values().size(), 1u);
-  auto* add = tc->values()[0]->As<ast::BinaryExpression>();
-  ASSERT_NE(add, nullptr);
-  auto* add_lhs = add->lhs()->As<ast::IdentifierExpression>();
-  ASSERT_NE(add_lhs, nullptr);
-  ASSERT_EQ(add_lhs->name(), "c");
-  auto* add_rhs = add->rhs()->As<ast::BinaryExpression>();
-  ASSERT_NE(add_rhs, nullptr);
-  ASSERT_TRUE(add_rhs->lhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->lhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            2u);
-  ASSERT_TRUE(add_rhs->rhs()->Is<ast::ScalarConstructorExpression>());
-  ASSERT_EQ(add_rhs->rhs()
-                ->As<ast::ScalarConstructorExpression>()
-                ->literal()
-                ->As<ast::UintLiteral>()
-                ->value(),
-            3u);
-
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(idx->params()[1]->Is<ast::ScalarConstructorExpression>());
-  scalar = idx->params()[1]->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::U32>());
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_Negative_Column) {
-  // var a : mat3x2<f32>
-  // var b : f32 = a[-1][1]
-  //
-  // -> var b : f32 = a[0][1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", -1), 1), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[-1][1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[0][1];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 0);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::I32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 1);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::I32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_Negative_Row) {
-  // var a : mat3x2<f32>
-  // var b : f32 = a[2][-1]
-  //
-  // -> var b : f32 = a[2][0]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", 2), -1), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[2][-1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[2][0];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 2);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::I32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::SintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::SintLiteral>()->value(), 0);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::I32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_OutOfBounds_Column) {
-  // var a : mat3x2<f32>
-  // var b : f32 = a[5][1]
-  //
-  // -> var b : f32 = a[2][1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", 5u), 1u), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[5][1];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[2][1];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::U32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(BoundArrayAccessorsTest, Matrix_Idx_OutOfBounds_Row) {
-  // var a : mat3x2<f32>
-  // var b : f32 = a[2][5]
-  //
-  // -> var b : f32 = a[2][1]
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      Var("a", ast::StorageClass::kFunction, ty.mat3x2<f32>());
-      Var("b", ast::StorageClass::kFunction, ty.f32,
-          IndexAccessor(IndexAccessor("a", 2u), 5u), {});
-    }
-  };
+  auto* src = R"(
+var a : mat3x2<f32>;
 
-  ast::Module module = Transform(Builder{}.Module());
-  ASSERT_EQ(error, "");
+fn f() -> void {
+  var b : f32 = a[2][5];
+}
+)";
 
-  auto* b = FindVariable<ast::ArrayAccessorExpression>(&module, "b");
-  ASSERT_NE(b, nullptr);
+  auto* expect = R"(
+var a : mat3x2<f32>;
 
-  ASSERT_TRUE(b->Is<ast::ArrayAccessorExpression>());
+fn f() -> void {
+  var b : f32 = a[2][1];
+}
+)";
 
-  ASSERT_TRUE(b->array()->Is<ast::ArrayAccessorExpression>());
-  auto* ary = b->array()->As<ast::ArrayAccessorExpression>();
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(ary->idx_expr()->Is<ast::ScalarConstructorExpression>());
+  auto got = Transform<BoundArrayAccessors>(src);
 
-  auto* scalar = ary->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 2u);
-
-  ASSERT_NE(ary->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(ary->idx_expr()->result_type()->Is<ast::type::U32>());
-
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ConstructorExpression>());
-  ASSERT_TRUE(b->idx_expr()->Is<ast::ScalarConstructorExpression>());
-
-  scalar = b->idx_expr()->As<ast::ScalarConstructorExpression>();
-  ASSERT_TRUE(scalar->literal()->Is<ast::UintLiteral>());
-  EXPECT_EQ(scalar->literal()->As<ast::UintLiteral>()->value(), 1u);
-
-  ASSERT_NE(b->idx_expr()->result_type(), nullptr);
-  ASSERT_TRUE(b->idx_expr()->result_type()->Is<ast::type::U32>());
+  EXPECT_EQ(expect, got);
 }
 
 // TODO(dsinclair): Implement when constant_id exists
@@ -934,7 +416,7 @@
   // var a : vec3<f32>
   // var b : f32 = a[idx]
   //
-  // ->var b : f32 =  a[min(u32(idx), 2)]
+  // ->var b : f32 = a[min(u32(idx), 2)]
 }
 
 // TODO(dsinclair): Implement when constant_id exists
diff --git a/src/transform/emit_vertex_point_size_test.cc b/src/transform/emit_vertex_point_size_test.cc
index c3c5f70..9a4a359 100644
--- a/src/transform/emit_vertex_point_size_test.cc
+++ b/src/transform/emit_vertex_point_size_test.cc
@@ -14,203 +14,106 @@
 
 #include "src/transform/emit_vertex_point_size.h"
 
-#include <memory>
-#include <utility>
-
-#include "gtest/gtest.h"
-#include "src/ast/builder.h"
-#include "src/ast/stage_decoration.h"
-#include "src/ast/variable_decl_statement.h"
-#include "src/demangler.h"
-#include "src/diagnostic/formatter.h"
-#include "src/transform/manager.h"
+#include "src/transform/test_helper.h"
 
 namespace tint {
 namespace transform {
 namespace {
 
-class EmitVertexPointSizeTest : public testing::Test {
- public:
-  Transform::Output GetTransform(ast::Module in) {
-    Manager manager;
-    manager.append(std::make_unique<EmitVertexPointSize>());
-    return manager.Run(&in);
-  }
-};
-
-struct ModuleBuilder : public ast::BuilderWithModule {
-  ModuleBuilder() {}
-
-  ast::Module Module() {
-    Build();
-    return std::move(*mod);
-  }
-
- protected:
-  virtual void Build() = 0;
-};
+using EmitVertexPointSizeTest = TransformTest;
 
 TEST_F(EmitVertexPointSizeTest, VertexStageBasic) {
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      mod->AddFunction(Func("non_entry_a", ast::VariableList{}, ty.void_,
-                            ast::StatementList{},
-                            ast::FunctionDecorationList{}));
+  auto* src = R"(
+fn non_entry_a() -> void {
+}
 
-      auto* entry =
-          Func("entry", ast::VariableList{}, ty.void_,
-               ast::StatementList{
-                   create<ast::VariableDeclStatement>(
-                       Var("builtin_assignments_should_happen_before_this",
-                           tint::ast::StorageClass::kFunction, ty.f32)),
-               },
-               ast::FunctionDecorationList{
-                   create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-               });
-      mod->AddFunction(entry);
+[[stage(vertex)]]
+fn entry() -> void {
+  var builtin_assignments_should_happen_before_this : f32;
+}
 
-      mod->AddFunction(Func("non_entry_b", ast::VariableList{}, ty.void_,
-                            ast::StatementList{},
-                            ast::FunctionDecorationList{}));
-    }
-  };
-
-  auto result = GetTransform(Builder{}.Module());
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto* expected = R"(Module{
-  Variable{
-    Decorations{
-      BuiltinDecoration{pointsize}
-    }
-    tint_pointsize
-    out
-    __f32
-  }
-  Function non_entry_a -> __void
-  ()
-  {
-  }
-  Function entry -> __void
-  StageDecoration{vertex}
-  ()
-  {
-    Assignment{
-      Identifier[__ptr_out__f32]{tint_pointsize}
-      ScalarConstructor[__f32]{1.000000}
-    }
-    VariableDeclStatement{
-      Variable{
-        builtin_assignments_should_happen_before_this
-        function
-        __f32
-      }
-    }
-  }
-  Function non_entry_b -> __void
-  ()
-  {
-  }
+fn non_entry_b() -> void {
 }
 )";
-  EXPECT_EQ(expected,
-            Demangler().Demangle(result.module, result.module.to_str()));
+
+  auto* expect = R"(
+[[builtin(pointsize)]] var<out> tint_pointsize : f32;
+
+fn non_entry_a() -> void {
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  tint_pointsize = 1.0;
+  var builtin_assignments_should_happen_before_this : f32;
+}
+
+fn non_entry_b() -> void {
+}
+)";
+
+  auto got = Transform<EmitVertexPointSize>(src);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(EmitVertexPointSizeTest, VertexStageEmpty) {
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      mod->AddFunction(Func("non_entry_a", ast::VariableList{}, ty.void_,
-                            ast::StatementList{},
-                            ast::FunctionDecorationList{}));
+  auto* src = R"(
+fn non_entry_a() -> void {
+}
 
-      mod->AddFunction(
-          Func("entry", ast::VariableList{}, ty.void_, ast::StatementList{},
-               ast::FunctionDecorationList{
-                   create<ast::StageDecoration>(ast::PipelineStage::kVertex),
-               }));
+[[stage(vertex)]]
+fn entry() -> void {
+}
 
-      mod->AddFunction(Func("non_entry_b", ast::VariableList{}, ty.void_,
-                            ast::StatementList{},
-                            ast::FunctionDecorationList{}));
-    }
-  };
-
-  auto result = GetTransform(Builder{}.Module());
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto* expected = R"(Module{
-  Variable{
-    Decorations{
-      BuiltinDecoration{pointsize}
-    }
-    tint_pointsize
-    out
-    __f32
-  }
-  Function non_entry_a -> __void
-  ()
-  {
-  }
-  Function entry -> __void
-  StageDecoration{vertex}
-  ()
-  {
-    Assignment{
-      Identifier[__ptr_out__f32]{tint_pointsize}
-      ScalarConstructor[__f32]{1.000000}
-    }
-  }
-  Function non_entry_b -> __void
-  ()
-  {
-  }
+fn non_entry_b() -> void {
 }
 )";
-  EXPECT_EQ(expected,
-            Demangler().Demangle(result.module, result.module.to_str()));
+
+  auto* expect = R"(
+[[builtin(pointsize)]] var<out> tint_pointsize : f32;
+
+fn non_entry_a() -> void {
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  tint_pointsize = 1.0;
+}
+
+fn non_entry_b() -> void {
+}
+)";
+
+  auto got = Transform<EmitVertexPointSize>(src);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(EmitVertexPointSizeTest, NonVertexStage) {
-  struct Builder : ModuleBuilder {
-    void Build() override {
-      auto* fragment_entry = Func(
-          "fragment_entry", ast::VariableList{}, ty.void_, ast::StatementList{},
-          ast::FunctionDecorationList{
-              create<ast::StageDecoration>(ast::PipelineStage::kFragment),
-          });
-      mod->AddFunction(fragment_entry);
+  auto* src = R"(
+[[stage(fragment)]]
+fn fragment_entry() -> void {
+}
 
-      auto* compute_entry = Func(
-          "compute_entry", ast::VariableList{}, ty.void_, ast::StatementList{},
-          ast::FunctionDecorationList{
-              create<ast::StageDecoration>(ast::PipelineStage::kCompute),
-          });
-      mod->AddFunction(compute_entry);
-    }
-  };
-
-  auto result = GetTransform(Builder{}.Module());
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto* expected = R"(Module{
-  Function fragment_entry -> __void
-  StageDecoration{fragment}
-  ()
-  {
-  }
-  Function compute_entry -> __void
-  StageDecoration{compute}
-  ()
-  {
-  }
+[[stage(compute)]]
+fn compute_entry() -> void {
 }
 )";
-  EXPECT_EQ(expected,
-            Demangler().Demangle(result.module, result.module.to_str()));
+
+  auto* expect = R"(
+[[stage(fragment)]]
+fn fragment_entry() -> void {
+}
+
+[[stage(compute)]]
+fn compute_entry() -> void {
+}
+)";
+
+  auto got = Transform<EmitVertexPointSize>(src);
+
+  EXPECT_EQ(expect, got);
 }
 
 }  // namespace
diff --git a/src/transform/first_index_offset_test.cc b/src/transform/first_index_offset_test.cc
index 094df6d..c739810 100644
--- a/src/transform/first_index_offset_test.cc
+++ b/src/transform/first_index_offset_test.cc
@@ -15,413 +15,223 @@
 #include "src/transform/first_index_offset.h"
 
 #include <memory>
-#include <string>
 #include <utility>
+#include <vector>
 
-#include "gtest/gtest.h"
-#include "src/ast/block_statement.h"
-#include "src/ast/builder.h"
-#include "src/ast/builtin.h"
-#include "src/ast/builtin_decoration.h"
-#include "src/ast/call_expression.h"
-#include "src/ast/call_statement.h"
-#include "src/ast/function.h"
-#include "src/ast/identifier_expression.h"
-#include "src/ast/module.h"
-#include "src/ast/return_statement.h"
-#include "src/ast/storage_class.h"
-#include "src/ast/type/u32_type.h"
-#include "src/ast/variable.h"
-#include "src/ast/variable_decoration.h"
-#include "src/demangler.h"
-#include "src/diagnostic/formatter.h"
-#include "src/source.h"
-#include "src/transform/manager.h"
+#include "src/transform/test_helper.h"
 
 namespace tint {
 namespace transform {
 namespace {
 
-class FirstIndexOffsetTest : public testing::Test {};
-
-struct ModuleBuilder : public ast::BuilderWithModule {
-  ast::Module Module() {
-    Build();
-    return std::move(*mod);
-  }
-
- protected:
-  void AddBuiltinInput(const std::string& name, ast::Builtin builtin) {
-    mod->AddGlobalVariable(Var(name, ast::StorageClass::kInput, ty.u32, nullptr,
-                               {create<ast::BuiltinDecoration>(builtin)}));
-  }
-
-  ast::Function* AddFunction(const std::string& name,
-                             ast::StatementList stmts) {
-    auto* func = Func(name, ast::VariableList{}, ty.u32, stmts,
-                      ast::FunctionDecorationList{});
-    mod->AddFunction(func);
-    return func;
-  }
-
-  virtual void Build() = 0;
-};
+using FirstIndexOffsetTest = TransformTest;
 
 TEST_F(FirstIndexOffsetTest, Error_AlreadyTransformed) {
-  struct Builder : public ModuleBuilder {
-    void Build() override {
-      AddBuiltinInput("vert_idx", ast::Builtin::kVertexIdx);
-      AddFunction("test", {create<ast::ReturnStatement>(Expr("vert_idx"))});
-    }
-  };
+  auto* src = R"(
+[[builtin(vertex_idx)]] var<in> vert_idx : u32;
 
-  Manager manager;
-  manager.append(std::make_unique<FirstIndexOffset>(0, 0));
-  manager.append(std::make_unique<FirstIndexOffset>(1, 1));
+fn test() -> u32 {
+  return vert_idx;
+}
 
-  auto module = Builder{}.Module();
-  auto result = manager.Run(&module);
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
+}
+)";
 
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
+  auto* expect = R"(manager().Run() errored:
+error: First index offset transform has already been applied.)";
 
-  ASSERT_EQ(diag::Formatter().format(result.diagnostics),
-            "error: First index offset transform has already been applied.");
+  std::vector<std::unique_ptr<transform::Transform>> transforms;
+  transforms.emplace_back(std::make_unique<FirstIndexOffset>(0, 0));
+  transforms.emplace_back(std::make_unique<FirstIndexOffset>(1, 1));
+
+  auto got = Transform(src, std::move(transforms));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(FirstIndexOffsetTest, EmptyModule) {
-  Manager manager;
-  manager.append(std::make_unique<FirstIndexOffset>(0, 0));
+  auto* src = "";
+  auto* expect = "";
 
-  ast::Module module;
-  auto result = manager.Run(&module);
+  auto got = Transform<FirstIndexOffset>(src, 0, 0);
 
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
-
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto got = result.module.to_str();
-  auto* expected = "Module{\n}\n";
-  EXPECT_EQ(got, expected);
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(FirstIndexOffsetTest, BasicModuleVertexIndex) {
-  struct Builder : public ModuleBuilder {
-    void Build() override {
-      AddBuiltinInput("vert_idx", ast::Builtin::kVertexIdx);
-      AddFunction("test", {create<ast::ReturnStatement>(Expr("vert_idx"))});
-    }
-  };
+  auto* src = R"(
+[[builtin(vertex_idx)]] var<in> vert_idx : u32;
 
-  Manager manager;
-  manager.append(std::make_unique<FirstIndexOffset>(1, 2));
+fn test() -> u32 {
+  return vert_idx;
+}
 
-  auto module = Builder{}.Module();
-  auto result = manager.Run(&module);
-
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
-
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto got = result.module.to_str();
-  auto* expected =
-      R"(Module{
-  TintFirstIndexOffsetData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] tint_first_vertex_index: __u32}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    tint_first_index_offset_vert_idx
-    in
-    __u32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{1}
-      SetDecoration{2}
-    }
-    tint_first_index_data
-    uniform
-    __struct_TintFirstIndexOffsetData
-  }
-  Function test -> __u32
-  ()
-  {
-    VariableDeclStatement{
-      VariableConst{
-        vert_idx
-        none
-        __u32
-        {
-          Binary[__u32]{
-            Identifier[__ptr_in__u32]{tint_first_index_offset_vert_idx}
-            add
-            MemberAccessor[__ptr_uniform__u32]{
-              Identifier[__ptr_uniform__struct_TintFirstIndexOffsetData]{tint_first_index_data}
-              Identifier[not set]{tint_first_vertex_index}
-            }
-          }
-        }
-      }
-    }
-    Return{
-      {
-        Identifier[__u32]{vert_idx}
-      }
-    }
-  }
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
 }
 )";
-  EXPECT_EQ(Demangler().Demangle(result.module, got), expected);
+
+  auto* expect = R"(
+[[block]]
+struct TintFirstIndexOffsetData {
+  [[offset(0)]]
+  tint_first_vertex_index : u32;
+};
+
+[[builtin(vertex_idx)]] var<in> tint_first_index_offset_vert_idx : u32;
+[[binding(1), set(2)]] var<uniform> tint_first_index_data : TintFirstIndexOffsetData;
+
+fn test() -> u32 {
+  const vert_idx : u32 = (tint_first_index_offset_vert_idx + tint_first_index_data.tint_first_vertex_index);
+  return vert_idx;
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
+}
+)";
+
+  auto got = Transform<FirstIndexOffset>(src, 1, 2);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(FirstIndexOffsetTest, BasicModuleInstanceIndex) {
-  struct Builder : public ModuleBuilder {
-    void Build() override {
-      AddBuiltinInput("inst_idx", ast::Builtin::kInstanceIdx);
-      AddFunction("test", {create<ast::ReturnStatement>(Expr("inst_idx"))});
-    }
-  };
+  auto* src = R"(
+[[builtin(instance_idx)]] var<in> inst_idx : u32;
 
-  Manager manager;
-  manager.append(std::make_unique<FirstIndexOffset>(1, 7));
+fn test() -> u32 {
+  return inst_idx;
+}
 
-  auto module = Builder{}.Module();
-  auto result = manager.Run(&module);
-
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
-
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto got = result.module.to_str();
-  auto* expected = R"(Module{
-  TintFirstIndexOffsetData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] tint_first_instance_index: __u32}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{instance_idx}
-    }
-    tint_first_index_offset_inst_idx
-    in
-    __u32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{1}
-      SetDecoration{7}
-    }
-    tint_first_index_data
-    uniform
-    __struct_TintFirstIndexOffsetData
-  }
-  Function test -> __u32
-  ()
-  {
-    VariableDeclStatement{
-      VariableConst{
-        inst_idx
-        none
-        __u32
-        {
-          Binary[__u32]{
-            Identifier[__ptr_in__u32]{tint_first_index_offset_inst_idx}
-            add
-            MemberAccessor[__ptr_uniform__u32]{
-              Identifier[__ptr_uniform__struct_TintFirstIndexOffsetData]{tint_first_index_data}
-              Identifier[not set]{tint_first_instance_index}
-            }
-          }
-        }
-      }
-    }
-    Return{
-      {
-        Identifier[__u32]{inst_idx}
-      }
-    }
-  }
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
 }
 )";
-  EXPECT_EQ(Demangler().Demangle(result.module, got), expected);
+
+  auto* expect = R"(
+[[block]]
+struct TintFirstIndexOffsetData {
+  [[offset(0)]]
+  tint_first_instance_index : u32;
+};
+
+[[builtin(instance_idx)]] var<in> tint_first_index_offset_inst_idx : u32;
+[[binding(1), set(7)]] var<uniform> tint_first_index_data : TintFirstIndexOffsetData;
+
+fn test() -> u32 {
+  const inst_idx : u32 = (tint_first_index_offset_inst_idx + tint_first_index_data.tint_first_instance_index);
+  return inst_idx;
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
+}
+)";
+
+  auto got = Transform<FirstIndexOffset>(src, 1, 7);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(FirstIndexOffsetTest, BasicModuleBothIndex) {
-  struct Builder : public ModuleBuilder {
-    void Build() override {
-      AddBuiltinInput("inst_idx", ast::Builtin::kInstanceIdx);
-      AddBuiltinInput("vert_idx", ast::Builtin::kVertexIdx);
-      AddFunction("test", {
-                              create<ast::ReturnStatement>(Expr(1u)),
-                          });
-    }
-  };
+  auto* src = R"(
+[[builtin(instance_idx)]] var<in> instance_idx : u32;
+[[builtin(vertex_idx)]] var<in> vert_idx : u32;
 
-  auto transform = std::make_unique<FirstIndexOffset>(1, 7);
-  auto* transform_ptr = transform.get();
+fn test() -> u32 {
+  return instance_idx + vert_idx;
+}
 
-  Manager manager;
-  manager.append(std::move(transform));
-
-  auto module = Builder{}.Module();
-  auto result = manager.Run(&module);
-
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
-
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto got = result.module.to_str();
-  auto* expected = R"(Module{
-  TintFirstIndexOffsetData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] tint_first_vertex_index: __u32}
-    StructMember{[[ offset 4 ]] tint_first_instance_index: __u32}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{instance_idx}
-    }
-    tint_first_index_offset_inst_idx
-    in
-    __u32
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    tint_first_index_offset_vert_idx
-    in
-    __u32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{1}
-      SetDecoration{7}
-    }
-    tint_first_index_data
-    uniform
-    __struct_TintFirstIndexOffsetData
-  }
-  Function test -> __u32
-  ()
-  {
-    Return{
-      {
-        ScalarConstructor[__u32]{1}
-      }
-    }
-  }
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
 }
 )";
-  EXPECT_EQ(Demangler().Demangle(result.module, got), expected);
 
-  EXPECT_TRUE(transform_ptr->HasVertexIndex());
-  EXPECT_EQ(transform_ptr->GetFirstVertexOffset(), 0u);
+  auto* expect = R"(
+[[block]]
+struct TintFirstIndexOffsetData {
+  [[offset(0)]]
+  tint_first_vertex_index : u32;
+  [[offset(4)]]
+  tint_first_instance_index : u32;
+};
 
-  EXPECT_TRUE(transform_ptr->HasInstanceIndex());
-  EXPECT_EQ(transform_ptr->GetFirstInstanceOffset(), 4u);
+[[builtin(instance_idx)]] var<in> tint_first_index_offset_instance_idx : u32;
+[[builtin(vertex_idx)]] var<in> tint_first_index_offset_vert_idx : u32;
+[[binding(1), set(2)]] var<uniform> tint_first_index_data : TintFirstIndexOffsetData;
+
+fn test() -> u32 {
+  const instance_idx : u32 = (tint_first_index_offset_instance_idx + tint_first_index_data.tint_first_instance_index);
+  const vert_idx : u32 = (tint_first_index_offset_vert_idx + tint_first_index_data.tint_first_vertex_index);
+  return (instance_idx + vert_idx);
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  test();
+}
+)";
+
+  auto got = Transform<FirstIndexOffset>(src, 1, 2);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(FirstIndexOffsetTest, NestedCalls) {
-  struct Builder : public ModuleBuilder {
-    void Build() override {
-      AddBuiltinInput("vert_idx", ast::Builtin::kVertexIdx);
-      AddFunction("func1", {create<ast::ReturnStatement>(Expr("vert_idx"))});
-      AddFunction("func2", {create<ast::ReturnStatement>(Call("func1"))});
-    }
-  };
+  auto* src = R"(
+[[builtin(vertex_idx)]] var<in> vert_idx : u32;
 
-  auto transform = std::make_unique<FirstIndexOffset>(2, 2);
+fn func1() -> u32 {
+  return vert_idx;
+}
 
-  Manager manager;
-  manager.append(std::move(transform));
+fn func2() -> u32 {
+  return func1();
+}
 
-  auto module = Builder{}.Module();
-  auto result = manager.Run(&module);
-
-  // Release the source module to ensure there's no uncloned data in result
-  { auto tmp = std::move(module); }
-
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  auto got = result.module.to_str();
-  auto* expected = R"(Module{
-  TintFirstIndexOffsetData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] tint_first_vertex_index: __u32}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    tint_first_index_offset_vert_idx
-    in
-    __u32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{2}
-      SetDecoration{2}
-    }
-    tint_first_index_data
-    uniform
-    __struct_TintFirstIndexOffsetData
-  }
-  Function func1 -> __u32
-  ()
-  {
-    VariableDeclStatement{
-      VariableConst{
-        vert_idx
-        none
-        __u32
-        {
-          Binary[__u32]{
-            Identifier[__ptr_in__u32]{tint_first_index_offset_vert_idx}
-            add
-            MemberAccessor[__ptr_uniform__u32]{
-              Identifier[__ptr_uniform__struct_TintFirstIndexOffsetData]{tint_first_index_data}
-              Identifier[not set]{tint_first_vertex_index}
-            }
-          }
-        }
-      }
-    }
-    Return{
-      {
-        Identifier[__u32]{vert_idx}
-      }
-    }
-  }
-  Function func2 -> __u32
-  ()
-  {
-    Return{
-      {
-        Call[__u32]{
-          Identifier[__u32]{func1}
-          (
-          )
-        }
-      }
-    }
-  }
+[[stage(vertex)]]
+fn entry() -> void {
+  func2();
 }
 )";
-  EXPECT_EQ(Demangler().Demangle(result.module, got), expected);
+
+  auto* expect = R"(
+[[block]]
+struct TintFirstIndexOffsetData {
+  [[offset(0)]]
+  tint_first_vertex_index : u32;
+};
+
+[[builtin(vertex_idx)]] var<in> tint_first_index_offset_vert_idx : u32;
+[[binding(1), set(2)]] var<uniform> tint_first_index_data : TintFirstIndexOffsetData;
+
+fn func1() -> u32 {
+  const vert_idx : u32 = (tint_first_index_offset_vert_idx + tint_first_index_data.tint_first_vertex_index);
+  return vert_idx;
+}
+
+fn func2() -> u32 {
+  return func1();
+}
+
+[[stage(vertex)]]
+fn entry() -> void {
+  func2();
+}
+)";
+
+  auto got = Transform<FirstIndexOffset>(src, 1, 2);
+
+  EXPECT_EQ(expect, got);
 }
 
 }  // namespace
diff --git a/src/transform/test_helper.h b/src/transform/test_helper.h
new file mode 100644
index 0000000..269d8a9
--- /dev/null
+++ b/src/transform/test_helper.h
@@ -0,0 +1,115 @@
+// Copyright 2021 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_TRANSFORM_TEST_HELPER_H_
+#define SRC_TRANSFORM_TEST_HELPER_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "src/reader/wgsl/parser.h"
+#include "src/transform/manager.h"
+#include "src/type_determiner.h"
+#include "src/writer/wgsl/generator.h"
+
+namespace tint {
+namespace transform {
+
+/// Helper class for testing transforms
+class TransformTest : public testing::Test {
+ public:
+  /// Transforms and returns the WGSL source `in`, transformed using
+  /// `transforms`.
+  /// @param in the input WGSL source
+  /// @param transforms the list of transforms to apply
+  /// @return the transformed WGSL output
+  std::string Transform(
+      std::string in,
+      std::vector<std::unique_ptr<transform::Transform>> transforms) {
+    Source::File file("test", in);
+    reader::wgsl::Parser parser(&file);
+    if (!parser.Parse()) {
+      return "WGSL reader failed:\n" + parser.error();
+    }
+
+    auto module = parser.module();
+    TypeDeterminer td(&module);
+    if (!td.Determine()) {
+      return "Type determination failed:\n" + td.error();
+    }
+
+    Manager manager;
+    for (auto& transform : transforms) {
+      manager.append(std::move(transform));
+    }
+    auto result = manager.Run(&module);
+
+    if (result.diagnostics.contains_errors()) {
+      return "manager().Run() errored:\n" +
+             diag::Formatter().format(result.diagnostics);
+    }
+
+    // Release the source module to ensure there's no uncloned data in result
+    { auto tmp = std::move(module); }
+
+    writer::wgsl::Generator generator(std::move(result.module));
+    if (!generator.Generate()) {
+      return "WGSL writer failed:\n" + generator.error();
+    }
+
+    auto res = generator.result();
+    if (res.empty()) {
+      return res;
+    }
+    // The WGSL sometimes has two trailing newlines. Strip them
+    while (res.back() == '\n') {
+      res.pop_back();
+    }
+    if (res.empty()) {
+      return res;
+    }
+    return "\n" + res + "\n";
+  }
+
+  /// Transforms and returns the WGSL source `in`, transformed using
+  /// `transform`.
+  /// @param transform the transform to apply
+  /// @param in the input WGSL source
+  /// @return the transformed WGSL output
+  std::string Transform(std::string in,
+                        std::unique_ptr<transform::Transform> transform) {
+    std::vector<std::unique_ptr<transform::Transform>> transforms;
+    transforms.emplace_back(std::move(transform));
+    return Transform(std::move(in), std::move(transforms));
+  }
+
+  /// Transforms and returns the WGSL source `in`, transformed using
+  /// a transform of type `TRANSFORM`.
+  /// @param in the input WGSL source
+  /// @param args the TRANSFORM constructor arguments
+  /// @return the transformed WGSL output
+  template <typename TRANSFORM, typename... ARGS>
+  std::string Transform(std::string in, ARGS&&... args) {
+    return Transform(std::move(in),
+                     std::make_unique<TRANSFORM>(std::forward<ARGS>(args)...));
+  }
+};
+
+}  // namespace transform
+}  // namespace tint
+
+#endif  // SRC_TRANSFORM_TEST_HELPER_H_
diff --git a/src/transform/vertex_pulling_test.cc b/src/transform/vertex_pulling_test.cc
index 129a4f9..87e9a0c 100644
--- a/src/transform/vertex_pulling_test.cc
+++ b/src/transform/vertex_pulling_test.cc
@@ -16,1008 +16,369 @@
 
 #include <utility>
 
-#include "gtest/gtest.h"
-#include "src/ast/builder.h"
-#include "src/ast/function.h"
-#include "src/ast/pipeline_stage.h"
-#include "src/ast/stage_decoration.h"
-#include "src/ast/type/array_type.h"
-#include "src/ast/type/f32_type.h"
-#include "src/ast/type/i32_type.h"
-#include "src/ast/type/void_type.h"
-#include "src/demangler.h"
-#include "src/diagnostic/formatter.h"
-#include "src/transform/manager.h"
-#include "src/type_determiner.h"
-#include "src/validator/validator.h"
+#include "src/transform/test_helper.h"
 
 namespace tint {
 namespace transform {
 namespace {
 
-class VertexPullingHelper : public ast::BuilderWithModule {
- public:
-  VertexPullingHelper() {
-    manager_ = std::make_unique<Manager>();
-    auto transform = std::make_unique<VertexPulling>();
-    transform_ = transform.get();
-    manager_->append(std::move(transform));
-  }
-
-  // Create basic module with an entry point and vertex function
-  void InitBasicModule() {
-    auto* func =
-        Func("main", ast::VariableList{}, ty.void_, ast::StatementList{},
-             ast::FunctionDecorationList{
-                 create<ast::StageDecoration>(ast::PipelineStage::kVertex)});
-
-    mod->AddFunction(func);
-  }
-
-  // Set up the transformation, after building the module
-  void InitTransform(VertexStateDescriptor vertex_state) {
-    EXPECT_TRUE(mod->IsValid());
-
-    TypeDeterminer td(mod);
-    EXPECT_TRUE(td.Determine());
-
-    transform_->SetVertexState(vertex_state);
-    transform_->SetEntryPoint("main");
-  }
-
-  // Inserts a variable which will be converted to vertex pulling
-  void AddVertexInputVariable(uint32_t location,
-                              std::string name,
-                              ast::type::Type* type) {
-    auto* var = Var(name, ast::StorageClass::kInput, type, nullptr,
-                    ast::VariableDecorationList{
-                        create<ast::LocationDecoration>(location),
-                    });
-
-    mod->AddGlobalVariable(var);
-  }
-
-  Manager* manager() { return manager_.get(); }
-  VertexPulling* transform() { return transform_; }
-
- private:
-  std::unique_ptr<Manager> manager_;
-  VertexPulling* transform_;
-};
-
-class VertexPullingTest : public VertexPullingHelper, public testing::Test {};
+using VertexPullingTest = TransformTest;
 
 TEST_F(VertexPullingTest, Error_NoVertexState) {
-  auto result = manager()->Run(mod);
-  EXPECT_TRUE(result.diagnostics.contains_errors());
-  EXPECT_EQ(diag::Formatter().format(result.diagnostics),
-            "error: SetVertexState not called");
+  auto* src = R"(
+[[stage(vertex)]]
+fn main() -> void {}
+)";
+
+  auto* expect = R"(manager().Run() errored:
+error: SetVertexState not called)";
+
+  auto got = Transform<VertexPulling>(src);
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, Error_NoEntryPoint) {
-  transform()->SetVertexState({});
-  auto result = manager()->Run(mod);
-  EXPECT_TRUE(result.diagnostics.contains_errors());
-  EXPECT_EQ(diag::Formatter().format(result.diagnostics),
-            "error: Vertex stage entry point not found");
+  auto* src = "";
+
+  auto* expect = R"(manager().Run() errored:
+error: Vertex stage entry point not found)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState({});
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, Error_InvalidEntryPoint) {
-  InitBasicModule();
-  InitTransform({});
-  transform()->SetEntryPoint("_");
+  auto* src = R"(
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  auto result = manager()->Run(mod);
-  EXPECT_TRUE(result.diagnostics.contains_errors());
-  EXPECT_EQ(diag::Formatter().format(result.diagnostics),
-            "error: Vertex stage entry point not found");
+  auto* expect = R"(manager().Run() errored:
+error: Vertex stage entry point not found)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState({});
+  transform->SetEntryPoint("_");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, Error_EntryPointWrongStage) {
-  auto* func =
-      Func("main", ast::VariableList{}, ty.void_, ast::StatementList{},
-           ast::FunctionDecorationList{
-               create<ast::StageDecoration>(ast::PipelineStage::kFragment),
-           });
-  mod->AddFunction(func);
+  auto* src = R"(
+[[stage(fragment)]]
+fn main() -> void {}
+)";
 
-  InitTransform({});
-  auto result = manager()->Run(mod);
-  EXPECT_TRUE(result.diagnostics.contains_errors());
-  EXPECT_EQ(diag::Formatter().format(result.diagnostics),
-            "error: Vertex stage entry point not found");
+  auto* expect = R"(manager().Run() errored:
+error: Vertex stage entry point not found)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState({});
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, BasicModule) {
-  InitBasicModule();
-  InitTransform({});
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+  auto* src = R"(
+[[stage(vertex)]]
+fn main() -> void {}
+)";
+
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
+
+[[stage(vertex)]]
+fn main() -> void {
+  {
+    var _tint_pulling_pos : i32;
+  }
+}
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState({});
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, OneAttribute) {
-  InitBasicModule();
+  auto* src = R"(
+[[location(0)]] var<in> var_a : f32;
 
-  AddVertexInputVariable(0, "var_a", ty.f32);
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  InitTransform({{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}}}});
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
 
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+[[builtin(vertex_idx)]] var<in> _tint_pulling_vertex_index : i32;
+[[binding(0), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+var<private> var_a : f32;
 
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    _tint_pulling_vertex_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __f32
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
+[[stage(vertex)]]
+fn main() -> void {
   {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{4}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_a}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-    }
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 4u) + 0u);
+    var_a = bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[(_tint_pulling_pos / 4u)]);
   }
 }
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
+      {{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}}}});
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, OneInstancedAttribute) {
-  InitBasicModule();
+  auto* src = R"(
+[[location(0)]] var<in> var_a : f32;
 
-  AddVertexInputVariable(0, "var_a", ty.f32);
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  InitTransform(
-      {{{4, InputStepMode::kInstance, {{VertexFormat::kF32, 0, 0}}}}});
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
 
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+[[builtin(instance_idx)]] var<in> _tint_pulling_instance_index : i32;
+[[binding(0), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+var<private> var_a : f32;
 
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{instance_idx}
-    }
-    _tint_pulling_instance_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __f32
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
+[[stage(vertex)]]
+fn main() -> void {
   {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_instance_index}
-            multiply
-            ScalarConstructor[__u32]{4}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_a}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-    }
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((_tint_pulling_instance_index * 4u) + 0u);
+    var_a = bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[(_tint_pulling_pos / 4u)]);
   }
 }
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
+      {{{4, InputStepMode::kInstance, {{VertexFormat::kF32, 0, 0}}}}});
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, OneAttributeDifferentOutputSet) {
-  InitBasicModule();
+  auto* src = R"(
+[[location(0)]] var<in> var_a : f32;
 
-  AddVertexInputVariable(0, "var_a", ty.f32);
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  InitTransform({{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}}}});
-  transform()->SetPullingBufferBindingSet(5);
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
 
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+[[builtin(vertex_idx)]] var<in> _tint_pulling_vertex_index : i32;
+[[binding(0), set(5)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+var<private> var_a : f32;
 
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    _tint_pulling_vertex_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{5}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __f32
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
+[[stage(vertex)]]
+fn main() -> void {
   {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{4}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_a}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-    }
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 4u) + 0u);
+    var_a = bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[(_tint_pulling_pos / 4u)]);
   }
 }
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
+      {{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}}}});
+  transform->SetPullingBufferBindingSet(5);
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 // We expect the transform to use an existing builtin variables if it finds them
 TEST_F(VertexPullingTest, ExistingVertexIndexAndInstanceIndex) {
-  InitBasicModule();
+  auto* src = R"(
+[[location(0)]] var<in> var_a : f32;
+[[location(1)]] var<in> var_b : f32;
+[[builtin(vertex_idx)]] var<in> custom_vertex_index : i32;
+[[builtin(instance_idx)]] var<in> custom_instance_index : i32;
 
-  AddVertexInputVariable(0, "var_a", ty.f32);
-  AddVertexInputVariable(1, "var_b", ty.f32);
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  mod->AddGlobalVariable(
-      Var("custom_vertex_index", ast::StorageClass::kInput, ty.i32, nullptr,
-          ast::VariableDecorationList{
-              create<ast::BuiltinDecoration>(ast::Builtin::kVertexIdx),
-          }));
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
 
-  mod->AddGlobalVariable(
-      Var("custom_instance_index", ast::StorageClass::kInput, ty.i32, nullptr,
-          ast::VariableDecorationList{
-              create<ast::BuiltinDecoration>(ast::Builtin::kInstanceIdx),
-          }));
+[[binding(0), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+[[binding(1), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_1 : TintVertexData;
+var<private> var_a : f32;
+var<private> var_b : f32;
+[[builtin(vertex_idx)]] var<in> custom_vertex_index : i32;
+[[builtin(instance_idx)]] var<in> custom_instance_index : i32;
 
-  InitTransform(
-      {{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}},
-        {4, InputStepMode::kInstance, {{VertexFormat::kF32, 0, 1}}}}});
-
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
-
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{1}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_1
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __f32
-  }
-  Variable{
-    var_b
-    private
-    __f32
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    custom_vertex_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{instance_idx}
-    }
-    custom_instance_index
-    in
-    __i32
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
+[[stage(vertex)]]
+fn main() -> void {
   {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{custom_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{4}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_a}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{custom_instance_index}
-            multiply
-            ScalarConstructor[__u32]{4}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_b}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_1}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-    }
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((custom_vertex_index * 4u) + 0u);
+    var_a = bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[(_tint_pulling_pos / 4u)]);
+    _tint_pulling_pos = ((custom_instance_index * 4u) + 0u);
+    var_b = bitcast<f32>(_tint_pulling_vertex_buffer_1._tint_vertex_data[(_tint_pulling_pos / 4u)]);
   }
 }
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
+      {{{4, InputStepMode::kVertex, {{VertexFormat::kF32, 0, 0}}},
+        {4, InputStepMode::kInstance, {{VertexFormat::kF32, 0, 1}}}}});
+  transform->SetEntryPoint("main");
+
+  auto got = Transform(src, std::move(transform));
+
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, TwoAttributesSameBuffer) {
-  InitBasicModule();
+  auto* src = R"(
+[[location(0)]] var<in> var_a : f32;
+[[location(1)]] var<in> var_b : array<f32, 4>;
 
-  AddVertexInputVariable(0, "var_a", ty.f32);
-  AddVertexInputVariable(1, "var_b", ty.array<f32, 4>());
+[[stage(vertex)]]
+fn main() -> void {}
+)";
 
-  InitTransform(
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
+
+[[builtin(vertex_idx)]] var<in> _tint_pulling_vertex_index : i32;
+[[binding(0), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+var<private> var_a : f32;
+var<private> var_b : array<f32, 4>;
+
+[[stage(vertex)]]
+fn main() -> void {
+  {
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 16u) + 0u);
+    var_a = bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[(_tint_pulling_pos / 4u)]);
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 16u) + 0u);
+    var_b = vec4<f32>(bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 0u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 4u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 8u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 12u) / 4u)]));
+  }
+}
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
       {{{16,
          InputStepMode::kVertex,
          {{VertexFormat::kF32, 0, 0}, {VertexFormat::kVec4F32, 0, 1}}}}});
+  transform->SetEntryPoint("main");
 
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+  auto got = Transform(src, std::move(transform));
 
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    _tint_pulling_vertex_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __f32
-  }
-  Variable{
-    var_b
-    private
-    __array__f32_4
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
-  {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{16}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__f32]{var_a}
-        Bitcast[__f32]<__f32>{
-          ArrayAccessor[__ptr_storage_buffer__u32]{
-            MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-              Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-              Identifier[not set]{_tint_vertex_data}
-            }
-            Binary[__i32]{
-              Identifier[__ptr_function__i32]{_tint_pulling_pos}
-              divide
-              ScalarConstructor[__u32]{4}
-            }
-          }
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{16}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__array__f32_4]{var_b}
-        TypeConstructor[__vec_4__f32]{
-          __vec_4__f32
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{0}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{4}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{8}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{12}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-}
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+  EXPECT_EQ(expect, got);
 }
 
 TEST_F(VertexPullingTest, FloatVectorAttributes) {
-  InitBasicModule();
-  AddVertexInputVariable(0, "var_a", ty.array<f32, 2>());
-  AddVertexInputVariable(1, "var_b", ty.array<f32, 3>());
-  AddVertexInputVariable(2, "var_c", ty.array<f32, 4>());
+  auto* src = R"(
+[[location(0)]] var<in> var_a : array<f32, 2>;
+[[location(1)]] var<in> var_b : array<f32, 3>;
+[[location(2)]] var<in> var_c : array<f32, 4>;
 
-  InitTransform(
+[[stage(vertex)]]
+fn main() -> void {}
+)";
+
+  auto* expect = R"(
+[[block]]
+struct TintVertexData {
+  [[offset(0)]]
+  _tint_vertex_data : [[stride(4)]] array<u32>;
+};
+
+[[builtin(vertex_idx)]] var<in> _tint_pulling_vertex_index : i32;
+[[binding(0), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_0 : TintVertexData;
+[[binding(1), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_1 : TintVertexData;
+[[binding(2), set(4)]] var<storage_buffer> _tint_pulling_vertex_buffer_2 : TintVertexData;
+var<private> var_a : array<f32, 2>;
+var<private> var_b : array<f32, 3>;
+var<private> var_c : array<f32, 4>;
+
+[[stage(vertex)]]
+fn main() -> void {
+  {
+    var _tint_pulling_pos : i32;
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 8u) + 0u);
+    var_a = vec2<f32>(bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 0u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_0._tint_vertex_data[((_tint_pulling_pos + 4u) / 4u)]));
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 12u) + 0u);
+    var_b = vec3<f32>(bitcast<f32>(_tint_pulling_vertex_buffer_1._tint_vertex_data[((_tint_pulling_pos + 0u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_1._tint_vertex_data[((_tint_pulling_pos + 4u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_1._tint_vertex_data[((_tint_pulling_pos + 8u) / 4u)]));
+    _tint_pulling_pos = ((_tint_pulling_vertex_index * 16u) + 0u);
+    var_c = vec4<f32>(bitcast<f32>(_tint_pulling_vertex_buffer_2._tint_vertex_data[((_tint_pulling_pos + 0u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_2._tint_vertex_data[((_tint_pulling_pos + 4u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_2._tint_vertex_data[((_tint_pulling_pos + 8u) / 4u)]), bitcast<f32>(_tint_pulling_vertex_buffer_2._tint_vertex_data[((_tint_pulling_pos + 12u) / 4u)]));
+  }
+}
+)";
+
+  auto transform = std::make_unique<VertexPulling>();
+  transform->SetVertexState(
       {{{8, InputStepMode::kVertex, {{VertexFormat::kVec2F32, 0, 0}}},
         {12, InputStepMode::kVertex, {{VertexFormat::kVec3F32, 0, 1}}},
         {16, InputStepMode::kVertex, {{VertexFormat::kVec4F32, 0, 2}}}}});
+  transform->SetEntryPoint("main");
 
-  auto result = manager()->Run(mod);
-  ASSERT_FALSE(result.diagnostics.contains_errors())
-      << diag::Formatter().format(result.diagnostics);
+  auto got = Transform(src, std::move(transform));
 
-  EXPECT_EQ(R"(Module{
-  TintVertexData Struct{
-    [[block]]
-    StructMember{[[ offset 0 ]] _tint_vertex_data: __array__u32_stride_4}
-  }
-  Variable{
-    Decorations{
-      BuiltinDecoration{vertex_idx}
-    }
-    _tint_pulling_vertex_index
-    in
-    __i32
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{0}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_0
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{1}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_1
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    Decorations{
-      BindingDecoration{2}
-      SetDecoration{4}
-    }
-    _tint_pulling_vertex_buffer_2
-    storage_buffer
-    __struct_TintVertexData
-  }
-  Variable{
-    var_a
-    private
-    __array__f32_2
-  }
-  Variable{
-    var_b
-    private
-    __array__f32_3
-  }
-  Variable{
-    var_c
-    private
-    __array__f32_4
-  }
-  Function main -> __void
-  StageDecoration{vertex}
-  ()
-  {
-    Block{
-      VariableDeclStatement{
-        Variable{
-          _tint_pulling_pos
-          function
-          __i32
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{8}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__array__f32_2]{var_a}
-        TypeConstructor[__vec_2__f32]{
-          __vec_2__f32
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{0}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_0}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{4}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{12}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__array__f32_3]{var_b}
-        TypeConstructor[__vec_3__f32]{
-          __vec_3__f32
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_1}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{0}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_1}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{4}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_1}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{8}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-        }
-      }
-      Assignment{
-        Identifier[__ptr_function__i32]{_tint_pulling_pos}
-        Binary[__i32]{
-          Binary[__i32]{
-            Identifier[__ptr_in__i32]{_tint_pulling_vertex_index}
-            multiply
-            ScalarConstructor[__u32]{16}
-          }
-          add
-          ScalarConstructor[__u32]{0}
-        }
-      }
-      Assignment{
-        Identifier[__ptr_private__array__f32_4]{var_c}
-        TypeConstructor[__vec_4__f32]{
-          __vec_4__f32
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_2}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{0}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_2}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{4}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_2}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{8}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-          Bitcast[__f32]<__f32>{
-            ArrayAccessor[__ptr_storage_buffer__u32]{
-              MemberAccessor[__ptr_storage_buffer__array__u32_stride_4]{
-                Identifier[__ptr_storage_buffer__struct_TintVertexData]{_tint_pulling_vertex_buffer_2}
-                Identifier[not set]{_tint_vertex_data}
-              }
-              Binary[__i32]{
-                Binary[__i32]{
-                  Identifier[__ptr_function__i32]{_tint_pulling_pos}
-                  add
-                  ScalarConstructor[__u32]{12}
-                }
-                divide
-                ScalarConstructor[__u32]{4}
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-}
-)",
-            Demangler().Demangle(result.module, result.module.to_str()));
+  EXPECT_EQ(expect, got);
 }
 
 }  // namespace