[tint] Don't resolve moving from ProgramBuilder -> Program

Do this explicitly instead.

Breaks a circular dependency between Program and Resolver.

Change-Id: I67f66501da02f349b981a59ef64cb8b687230f95
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/143401
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/docs/tint/arch.md b/docs/tint/arch.md
index 704e7c4..586d529 100644
--- a/docs/tint/arch.md
+++ b/docs/tint/arch.md
@@ -68,9 +68,10 @@
 AST nodes. A `ProgramBuilder` can only be used once, and must be discarded after
 the `Program` is constructed.
 
-A `Program` is built from the `ProgramBuilder` by `std::move()`ing the
-`ProgramBuilder` to a new `Program` object. When built, resolution is performed
-so the produced `Program` will contain all the needed semantic information.
+A `Program` is built from the `ProgramBuilder` via a call to
+`resolver::Resolve()`, which will perform validation and semantic analysis.
+The returned program will contain the semantic information which can be obtained
+by calling `Program::Sem()`.
 
 At any time before building the `Program`, `ProgramBuilder::IsValid()` may be
 called to ensure that no error diagnostics have been raised during the
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 1b8d7b3..f56bf0d 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -299,6 +299,7 @@
     "lang/wgsl/resolver/intrinsic_table.cc",
     "lang/wgsl/resolver/intrinsic_table.h",
     "lang/wgsl/resolver/intrinsic_table.inl",
+    "lang/wgsl/resolver/resolve.cc",
     "lang/wgsl/resolver/resolver.cc",
     "lang/wgsl/resolver/sem_helper.cc",
     "lang/wgsl/resolver/sem_helper.h",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 6d17a41..a44a9d8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -519,6 +519,8 @@
   lang/wgsl/resolver/intrinsic_table.cc
   lang/wgsl/resolver/intrinsic_table.h
   lang/wgsl/resolver/intrinsic_table.inl
+  lang/wgsl/resolver/resolve.cc
+  lang/wgsl/resolver/resolve.h
   lang/wgsl/resolver/resolver.cc
   lang/wgsl/resolver/resolver.h
   lang/wgsl/resolver/sem_helper.cc
diff --git a/src/tint/cmd/generate_external_texture_bindings_test.cc b/src/tint/cmd/generate_external_texture_bindings_test.cc
index a7de60b..e0f78a1 100644
--- a/src/tint/cmd/generate_external_texture_bindings_test.cc
+++ b/src/tint/cmd/generate_external_texture_bindings_test.cc
@@ -17,6 +17,7 @@
 #include "gtest/gtest.h"
 #include "src/tint/cmd/generate_external_texture_bindings.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "tint/binding_point.h"
 
 namespace tint::cmd {
@@ -31,7 +32,7 @@
 TEST_F(GenerateExternalTextureBindingsTest, None) {
     ProgramBuilder b;
 
-    tint::Program program(std::move(b));
+    tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
     auto bindings = GenerateExternalTextureBindings(&program);
     ASSERT_TRUE(bindings.empty());
@@ -41,7 +42,7 @@
     ProgramBuilder b;
     b.GlobalVar("v0", b.ty.external_texture(), b.Group(0_a), b.Binding(0_a));
 
-    tint::Program program(std::move(b));
+    tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
     auto bindings = GenerateExternalTextureBindings(&program);
     ASSERT_EQ(bindings.size(), 1u);
@@ -58,7 +59,7 @@
     b.GlobalVar("v0", b.ty.external_texture(), b.Group(0_a), b.Binding(0_a));
     b.GlobalVar("v1", b.ty.external_texture(), b.Group(0_a), b.Binding(1_a));
 
-    tint::Program program(std::move(b));
+    tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
     auto bindings = GenerateExternalTextureBindings(&program);
     ASSERT_EQ(bindings.size(), 2u);
@@ -81,7 +82,7 @@
     b.GlobalVar("v0", b.ty.external_texture(), b.Group(0_a), b.Binding(0_a));
     b.GlobalVar("v1", b.ty.external_texture(), b.Group(1_a), b.Binding(0_a));
 
-    tint::Program program(std::move(b));
+    tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
     auto bindings = GenerateExternalTextureBindings(&program);
     ASSERT_EQ(bindings.size(), 2u);
@@ -107,7 +108,7 @@
     b.GlobalVar("v3", b.ty.external_texture(), b.Group(0_a), b.Binding(3_a));
     b.GlobalVar("v4", b.ty.i32(), b.Group(0_a), b.Binding(4_a), kUniform);
 
-    tint::Program program(std::move(b));
+    tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
     auto bindings = GenerateExternalTextureBindings(&program);
     ASSERT_EQ(bindings.size(), 2u);
diff --git a/src/tint/fuzzers/shuffle_transform.cc b/src/tint/fuzzers/shuffle_transform.cc
index f014a46..d4b5fbe 100644
--- a/src/tint/fuzzers/shuffle_transform.cc
+++ b/src/tint/fuzzers/shuffle_transform.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::fuzzers {
 
@@ -38,7 +39,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::fuzzers
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
index 44f40f3..eab45ca 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -27,6 +27,7 @@
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::fuzzers::ast_fuzzer {
 namespace {
@@ -92,7 +93,7 @@
     }
 
     clone_context.Clone();
-    *out_program = tint::Program(std::move(mutated));
+    *out_program = tint::resolver::Resolve(mutated);
     *out_node_id_map = std::move(new_node_id_map);
     return true;
 }
diff --git a/src/tint/lang/core/type/test_helper.h b/src/tint/lang/core/type/test_helper.h
index 3917b5d..0b093e8 100644
--- a/src/tint/lang/core/type/test_helper.h
+++ b/src/tint/lang/core/type/test_helper.h
@@ -19,6 +19,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::type {
 
@@ -32,7 +33,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        return Program(std::move(*this));
+        return resolver::Resolve(*this);
     }
 };
 using TestHelper = TestHelperBase<testing::Test>;
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
index f56faae..cd915cd 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
@@ -24,7 +24,7 @@
 TEST_F(GlslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(std::move(*this));
+    auto program = std::make_unique<Program>(resolver::Resolve(*this));
     ASSERT_FALSE(program->IsValid());
     auto result = Generate(program.get(), Options{}, "");
     EXPECT_EQ(result.error, "input program is not valid");
diff --git a/src/tint/lang/glsl/writer/ast_printer/test_helper.h b/src/tint/lang/glsl/writer/ast_printer/test_helper.h
index d7e28fb..636fac2 100644
--- a/src/tint/lang/glsl/writer/ast_printer/test_helper.h
+++ b/src/tint/lang/glsl/writer/ast_printer/test_helper.h
@@ -24,6 +24,7 @@
 #include "src/tint/lang/glsl/writer/version.h"
 #include "src/tint/lang/glsl/writer/writer.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::glsl::writer {
 
@@ -54,7 +55,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         gen_ = std::make_unique<ASTPrinter>(program.get(), version);
         return *gen_;
@@ -75,7 +76,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
         auto sanitized_result = Sanitize(program.get(), options, /* entry_point */ "");
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
index 304dbe9..72af79f 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
@@ -24,7 +24,7 @@
 TEST_F(HlslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(std::move(*this));
+    auto program = std::make_unique<Program>(resolver::Resolve(*this));
     ASSERT_FALSE(program->IsValid());
     auto result = Generate(program.get(), Options{});
     EXPECT_EQ(result.error, "input program is not valid");
diff --git a/src/tint/lang/hlsl/writer/ast_printer/test_helper.h b/src/tint/lang/hlsl/writer/ast_printer/test_helper.h
index 3eb5c90..3f8a279 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/test_helper.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/test_helper.h
@@ -24,6 +24,7 @@
 #include "src/tint/lang/hlsl/writer/options.h"
 #include "src/tint/lang/wgsl/ast/transform/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/renamer.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::hlsl::writer {
 
@@ -53,7 +54,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         gen_ = std::make_unique<ASTPrinter>(program.get());
         return *gen_;
@@ -72,7 +73,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
         auto sanitized_result = Sanitize(program.get(), options);
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
index 0e6326e..1603836 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
@@ -26,7 +26,7 @@
 TEST_F(MslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(std::move(*this));
+    auto program = std::make_unique<Program>(resolver::Resolve(*this));
     ASSERT_FALSE(program->IsValid());
     auto result = Generate(program.get(), Options{});
     EXPECT_EQ(result.error, "input program is not valid");
diff --git a/src/tint/lang/msl/writer/ast_printer/test_helper.h b/src/tint/lang/msl/writer/ast_printer/test_helper.h
index 70dadf5..dd79b5a 100644
--- a/src/tint/lang/msl/writer/ast_printer/test_helper.h
+++ b/src/tint/lang/msl/writer/ast_printer/test_helper.h
@@ -22,6 +22,7 @@
 #include "gtest/gtest.h"
 #include "src/tint/lang/msl/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::msl::writer {
 
@@ -51,7 +52,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         gen_ = std::make_unique<ASTPrinter>(program.get());
         return *gen_;
@@ -70,7 +71,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
         auto result = Sanitize(program.get(), options);
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
index 41276fe..c22baaf 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.cc
@@ -30,6 +30,7 @@
 #include "src/tint/lang/wgsl/ast/id_attribute.h"
 #include "src/tint/lang/wgsl/ast/interpolate_attribute.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/containers/unique_vector.h"
 #include "src/tint/utils/rtti/switch.h"
 
@@ -330,10 +331,14 @@
     return success_;
 }
 
-Program ASTParser::Program() {
+Program ASTParser::Program(bool resolve) {
     // TODO(dneto): Should we clear out spv_binary_ here, to reduce
     // memory usage?
-    return tint::Program(std::move(builder_));
+    if (resolve) {
+        return tint::resolver::Resolve(builder_);
+    } else {
+        return tint::Program(std::move(builder_));
+    }
 }
 
 const Type* ASTParser::ConvertType(uint32_t type_id, PtrAs ptr_as) {
diff --git a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
index 54b4e7a..4b0a323 100644
--- a/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
+++ b/src/tint/lang/spirv/reader/ast_parser/ast_parser.h
@@ -137,9 +137,9 @@
     /// @returns true if the parse was successful, false otherwise.
     bool Parse();
 
-    /// @returns the program. The program builder in the parser will be reset
-    /// after this.
-    tint::Program Program();
+    /// @param resolve if true then the program will be resolved before returning
+    /// @returns the program. The program builder in the parser will be reset after this.
+    tint::Program Program(bool resolve = true);
 
     /// @returns a reference to the internal builder, without building the
     /// program. To be used only for testing.
diff --git a/src/tint/lang/spirv/reader/ast_parser/test_helper.h b/src/tint/lang/spirv/reader/ast_parser/test_helper.h
index 43b4b8f..1c9c056 100644
--- a/src/tint/lang/spirv/reader/ast_parser/test_helper.h
+++ b/src/tint/lang/spirv/reader/ast_parser/test_helper.h
@@ -77,7 +77,7 @@
 
     /// @returns the program. The program builder in the parser will be reset
     /// after this.
-    Program program() { return impl_.Program(); }
+    Program program() { return impl_.Program(resolve_); }
 
     /// @returns the namer object
     Namer& namer() { return impl_.namer(); }
@@ -248,6 +248,9 @@
         return impl_.GetSourceForResultIdForTest(id);
     }
 
+    /// @param resolve if true, the resolver should be run on the program when its build
+    void SetResolveOnBuild(bool resolve) { resolve_ = resolve; }
+
   private:
     ASTParser impl_;
     /// When true, indicates the input SPIR-V module should not be emitted.
@@ -255,6 +258,7 @@
     /// reason.
     bool skip_dumping_spirv_ = false;
     static bool dump_successfully_converted_spirv_;
+    bool resolve_ = true;
 };
 
 // Sets global state to force dumping of the assembly text of succesfully
@@ -294,10 +298,9 @@
     /// @returns a parser for the given binary
     std::unique_ptr<test::ASTParserWrapperForTest> parser(const std::vector<uint32_t>& input) {
         auto parser = std::make_unique<test::ASTParserWrapperForTest>(input);
-
         // Don't run the Resolver when building the program.
         // We're not interested in type information with these tests.
-        parser->builder().SetResolveOnBuild(false);
+        parser->SetResolveOnBuild(false);
         return parser;
     }
 };
diff --git a/src/tint/lang/spirv/reader/reader.cc b/src/tint/lang/spirv/reader/reader.cc
index 41408e5..b91aeaa 100644
--- a/src/tint/lang/spirv/reader/reader.cc
+++ b/src/tint/lang/spirv/reader/reader.cc
@@ -26,6 +26,7 @@
 #include "src/tint/lang/wgsl/ast/transform/spirv_atomic.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::spirv::reader {
 
@@ -48,12 +49,11 @@
 
     // The SPIR-V parser can construct disjoint AST nodes, which is invalid for
     // the Resolver. Clone the Program to clean these up.
-    builder.SetResolveOnBuild(false);
     Program program_with_disjoint_ast(std::move(builder));
 
     ProgramBuilder output;
     program::CloneContext(&output, &program_with_disjoint_ast, false).Clone();
-    auto program = Program(std::move(output));
+    auto program = Program(resolver::Resolve(output));
     if (!program.IsValid()) {
         return program;
     }
diff --git a/src/tint/lang/spirv/writer/ast_printer/assign_test.cc b/src/tint/lang/spirv/writer/ast_printer/assign_test.cc
index ea5f40d..8ab2713 100644
--- a/src/tint/lang/spirv/writer/ast_printer/assign_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/assign_test.cc
@@ -64,7 +64,7 @@
 
             pb.WrapInFunction(assign);
 
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateGlobalVariable(v);
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_if_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_if_test.cc
index dbbc891..2bff021 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_if_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_if_test.cc
@@ -60,7 +60,7 @@
             auto* expr = pb.If(true, block);
             pb.WrapInFunction(expr);
 
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateIfStatement(expr);
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
index 96b5aef..91b6a42 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
@@ -23,7 +23,7 @@
 TEST_F(SpirvASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(std::move(*this));
+    auto program = std::make_unique<Program>(resolver::Resolve(*this));
     ASSERT_FALSE(program->IsValid());
     auto result = Generate(program.get(), Options{});
     EXPECT_EQ(result.error, "input program is not valid");
@@ -32,7 +32,7 @@
 TEST_F(SpirvASTPrinterTest, UnsupportedExtension) {
     Enable(Source{{12, 34}}, builtin::Extension::kUndefined);
 
-    auto program = std::make_unique<Program>(std::move(*this));
+    auto program = std::make_unique<Program>(resolver::Resolve(*this));
     auto result = Generate(program.get(), Options{});
     EXPECT_EQ(result.error,
               R"(12:34 error: SPIR-V backend does not support extension 'undefined')");
diff --git a/src/tint/lang/spirv/writer/ast_printer/builtin_texture_test.cc b/src/tint/lang/spirv/writer/ast_printer/builtin_texture_test.cc
index c0dc007..a24f152 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builtin_texture_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builtin_texture_test.cc
@@ -3786,7 +3786,7 @@
                         pb.Stage(ast::PipelineStage::kFragment),
                     });
 
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateGlobalVariable(texture);
diff --git a/src/tint/lang/spirv/writer/ast_printer/function_attribute_test.cc b/src/tint/lang/spirv/writer/ast_printer/function_attribute_test.cc
index 71182d8..4f0365e 100644
--- a/src/tint/lang/spirv/writer/ast_printer/function_attribute_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/function_attribute_test.cc
@@ -164,7 +164,7 @@
                                      pb.WorkgroupSize("width", "height", "depth"),
                                      pb.Stage(ast::PipelineStage::kCompute),
                                  });
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateExecutionModes(func, 3);
@@ -185,7 +185,7 @@
                                      pb.Stage(ast::PipelineStage::kCompute),
                                  });
 
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateExecutionModes(func, 3);
diff --git a/src/tint/lang/spirv/writer/ast_printer/global_variable_test.cc b/src/tint/lang/spirv/writer/ast_printer/global_variable_test.cc
index 4644e82..f5b8738 100644
--- a/src/tint/lang/spirv/writer/ast_printer/global_variable_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/global_variable_test.cc
@@ -513,7 +513,7 @@
                                        });
     auto* var_struct = GlobalVar("c", ty.Of(type_struct), builtin::AddressSpace::kWorkgroup);
 
-    program = std::make_unique<Program>(std::move(*this));
+    program = std::make_unique<Program>(resolver::Resolve(*this));
 
     constexpr bool kZeroInitializeWorkgroupMemory = true;
     std::unique_ptr<Builder> b =
diff --git a/src/tint/lang/spirv/writer/ast_printer/ident_expression_test.cc b/src/tint/lang/spirv/writer/ast_printer/ident_expression_test.cc
index 5904f3e..1a472aa 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ident_expression_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ident_expression_test.cc
@@ -36,7 +36,7 @@
             auto* expr = pb.Expr("c");
             pb.WrapInFunction(expr);
 
-            auto program = std::make_unique<Program>(std::move(pb));
+            auto program = std::make_unique<Program>(resolver::Resolve(pb));
             auto b = std::make_unique<Builder>(program.get());
 
             b->GenerateGlobalVariable(v);
diff --git a/src/tint/lang/spirv/writer/ast_printer/test_helper.h b/src/tint/lang/spirv/writer/ast_printer/test_helper.h
index 72ea294..49a12c8 100644
--- a/src/tint/lang/spirv/writer/ast_printer/test_helper.h
+++ b/src/tint/lang/spirv/writer/ast_printer/test_helper.h
@@ -23,6 +23,7 @@
 #include "spirv-tools/libspirv.hpp"
 #include "src/tint/lang/spirv/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/spirv/writer/binary_writer.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::spirv::writer {
 
@@ -55,7 +56,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         spirv_builder = std::make_unique<Builder>(program.get());
         return *spirv_builder;
@@ -74,7 +75,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        program = std::make_unique<Program>(std::move(*this));
+        program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         auto result = Sanitize(program.get(), options);
         [&] { ASSERT_TRUE(result.program.IsValid()) << result.program.Diagnostics().str(); }();
diff --git a/src/tint/lang/wgsl/ast/clone_context_test.cc b/src/tint/lang/wgsl/ast/clone_context_test.cc
index 9d0e337..44257ef 100644
--- a/src/tint/lang/wgsl/ast/clone_context_test.cc
+++ b/src/tint/lang/wgsl/ast/clone_context_test.cc
@@ -18,6 +18,7 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/wgsl/ast/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::ast {
 namespace {
@@ -103,7 +104,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -161,7 +162,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -254,7 +255,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<TestNode>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -287,7 +288,7 @@
 
     ProgramBuilder builder;
     auto* original_Testnode = a.Create<TestNode>(builder.Symbols().New("root"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -308,7 +309,7 @@
     original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
     original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
     original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -340,7 +341,7 @@
     original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
     original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
     original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -379,7 +380,7 @@
     original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
     original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
     original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -411,7 +412,7 @@
     original_root->a = a.Create<TestNode>(builder.Symbols().New("a"));
     original_root->b = a.Create<TestNode>(builder.Symbols().New("b"));
     original_root->c = a.Create<TestNode>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -452,7 +453,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* cloned_root = CloneContext(&cloned, original.ID())
@@ -479,7 +480,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -511,7 +512,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -540,7 +541,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -561,7 +562,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -587,7 +588,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -615,7 +616,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -640,7 +641,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -661,7 +662,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -683,7 +684,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion_back = a.Create<TestNode>(cloned.Symbols().New("insertion_back"));
@@ -707,7 +708,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<TestNode>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -737,7 +738,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -765,7 +766,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -794,7 +795,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<TestNode>(cloned.Symbols().New("insertion"));
@@ -822,7 +823,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -852,7 +853,7 @@
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -884,7 +885,7 @@
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -916,7 +917,7 @@
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -948,7 +949,7 @@
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -979,7 +980,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion_before = a.Create<TestNode>(cloned.Symbols().New("insertion_before"));
@@ -1011,7 +1012,7 @@
         a.Create<TestNode>(builder.Symbols().Register("b")),
         a.Create<TestNode>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -1106,7 +1107,7 @@
     EXPECT_EQ(old_b.Name(), "tint_symbol_1");
     EXPECT_EQ(old_c.Name(), "tint_symbol_2");
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
@@ -1134,7 +1135,7 @@
     EXPECT_EQ(old_b.Name(), "b");
     EXPECT_EQ(old_c.Name(), "c");
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, original.ID());
diff --git a/src/tint/lang/wgsl/ast/module_test.cc b/src/tint/lang/wgsl/ast/module_test.cc
index e896f7c..429af55 100644
--- a/src/tint/lang/wgsl/ast/module_test.cc
+++ b/src/tint/lang/wgsl/ast/module_test.cc
@@ -16,6 +16,7 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/wgsl/ast/test_helper.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::ast {
 namespace {
@@ -23,7 +24,7 @@
 using ModuleTest = TestHelper;
 
 TEST_F(ModuleTest, Creation) {
-    EXPECT_EQ(Program(std::move(*this)).AST().Functions().Length(), 0u);
+    EXPECT_EQ(resolver::Resolve(*this).AST().Functions().Length(), 0u);
 }
 
 TEST_F(ModuleTest, LookupFunction) {
@@ -93,7 +94,7 @@
         b.Func("F", {}, b.ty.void_(), {});
         b.Alias("A", b.ty.u32());
         b.GlobalVar("V", b.ty.i32(), builtin::AddressSpace::kPrivate);
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }();
 
     // Clone the program, using ReplaceAll() to create new module-scope
@@ -136,7 +137,6 @@
     auto* enable_2 = Enable(builtin::Extension::kChromiumExperimentalFullPtrParameters);
     auto* diagnostic_2 = DiagnosticDirective(builtin::DiagnosticSeverity::kOff, "bar");
 
-    this->SetResolveOnBuild(false);
     Program program(std::move(*this));
     EXPECT_THAT(program.AST().GlobalDeclarations(), ::testing::ContainerEq(tint::Vector{
                                                         enable_1,
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
index 72394b4..04cd69a 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/hashset.h"
@@ -99,7 +100,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 AddBlockAttribute::BlockAttribute::BlockAttribute(GenerationID pid, NodeID nid)
diff --git a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
index 5894052..142984a 100644
--- a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.cc
@@ -18,6 +18,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::AddEmptyEntryPoint);
 
@@ -58,7 +59,7 @@
            });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
index 8d1c234..f5092bb 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -70,7 +71,7 @@
                 diag::System::Transform,
                 "missing transform data for " +
                     std::string(tint::TypeInfo::Of<ArrayLengthFromUniform>().name));
-            return Program(std::move(b));
+            return resolver::Resolve(b);
         }
 
         if (!ShouldRun(ctx.src)) {
@@ -169,7 +170,7 @@
         outputs.Add<Result>(used_size_indices);
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
index f63c433..cade00c 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/text/string.h"
@@ -51,7 +52,7 @@
     if (!remappings) {
         b.Diagnostics().add_error(diag::System::Transform,
                                   "missing transform data for " + std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     if (remappings->binding_points.empty() && remappings->access_controls.empty()) {
@@ -127,7 +128,7 @@
                                               "invalid access mode (" +
                                                   std::to_string(static_cast<uint32_t>(access)) +
                                                   ")");
-                    return Program(std::move(b));
+                    return resolver::Resolve(b);
                 }
                 auto* sem = src->Sem().Get(var);
                 if (sem->AddressSpace() != builtin::AddressSpace::kStorage) {
@@ -135,7 +136,7 @@
                         diag::System::Transform,
                         "cannot apply access control to variable with address space " +
                             std::string(tint::ToString(sem->AddressSpace())));
-                    return Program(std::move(b));
+                    return resolver::Resolve(b);
                 }
                 auto* ty = sem->Type()->UnwrapRef();
                 auto inner_ty = CreateASTTypeFor(ctx, ty);
@@ -159,7 +160,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
index 45047ba..da63702 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/type/texture_dimension.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
@@ -136,7 +137,7 @@
         }
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc b/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
index 21a1059..ac9c811 100644
--- a/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
+++ b/src/tint/lang/wgsl/ast/transform/calculate_array_length.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -237,7 +238,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index 4cc81fe..fddfd8c 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -25,6 +25,7 @@
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -836,7 +837,7 @@
     if (cfg == nullptr) {
         b.Diagnostics().add_error(diag::System::Transform,
                                   "missing transform data for " + std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     // Remove entry point IO attributes from struct declarations.
@@ -863,7 +864,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 CanonicalizeEntryPointIO::Config::Config(ShaderStyle style,
diff --git a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
index 0964a81..ae15969 100644
--- a/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
+++ b/src/tint/lang/wgsl/ast/transform/clamp_frag_depth.cc
@@ -24,6 +24,7 @@
 #include "src/tint/lang/wgsl/ast/struct.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
@@ -58,7 +59,7 @@
                     TINT_ICE()
                         << "ClampFragDepth doesn't know how to handle module that already use push "
                            "constants";
-                    return Program(std::move(b));
+                    return resolver::Resolve(b);
                 }
             }
         }
@@ -167,7 +168,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc b/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
index 5b8c7da..9553ea2 100644
--- a/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
+++ b/src/tint/lang/wgsl/ast/transform/combine_samplers.cc
@@ -21,6 +21,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 
@@ -326,7 +327,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
@@ -342,7 +343,7 @@
         ProgramBuilder b;
         b.Diagnostics().add_error(diag::System::Transform,
                                   "missing transform data for " + std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     return State(src, binding_info).Run();
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
index 18267a1..f56c6c1 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_memory_access.cc
@@ -29,6 +29,7 @@
 #include "src/tint/lang/wgsl/ast/unary_op.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -977,7 +978,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
index 8982cf0..c1de1a4 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_array.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
@@ -173,7 +174,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
index 79778db..0f5ca80 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_array_test.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -33,7 +34,7 @@
 
 TEST_F(DecomposeStridedArrayTest, ShouldRunEmptyModule) {
     ProgramBuilder b;
-    EXPECT_FALSE(ShouldRun<DecomposeStridedArray>(Program(std::move(b))));
+    EXPECT_FALSE(ShouldRun<DecomposeStridedArray>(resolver::Resolve(b)));
 }
 
 TEST_F(DecomposeStridedArrayTest, ShouldRunNonStridedArray) {
@@ -41,7 +42,7 @@
 
     ProgramBuilder b;
     b.GlobalVar("arr", b.ty.array<f32, 4u>(), builtin::AddressSpace::kPrivate);
-    EXPECT_FALSE(ShouldRun<DecomposeStridedArray>(Program(std::move(b))));
+    EXPECT_FALSE(ShouldRun<DecomposeStridedArray>(resolver::Resolve(b)));
 }
 
 TEST_F(DecomposeStridedArrayTest, ShouldRunDefaultStridedArray) {
@@ -53,7 +54,7 @@
                     b.Stride(4),
                 }),
                 builtin::AddressSpace::kPrivate);
-    EXPECT_TRUE(ShouldRun<DecomposeStridedArray>(Program(std::move(b))));
+    EXPECT_TRUE(ShouldRun<DecomposeStridedArray>(resolver::Resolve(b)));
 }
 
 TEST_F(DecomposeStridedArrayTest, ShouldRunExplicitStridedArray) {
@@ -65,7 +66,7 @@
                     b.Stride(16),
                 }),
                 builtin::AddressSpace::kPrivate);
-    EXPECT_TRUE(ShouldRun<DecomposeStridedArray>(Program(std::move(b))));
+    EXPECT_TRUE(ShouldRun<DecomposeStridedArray>(resolver::Resolve(b)));
 }
 
 TEST_F(DecomposeStridedArrayTest, Empty) {
@@ -116,7 +117,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -165,7 +166,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -219,7 +220,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -273,7 +274,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -327,7 +328,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -376,7 +377,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -436,7 +437,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -493,7 +494,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -558,7 +559,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -622,7 +623,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -734,7 +735,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedArray>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
index e3124fe..a4e1295 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/simplify_pointers.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/containers/map.h"
@@ -201,7 +202,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
index 2bdc058..b724f356 100644
--- a/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/decompose_strided_matrix_test.cc
@@ -24,6 +24,7 @@
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::ast::transform {
 namespace {
@@ -108,7 +109,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -162,7 +163,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -216,7 +217,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -274,7 +275,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -329,7 +330,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -389,7 +390,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -444,7 +445,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -523,7 +524,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -577,7 +578,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
@@ -633,7 +634,7 @@
 }
 )";
 
-    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(Program(std::move(b)));
+    auto got = Run<Unshadow, SimplifyPointers, DecomposeStridedMatrix>(resolver::Resolve(b));
 
     EXPECT_EQ(expect, str(got));
 }
diff --git a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
index 045d306..2c49c40 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -243,7 +244,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
index 5d7543f..9862cb7 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
@@ -255,7 +256,7 @@
         CloneState state;
         clone_state = &state;
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
index 8946945..7671b08 100644
--- a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
+++ b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
@@ -18,6 +18,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/module.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::DisableUniformityAnalysis);
@@ -41,7 +42,7 @@
     b.Enable(builtin::Extension::kChromiumDisableUniformityAnalysis);
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
index 958a4c2..2719264 100644
--- a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
+++ b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -183,7 +184,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/first_index_offset.cc b/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
index 221a0d0..bd2b81f 100644
--- a/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
+++ b/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/core/builtin/builtin_value.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
@@ -165,7 +166,7 @@
     outputs.Add<Data>(has_vertex_index, has_instance_index);
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc b/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
index 110263e..e4a8e16 100644
--- a/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
+++ b/src/tint/lang/wgsl/ast/transform/fold_trivial_lets.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/containers/hashmap.h"
 
@@ -143,7 +144,7 @@
             }
         }
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc b/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
index caecbe6..873849f 100644
--- a/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
+++ b/src/tint/lang/wgsl/ast/transform/for_loop_to_loop.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::ForLoopToLoop);
 
@@ -80,7 +81,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc b/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
index 387135f..23774fe 100644
--- a/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
+++ b/src/tint/lang/wgsl/ast/transform/localize_struct_array_assignment.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
@@ -147,7 +148,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/manager.cc b/src/tint/lang/wgsl/ast/transform/manager.cc
index 7a12a43..6e05071 100644
--- a/src/tint/lang/wgsl/ast/transform/manager.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager.cc
@@ -16,6 +16,7 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 /// If set to 1 then the transform::Manager will dump the WGSL of the program
 /// before and after each transform. Helpful for debugging bad output.
@@ -77,7 +78,7 @@
         ProgramBuilder b;
         program::CloneContext ctx{&b, program, /* auto_clone_symbols */ true};
         ctx.Clone();
-        output = Program(std::move(b));
+        output = resolver::Resolve(b);
     }
     return std::move(output.value());
 }
diff --git a/src/tint/lang/wgsl/ast/transform/manager_test.cc b/src/tint/lang/wgsl/ast/transform/manager_test.cc
index 3215305..1d8b61a 100644
--- a/src/tint/lang/wgsl/ast/transform/manager_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager_test.cc
@@ -20,6 +20,7 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::ast::transform {
 namespace {
@@ -38,14 +39,14 @@
         program::CloneContext ctx{&b, src};
         b.Func(b.Sym("ast_func"), {}, b.ty.void_(), {});
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
 Program MakeAST() {
     ProgramBuilder b;
     b.Func(b.Sym("main"), {}, b.ty.void_(), {});
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 // Test that an AST program is always cloned, even if all transforms are skipped.
diff --git a/src/tint/lang/wgsl/ast/transform/merge_return.cc b/src/tint/lang/wgsl/ast/transform/merge_return.cc
index d3a1ede..9ec23c1 100644
--- a/src/tint/lang/wgsl/ast/transform/merge_return.cc
+++ b/src/tint/lang/wgsl/ast/transform/merge_return.cc
@@ -18,6 +18,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/rtti/switch.h"
@@ -236,7 +237,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc b/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
index 27748f0..854a2b3 100644
--- a/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/lang/wgsl/ast/transform/module_scope_var_to_entry_point_param.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/disable_validation_attribute.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/module.h"
@@ -600,7 +601,7 @@
     state.Process();
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
index 9aaa641..01e6fb4 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/function.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -515,7 +516,7 @@
     if (!new_binding_points) {
         b.Diagnostics().add_error(diag::System::Transform, "missing new binding point data for " +
                                                                std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     State state(ctx, new_binding_points);
@@ -523,7 +524,7 @@
     state.Process();
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
index 3e4563a..fbec666 100644
--- a/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/num_workgroups_from_uniform.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/utils/math/hash.h"
 
@@ -74,7 +75,7 @@
     if (cfg == nullptr) {
         b.Diagnostics().add_error(diag::System::Transform,
                                   "missing transform data for " + std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     if (!ShouldRun(src)) {
@@ -183,7 +184,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 NumWorkgroupsFromUniform::Config::Config(std::optional<BindingPoint> ubo_bp)
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc b/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
index dbc0270..8db3efd 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
+++ b/src/tint/lang/wgsl/ast/transform/packed_vec3.cc
@@ -25,6 +25,7 @@
 #include "src/tint/lang/wgsl/ast/assignment_statement.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/array_count.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/load.h"
@@ -497,7 +498,7 @@
         }
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc b/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc
index 3280edc..1db92d1 100644
--- a/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/packed_vec3_test.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/module.h"
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/text/string.h"
@@ -4171,7 +4172,7 @@
                      });
     b.GlobalVar("P", builtin::AddressSpace::kStorage, b.ty("S"),
                 tint::Vector{b.Group(AInt(0)), b.Binding(AInt(0))});
-    Program src(std::move(b));
+    Program src(resolver::Resolve(b));
 
     auto* expect =
         R"(
diff --git a/src/tint/lang/wgsl/ast/transform/pad_structs.cc b/src/tint/lang/wgsl/ast/transform/pad_structs.cc
index 8403708..f3a189b 100644
--- a/src/tint/lang/wgsl/ast/transform/pad_structs.cc
+++ b/src/tint/lang/wgsl/ast/transform/pad_structs.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/parameter.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/module.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
@@ -154,7 +155,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
index 2d92ee8..a64be30 100644
--- a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
+++ b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
@@ -20,6 +20,7 @@
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/containers/map.h"
 #include "src/tint/utils/containers/vector.h"
@@ -89,7 +90,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     /// Create a statement that will perform the assignment `lhs = rhs`, creating and using helper
diff --git a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
index f4ac2e0..1f8b05d 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
+++ b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
@@ -110,7 +111,7 @@
                     return child == expr ? TraverseAction::Descend : TraverseAction::Skip;
                 });
                 if (!ok) {
-                    return Program(std::move(b));
+                    return resolver::Resolve(b);
                 }
                 const_chains.Add(expr);
             } else if (should_hoist(sem)) {
@@ -144,12 +145,12 @@
     for (auto* expr : to_hoist) {
         if (!hoist_to_decl_before.Add(expr, expr->Declaration(),
                                       HoistToDeclBefore::VariableKind::kLet)) {
-            return Program(std::move(b));
+            return resolver::Resolve(b);
         }
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
index 0b367f0..a0a7e30 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
+++ b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.cc
@@ -26,6 +26,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
@@ -83,7 +84,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 // Decomposes side-effecting expressions to ensure order of evaluation. This
@@ -660,7 +661,7 @@
     decompose_state.Run();
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc b/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
index 8532759..0ff7594 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_continue_in_switch.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/lang/wgsl/sem/loop_statement.h"
@@ -89,7 +90,7 @@
         }
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
index 15b84ca..0c653f6 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -150,7 +151,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
index dc9c8ee..82bd693 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/traverse_expressions.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -58,7 +59,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/renamer.cc b/src/tint/lang/wgsl/ast/transform/renamer.cc
index c981130..1d9558b 100644
--- a/src/tint/lang/wgsl/ast/transform/renamer.cc
+++ b/src/tint/lang/wgsl/ast/transform/renamer.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/builtin_enum_expression.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -1399,7 +1400,7 @@
     }
     outputs.Add<Data>(std::move(out));
 
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.cc b/src/tint/lang/wgsl/ast/transform/robustness.cc
index 1a8165e..b63842f 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/lang/wgsl/sem/call.h"
@@ -198,7 +199,7 @@
         }
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
index 9deb98c..6166ccf 100644
--- a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
+++ b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
@@ -22,6 +22,7 @@
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -250,7 +251,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index 58135f1..5684e25 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/rtti/switch.h"
@@ -42,7 +43,7 @@
     if (cfg == nullptr) {
         b.Diagnostics().add_error(diag::System::Transform,
                                   "missing transform data for " + std::string(TypeInfo().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     // Find the target entry point.
@@ -59,7 +60,7 @@
     if (entry_point == nullptr) {
         b.Diagnostics().add_error(diag::System::Transform,
                                   "entry point '" + cfg->entry_point_name + "' not found");
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     auto& sem = src->Sem();
@@ -125,7 +126,7 @@
     // Clone the entry point.
     b.AST().AddFunction(ctx.Clone(entry_point));
 
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 SingleEntryPoint::Config::Config(std::string entry_point) : entry_point_name(entry_point) {}
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc b/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
index 5507da9..edad7cb 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
+++ b/src/tint/lang/wgsl/ast/transform/spirv_atomic.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/core/type/reference.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
@@ -152,7 +153,7 @@
         ReplaceLoadsAndStores();
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc b/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc
index 697b7dd..40c9319 100644
--- a/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/spirv_atomic_test.cc
@@ -21,6 +21,7 @@
 
 #include "src/tint/lang/wgsl/ast/transform/test_helper.h"
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -144,7 +145,7 @@
         // Keep this pointer alive after Transform() returns
         files_.emplace_back(std::move(file));
 
-        return TransformTest::Run<SpirvAtomic>(Program(std::move(b)));
+        return TransformTest::Run<SpirvAtomic>(resolver::Resolve(b));
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/std140.cc b/src/tint/lang/wgsl/ast/transform/std140.cc
index dfe519a..ebab82d 100644
--- a/src/tint/lang/wgsl/ast/transform/std140.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140.cc
@@ -21,6 +21,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/module.h"
@@ -125,7 +126,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     /// @returns true if this transform should be run for the given program
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.cc b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
index 7e9103e..390659f 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.cc
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
@@ -20,6 +20,7 @@
 #include "src/tint/lang/core/builtin/function.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -55,7 +56,7 @@
     const auto* data = config.Get<Config>();
     if (!data) {
         b.Diagnostics().add_error(diag::System::Transform, "Missing override substitution data");
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     if (!ShouldRun(ctx.src)) {
@@ -117,7 +118,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 SubstituteOverride::Config::Config() = default;
diff --git a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc b/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
index 7605cb4..0bae0d4 100644
--- a/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
+++ b/src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/core/type/texture_dimension.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
@@ -178,7 +179,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/transform.cc b/src/tint/lang/wgsl/ast/transform/transform.cc
index 34cc03b..3a4ad87 100644
--- a/src/tint/lang/wgsl/ast/transform/transform.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform.cc
@@ -24,6 +24,7 @@
 #include "src/tint/lang/core/type/sampler.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/for_loop_statement.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
@@ -45,7 +46,7 @@
         ProgramBuilder b;
         program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
         ctx.Clone();
-        output.program = Program(std::move(b));
+        output.program = resolver::Resolve(b);
     }
     return output;
 }
diff --git a/src/tint/lang/wgsl/ast/transform/transform_test.cc b/src/tint/lang/wgsl/ast/transform/transform_test.cc
index 09d1321..048d5c5 100644
--- a/src/tint/lang/wgsl/ast/transform/transform_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform_test.cc
@@ -18,6 +18,7 @@
 #include "src/tint/lang/wgsl/ast/transform/transform.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 #include "gtest/gtest.h"
 
@@ -35,7 +36,7 @@
     Type create(std::function<type::Type*(ProgramBuilder&)> create_sem_type) {
         ProgramBuilder sem_type_builder;
         auto* sem_type = create_sem_type(sem_type_builder);
-        Program program(std::move(sem_type_builder));
+        Program program = resolver::Resolve(sem_type_builder);
         program::CloneContext ctx(&ast_type_builder, &program, false);
         return CreateASTTypeFor(ctx, sem_type);
     }
@@ -106,7 +107,7 @@
     b.Override("O", b.Expr(123_a));
     auto* alias = b.Alias("A", b.ty.array(b.ty.i32(), arr_len));
 
-    Program program(std::move(b));
+    Program program(resolver::Resolve(b));
 
     auto* arr_ty = program.Sem().Get(alias);
 
diff --git a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc b/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
index 06aa9bf..7b9bcf6 100644
--- a/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
+++ b/src/tint/lang/wgsl/ast/transform/truncate_interstage_variables.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
@@ -59,7 +60,7 @@
             diag::System::Transform,
             "missing transform data for " +
                 std::string(tint::TypeInfo::Of<TruncateInterstageVariables>().name));
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
     auto& sem = ctx.src->Sem();
@@ -179,7 +180,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 TruncateInterstageVariables::Config::Config() = default;
diff --git a/src/tint/lang/wgsl/ast/transform/unshadow.cc b/src/tint/lang/wgsl/ast/transform/unshadow.cc
index a2c6a97..e6655bb 100644
--- a/src/tint/lang/wgsl/ast/transform/unshadow.cc
+++ b/src/tint/lang/wgsl/ast/transform/unshadow.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/block_statement.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -115,7 +116,7 @@
         });
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
index f690c27..cbf4a4f 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/get_insertion_point_test.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/get_insertion_point.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/ice/ice.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -38,7 +39,7 @@
     auto* block = b.Block(var);
     b.Func("f", tint::Empty, b.ty.void_(), Vector{block});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -60,7 +61,7 @@
     auto* func_block = b.Block(fl);
     b.Func("f", tint::Empty, b.ty.void_(), Vector{func_block});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -81,7 +82,7 @@
     auto* s = b.For({}, b.Expr(true), var, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
diff --git a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
index 0adc716..70752be 100644
--- a/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before_test.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/if_statement.h"
 #include "src/tint/lang/wgsl/sem/index_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/statement.h"
@@ -39,7 +40,7 @@
     auto* var = b.Decl(b.Var("a", expr));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -48,7 +49,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kLet);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -70,7 +71,7 @@
     auto* s = b.For(b.Decl(b.Var("a", expr)), b.Expr(true), nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -79,7 +80,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kVar);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -112,7 +113,7 @@
     auto* s = b.For(nullptr, expr, nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -121,7 +122,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kConst);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -150,7 +151,7 @@
     auto* s = b.For(nullptr, b.Expr(true), b.Decl(b.Var("a", expr)), b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -159,7 +160,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kLet);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -193,7 +194,7 @@
     auto* s = b.While(expr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -202,7 +203,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kVar);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -237,7 +238,7 @@
                                b.Else(b.Block()))));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -246,7 +247,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kConst);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -275,7 +276,7 @@
     auto* var2 = b.Decl(b.Var("b", expr));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var1, var2});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -284,7 +285,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kLet);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -309,7 +310,7 @@
     auto* var2 = b.Decl(b.Var("b", expr));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var1, var2});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -318,7 +319,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kVar);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -343,7 +344,7 @@
     auto* s = b.For(nullptr, expr, nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -352,7 +353,7 @@
     hoistToDeclBefore.Prepare(sem_expr);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -380,7 +381,7 @@
     auto* s = b.For(nullptr, b.Expr(true), b.Decl(b.Var("a", expr)), b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -389,7 +390,7 @@
     hoistToDeclBefore.Prepare(sem_expr);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -426,7 +427,7 @@
                                b.Else(b.Block()))));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -435,7 +436,7 @@
     hoistToDeclBefore.Prepare(sem_expr);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -463,7 +464,7 @@
     auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -473,7 +474,7 @@
     hoistToDeclBefore.InsertBefore(before_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -499,7 +500,7 @@
     auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -509,7 +510,7 @@
                                    [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -537,7 +538,7 @@
     auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -547,7 +548,7 @@
     hoistToDeclBefore.InsertBefore(before_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -584,7 +585,7 @@
     auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -594,7 +595,7 @@
                                    [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -633,7 +634,7 @@
     auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -643,7 +644,7 @@
     hoistToDeclBefore.InsertBefore(before_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -684,7 +685,7 @@
     auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -694,7 +695,7 @@
                                    [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -738,7 +739,7 @@
                    b.Else(elseif));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -748,7 +749,7 @@
     hoistToDeclBefore.InsertBefore(before_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -787,7 +788,7 @@
                    b.Else(elseif));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -797,7 +798,7 @@
                                    [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -827,7 +828,7 @@
     auto* var = b.Decl(b.Var("a", b.ty.array(b.ty.f32(), 1_a), expr));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -836,7 +837,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kLet);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -857,7 +858,7 @@
     auto* var = b.Decl(b.Var("a", b.ty.array(b.ty.f32(), 1_a), expr));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -866,7 +867,7 @@
     hoistToDeclBefore.Add(sem_expr, expr, HoistToDeclBefore::VariableKind::kVar);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn f() {
@@ -889,7 +890,7 @@
     auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -899,7 +900,7 @@
     hoistToDeclBefore.Replace(target_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -924,7 +925,7 @@
     auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -933,7 +934,7 @@
     hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -960,7 +961,7 @@
     auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -970,7 +971,7 @@
     hoistToDeclBefore.Replace(target_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -1006,7 +1007,7 @@
     auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -1015,7 +1016,7 @@
     hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -1053,7 +1054,7 @@
     auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -1063,7 +1064,7 @@
     hoistToDeclBefore.Replace(target_stmt, new_stmt);
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
@@ -1103,7 +1104,7 @@
     auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
     b.Func("f", tint::Empty, b.ty.void_(), Vector{var, s});
 
-    Program original(std::move(b));
+    Program original(resolver::Resolve(b));
     ProgramBuilder cloned_b;
     program::CloneContext ctx(&cloned_b, &original);
 
@@ -1112,7 +1113,7 @@
     hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
 
     ctx.Clone();
-    Program cloned(std::move(cloned_b));
+    Program cloned(resolver::Resolve(cloned_b));
 
     auto* expect = R"(
 fn foo() {
diff --git a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc b/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
index 994f9a1..2b1deb1 100644
--- a/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
+++ b/src/tint/lang/wgsl/ast/transform/var_for_dynamic_index.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/transform/utils/hoist_to_decl_before.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::VarForDynamicIndex);
 
@@ -65,7 +66,7 @@
     for (auto* node : src->ASTNodes().Objects()) {
         if (auto* access_expr = node->As<IndexAccessorExpression>()) {
             if (!dynamic_index_to_var(access_expr)) {
-                return Program(std::move(b));
+                return resolver::Resolve(b);
             }
             index_accessor_found = true;
         }
@@ -75,7 +76,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc b/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
index 489311f..df9a7a4 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
+++ b/src/tint/lang/wgsl/ast/transform/vectorize_matrix_conversions.cc
@@ -21,6 +21,7 @@
 #include "src/tint/lang/core/type/abstract_numeric.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/value_conversion.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
@@ -142,7 +143,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
index 8432dab..52d628d 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
+++ b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.cc
@@ -20,6 +20,7 @@
 #include "src/tint/lang/core/type/abstract_numeric.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
@@ -140,7 +141,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index 693f923..d4fb145 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -23,6 +23,7 @@
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/map.h"
 #include "src/tint/utils/macros/compiler.h"
@@ -247,7 +248,7 @@
                     b.Diagnostics().add_error(
                         diag::System::Transform,
                         "VertexPulling found more than one vertex entry point");
-                    return Program(std::move(b));
+                    return resolver::Resolve(b);
                 }
                 func = fn;
             }
@@ -255,14 +256,14 @@
         if (func == nullptr) {
             b.Diagnostics().add_error(diag::System::Transform,
                                       "Vertex stage entry point not found");
-            return Program(std::move(b));
+            return resolver::Resolve(b);
         }
 
         AddVertexStorageBuffers();
         Process(func);
 
         ctx.Clone();
-        return Program(std::move(b));
+        return resolver::Resolve(b);
     }
 
   private:
diff --git a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc b/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
index 5d0191a..93b1ee7 100644
--- a/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
+++ b/src/tint/lang/wgsl/ast/transform/while_to_loop.cc
@@ -19,6 +19,7 @@
 #include "src/tint/lang/wgsl/ast/break_statement.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::WhileToLoop);
 
@@ -74,7 +75,7 @@
     });
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
index 5609227..4255ad0 100644
--- a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
@@ -25,6 +25,7 @@
 #include "src/tint/lang/wgsl/ast/workgroup_attribute.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/function.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/containers/map.h"
@@ -475,7 +476,7 @@
     }
 
     ctx.Clone();
-    return Program(std::move(b));
+    return resolver::Resolve(b);
 }
 
 }  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
index 113723c..f39225b 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
@@ -19,6 +19,7 @@
 #include "gtest/gtest.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 
 namespace tint::writer {
@@ -30,7 +31,7 @@
 
 TEST_F(FlattenBindingsTest, NoBindings) {
     ProgramBuilder b;
-    Program program(std::move(b));
+    Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     auto flattened = tint::writer::FlattenBindings(&program);
@@ -43,7 +44,7 @@
     b.GlobalVar("b", b.ty.i32(), builtin::AddressSpace::kUniform, b.Group(0_a), b.Binding(1_a));
     b.GlobalVar("c", b.ty.i32(), builtin::AddressSpace::kUniform, b.Group(0_a), b.Binding(2_a));
 
-    Program program(std::move(b));
+    Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     auto flattened = tint::writer::FlattenBindings(&program);
@@ -57,7 +58,7 @@
     b.GlobalVar("c", b.ty.i32(), builtin::AddressSpace::kUniform, b.Group(2_a), b.Binding(2_a));
     b.WrapInFunction(b.Expr("a"), b.Expr("b"), b.Expr("c"));
 
-    Program program(std::move(b));
+    Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     auto flattened = tint::writer::FlattenBindings(&program);
@@ -120,7 +121,7 @@
                      b.Assign(b.Phony(), "texture4"), b.Assign(b.Phony(), "texture5"),
                      b.Assign(b.Phony(), "texture6"));
 
-    Program program(std::move(b));
+    Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     auto flattened = tint::writer::FlattenBindings(&program);
diff --git a/src/tint/lang/wgsl/helpers/ir_program_test.h b/src/tint/lang/wgsl/helpers/ir_program_test.h
index 0bcb2d3..c8d15ba 100644
--- a/src/tint/lang/wgsl/helpers/ir_program_test.h
+++ b/src/tint/lang/wgsl/helpers/ir_program_test.h
@@ -26,6 +26,7 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/text/string_stream.h"
 
 namespace tint::wgsl::helpers {
@@ -40,9 +41,7 @@
     /// Build the module, cleaning up the program before returning.
     /// @returns the generated module
     tint::Result<ir::Module, std::string> Build() {
-        SetResolveOnBuild(true);
-
-        Program program{std::move(*this)};
+        Program program{resolver::Resolve(*this)};
         if (!program.IsValid()) {
             return program.Diagnostics().str();
         }
diff --git a/src/tint/lang/wgsl/inspector/inspector_builder_test.cc b/src/tint/lang/wgsl/inspector/inspector_builder_test.cc
new file mode 100644
index 0000000..2c9d041
--- /dev/null
+++ b/src/tint/lang/wgsl/inspector/inspector_builder_test.cc
@@ -0,0 +1,360 @@
+// 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.
+
+#include "src/tint/lang/wgsl/inspector/inspector_builder_test.h"
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+namespace tint::inspector {
+
+InspectorBuilder::InspectorBuilder() = default;
+InspectorBuilder::~InspectorBuilder() = default;
+
+void InspectorBuilder::MakeEmptyBodyFunction(std::string name,
+                                             VectorRef<const ast::Attribute*> attributes) {
+    Func(name, tint::Empty, ty.void_(), Vector{Return()}, attributes);
+}
+
+void InspectorBuilder::MakeCallerBodyFunction(std::string caller,
+                                              VectorRef<std::string> callees,
+                                              VectorRef<const ast::Attribute*> attributes) {
+    Vector<const ast::Statement*, 8> body;
+    body.Reserve(callees.Length() + 1);
+    for (auto callee : callees) {
+        body.Push(CallStmt(Call(callee)));
+    }
+    body.Push(Return());
+
+    Func(caller, tint::Empty, ty.void_(), body, attributes);
+}
+
+const ast::Struct* InspectorBuilder::MakeInOutStruct(std::string name,
+                                                     VectorRef<InOutInfo> inout_vars) {
+    Vector<const ast::StructMember*, 8> members;
+    for (auto var : inout_vars) {
+        std::string member_name;
+        uint32_t location;
+        std::tie(member_name, location) = var;
+        members.Push(Member(member_name, ty.u32(),
+                            Vector{
+                                Location(AInt(location)),
+                                Flat(),
+                            }));
+    }
+    return Structure(name, members);
+}
+
+const ast::Function* InspectorBuilder::MakePlainGlobalReferenceBodyFunction(
+    std::string func,
+    std::string var,
+    ast::Type type,
+    VectorRef<const ast::Attribute*> attributes) {
+    Vector<const ast::Statement*, 3> stmts;
+    stmts.Push(Decl(Var("local_" + var, type)));
+    stmts.Push(Assign("local_" + var, var));
+    stmts.Push(Return());
+    return Func(func, tint::Empty, ty.void_(), std::move(stmts), std::move(attributes));
+}
+
+bool InspectorBuilder::ContainsName(VectorRef<StageVariable> vec, const std::string& name) {
+    for (auto& s : vec) {
+        if (s.name == name) {
+            return true;
+        }
+    }
+    return false;
+}
+
+std::string InspectorBuilder::StructMemberName(size_t idx, ast::Type type) {
+    return std::to_string(idx) + type->identifier->symbol.Name();
+}
+
+const ast::Struct* InspectorBuilder::MakeStructType(const std::string& name,
+                                                    VectorRef<ast::Type> member_types) {
+    Vector<const ast::StructMember*, 8> members;
+    for (auto type : member_types) {
+        members.Push(MakeStructMember(members.Length(), type, {}));
+    }
+    return MakeStructTypeFromMembers(name, std::move(members));
+}
+
+const ast::Struct* InspectorBuilder::MakeStructTypeFromMembers(
+    const std::string& name,
+    VectorRef<const ast::StructMember*> members) {
+    return Structure(name, std::move(members));
+}
+
+const ast::StructMember* InspectorBuilder::MakeStructMember(
+    size_t index,
+    ast::Type type,
+    VectorRef<const ast::Attribute*> attributes) {
+    return Member(StructMemberName(index, type), type, std::move(attributes));
+}
+
+const ast::Struct* InspectorBuilder::MakeUniformBufferType(const std::string& name,
+                                                           VectorRef<ast::Type> member_types) {
+    return MakeStructType(name, member_types);
+}
+
+std::function<ast::Type()> InspectorBuilder::MakeStorageBufferTypes(
+    const std::string& name,
+    VectorRef<ast::Type> member_types) {
+    MakeStructType(name, member_types);
+    return [this, name] { return ty(name); };
+}
+
+void InspectorBuilder::AddUniformBuffer(const std::string& name,
+                                        ast::Type type,
+                                        uint32_t group,
+                                        uint32_t binding) {
+    GlobalVar(name, type, builtin::AddressSpace::kUniform, Binding(AInt(binding)),
+              Group(AInt(group)));
+}
+
+void InspectorBuilder::AddWorkgroupStorage(const std::string& name, ast::Type type) {
+    GlobalVar(name, type, builtin::AddressSpace::kWorkgroup);
+}
+
+void InspectorBuilder::AddStorageBuffer(const std::string& name,
+                                        ast::Type type,
+                                        builtin::Access access,
+                                        uint32_t group,
+                                        uint32_t binding) {
+    GlobalVar(name, type, builtin::AddressSpace::kStorage, access, Binding(AInt(binding)),
+              Group(AInt(group)));
+}
+
+void InspectorBuilder::MakeStructVariableReferenceBodyFunction(
+    std::string func_name,
+    std::string struct_name,
+    VectorRef<std::tuple<size_t, ast::Type>> members) {
+    Vector<const ast::Statement*, 8> stmts;
+    for (auto member : members) {
+        size_t member_idx;
+        ast::Type member_type;
+        std::tie(member_idx, member_type) = member;
+        std::string member_name = StructMemberName(member_idx, member_type);
+
+        stmts.Push(Decl(Var("local" + member_name, member_type)));
+    }
+
+    for (auto member : members) {
+        size_t member_idx;
+        ast::Type member_type;
+        std::tie(member_idx, member_type) = member;
+        std::string member_name = StructMemberName(member_idx, member_type);
+
+        stmts.Push(Assign("local" + member_name, MemberAccessor(struct_name, member_name)));
+    }
+
+    stmts.Push(Return());
+
+    Func(func_name, tint::Empty, ty.void_(), stmts);
+}
+
+void InspectorBuilder::AddSampler(const std::string& name, uint32_t group, uint32_t binding) {
+    GlobalVar(name, ty.sampler(type::SamplerKind::kSampler), Binding(AInt(binding)),
+              Group(AInt(group)));
+}
+
+void InspectorBuilder::AddComparisonSampler(const std::string& name,
+                                            uint32_t group,
+                                            uint32_t binding) {
+    GlobalVar(name, ty.sampler(type::SamplerKind::kComparisonSampler), Binding(AInt(binding)),
+              Group(AInt(group)));
+}
+
+void InspectorBuilder::AddResource(const std::string& name,
+                                   ast::Type type,
+                                   uint32_t group,
+                                   uint32_t binding) {
+    GlobalVar(name, type, Binding(AInt(binding)), Group(AInt(group)));
+}
+
+void InspectorBuilder::AddGlobalVariable(const std::string& name, ast::Type type) {
+    GlobalVar(name, type, builtin::AddressSpace::kPrivate);
+}
+
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    ast::Type base_type,
+    VectorRef<const ast::Attribute*> attributes) {
+    std::string result_name = "sampler_result";
+
+    Vector stmts{
+        Decl(Var(result_name, ty.vec(base_type, 4))),
+        Assign(result_name, Call("textureSample", texture_name, sampler_name, coords_name)),
+        Return(),
+    };
+    return Func(func_name, tint::Empty, ty.void_(), std::move(stmts), std::move(attributes));
+}
+
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    const std::string& array_index,
+    ast::Type base_type,
+    VectorRef<const ast::Attribute*> attributes) {
+    std::string result_name = "sampler_result";
+
+    Vector stmts{
+        Decl(Var("sampler_result", ty.vec(base_type, 4))),
+        Assign("sampler_result",
+               Call("textureSample", texture_name, sampler_name, coords_name, array_index)),
+        Return(),
+    };
+    return Func(func_name, tint::Empty, ty.void_(), std::move(stmts), std::move(attributes));
+}
+
+const ast::Function* InspectorBuilder::MakeComparisonSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    const std::string& depth_name,
+    ast::Type base_type,
+    VectorRef<const ast::Attribute*> attributes) {
+    std::string result_name = "sampler_result";
+
+    Vector stmts{
+        Decl(Var("sampler_result", base_type)),
+        Assign("sampler_result",
+               Call("textureSampleCompare", texture_name, sampler_name, coords_name, depth_name)),
+        Return(),
+    };
+    return Func(func_name, tint::Empty, ty.void_(), std::move(stmts), std::move(attributes));
+}
+
+ast::Type InspectorBuilder::GetBaseType(ResourceBinding::SampledKind sampled_kind) {
+    switch (sampled_kind) {
+        case ResourceBinding::SampledKind::kFloat:
+            return ty.f32();
+        case ResourceBinding::SampledKind::kSInt:
+            return ty.i32();
+        case ResourceBinding::SampledKind::kUInt:
+            return ty.u32();
+        default:
+            return ast::Type{};
+    }
+}
+
+ast::Type InspectorBuilder::GetCoordsType(type::TextureDimension dim, ast::Type scalar) {
+    switch (dim) {
+        case type::TextureDimension::k1d:
+            return scalar;
+        case type::TextureDimension::k2d:
+        case type::TextureDimension::k2dArray:
+            return ty.vec2(scalar);
+        case type::TextureDimension::k3d:
+        case type::TextureDimension::kCube:
+        case type::TextureDimension::kCubeArray:
+            return ty.vec3(scalar);
+        default:
+            [=] {
+                StringStream str;
+                str << dim;
+                FAIL() << "Unsupported texture dimension: " << str.str();
+            }();
+    }
+    return ast::Type{};
+}
+
+ast::Type InspectorBuilder::MakeStorageTextureTypes(type::TextureDimension dim,
+                                                    builtin::TexelFormat format) {
+    return ty.storage_texture(dim, format, builtin::Access::kWrite);
+}
+
+void InspectorBuilder::AddStorageTexture(const std::string& name,
+                                         ast::Type type,
+                                         uint32_t group,
+                                         uint32_t binding) {
+    GlobalVar(name, type, Binding(AInt(binding)), Group(AInt(group)));
+}
+
+const ast::Function* InspectorBuilder::MakeStorageTextureBodyFunction(
+    const std::string& func_name,
+    const std::string& st_name,
+    ast::Type dim_type,
+    VectorRef<const ast::Attribute*> attributes) {
+    Vector stmts{
+        Decl(Var("dim", dim_type)),
+        Assign("dim", Call("textureDimensions", st_name)),
+        Return(),
+    };
+
+    return Func(func_name, tint::Empty, ty.void_(), std::move(stmts), std::move(attributes));
+}
+
+std::function<ast::Type()> InspectorBuilder::GetTypeFunction(ComponentType component,
+                                                             CompositionType composition) {
+    std::function<ast::Type()> func;
+    switch (component) {
+        case ComponentType::kF32:
+            func = [this] { return ty.f32(); };
+            break;
+        case ComponentType::kI32:
+            func = [this] { return ty.i32(); };
+            break;
+        case ComponentType::kU32:
+            func = [this] { return ty.u32(); };
+            break;
+        case ComponentType::kF16:
+            func = [this] { return ty.f16(); };
+            break;
+        case ComponentType::kUnknown:
+            return [] { return ast::Type{}; };
+    }
+
+    uint32_t n;
+    switch (composition) {
+        case CompositionType::kScalar:
+            return func;
+        case CompositionType::kVec2:
+            n = 2;
+            break;
+        case CompositionType::kVec3:
+            n = 3;
+            break;
+        case CompositionType::kVec4:
+            n = 4;
+            break;
+        default:
+            return [] { return ast::Type{}; };
+    }
+
+    return [this, func, n] { return ty.vec(func(), n); };
+}
+
+Inspector& InspectorBuilder::Build() {
+    if (inspector_) {
+        return *inspector_;
+    }
+    program_ = std::make_unique<Program>(resolver::Resolve(*this));
+    [&] { ASSERT_TRUE(program_->IsValid()) << program_->Diagnostics().str(); }();
+    inspector_ = std::make_unique<Inspector>(program_.get());
+    return *inspector_;
+}
+
+}  // namespace tint::inspector
diff --git a/src/tint/lang/wgsl/inspector/test_inspector_builder.cc b/src/tint/lang/wgsl/inspector/test_inspector_builder.cc
index e247141..6ee260e 100644
--- a/src/tint/lang/wgsl/inspector/test_inspector_builder.cc
+++ b/src/tint/lang/wgsl/inspector/test_inspector_builder.cc
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::inspector {
 
@@ -351,7 +352,7 @@
     if (inspector_) {
         return *inspector_;
     }
-    program_ = std::make_unique<Program>(std::move(*this));
+    program_ = std::make_unique<Program>(resolver::Resolve(*this));
     [&] { ASSERT_TRUE(program_->IsValid()) << program_->Diagnostics().str(); }();
     inspector_ = std::make_unique<Inspector>(program_.get());
     return *inspector_;
diff --git a/src/tint/lang/wgsl/program/clone_context_test.cc b/src/tint/lang/wgsl/program/clone_context_test.cc
index 228802e..933a79c 100644
--- a/src/tint/lang/wgsl/program/clone_context_test.cc
+++ b/src/tint/lang/wgsl/program/clone_context_test.cc
@@ -19,6 +19,7 @@
 #include "gtest/gtest-spi.h"
 #include "src/tint/lang/wgsl/program/clone_context.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::program {
 namespace {
@@ -104,7 +105,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -162,7 +163,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -255,7 +256,7 @@
         auto* c = b;  // Aliased
         original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
     }
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -288,7 +289,7 @@
 
     ProgramBuilder builder;
     auto* original_node = a.Create<Node>(builder.Symbols().New("root"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
@@ -309,7 +310,7 @@
     original_root->a = a.Create<Node>(builder.Symbols().New("a"));
     original_root->b = a.Create<Node>(builder.Symbols().New("b"));
     original_root->c = a.Create<Node>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -341,7 +342,7 @@
     original_root->a = a.Create<Node>(builder.Symbols().New("a"));
     original_root->b = a.Create<Node>(builder.Symbols().New("b"));
     original_root->c = a.Create<Node>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -380,7 +381,7 @@
     original_root->a = a.Create<Node>(builder.Symbols().New("a"));
     original_root->b = a.Create<Node>(builder.Symbols().New("b"));
     original_root->c = a.Create<Node>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     //                          root
     //        ╭──────────────────┼──────────────────╮
@@ -412,7 +413,7 @@
     original_root->a = a.Create<Node>(builder.Symbols().New("a"));
     original_root->b = a.Create<Node>(builder.Symbols().New("b"));
     original_root->c = a.Create<Node>(builder.Symbols().New("c"));
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -453,7 +454,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* cloned_root = CloneContext(&cloned, &original)
@@ -480,7 +481,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -512,7 +513,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -541,7 +542,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -562,7 +563,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -588,7 +589,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -616,7 +617,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -641,7 +642,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -662,7 +663,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -684,7 +685,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion_back = a.Create<Node>(cloned.Symbols().New("insertion_back"));
@@ -708,7 +709,7 @@
     ProgramBuilder builder;
     auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
     original_root->vec.Clear();
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -737,7 +738,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -765,7 +766,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -794,7 +795,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
@@ -822,7 +823,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -852,7 +853,7 @@
         a.Create<Node>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
@@ -884,7 +885,7 @@
         a.Create<Node>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
@@ -916,7 +917,7 @@
         a.Create<Node>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
@@ -948,7 +949,7 @@
         a.Create<Node>(builder.Symbols().Register("c")),
     };
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
@@ -979,7 +980,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     auto* insertion_before = a.Create<Node>(cloned.Symbols().New("insertion_before"));
@@ -1011,7 +1012,7 @@
         a.Create<Node>(builder.Symbols().Register("b")),
         a.Create<Node>(builder.Symbols().Register("c")),
     };
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
 
@@ -1104,7 +1105,7 @@
     EXPECT_EQ(old_b.Name(), "tint_symbol_1");
     EXPECT_EQ(old_c.Name(), "tint_symbol_2");
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original, false);
@@ -1132,7 +1133,7 @@
     EXPECT_EQ(old_b.Name(), "b");
     EXPECT_EQ(old_c.Name(), "c");
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original, false);
@@ -1160,7 +1161,7 @@
     EXPECT_EQ(old_b.Name(), "b");
     EXPECT_EQ(old_c.Name(), "c");
 
-    Program original(std::move(builder));
+    Program original(resolver::Resolve(builder));
 
     ProgramBuilder cloned;
     CloneContext ctx(&cloned, &original);
diff --git a/src/tint/lang/wgsl/program/program.cc b/src/tint/lang/wgsl/program/program.cc
index b9cdc75..ee199ca 100644
--- a/src/tint/lang/wgsl/program/program.cc
+++ b/src/tint/lang/wgsl/program/program.cc
@@ -17,7 +17,6 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/program/clone_context.h"
-#include "src/tint/lang/wgsl/resolver/resolver.h"
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/rtti/switch.h"
@@ -53,14 +52,7 @@
 Program::Program(ProgramBuilder&& builder) {
     id_ = builder.ID();
     highest_node_id_ = builder.LastAllocatedNodeID();
-
     is_valid_ = builder.IsValid();
-    if (builder.ResolveOnBuild() && builder.IsValid()) {
-        resolver::Resolver resolver(&builder);
-        if (!resolver.Resolve()) {
-            is_valid_ = false;
-        }
-    }
 
     // The above must be called *before* the calls to std::move() below
     constants_ = std::move(builder.constants);
diff --git a/src/tint/lang/wgsl/program/program_builder.h b/src/tint/lang/wgsl/program/program_builder.h
index 6d01e69..9e20af9 100644
--- a/src/tint/lang/wgsl/program/program_builder.h
+++ b/src/tint/lang/wgsl/program/program_builder.h
@@ -134,14 +134,6 @@
         return sem_;
     }
 
-    /// Controls whether the Resolver will be run on the program when it is built.
-    /// @param enable the new flag value (defaults to true)
-    void SetResolveOnBuild(bool enable) { resolve_on_build_ = enable; }
-
-    /// @return true if the Resolver will be run on the program when it is
-    /// built.
-    bool ResolveOnBuild() const { return resolve_on_build_; }
-
     /// Overlay Builder::create() overloads
     using Builder::create;
 
@@ -205,9 +197,6 @@
   private:
     SemNodeAllocator sem_nodes_;
     sem::Info sem_;
-
-    /// Set by SetResolveOnBuild(). If set, the Resolver will be run on the program when built.
-    bool resolve_on_build_ = true;
 };
 
 /// @param builder the ProgramBuilder
diff --git a/src/tint/lang/wgsl/reader/parser/parser.h b/src/tint/lang/wgsl/reader/parser/parser.h
index 67d676d..4fe6f9e 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.h
+++ b/src/tint/lang/wgsl/reader/parser/parser.h
@@ -26,6 +26,7 @@
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/reader/parser/detail.h"
 #include "src/tint/lang/wgsl/reader/parser/token.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/diagnostic/formatter.h"
 
 namespace tint::ast {
@@ -319,7 +320,7 @@
 
     /// @returns the Program. The program builder in the parser will be reset
     /// after this.
-    Program program() { return Program(std::move(builder_)); }
+    Program program() { return resolver::Resolve(builder_); }
 
     /// @returns the program builder.
     ProgramBuilder& builder() { return builder_; }
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index f41cdce..beec36b 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -17,13 +17,14 @@
 #include <utility>
 
 #include "src/tint/lang/wgsl/reader/parser/parser.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::wgsl::reader {
 
 Program Parse(Source::File const* file) {
     Parser parser(file);
     parser.Parse();
-    return Program(std::move(parser.builder()));
+    return resolver::Resolve(parser.builder());
 }
 
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/resolver/resolve.cc b/src/tint/lang/wgsl/resolver/resolve.cc
new file mode 100644
index 0000000..d076dc4
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/resolve.cc
@@ -0,0 +1,29 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+
+#include <utility>
+
+#include "src/tint/lang/wgsl/resolver/resolver.h"
+
+namespace tint::resolver {
+
+Program Resolve(ProgramBuilder& builder) {
+    Resolver resolver(&builder);
+    resolver.Resolve();
+    return Program(std::move(builder));
+}
+
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolve.h b/src/tint/lang/wgsl/resolver/resolve.h
new file mode 100644
index 0000000..0466fc2
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/resolve.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_WGSL_RESOLVER_RESOLVE_H_
+#define SRC_TINT_LANG_WGSL_RESOLVER_RESOLVE_H_
+
+namespace tint {
+class Program;
+class ProgramBuilder;
+}  // namespace tint
+
+namespace tint::resolver {
+
+/// Performs semantic analysis and validation on the program builder @p builder
+/// @returns the resolved Program. Program.Diagnostics() may contain validation errors.
+Program Resolve(ProgramBuilder& builder);
+
+}  // namespace tint::resolver
+
+#endif  // SRC_TINT_LANG_WGSL_RESOLVER_RESOLVE_H_
diff --git a/src/tint/lang/wgsl/resolver/uniformity_test.cc b/src/tint/lang/wgsl/resolver/uniformity_test.cc
index c7e1b71..df9e366 100644
--- a/src/tint/lang/wgsl/resolver/uniformity_test.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity_test.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/reader/reader.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/resolver/uniformity.h"
 #include "src/tint/utils/text/string_stream.h"
 
@@ -65,7 +66,7 @@
     /// @param builder the program builder
     /// @param should_pass true if `builder` program should pass the analysis, otherwise false
     void RunTest(ProgramBuilder&& builder, bool should_pass) {
-        auto program = Program(std::move(builder));
+        auto program = resolver::Resolve(builder);
         return RunTest(std::move(program), should_pass);
     }
 
diff --git a/src/tint/lang/wgsl/resolver/validation_test.cc b/src/tint/lang/wgsl/resolver/validation_test.cc
index 874d23d..4255005 100644
--- a/src/tint/lang/wgsl/resolver/validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/validation_test.cc
@@ -33,6 +33,7 @@
 #include "src/tint/lang/wgsl/ast/switch_statement.h"
 #include "src/tint/lang/wgsl/ast/unary_op_expression.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/resolver/resolver_test_helper.h"
 #include "src/tint/lang/wgsl/sem/call.h"
 #include "src/tint/lang/wgsl/sem/function.h"
@@ -123,7 +124,7 @@
         {
             ProgramBuilder b;
             b.WrapInFunction(b.create<FakeStmt>());
-            Program(std::move(b));
+            resolver::Resolve(b);
         },
         "internal compiler error: unhandled node type: tint::resolver::FakeStmt");
 }
diff --git a/src/tint/lang/wgsl/sem/test_helper.h b/src/tint/lang/wgsl/sem/test_helper.h
index 0e0ad7a..8006d17 100644
--- a/src/tint/lang/wgsl/sem/test_helper.h
+++ b/src/tint/lang/wgsl/sem/test_helper.h
@@ -19,6 +19,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 
 namespace tint::sem {
 
@@ -32,7 +33,7 @@
         [&] {
             ASSERT_TRUE(IsValid()) << "Builder program is not valid\n" << Diagnostics().str();
         }();
-        return Program(std::move(*this));
+        return resolver::Resolve(*this);
     }
 };
 using TestHelper = TestHelperBase<testing::Test>;
diff --git a/src/tint/lang/wgsl/writer/ast_printer/literal_test.cc b/src/tint/lang/wgsl/writer/ast_printer/literal_test.cc
index 77c1fce..a7ed374 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/literal_test.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/literal_test.cc
@@ -114,8 +114,7 @@
 TEST_P(WgslGenerator_F32LiteralTest, Emit) {
     auto* v = Expr(GetParam().value);
 
-    SetResolveOnBuild(false);
-    ASTPrinter& gen = Build();
+    ASTPrinter& gen = Build(/* resolve */ false);
 
     StringStream out;
     gen.EmitLiteral(out, v);
@@ -163,8 +162,7 @@
 
     auto* v = Expr(GetParam().value);
 
-    SetResolveOnBuild(false);
-    ASTPrinter& gen = Build();
+    ASTPrinter& gen = Build(/* resolve */ false);
 
     StringStream out;
     gen.EmitLiteral(out, v);
diff --git a/src/tint/lang/wgsl/writer/ast_printer/test_helper.h b/src/tint/lang/wgsl/writer/ast_printer/test_helper.h
index aeba391..94b2401 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/test_helper.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/test_helper.h
@@ -20,6 +20,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/writer/ast_printer/ast_printer.h"
 
 namespace tint::wgsl::writer {
@@ -33,14 +34,19 @@
     ~TestHelperBase() override = default;
 
     /// Builds and returns a ASTPrinter from the program.
+    /// @param resolve if true, the program will be resolved before returning
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same ASTPrinter without rebuilding.
     /// @return the built generator
-    ASTPrinter& Build() {
+    ASTPrinter& Build(bool resolve = true) {
         if (gen_) {
             return *gen_;
         }
-        program = std::make_unique<Program>(std::move(*this));
+        if (resolve) {
+            program = std::make_unique<Program>(resolver::Resolve(*this));
+        } else {
+            program = std::make_unique<Program>(std::move(*this));
+        }
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
         gen_ = std::make_unique<ASTPrinter>(program.get());
         return *gen_;
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
index 4ee82b2..98c580e 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program.cc
@@ -63,6 +63,7 @@
 #include "src/tint/lang/core/type/sampler.h"
 #include "src/tint/lang/core/type/texture.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/lang/wgsl/writer/ir_to_program/rename_conflicts.h"
 #include "src/tint/utils/containers/hashmap.h"
 #include "src/tint/utils/containers/predicates.h"
@@ -96,7 +97,7 @@
         if (auto res = ir::Validate(mod); !res) {
             // IR module failed validation.
             b.Diagnostics() = res.Failure();
-            return Program{std::move(b)};
+            return Program{resolver::Resolve(b)};
         }
 
         RenameConflicts{}.Run(&mod);
@@ -108,7 +109,7 @@
         for (auto* fn : mod.functions) {
             Fn(fn);
         }
-        return Program{std::move(b)};
+        return Program{resolver::Resolve(b)};
     }
 
   private: