[tint] Pass Program by reference, not pointer.

Also use references in fields, where possible.

Bug: tint:1698
Change-Id: I61e42df007ab233cef841fef2d646653ed190893
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/152545
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
diff --git a/src/dawn/native/ShaderModule.cpp b/src/dawn/native/ShaderModule.cpp
index 3e55606..dae62c0 100644
--- a/src/dawn/native/ShaderModule.cpp
+++ b/src/dawn/native/ShaderModule.cpp
@@ -867,7 +867,7 @@
                                   WGSLExtensionSet* enabledWGSLExtensions) {
     DAWN_ASSERT(program->IsValid());
 
-    tint::inspector::Inspector inspector(program);
+    tint::inspector::Inspector inspector(*program);
 
     DAWN_ASSERT(enabledWGSLExtensions->empty());
     auto usedExtensionNames = inspector.GetUsedExtensionNames();
@@ -897,7 +897,7 @@
     const tint::Program& program,
     const char* entryPointName,
     const LimitsForCompilationRequest& limits) {
-    tint::inspector::Inspector inspector(&program);
+    tint::inspector::Inspector inspector(program);
     // At this point the entry point must exist and must have workgroup size values.
     tint::inspector::EntryPoint entryPoint = inspector.GetEntryPoint(entryPointName);
     DAWN_ASSERT(entryPoint.workgroup_size.has_value());
@@ -1050,8 +1050,9 @@
                                            const tint::ast::transform::DataMap& inputs,
                                            tint::ast::transform::DataMap* outputs,
                                            OwnedCompilationMessages* outMessages) {
+    DAWN_ASSERT(program != nullptr);
     tint::ast::transform::DataMap transform_outputs;
-    tint::Program result = transformManager->Run(program, inputs, transform_outputs);
+    tint::Program result = transformManager->Run(*program, inputs, transform_outputs);
     if (outMessages != nullptr) {
         DAWN_TRY(outMessages->AddMessages(result.Diagnostics()));
     }
diff --git a/src/dawn/native/StreamImplTint.cpp b/src/dawn/native/StreamImplTint.cpp
index 042fc18..1b7db58 100644
--- a/src/dawn/native/StreamImplTint.cpp
+++ b/src/dawn/native/StreamImplTint.cpp
@@ -33,7 +33,7 @@
 void stream::Stream<tint::Program>::Write(stream::Sink* sink, const tint::Program& p) {
 #if TINT_BUILD_WGSL_WRITER
     tint::wgsl::writer::Options options{};
-    StreamIn(sink, tint::wgsl::writer::Generate(&p, options)->wgsl);
+    StreamIn(sink, tint::wgsl::writer::Generate(p, options)->wgsl);
 #else
     // TODO(crbug.com/dawn/1481): We shouldn't need to write back to WGSL if we have a CacheKey
     // built from the initial shader module input. Then, we would never need to parse the program
diff --git a/src/dawn/native/d3d/ShaderUtils.cpp b/src/dawn/native/d3d/ShaderUtils.cpp
index 785c966..6ec2f4a 100644
--- a/src/dawn/native/d3d/ShaderUtils.cpp
+++ b/src/dawn/native/d3d/ShaderUtils.cpp
@@ -253,7 +253,7 @@
         std::move(r.bindingPointsIgnoredInRobustnessTransform);
 
     TRACE_EVENT0(tracePlatform.UnsafeGetValue(), General, "tint::hlsl::writer::Generate");
-    auto result = tint::hlsl::writer::Generate(&transformedProgram, options);
+    auto result = tint::hlsl::writer::Generate(transformedProgram, options);
     DAWN_INVALID_IF(!result, "An error occured while generating HLSL: %s", result.Failure());
 
     compiledShader->usesVertexIndex = usesVertexIndex;
diff --git a/src/dawn/native/metal/ShaderModuleMTL.mm b/src/dawn/native/metal/ShaderModuleMTL.mm
index 5396e2b..5f1bd48 100644
--- a/src/dawn/native/metal/ShaderModuleMTL.mm
+++ b/src/dawn/native/metal/ShaderModuleMTL.mm
@@ -280,7 +280,7 @@
             options.external_texture_options = r.externalTextureOptions;
 
             TRACE_EVENT0(r.platform.UnsafeGetValue(), General, "tint::msl::writer::Generate");
-            auto result = tint::msl::writer::Generate(&program, options);
+            auto result = tint::msl::writer::Generate(program, options);
             DAWN_INVALID_IF(!result, "An error occured while generating MSL: %s.",
                             result.Failure());
 
diff --git a/src/dawn/native/null/DeviceNull.cpp b/src/dawn/native/null/DeviceNull.cpp
index 9a1a231..25891c2 100644
--- a/src/dawn/native/null/DeviceNull.cpp
+++ b/src/dawn/native/null/DeviceNull.cpp
@@ -404,7 +404,6 @@
     const ProgrammableStage& computeStage = GetStage(SingleShaderStage::Compute);
 
     tint::Program transformedProgram;
-    const tint::Program* program;
     tint::ast::transform::Manager transformManager;
     tint::ast::transform::DataMap transformInputs;
 
@@ -424,13 +423,11 @@
                     RunTransforms(&transformManager, computeStage.module->GetTintProgram(),
                                   transformInputs, nullptr, nullptr));
 
-    program = &transformedProgram;
-
     // Do the workgroup size validation as it is actually backend agnostic.
     const CombinedLimits& limits = GetDevice()->GetLimits();
     Extent3D _;
     DAWN_TRY_ASSIGN(
-        _, ValidateComputeStageWorkgroupSize(*program, computeStage.entryPoint.c_str(),
+        _, ValidateComputeStageWorkgroupSize(transformedProgram, computeStage.entryPoint.c_str(),
                                              LimitsForCompilationRequest::Create(limits.v1)));
 
     return {};
diff --git a/src/dawn/native/opengl/ShaderModuleGL.cpp b/src/dawn/native/opengl/ShaderModuleGL.cpp
index 9ccb3fc..7fe1da0 100644
--- a/src/dawn/native/opengl/ShaderModuleGL.cpp
+++ b/src/dawn/native/opengl/ShaderModuleGL.cpp
@@ -265,7 +265,7 @@
             BindingPoint placeholderBindingPoint{static_cast<uint32_t>(kMaxBindGroupsTyped), 0};
 
             bool needsPlaceholderSampler = false;
-            tint::inspector::Inspector inspector(&program);
+            tint::inspector::Inspector inspector(program);
             // Find all the sampler/texture pairs for this entry point, and create
             // CombinedSamplers for them. CombinedSampler records the binding points
             // of the original texture and sampler, and generates a unique name. The
@@ -298,7 +298,7 @@
             tintOptions.allow_collisions = true;
             tintOptions.texture_builtins_from_uniform = r.textureBuiltinsFromUniform;
 
-            auto result = tint::glsl::writer::Generate(&program, tintOptions, r.entryPointName);
+            auto result = tint::glsl::writer::Generate(program, tintOptions, r.entryPointName);
             DAWN_INVALID_IF(!result, "An error occured while generating GLSL: %s.",
                             result.Failure());
 
diff --git a/src/dawn/native/vulkan/ShaderModuleVk.cpp b/src/dawn/native/vulkan/ShaderModuleVk.cpp
index 693baf7..533cfe7 100644
--- a/src/dawn/native/vulkan/ShaderModuleVk.cpp
+++ b/src/dawn/native/vulkan/ShaderModuleVk.cpp
@@ -358,7 +358,7 @@
             options.use_tint_ir = r.useTintIR;
 
             TRACE_EVENT0(r.platform.UnsafeGetValue(), General, "tint::spirv::writer::Generate()");
-            auto tintResult = tint::spirv::writer::Generate(&program, options);
+            auto tintResult = tint::spirv::writer::Generate(program, options);
             DAWN_INVALID_IF(!tintResult, "An error occured while generating SPIR-V: %s.",
                             tintResult.Failure());
 
diff --git a/src/tint/api/tint.cc b/src/tint/api/tint.cc
index e6a8d2a..3cb2ebb 100644
--- a/src/tint/api/tint.cc
+++ b/src/tint/api/tint.cc
@@ -55,7 +55,7 @@
 void Initialize() {
 #if TINT_BUILD_WGSL_WRITER
     // Register the Program printer. This is used for debugging purposes.
-    tint::Program::printer = [](const tint::Program* program) {
+    tint::Program::printer = [](const tint::Program& program) {
         auto result = wgsl::writer::Generate(program, {});
         if (!result) {
             return "error: " + result.Failure();
diff --git a/src/tint/cmd/bench/main_bench.cc b/src/tint/cmd/bench/main_bench.cc
index eaf8546..3ee05d3 100644
--- a/src/tint/cmd/bench/main_bench.cc
+++ b/src/tint/cmd/bench/main_bench.cc
@@ -107,7 +107,7 @@
             if (!program.IsValid()) {
                 return Error{program.Diagnostics().str()};
             }
-            auto result = tint::wgsl::writer::Generate(&program, {});
+            auto result = tint::wgsl::writer::Generate(program, {});
             if (!result) {
                 return Error{result.Failure()};
             }
diff --git a/src/tint/cmd/common/generate_external_texture_bindings.cc b/src/tint/cmd/common/generate_external_texture_bindings.cc
index 0253b71..afa4e5a 100644
--- a/src/tint/cmd/common/generate_external_texture_bindings.cc
+++ b/src/tint/cmd/common/generate_external_texture_bindings.cc
@@ -26,15 +26,15 @@
 
 namespace tint::cmd {
 
-ExternalTextureOptions::BindingsMap GenerateExternalTextureBindings(const Program* program) {
+ExternalTextureOptions::BindingsMap GenerateExternalTextureBindings(const Program& program) {
     // TODO(tint:1491): Use Inspector once we can get binding info for all
     // variables, not just those referenced by entry points.
 
     // Collect next valid binding number per group
     std::unordered_map<uint32_t, uint32_t> group_to_next_binding_number;
     std::vector<tint::BindingPoint> ext_tex_bps;
-    for (auto* var : program->AST().GlobalVariables()) {
-        if (auto* sem_var = program->Sem().Get(var)->As<sem::GlobalVariable>()) {
+    for (auto* var : program.AST().GlobalVariables()) {
+        if (auto* sem_var = program.Sem().Get(var)->As<sem::GlobalVariable>()) {
             if (auto bp = sem_var->BindingPoint()) {
                 auto& n = group_to_next_binding_number[bp->group];
                 n = std::max(n, bp->binding + 1);
diff --git a/src/tint/cmd/common/generate_external_texture_bindings.h b/src/tint/cmd/common/generate_external_texture_bindings.h
index 7c3d79c..edfb68b 100644
--- a/src/tint/cmd/common/generate_external_texture_bindings.h
+++ b/src/tint/cmd/common/generate_external_texture_bindings.h
@@ -24,7 +24,7 @@
 
 namespace tint::cmd {
 
-ExternalTextureOptions::BindingsMap GenerateExternalTextureBindings(const Program* program);
+ExternalTextureOptions::BindingsMap GenerateExternalTextureBindings(const Program& program);
 
 }  // namespace tint::cmd
 
diff --git a/src/tint/cmd/common/generate_external_texture_bindings_test.cc b/src/tint/cmd/common/generate_external_texture_bindings_test.cc
index c404294..89a45c6 100644
--- a/src/tint/cmd/common/generate_external_texture_bindings_test.cc
+++ b/src/tint/cmd/common/generate_external_texture_bindings_test.cc
@@ -34,7 +34,7 @@
 
     tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
-    auto bindings = GenerateExternalTextureBindings(&program);
+    auto bindings = GenerateExternalTextureBindings(program);
     ASSERT_TRUE(bindings.empty());
 }
 
@@ -44,7 +44,7 @@
 
     tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
-    auto bindings = GenerateExternalTextureBindings(&program);
+    auto bindings = GenerateExternalTextureBindings(program);
     ASSERT_EQ(bindings.size(), 1u);
 
     auto to = bindings[BindingPoint{0, 0}];
@@ -61,7 +61,7 @@
 
     tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
-    auto bindings = GenerateExternalTextureBindings(&program);
+    auto bindings = GenerateExternalTextureBindings(program);
     ASSERT_EQ(bindings.size(), 2u);
 
     auto to0 = bindings[BindingPoint{0, 0}];
@@ -84,7 +84,7 @@
 
     tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid());
-    auto bindings = GenerateExternalTextureBindings(&program);
+    auto bindings = GenerateExternalTextureBindings(program);
     ASSERT_EQ(bindings.size(), 2u);
 
     auto to0 = bindings[BindingPoint{0, 0}];
@@ -110,7 +110,7 @@
 
     tint::Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
-    auto bindings = GenerateExternalTextureBindings(&program);
+    auto bindings = GenerateExternalTextureBindings(program);
     ASSERT_EQ(bindings.size(), 2u);
 
     auto to0 = bindings[BindingPoint{0, 1}];
diff --git a/src/tint/cmd/common/helper.cc b/src/tint/cmd/common/helper.cc
index 77b18f9..984628f 100644
--- a/src/tint/cmd/common/helper.cc
+++ b/src/tint/cmd/common/helper.cc
@@ -34,6 +34,7 @@
 #include "src/tint/utils/diagnostic/formatter.h"
 #include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/text/string.h"
+#include "src/tint/utils/traits/traits.h"
 
 namespace tint::cmd {
 namespace {
@@ -45,6 +46,24 @@
     kSpirvAsm,
 };
 
+/// @param out the stream to write to
+/// @param value the InputFormat
+/// @returns @p out so calls can be chained
+template <typename STREAM, typename = traits::EnableIfIsOStream<STREAM>>
+auto& operator<<(STREAM& out, InputFormat value) {
+    switch (value) {
+        case InputFormat::kUnknown:
+            break;
+        case InputFormat::kWgsl:
+            return out << "wgsl";
+        case InputFormat::kSpirvBin:
+            return out << "spirv";
+        case InputFormat::kSpirvAsm:
+            return out << "spirv asm";
+    }
+    return out << "unknown";
+}
+
 InputFormat InputFormatFromFilename(const std::string& filename) {
     auto input_format = InputFormat::kUnknown;
     if (tint::HasSuffix(filename, ".wgsl")) {
@@ -98,7 +117,7 @@
 void PrintWGSL(std::ostream& out, const tint::Program& program) {
 #if TINT_BUILD_WGSL_WRITER
     tint::wgsl::writer::Options options;
-    auto result = tint::wgsl::writer::Generate(&program, options);
+    auto result = tint::wgsl::writer::Generate(program, options);
     if (result) {
         out << std::endl << result->wgsl << std::endl;
     } else {
@@ -111,95 +130,104 @@
 }
 
 ProgramInfo LoadProgramInfo(const LoadProgramOptions& opts) {
-    std::unique_ptr<tint::Program> program;
-    std::unique_ptr<tint::Source::File> source_file;
-
     auto input_format = InputFormatFromFilename(opts.filename);
-    switch (input_format) {
-        case InputFormat::kUnknown: {
-            std::cerr << "Unknown input format" << std::endl;
-            exit(1);
-        }
-        case InputFormat::kWgsl: {
-#if TINT_BUILD_WGSL_READER
-            std::vector<uint8_t> data;
-            if (!ReadFile<uint8_t>(opts.filename, &data)) {
-                exit(1);
-            }
-            source_file = std::make_unique<tint::Source::File>(
-                opts.filename, std::string(data.begin(), data.end()));
-            program = std::make_unique<tint::Program>(tint::wgsl::reader::Parse(source_file.get()));
-            break;
-#else
-            std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
-            exit(1);
-#endif  // TINT_BUILD_WGSL_READER
-        }
-        case InputFormat::kSpirvBin: {
-#if TINT_BUILD_SPV_READER
-            std::vector<uint32_t> data;
-            if (!ReadFile<uint32_t>(opts.filename, &data)) {
-                exit(1);
-            }
-            program = std::make_unique<tint::Program>(
-                tint::spirv::reader::Read(data, opts.spirv_reader_options));
-            break;
-#else
-            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
-            exit(1);
-#endif  // TINT_BUILD_SPV_READER
-        }
-        case InputFormat::kSpirvAsm: {
-#if TINT_BUILD_SPV_READER
-            std::vector<char> text;
-            if (!ReadFile<char>(opts.filename, &text)) {
-                exit(1);
-            }
-            // Use Vulkan 1.1, since this is what Tint, internally, is expecting.
-            spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
-            tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& pos,
-                                        const char* msg) {
-                std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl;
-            });
-            std::vector<uint32_t> data;
-            if (!tools.Assemble(text.data(), text.size(), &data,
-                                SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
-                exit(1);
-            }
-            program = std::make_unique<tint::Program>(
-                tint::spirv::reader::Read(data, opts.spirv_reader_options));
-            break;
-#else
-            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
-            exit(1);
-#endif  // TINT_BUILD_SPV_READER
-        }
-    }
 
-    if (!program) {
-        std::cerr << "Failed to parse input file: " << opts.filename << std::endl;
+    auto load = [&]() -> ProgramInfo {
+        switch (input_format) {
+            case InputFormat::kUnknown:
+                break;
+
+            case InputFormat::kWgsl: {
+#if TINT_BUILD_WGSL_READER
+                std::vector<uint8_t> data;
+                if (!ReadFile<uint8_t>(opts.filename, &data)) {
+                    exit(1);
+                }
+
+                auto file = std::make_unique<tint::Source::File>(
+                    opts.filename, std::string(data.begin(), data.end()));
+
+                return ProgramInfo{
+                    /* program */ tint::wgsl::reader::Parse(file.get()),
+                    /* source_file */ std::move(file),
+                };
+#else
+                std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
+                exit(1);
+#endif  // TINT_BUILD_WGSL_READER
+            }
+            case InputFormat::kSpirvBin: {
+#if TINT_BUILD_SPV_READER
+                std::vector<uint32_t> data;
+                if (!ReadFile<uint32_t>(opts.filename, &data)) {
+                    exit(1);
+                }
+
+                return ProgramInfo{
+                    /* program */ tint::spirv::reader::Read(data, opts.spirv_reader_options),
+                    /* source_file */ nullptr,
+                };
+#else
+                std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+                exit(1);
+#endif  // TINT_BUILD_SPV_READER
+            }
+            case InputFormat::kSpirvAsm: {
+#if TINT_BUILD_SPV_READER
+                std::vector<char> text;
+                if (!ReadFile<char>(opts.filename, &text)) {
+                    exit(1);
+                }
+                // Use Vulkan 1.1, since this is what Tint, internally, is expecting.
+                spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
+                tools.SetMessageConsumer([](spv_message_level_t, const char*,
+                                            const spv_position_t& pos, const char* msg) {
+                    std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg
+                              << std::endl;
+                });
+                std::vector<uint32_t> data;
+                if (!tools.Assemble(text.data(), text.size(), &data,
+                                    SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
+                    exit(1);
+                }
+
+                auto file = std::make_unique<tint::Source::File>(
+                    opts.filename, std::string(text.begin(), text.end()));
+
+                return ProgramInfo{
+                    /* program */ tint::spirv::reader::Read(data, opts.spirv_reader_options),
+                    /* source_file */ std::move(file),
+                };
+#else
+                std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+                exit(1);
+#endif  // TINT_BUILD_SPV_READER
+            }
+        }
+
+        std::cerr << "Unknown input format: " << input_format << std::endl;
         exit(1);
-    }
-    if (program->Diagnostics().count() > 0) {
-        if (!program->IsValid() && input_format != InputFormat::kWgsl) {
-            // Invalid program from a non-wgsl source. Print the WGSL, to help
-            // understand the diagnostics.
-            PrintWGSL(std::cout, *program);
+    };
+
+    ProgramInfo info = load();
+
+    if (info.program.Diagnostics().count() > 0) {
+        if (!info.program.IsValid() && input_format != InputFormat::kWgsl) {
+            // Invalid program from a non-wgsl source.
+            // Print the WGSL, to help understand the diagnostics.
+            PrintWGSL(std::cout, info.program);
         }
 
         auto diag_printer = tint::diag::Printer::create(stderr, true);
         tint::diag::Formatter diag_formatter;
-        diag_formatter.format(program->Diagnostics(), diag_printer.get());
+        diag_formatter.format(info.program.Diagnostics(), diag_printer.get());
     }
 
-    if (!program->IsValid()) {
+    if (!info.program.IsValid()) {
         exit(1);
     }
 
-    return ProgramInfo{
-        std::move(program),
-        std::move(source_file),
-    };
+    return info;
 }
 
 void PrintInspectorData(tint::inspector::Inspector& inspector) {
diff --git a/src/tint/cmd/common/helper.h b/src/tint/cmd/common/helper.h
index 9b0ed81..e7de224 100644
--- a/src/tint/cmd/common/helper.h
+++ b/src/tint/cmd/common/helper.h
@@ -38,7 +38,7 @@
 /// Information on a loaded program
 struct ProgramInfo {
     /// The loaded program
-    std::unique_ptr<tint::Program> program;
+    tint::Program program;
     /// The source file information
     std::unique_ptr<tint::Source::File> source_file;
 };
@@ -70,7 +70,9 @@
 #endif
 };
 
-/// Loads the source and program information for the given file
+/// Loads the source and program information for the given file.
+/// If the file cannot be loaded then an error is printed and the program is aborted before
+/// returning.
 /// @param opts the loading options
 ProgramInfo LoadProgramInfo(const LoadProgramOptions& opts);
 
diff --git a/src/tint/cmd/info/main.cc b/src/tint/cmd/info/main.cc
index f0d64db..d3fb1fe 100644
--- a/src/tint/cmd/info/main.cc
+++ b/src/tint/cmd/info/main.cc
@@ -71,7 +71,7 @@
     return true;
 }
 
-void EmitJson(const tint::Program* program) {
+void EmitJson(const tint::Program& program) {
     tint::inspector::Inspector inspector(program);
 
     std::cout << "{" << std::endl;
@@ -223,7 +223,7 @@
     std::cout << "\"structures\": [";
 
     bool struct_first = true;
-    for (const auto* ty : program->Types()) {
+    for (const auto* ty : program.Types()) {
         if (!ty->Is<tint::core::type::Struct>()) {
             continue;
         }
@@ -277,7 +277,7 @@
     std::cout << "}" << std::endl;
 }
 
-void EmitText(const tint::Program* program) {
+void EmitText(const tint::Program& program) {
     tint::inspector::Inspector inspector(program);
     if (!inspector.GetUsedExtensionNames().empty()) {
         std::cout << "Extensions:" << std::endl;
@@ -290,7 +290,7 @@
     tint::cmd::PrintInspectorData(inspector);
 
     bool has_struct = false;
-    for (const auto* ty : program->Types()) {
+    for (const auto* ty : program.Types()) {
         if (!ty->Is<tint::core::type::Struct>()) {
             continue;
         }
@@ -300,7 +300,7 @@
 
     if (has_struct) {
         std::cout << "Structures" << std::endl;
-        for (const auto* ty : program->Types()) {
+        for (const auto* ty : program.Types()) {
             if (!ty->Is<tint::core::type::Struct>()) {
                 continue;
             }
@@ -328,25 +328,18 @@
         return 0;
     }
 
-    std::unique_ptr<tint::Program> program;
-    std::unique_ptr<tint::Source::File> source_file;
-
-    {
-        tint::cmd::LoadProgramOptions opts;
-        opts.filename = options.input_filename;
+    tint::cmd::LoadProgramOptions opts;
+    opts.filename = options.input_filename;
 #if TINT_BUILD_SPV_READER
-        opts.spirv_reader_options = options.spirv_reader_options;
+    opts.spirv_reader_options = options.spirv_reader_options;
 #endif
 
-        auto info = tint::cmd::LoadProgramInfo(opts);
-        program = std::move(info.program);
-        source_file = std::move(info.source_file);
-    }
+    auto info = tint::cmd::LoadProgramInfo(opts);
 
     if (options.emit_json) {
-        EmitJson(program.get());
+        EmitJson(info.program);
     } else {
-        EmitText(program.get());
+        EmitText(info.program);
     }
 
     return 0;
diff --git a/src/tint/cmd/loopy/main.cc b/src/tint/cmd/loopy/main.cc
index 8949dc0..e488fad 100644
--- a/src/tint/cmd/loopy/main.cc
+++ b/src/tint/cmd/loopy/main.cc
@@ -189,14 +189,14 @@
 /// Generate SPIR-V code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateSpirv(const tint::Program* program) {
+bool GenerateSpirv(const tint::Program& program) {
 #if TINT_BUILD_SPV_WRITER
     tint::spirv::writer::Options gen_options;
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(program);
     auto result = tint::spirv::writer::Generate(program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -211,7 +211,7 @@
 /// Generate WGSL code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateWgsl(const tint::Program* program) {
+bool GenerateWgsl(const tint::Program& program) {
 #if TINT_BUILD_WGSL_WRITER
     tint::wgsl::writer::Options gen_options;
     auto result = tint::wgsl::writer::Generate(program, gen_options);
@@ -231,11 +231,11 @@
 /// Generate MSL code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateMsl(const tint::Program* program) {
+bool GenerateMsl(const tint::Program& program) {
 #if TINT_BUILD_MSL_WRITER
     // Remap resource numbers to a flat namespace.
     // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
-    const tint::Program* input_program = program;
+    const tint::Program* input_program = &program;
     auto flattened = tint::writer::FlattenBindings(program);
     if (flattened) {
         input_program = &*flattened;
@@ -243,15 +243,15 @@
 
     tint::msl::writer::Options gen_options;
     gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(input_program);
+        tint::cmd::GenerateExternalTextureBindings(*input_program);
     gen_options.array_length_from_uniform.ubo_binding = tint::BindingPoint{0, 30};
     gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(tint::BindingPoint{0, 0},
                                                                           0);
     gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(tint::BindingPoint{0, 1},
                                                                           1);
-    auto result = tint::msl::writer::Generate(input_program, gen_options);
+    auto result = tint::msl::writer::Generate(*input_program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -267,14 +267,14 @@
 /// Generate HLSL code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateHlsl(const tint::Program* program) {
+bool GenerateHlsl(const tint::Program& program) {
 #if TINT_BUILD_HLSL_WRITER
     tint::hlsl::writer::Options gen_options;
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(program);
     auto result = tint::hlsl::writer::Generate(program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -290,14 +290,14 @@
 /// Generate GLSL code for a program.
 /// @param program the program to generate
 /// @returns true on success
-bool GenerateGlsl(const tint::Program* program) {
+bool GenerateGlsl(const tint::Program& program) {
 #if TINT_BUILD_GLSL_WRITER
     tint::glsl::writer::Options gen_options;
     gen_options.external_texture_options.bindings_map =
         tint::cmd::GenerateExternalTextureBindings(program);
     auto result = tint::glsl::writer::Generate(program, gen_options, "");
     if (result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -377,21 +377,17 @@
     }
 
     // Load the program that will actually be used
-    {
-        tint::cmd::LoadProgramOptions opts;
-        opts.filename = options.input_filename;
+    tint::cmd::LoadProgramOptions opts;
+    opts.filename = options.input_filename;
 
-        auto info = tint::cmd::LoadProgramInfo(opts);
-        program = std::move(info.program);
-        source_file = std::move(info.source_file);
-    }
+    auto info = tint::cmd::LoadProgramInfo(opts);
     {
         uint32_t loop_count = 1;
         if (options.loop == Looper::kIRGenerate) {
             loop_count = options.loop_count;
         }
         for (uint32_t i = 0; i < loop_count; ++i) {
-            auto result = tint::wgsl::reader::ProgramToIR(program.get());
+            auto result = tint::wgsl::reader::ProgramToIR(info.program);
             if (!result) {
                 std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
             }
@@ -408,27 +404,27 @@
         switch (options.format) {
             case Format::kSpirv:
                 for (uint32_t i = 0; i < loop_count; ++i) {
-                    success = GenerateSpirv(program.get());
+                    success = GenerateSpirv(info.program);
                 }
                 break;
             case Format::kWgsl:
                 for (uint32_t i = 0; i < loop_count; ++i) {
-                    success = GenerateWgsl(program.get());
+                    success = GenerateWgsl(info.program);
                 }
                 break;
             case Format::kMsl:
                 for (uint32_t i = 0; i < loop_count; ++i) {
-                    success = GenerateMsl(program.get());
+                    success = GenerateMsl(info.program);
                 }
                 break;
             case Format::kHlsl:
                 for (uint32_t i = 0; i < loop_count; ++i) {
-                    success = GenerateHlsl(program.get());
+                    success = GenerateHlsl(info.program);
                 }
                 break;
             case Format::kGlsl:
                 for (uint32_t i = 0; i < loop_count; ++i) {
-                    success = GenerateGlsl(program.get());
+                    success = GenerateGlsl(info.program);
                 }
                 break;
             case Format::kNone:
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 31d7286..4780e81 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -573,7 +573,7 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateSpirv(const tint::Program* program, const Options& options) {
+bool GenerateSpirv(const tint::Program& program, const Options& options) {
 #if TINT_BUILD_SPV_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::spirv::writer::Options gen_options;
@@ -585,7 +585,7 @@
 
     auto result = tint::spirv::writer::Generate(program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -631,7 +631,7 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateWgsl(const tint::Program* program, const Options& options) {
+bool GenerateWgsl(const tint::Program& program, const Options& options) {
 #if TINT_BUILD_WGSL_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::wgsl::writer::Options gen_options;
@@ -675,11 +675,11 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateMsl(const tint::Program* program, const Options& options) {
+bool GenerateMsl(const tint::Program& program, const Options& options) {
 #if TINT_BUILD_MSL_WRITER
     // Remap resource numbers to a flat namespace.
     // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
-    const tint::Program* input_program = program;
+    const tint::Program* input_program = &program;
     auto flattened = tint::writer::FlattenBindings(program);
     if (flattened) {
         input_program = &*flattened;
@@ -692,15 +692,15 @@
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.pixel_local_options = options.pixel_local_options;
     gen_options.external_texture_options.bindings_map =
-        tint::cmd::GenerateExternalTextureBindings(input_program);
+        tint::cmd::GenerateExternalTextureBindings(*input_program);
     gen_options.array_length_from_uniform.ubo_binding = tint::BindingPoint{0, 30};
     gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(tint::BindingPoint{0, 0},
                                                                           0);
     gen_options.array_length_from_uniform.bindpoint_to_size_index.emplace(tint::BindingPoint{0, 1},
                                                                           1);
-    auto result = tint::msl::writer::Generate(input_program, gen_options);
+    auto result = tint::msl::writer::Generate(*input_program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -717,7 +717,7 @@
     // Default to validating against MSL 1.2.
     // If subgroups are used, bump the version to 2.1.
     auto msl_version = tint::msl::validate::MslVersion::kMsl_1_2;
-    for (auto* enable : program->AST().Enables()) {
+    for (auto* enable : program.AST().Enables()) {
         if (enable->HasExtension(tint::wgsl::Extension::kChromiumExperimentalSubgroups)) {
             msl_version = std::max(msl_version, tint::msl::validate::MslVersion::kMsl_2_1);
         }
@@ -764,7 +764,7 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateHlsl(const tint::Program* program, const Options& options) {
+bool GenerateHlsl(const tint::Program& program, const Options& options) {
 #if TINT_BUILD_HLSL_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::hlsl::writer::Options gen_options;
@@ -775,7 +775,7 @@
     gen_options.root_constant_binding_point = options.hlsl_root_constant_binding_point;
     auto result = tint::hlsl::writer::Generate(program, gen_options);
     if (!result) {
-        tint::cmd::PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, program);
         std::cerr << "Failed to generate: " << result.Failure() << std::endl;
         return false;
     }
@@ -803,7 +803,7 @@
             if (dxc.Found()) {
                 dxc_found = true;
 
-                auto enable_list = program->AST().Enables();
+                auto enable_list = program.AST().Enables();
                 bool dxc_require_16bit_types = false;
                 for (auto* enable : enable_list) {
                     if (enable->HasExtension(tint::wgsl::Extension::kF16)) {
@@ -903,13 +903,13 @@
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
 /// @returns true on success
-bool GenerateGlsl(const tint::Program* program, const Options& options) {
+bool GenerateGlsl(const tint::Program& program, const Options& options) {
 #if TINT_BUILD_GLSL_WRITER
     if (options.validate) {
         glslang::InitializeProcess();
     }
 
-    auto generate = [&](const tint::Program* prg, const std::string entry_point_name) -> bool {
+    auto generate = [&](const tint::Program& prg, const std::string entry_point_name) -> bool {
         tint::glsl::writer::Options gen_options;
         gen_options.disable_robustness = !options.enable_robustness;
         gen_options.external_texture_options.bindings_map =
@@ -920,7 +920,7 @@
         gen_options.texture_builtins_from_uniform = std::move(textureBuiltinsFromUniform);
         auto result = tint::glsl::writer::Generate(prg, gen_options, entry_point_name);
         if (!result) {
-            tint::cmd::PrintWGSL(std::cerr, *prg);
+            tint::cmd::PrintWGSL(std::cerr, prg);
             std::cerr << "Failed to generate: " << result.Failure() << std::endl;
             return false;
         }
@@ -1083,20 +1083,13 @@
         options.format = Format::kSpvAsm;
     }
 
-    std::unique_ptr<tint::Program> program;
-    std::unique_ptr<tint::Source::File> source_file;
-
-    {
-        tint::cmd::LoadProgramOptions opts;
-        opts.filename = options.input_filename;
+    tint::cmd::LoadProgramOptions opts;
+    opts.filename = options.input_filename;
 #if TINT_BUILD_SPV_READER
-        opts.spirv_reader_options = options.spirv_reader_options;
+    opts.spirv_reader_options = options.spirv_reader_options;
 #endif
 
-        auto info = tint::cmd::LoadProgramInfo(opts);
-        program = std::move(info.program);
-        source_file = std::move(info.source_file);
-    }
+    auto info = tint::cmd::LoadProgramInfo(opts);
 
     if (options.parse_only) {
         return 1;
@@ -1106,7 +1099,7 @@
     if (options.dump_ast) {
         tint::wgsl::writer::Options gen_options;
         gen_options.use_syntax_tree_writer = true;
-        auto result = tint::wgsl::writer::Generate(program.get(), gen_options);
+        auto result = tint::wgsl::writer::Generate(info.program, gen_options);
         if (!result) {
             std::cerr << "Failed to dump AST: " << result.Failure() << std::endl;
         } else {
@@ -1117,7 +1110,7 @@
 
 #if TINT_BUILD_WGSL_READER
     if (options.dump_ir) {
-        auto result = tint::wgsl::reader::ProgramToIR(program.get());
+        auto result = tint::wgsl::reader::ProgramToIR(info.program);
         if (!result) {
             std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
         } else {
@@ -1130,7 +1123,7 @@
     }
 #endif  // TINT_BUILD_WGSL_READER
 
-    tint::inspector::Inspector inspector(program.get());
+    tint::inspector::Inspector inspector(info.program);
     if (options.dump_inspector_bindings) {
         tint::cmd::PrintInspectorBindings(inspector);
     }
@@ -1212,32 +1205,30 @@
     }
 
     tint::ast::transform::DataMap outputs;
-    auto out = transform_manager.Run(program.get(), std::move(transform_inputs), outputs);
-    if (!out.IsValid()) {
-        tint::cmd::PrintWGSL(std::cerr, out);
-        std::cerr << out.Diagnostics().str() << std::endl;
+    auto program = transform_manager.Run(info.program, std::move(transform_inputs), outputs);
+    if (!program.IsValid()) {
+        tint::cmd::PrintWGSL(std::cerr, program);
+        std::cerr << program.Diagnostics().str() << std::endl;
         return 1;
     }
 
-    *program = std::move(out);
-
     bool success = false;
     switch (options.format) {
         case Format::kSpirv:
         case Format::kSpvAsm:
-            success = GenerateSpirv(program.get(), options);
+            success = GenerateSpirv(program, options);
             break;
         case Format::kWgsl:
-            success = GenerateWgsl(program.get(), options);
+            success = GenerateWgsl(program, options);
             break;
         case Format::kMsl:
-            success = GenerateMsl(program.get(), options);
+            success = GenerateMsl(program, options);
             break;
         case Format::kHlsl:
-            success = GenerateHlsl(program.get(), options);
+            success = GenerateHlsl(program, options);
             break;
         case Format::kGlsl:
-            success = GenerateGlsl(program.get(), options);
+            success = GenerateGlsl(program, options);
             break;
         case Format::kNone:
             break;
diff --git a/src/tint/fuzzers/apply_substitute_overrides.cc b/src/tint/fuzzers/apply_substitute_overrides.cc
index 16ea79c..e5ef24d 100644
--- a/src/tint/fuzzers/apply_substitute_overrides.cc
+++ b/src/tint/fuzzers/apply_substitute_overrides.cc
@@ -26,7 +26,7 @@
 
 Program ApplySubstituteOverrides(Program&& program) {
     ast::transform::SubstituteOverride::Config cfg;
-    inspector::Inspector inspector(&program);
+    inspector::Inspector inspector(program);
     auto default_values = inspector.GetOverrideDefaultValues();
     for (const auto& [override_id, scalar] : default_values) {
         // If the override is not null, then it has a default value, we can just let it use the
@@ -47,7 +47,7 @@
     mgr.append(std::make_unique<ast::transform::SubstituteOverride>());
 
     ast::transform::DataMap outputs;
-    return mgr.Run(&program, override_data, outputs);
+    return mgr.Run(program, override_data, outputs);
 }
 
 }  // namespace tint::fuzzers
diff --git a/src/tint/fuzzers/shuffle_transform.cc b/src/tint/fuzzers/shuffle_transform.cc
index d4b5fbe..e484715 100644
--- a/src/tint/fuzzers/shuffle_transform.cc
+++ b/src/tint/fuzzers/shuffle_transform.cc
@@ -25,13 +25,13 @@
 
 ShuffleTransform::ShuffleTransform(size_t seed) : seed_(seed) {}
 
-ast::transform::Transform::ApplyResult ShuffleTransform::Apply(const Program* src,
+ast::transform::Transform::ApplyResult ShuffleTransform::Apply(const Program& src,
                                                                const ast::transform::DataMap&,
                                                                ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
-    auto decls = src->AST().GlobalDeclarations();
+    auto decls = src.AST().GlobalDeclarations();
     auto rng = std::mt19937_64{seed_};
     std::shuffle(std::begin(decls), std::end(decls), rng);
     for (auto* decl : decls) {
diff --git a/src/tint/fuzzers/shuffle_transform.h b/src/tint/fuzzers/shuffle_transform.h
index 213fddb..5146016 100644
--- a/src/tint/fuzzers/shuffle_transform.h
+++ b/src/tint/fuzzers/shuffle_transform.h
@@ -27,7 +27,7 @@
     explicit ShuffleTransform(size_t seed);
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/fuzzers/tint_ast_clone_fuzzer.cc b/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
index cb571ed..805b762 100644
--- a/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
+++ b/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
@@ -67,7 +67,7 @@
     tint::Program dst(src.Clone());
 
     // Expect the printed strings to match
-    ASSERT_EQ(tint::Program::printer(&src), tint::Program::printer(&dst));
+    ASSERT_EQ(tint::Program::printer(src), tint::Program::printer(dst));
 
     // Check that none of the AST nodes or type pointers in dst are found in src
     std::unordered_set<const tint::ast::Node*> src_nodes;
@@ -91,7 +91,7 @@
     std::string src_wgsl;
     tint::wgsl::writer::Options wgsl_options;
     {
-        auto result = tint::wgsl::writer::Generate(&src, wgsl_options);
+        auto result = tint::wgsl::writer::Generate(src, wgsl_options);
         ASSERT_TRUE(result == true);
         src_wgsl = result->wgsl;
 
@@ -104,7 +104,7 @@
     }
 
     // Print the dst program, check it matches the original source
-    auto result = tint::wgsl::writer::Generate(&dst, wgsl_options);
+    auto result = tint::wgsl::writer::Generate(dst, wgsl_options);
     ASSERT_TRUE(result == true);
     auto dst_wgsl = result->wgsl;
     ASSERT_EQ(src_wgsl, dst_wgsl);
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc
index 269c3ef..da8290a 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc
@@ -65,7 +65,7 @@
         return 0;
     }
 
-    auto result = wgsl::writer::Generate(&program, wgsl::writer::Options());
+    auto result = wgsl::writer::Generate(program, wgsl::writer::Options());
     if (!result) {
         std::cout << "Can't generate WGSL for a valid tint::Program:" << std::endl
                   << result.Failure() << std::endl;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
index 00df1e0..4feadfd 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
@@ -138,11 +138,11 @@
 
     ASSERT_TRUE(MaybeApplyMutation(
         program, MutationChangeBinaryOperator(sum_expr_id, core::BinaryOp::kSubtract), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -214,12 +214,12 @@
                 ASSERT_FALSE(invalid_program.IsValid()) << program.Diagnostics().str();
             }
         } else {
-            ASSERT_TRUE(MaybeApplyMutation(program, mutation, node_id_map, &program, &node_id_map,
-                                           nullptr));
+            ASSERT_TRUE(
+                MaybeApplyMutation(program, mutation, node_id_map, program, &node_id_map, nullptr));
             ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
             wgsl::writer::Options options;
-            auto result = wgsl::writer::Generate(&program, options);
+            auto result = wgsl::writer::Generate(program, options);
             ASSERT_TRUE(result) << result.Failure();
 
             ASSERT_EQ(expected_shader.str(), result->wgsl);
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator_test.cc
index 3a6773c..7d9cff2 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator_test.cc
@@ -82,49 +82,49 @@
     // the operator of float types to any other.
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(neg_a_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(neg_a_id, core::UnaryOp::kNot),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(neg_a_id, core::UnaryOp::kNegation),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
 
     auto not_b_id = node_id_map.GetId(not_b_expr);
     // Only complement and negation is allowed for signed integer type.
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(not_b_id, core::UnaryOp::kNot),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
     // Cannot change to the same unary operator.
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(not_b_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
 
     auto not_c_id = node_id_map.GetId(not_c_expr);
     // Only complement is allowed for unsigned integer.Cannot change
     //  // the operator of float types to any other.
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(not_c_id, core::UnaryOp::kNot),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(not_c_id, core::UnaryOp::kNegation),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(not_c_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
 
     auto neg_d_id = node_id_map.GetId(neg_d_expr);
     // Only logical negation (not) is allowed for bool type.  Cannot change
     // the operator of float types to any other.
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(neg_d_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(neg_d_id, core::UnaryOp::kNegation),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
     ASSERT_FALSE(MaybeApplyMutation(program,
                                     MutationChangeUnaryOperator(neg_d_id, core::UnaryOp::kNot),
-                                    node_id_map, &program, &node_id_map, nullptr));
+                                    node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(ChangeUnaryOperatorTest, Signed_Integer_Types_Applicable1) {
@@ -154,11 +154,11 @@
     auto comp_a_id = node_id_map.GetId(comp_a_expr);
     ASSERT_TRUE(MaybeApplyMutation(program,
                                    MutationChangeUnaryOperator(comp_a_id, core::UnaryOp::kNegation),
-                                   node_id_map, &program, &node_id_map, nullptr));
+                                   node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -195,11 +195,11 @@
     auto comp_b_id = node_id_map.GetId(comp_b_expr);
     ASSERT_TRUE(MaybeApplyMutation(program,
                                    MutationChangeUnaryOperator(comp_b_id, core::UnaryOp::kNegation),
-                                   node_id_map, &program, &node_id_map, nullptr));
+                                   node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -238,11 +238,11 @@
     auto neg_a_id = node_id_map.GetId(neg_a_expr);
     ASSERT_TRUE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(neg_a_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -280,11 +280,11 @@
     auto neg_b_id = node_id_map.GetId(neg_b_expr);
     ASSERT_TRUE(MaybeApplyMutation(
         program, MutationChangeUnaryOperator(neg_b_id, core::UnaryOp::kComplement), node_id_map,
-        &program, &node_id_map, nullptr));
+        program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc
index 0be25af..1f0bb26 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc
@@ -54,11 +54,11 @@
     auto statement_id = node_id_map.GetId(statement);
     ASSERT_NE(statement_id, 0);
     ASSERT_TRUE(MaybeApplyMutation(program, MutationDeleteStatement(statement_id), node_id_map,
-                                   &program, &node_id_map, nullptr));
+                                   program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
     wgsl::writer::Options options;
-    auto transformed_result = wgsl::writer::Generate(&program, options);
-    auto expected_result = wgsl::writer::Generate(&expected_program, options);
+    auto transformed_result = wgsl::writer::Generate(program, options);
+    auto expected_result = wgsl::writer::Generate(expected_program, options);
     ASSERT_TRUE(transformed_result) << transformed_result.Failure();
     ASSERT_TRUE(expected_result) << expected_result.Failure();
     ASSERT_EQ(expected_result->wgsl, transformed_result->wgsl);
@@ -78,7 +78,7 @@
     auto statement_id = node_id_map.GetId(statement);
     ASSERT_NE(statement_id, 0);
     ASSERT_FALSE(MaybeApplyMutation(program, MutationDeleteStatement(statement_id), node_id_map,
-                                    &program, &node_id_map, nullptr));
+                                    program, &node_id_map, nullptr));
 }
 
 TEST(DeleteStatementTest, DeleteAssignStatement) {
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
index 5d071f4..f2d4b8f 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
@@ -483,11 +483,11 @@
     ASSERT_NE(replacement_id, 0);
 
     ASSERT_TRUE(MaybeApplyMutation(program, MutationReplaceIdentifier(use_id, replacement_id),
-                                   node_id_map, &program, &node_id_map, nullptr));
+                                   node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn f() {
@@ -527,11 +527,11 @@
     ASSERT_NE(replacement_id, 0);
 
     ASSERT_TRUE(MaybeApplyMutation(program, MutationReplaceIdentifier(use_id, replacement_id),
-                                   node_id_map, &program, &node_id_map, nullptr));
+                                   node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn f(b : ptr<function, vec2<u32>>) {
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc
index a411255..6e8b0b1 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc
@@ -51,11 +51,11 @@
     ASSERT_TRUE(MaybeApplyMutation(
         program,
         MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(), core::UnaryOp::kNot),
-        node_id_map, &program, &node_id_map, nullptr));
+        node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -92,11 +92,11 @@
     ASSERT_TRUE(MaybeApplyMutation(
         program,
         MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(), core::UnaryOp::kNot),
-        node_id_map, &program, &node_id_map, nullptr));
+        node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -130,11 +130,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kComplement),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -172,11 +172,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kComplement),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() -> vec2<bool> {
@@ -213,11 +213,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -252,11 +252,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -295,11 +295,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -337,11 +337,11 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kComplement),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     ASSERT_TRUE(result) << result.Failure();
 
     std::string expected_shader = R"(fn main() {
@@ -377,7 +377,7 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable2) {
@@ -405,7 +405,7 @@
     ASSERT_FALSE(MaybeApplyMutation(
         program,
         MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(), core::UnaryOp::kNot),
-        node_id_map, &program, &node_id_map, nullptr));
+        node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable3) {
@@ -434,7 +434,7 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable4) {
@@ -463,7 +463,7 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kComplement),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable5) {
@@ -490,7 +490,7 @@
     // Id for the replacement expression is not fresh.
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationWrapUnaryOperator(expression_id, expression_id, core::UnaryOp::kNegation),
-        node_id_map, &program, &node_id_map, nullptr));
+        node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable6) {
@@ -517,7 +517,7 @@
         MaybeApplyMutation(program,
                            MutationWrapUnaryOperator(statement_id, node_id_map.TakeFreshId(),
                                                      core::UnaryOp::kNegation),
-                           node_id_map, &program, &node_id_map, nullptr));
+                           node_id_map, program, &node_id_map, nullptr));
 }
 
 TEST(WrapUnaryOperatorTest, NotApplicable_CallStmt) {
@@ -546,7 +546,7 @@
     // The id provided for the expression is not a valid expression type.
     ASSERT_FALSE(MaybeApplyMutation(
         program, MutationWrapUnaryOperator(expr_id, node_id_map.TakeFreshId(), core::UnaryOp::kNot),
-        node_id_map, &program, &node_id_map, nullptr));
+        node_id_map, program, &node_id_map, nullptr));
 }
 
 }  // namespace
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
index eab45ca..a7b4702 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -65,10 +65,9 @@
 bool MaybeApplyMutation(const tint::Program& program,
                         const Mutation& mutation,
                         const NodeIdMap& node_id_map,
-                        tint::Program* out_program,
+                        tint::Program& out_program,
                         NodeIdMap* out_node_id_map,
                         protobufs::MutationSequence* mutation_sequence) {
-    assert(out_program && "`out_program` may not be a nullptr");
     assert(out_node_id_map && "`out_node_id_map` may not be a nullptr");
 
     if (!mutation.IsApplicable(program, node_id_map)) {
@@ -93,7 +92,7 @@
     }
 
     clone_context.Clone();
-    *out_program = tint::resolver::Resolve(mutated);
+    out_program = tint::resolver::Resolve(mutated);
     *out_node_id_map = std::move(new_node_id_map);
     return true;
 }
@@ -105,7 +104,7 @@
     for (const auto& mutation_message : mutation_sequence.mutation()) {
         auto mutation = Mutation::FromMessage(mutation_message);
         auto status =
-            MaybeApplyMutation(program, *mutation, node_id_map, &program, &node_id_map, nullptr);
+            MaybeApplyMutation(program, *mutation, node_id_map, program, &node_id_map, nullptr);
         (void)status;  // `status` will be unused in release mode.
         assert(status && "`mutation` is inapplicable - it's most likely a bug");
         if (!program.IsValid()) {
@@ -156,7 +155,7 @@
                 continue;
             }
 
-            if (!MaybeApplyMutation(program, *mutation, node_id_map, &program, &node_id_map,
+            if (!MaybeApplyMutation(program, *mutation, node_id_map, program, &node_id_map,
                                     mutation_sequence)) {
                 // This `mutation` is inapplicable. This may happen if some of the
                 // earlier mutations cancelled this one.
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutator.h
index 649781a..b1c23c4 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.h
@@ -52,7 +52,7 @@
 bool MaybeApplyMutation(const tint::Program& program,
                         const Mutation& mutation,
                         const NodeIdMap& node_id_map,
-                        tint::Program* out_program,
+                        tint::Program& out_program,
                         NodeIdMap* out_node_id_map,
                         protobufs::MutationSequence* mutation_sequence);
 
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 93ec9ea..a9e2e83 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -68,14 +68,14 @@
 // Wrapping in a macro, so it can be a one-liner in the code, but not
 // introduce another level in the stack trace. This will help with de-duping
 // ClusterFuzz issues.
-#define CHECK_INSPECTOR(program, inspector)                                                  \
-    do {                                                                                     \
-        if ((inspector).has_error()) {                                                       \
-            if (!enforce_validity) {                                                         \
-                return;                                                                      \
-            }                                                                                \
-            FATAL_ERROR(program->Diagnostics(), "Inspector failed: " + (inspector).error()); \
-        }                                                                                    \
+#define CHECK_INSPECTOR(program, inspector)                                                 \
+    do {                                                                                    \
+        if ((inspector).has_error()) {                                                      \
+            if (!enforce_validity) {                                                        \
+                return;                                                                     \
+            }                                                                               \
+            FATAL_ERROR(program.Diagnostics(), "Inspector failed: " + (inspector).error()); \
+        }                                                                                   \
     } while (false)
 
 // Wrapping in a macro to make code more readable and help with issue de-duping.
@@ -205,7 +205,7 @@
     }
 #endif  // TINT_BUILD_SPV_READER
 
-    RunInspector(&program);
+    RunInspector(program);
     diagnostics_ = program.Diagnostics();
 
     auto validate_program = [&](auto& out) {
@@ -224,13 +224,13 @@
         }
 
         program = std::move(out);
-        RunInspector(&program);
+        RunInspector(program);
         return 1;
     };
 
     if (transform_manager_) {
         ast::transform::DataMap outputs;
-        auto out = transform_manager_->Run(&program, *transform_inputs_, outputs);
+        auto out = transform_manager_->Run(program, *transform_inputs_, outputs);
         if (!validate_program(out)) {  // Will move: program <- out on success
             return 0;
         }
@@ -293,7 +293,7 @@
     switch (output_) {
         case OutputFormat::kWGSL: {
 #if TINT_BUILD_WGSL_WRITER
-            (void)wgsl::writer::Generate(&program, options_wgsl_);
+            (void)wgsl::writer::Generate(program, options_wgsl_);
 #endif  // TINT_BUILD_WGSL_WRITER
             break;
         }
@@ -306,7 +306,7 @@
                 return 0;
             }
 
-            auto result = spirv::writer::Generate(&program, options_spirv_);
+            auto result = spirv::writer::Generate(program, options_spirv_);
             if (result) {
                 generated_spirv_ = std::move(result->spirv);
 
@@ -321,7 +321,7 @@
         }
         case OutputFormat::kHLSL: {
 #if TINT_BUILD_HLSL_WRITER
-            (void)hlsl::writer::Generate(&program, options_hlsl_);
+            (void)hlsl::writer::Generate(program, options_hlsl_);
 #endif  // TINT_BUILD_HLSL_WRITER
             break;
         }
@@ -335,13 +335,11 @@
 
             // Remap resource numbers to a flat namespace.
             // TODO(crbug.com/tint/1501): Do this via Options::BindingMap.
-            auto input_program = &program;
-            auto flattened = tint::writer::FlattenBindings(&program);
-            if (flattened) {
-                input_program = &*flattened;
+            if (auto flattened = tint::writer::FlattenBindings(program)) {
+                program = std::move(*flattened);
             }
 
-            (void)msl::writer::Generate(input_program, options_msl_);
+            (void)msl::writer::Generate(program, options_msl_);
 #endif  // TINT_BUILD_MSL_WRITER
             break;
         }
@@ -350,11 +348,11 @@
     return 0;
 }
 
-void CommonFuzzer::RunInspector(Program* program) {
+void CommonFuzzer::RunInspector(Program& program) {
     inspector::Inspector inspector(program);
-    diagnostics_ = program->Diagnostics();
+    diagnostics_ = program.Diagnostics();
 
-    if (!program->IsValid()) {
+    if (!program.IsValid()) {
         // It's not safe to use the inspector on invalid programs.
         return;
     }
diff --git a/src/tint/fuzzers/tint_common_fuzzer.h b/src/tint/fuzzers/tint_common_fuzzer.h
index 7683066..f0acc37 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.h
+++ b/src/tint/fuzzers/tint_common_fuzzer.h
@@ -142,7 +142,7 @@
 #endif  // TINT_BUILD_WGSL_READER
 
     /// Runs a series of reflection operations to exercise the Inspector API.
-    void RunInspector(Program* program);
+    void RunInspector(Program& program);
 };
 
 }  // namespace tint::fuzzers
diff --git a/src/tint/fuzzers/tint_concurrency_fuzzer.cc b/src/tint/fuzzers/tint_concurrency_fuzzer.cc
index 1d97c8d..9301399 100644
--- a/src/tint/fuzzers/tint_concurrency_fuzzer.cc
+++ b/src/tint/fuzzers/tint_concurrency_fuzzer.cc
@@ -52,7 +52,7 @@
         return 0;
     }
 
-    tint::inspector::Inspector inspector(&program);
+    tint::inspector::Inspector inspector(program);
     auto entry_points = inspector.GetEntryPoints();
     std::string entry_point = entry_points.empty() ? "" : entry_points.front().name;
 
@@ -81,28 +81,28 @@
             switch (static_cast<Writer>(thread_idx % static_cast<size_t>(Writer::kCount))) {
 #if TINT_BUILD_WGSL_WRITER
                 case Writer::kWGSL: {
-                    (void)tint::wgsl::writer::Generate(&program, {});
+                    (void)tint::wgsl::writer::Generate(program, {});
                     break;
                 }
 #endif  // TINT_BUILD_WGSL_WRITER
 
 #if TINT_BUILD_SPV_WRITER
                 case Writer::kSPIRV: {
-                    (void)tint::spirv::writer::Generate(&program, {});
+                    (void)tint::spirv::writer::Generate(program, {});
                     break;
                 }
 #endif  // TINT_BUILD_SPV_WRITER
 
 #if TINT_BUILD_HLSL_WRITER
                 case Writer::kHLSL: {
-                    (void)tint::hlsl::writer::Generate(&program, {});
+                    (void)tint::hlsl::writer::Generate(program, {});
                     break;
                 }
 #endif  // TINT_BUILD_HLSL_WRITER
 
 #if TINT_BUILD_GLSL_WRITER
                 case Writer::kGLSL: {
-                    (void)tint::glsl::writer::Generate(&program, {}, entry_point);
+                    (void)tint::glsl::writer::Generate(program, {}, entry_point);
                     break;
                 }
 #endif  // TINT_BUILD_GLSL_WRITER
@@ -110,8 +110,8 @@
 #if TINT_BUILD_MSL_WRITER
                 case Writer::kMSL: {
                     // Remap resource numbers to a flat namespace.
-                    if (auto flattened = tint::writer::FlattenBindings(&program)) {
-                        (void)tint::msl::writer::Generate(&flattened.value(), {});
+                    if (auto flattened = tint::writer::FlattenBindings(program)) {
+                        (void)tint::msl::writer::Generate(flattened.value(), {});
                     }
                     break;
                 }
diff --git a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
index 74e198f..58cd879 100644
--- a/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
+++ b/src/tint/fuzzers/tint_ir_roundtrip_fuzzer.cc
@@ -71,7 +71,7 @@
         return 0;
     }
 
-    auto ir = tint::wgsl::reader::ProgramToIR(&src);
+    auto ir = tint::wgsl::reader::ProgramToIR(src);
     if (!ir) {
         std::cerr << ir.Failure() << std::endl;
         __builtin_trap();
@@ -80,7 +80,7 @@
     auto dst = tint::wgsl::writer::IRToProgram(ir.Get());
     if (!dst.IsValid()) {
 #if TINT_BUILD_WGSL_WRITER
-        if (auto result = tint::wgsl::writer::Generate(&dst, {}); result) {
+        if (auto result = tint::wgsl::writer::Generate(dst, {}); result) {
             std::cerr << result->wgsl << std::endl << std::endl;
         }
 #endif
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index 1eb6050..f5ada94 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -155,7 +155,7 @@
 SanitizedResult::~SanitizedResult() = default;
 SanitizedResult::SanitizedResult(SanitizedResult&&) = default;
 
-SanitizedResult Sanitize(const Program* in,
+SanitizedResult Sanitize(const Program& in,
                          const Options& options,
                          const std::string& entry_point) {
     ast::transform::Manager manager;
@@ -271,7 +271,7 @@
     return result;
 }
 
-ASTPrinter::ASTPrinter(const Program* program, const Version& version)
+ASTPrinter::ASTPrinter(const Program& program, const Version& version)
     : builder_(ProgramBuilder::Wrap(program)), version_(version) {}
 
 ASTPrinter::~ASTPrinter() = default;
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
index eafc002..5d1fa1f 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
@@ -68,7 +68,7 @@
 /// @param options The HLSL generator options.
 /// @param entry_point the entry point to generate GLSL for
 /// @returns the sanitized program and any supplementary information
-SanitizedResult Sanitize(const Program* program,
+SanitizedResult Sanitize(const Program& program,
                          const Options& options,
                          const std::string& entry_point);
 
@@ -78,7 +78,7 @@
     /// Constructor
     /// @param program the program to generate
     /// @param version the GLSL version to use
-    ASTPrinter(const Program* program, const Version& version);
+    ASTPrinter(const Program& program, const Version& version);
     ~ASTPrinter() override;
 
     /// Generates the GLSL shader
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 d0445c3..fe63847 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,9 +24,9 @@
 TEST_F(GlslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(resolver::Resolve(*this));
-    ASSERT_FALSE(program->IsValid());
-    auto result = Generate(program.get(), Options{}, "");
+    auto program = resolver::Resolve(*this);
+    ASSERT_FALSE(program.IsValid());
+    auto result = Generate(program, Options{}, "");
     EXPECT_FALSE(result);
     EXPECT_EQ(result.Failure(), "input program is not valid");
 }
diff --git a/src/tint/lang/glsl/writer/ast_printer/helper_test.h b/src/tint/lang/glsl/writer/ast_printer/helper_test.h
index d2f150a..e67c334 100644
--- a/src/tint/lang/glsl/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/glsl/writer/ast_printer/helper_test.h
@@ -57,7 +57,7 @@
         }();
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        gen_ = std::make_unique<ASTPrinter>(program.get(), version);
+        gen_ = std::make_unique<ASTPrinter>(*program, version);
         return *gen_;
     }
 
@@ -79,14 +79,14 @@
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
-        auto sanitized_result = Sanitize(program.get(), options, /* entry_point */ "");
+        auto sanitized_result = Sanitize(*program, options, /* entry_point */ "");
         [&] {
             ASSERT_TRUE(sanitized_result.program.IsValid())
                 << sanitized_result.program.Diagnostics().str();
         }();
 
         *program = std::move(sanitized_result.program);
-        gen_ = std::make_unique<ASTPrinter>(program.get(), version);
+        gen_ = std::make_unique<ASTPrinter>(*program, version);
         return *gen_;
     }
 
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
index a8f8267..0908a70 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
@@ -51,11 +51,11 @@
 /// PIMPL state for the transform
 struct CombineSamplers::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// The binding info
     const BindingInfo* binding_info;
@@ -91,7 +91,7 @@
     /// Constructor
     /// @param program the source program
     /// @param info the binding map information
-    State(const Program* program, const BindingInfo* info) : src(program), binding_info(info) {}
+    State(const Program& program, const BindingInfo* info) : src(program), binding_info(info) {}
 
     /// Creates a combined sampler global variables.
     /// (Note this is actually a Texture node at the AST level, but it will be
@@ -336,7 +336,7 @@
 
 CombineSamplers::~CombineSamplers() = default;
 
-ast::transform::Transform::ApplyResult CombineSamplers::Apply(const Program* src,
+ast::transform::Transform::ApplyResult CombineSamplers::Apply(const Program& src,
                                                               const ast::transform::DataMap& inputs,
                                                               ast::transform::DataMap&) const {
     auto* binding_info = inputs.Get<BindingInfo>();
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
index 725e66d..e8c1b80 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.h
@@ -89,7 +89,7 @@
     ~CombineSamplers() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
index aa35141..8057eee 100644
--- a/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs.cc
@@ -56,12 +56,12 @@
 
 PadStructs::~PadStructs() = default;
 
-ast::transform::Transform::ApplyResult PadStructs::Apply(const Program* src,
+ast::transform::Transform::ApplyResult PadStructs::Apply(const Program& src,
                                                          const ast::transform::DataMap&,
                                                          ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
-    auto& sem = src->Sem();
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+    auto& sem = src.Sem();
 
     std::unordered_map<const ast::Struct*, const ast::Struct*> replaced_structs;
     Hashset<const ast::StructMember*, 8> padding_members;
diff --git a/src/tint/lang/glsl/writer/ast_raise/pad_structs.h b/src/tint/lang/glsl/writer/ast_raise/pad_structs.h
index 33fe20d..1529b87 100644
--- a/src/tint/lang/glsl/writer/ast_raise/pad_structs.h
+++ b/src/tint/lang/glsl/writer/ast_raise/pad_structs.h
@@ -33,7 +33,7 @@
     ~PadStructs() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
index 9636e07..3306a9a 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.cc
@@ -33,9 +33,9 @@
 
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* fn : program->AST().Functions()) {
-        if (auto* sem_fn = program->Sem().Get(fn)) {
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
+        if (auto* sem_fn = program.Sem().Get(fn)) {
             for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
                 const auto& signature = builtin->Signature();
                 auto texture = signature.Parameter(core::ParameterUsage::kTexture);
@@ -48,9 +48,9 @@
             }
         }
     }
-    for (auto* var : program->AST().GlobalVariables()) {
+    for (auto* var : program.AST().GlobalVariables()) {
         if (Switch(
-                program->Sem().Get(var)->Type()->UnwrapRef(),
+                program.Sem().Get(var)->Type()->UnwrapRef(),
                 [&](const core::type::SampledTexture* tex) {
                     return tex->dim() == core::type::TextureDimension::k1d;
                 },
@@ -68,22 +68,22 @@
 /// PIMPL state for the transform
 struct Texture1DTo2D::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
-        if (!ShouldRun(ctx.src)) {
+        if (!ShouldRun(src)) {
             return SkipTransform;
         }
 
@@ -187,7 +187,7 @@
 
 Texture1DTo2D::~Texture1DTo2D() = default;
 
-ast::transform::Transform::ApplyResult Texture1DTo2D::Apply(const Program* src,
+ast::transform::Transform::ApplyResult Texture1DTo2D::Apply(const Program& src,
                                                             const ast::transform::DataMap&,
                                                             ast::transform::DataMap&) const {
     return State(src).Run();
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
index bd43aea..7f8ef20 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_1d_to_2d.h
@@ -30,7 +30,7 @@
     ~Texture1DTo2D() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
index db883b0..61851e4 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
@@ -45,9 +45,9 @@
 /// The member name of the texture builtin values.
 constexpr std::string_view kTextureBuiltinValuesMemberNamePrefix = "texture_builtin_value_";
 
-bool ShouldRun(const Program* program) {
-    for (auto* fn : program->AST().Functions()) {
-        if (auto* sem_fn = program->Sem().Get(fn)) {
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
+        if (auto* sem_fn = program.Sem().Get(fn)) {
             for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
                 // GLSL ES  has no native support for the counterpart of
                 // textureNumLevels (textureQueryLevels) and textureNumSamples (textureSamples)
@@ -74,7 +74,7 @@
     /// @param program the source program
     /// @param in the input transform data
     /// @param out the output transform data
-    explicit State(const Program* program,
+    explicit State(const Program& program,
                    const ast::transform::DataMap& in,
                    ast::transform::DataMap& out)
         : src(program), inputs(in), outputs(out) {}
@@ -91,7 +91,7 @@
             return resolver::Resolve(b);
         }
 
-        if (!ShouldRun(ctx.src)) {
+        if (!ShouldRun(src)) {
             return SkipTransform;
         }
 
@@ -262,7 +262,7 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The transform inputs
     const ast::transform::DataMap& inputs;
     /// The transform outputs
@@ -270,9 +270,9 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in ctx.src
-    const sem::Info& sem = ctx.src->Sem();
+    const sem::Info& sem = src.Sem();
 
     /// The bindpoint to byte offset and field to pass out in transform result.
     /// For one texture type, it could only be passed into one of the
@@ -352,7 +352,7 @@
         }
 
         // Find if there's any existing global variable using the same cfg->ubo_binding
-        for (auto* var : src->AST().Globals<ast::Var>()) {
+        for (auto* var : src.AST().Globals<ast::Var>()) {
             if (var->HasBindingPoint()) {
                 auto* global_sem = sem.Get<sem::GlobalVariable>(var);
 
@@ -476,7 +476,7 @@
 };
 
 ast::transform::Transform::ApplyResult TextureBuiltinsFromUniform::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap& inputs,
     ast::transform::DataMap& outputs) const {
     return State{src, inputs, outputs}.Run();
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
index 53ba1e3..81c6dce 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.h
@@ -107,7 +107,7 @@
     };
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index 2fd23d8..5931ad5 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -22,10 +22,10 @@
 
 namespace tint::glsl::writer {
 
-Result<Output, std::string> Generate(const Program* program,
+Result<Output, std::string> Generate(const Program& program,
                                      const Options& options,
                                      const std::string& entry_point) {
-    if (!program->IsValid()) {
+    if (!program.IsValid()) {
         return std::string("input program is not valid");
     }
 
@@ -36,7 +36,7 @@
     }
 
     // Generate the GLSL code.
-    auto impl = std::make_unique<ASTPrinter>(&sanitized_result.program, options.version);
+    auto impl = std::make_unique<ASTPrinter>(sanitized_result.program, options.version);
     if (!impl->Generate()) {
         return impl->Diagnostics().str();
     }
diff --git a/src/tint/lang/glsl/writer/writer.h b/src/tint/lang/glsl/writer/writer.h
index 7ced20e..7b6be46 100644
--- a/src/tint/lang/glsl/writer/writer.h
+++ b/src/tint/lang/glsl/writer/writer.h
@@ -35,7 +35,7 @@
 /// @param options the configuration options to use when generating GLSL
 /// @param entry_point the entry point to generate GLSL for
 /// @returns the resulting GLSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program* program,
+Result<Output, std::string> Generate(const Program& program,
                                      const Options& options,
                                      const std::string& entry_point);
 
diff --git a/src/tint/lang/glsl/writer/writer_bench.cc b/src/tint/lang/glsl/writer/writer_bench.cc
index 6d433f7..1c58f4b 100644
--- a/src/tint/lang/glsl/writer/writer_bench.cc
+++ b/src/tint/lang/glsl/writer/writer_bench.cc
@@ -38,7 +38,7 @@
 
     for (auto _ : state) {
         for (auto& ep : entry_points) {
-            auto res = Generate(&program, {}, ep);
+            auto res = Generate(program, {}, ep);
             if (!res) {
                 state.SkipWithError(res.Failure().c_str());
             }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index f51312a..a22ac90 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -168,7 +168,7 @@
 SanitizedResult::~SanitizedResult() = default;
 SanitizedResult::SanitizedResult(SanitizedResult&&) = default;
 
-SanitizedResult Sanitize(const Program* in, const Options& options) {
+SanitizedResult Sanitize(const Program& in, const Options& options) {
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
@@ -329,7 +329,7 @@
     return result;
 }
 
-ASTPrinter::ASTPrinter(const Program* program) : builder_(ProgramBuilder::Wrap(program)) {}
+ASTPrinter::ASTPrinter(const Program& program) : builder_(ProgramBuilder::Wrap(program)) {}
 
 ASTPrinter::~ASTPrinter() = default;
 
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
index 0c8072a..b3a5002 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.h
@@ -61,14 +61,14 @@
 /// @param program the input program
 /// @param options The HLSL generator options.
 /// @returns the sanitized program and any supplementary information
-SanitizedResult Sanitize(const Program* program, const Options& options);
+SanitizedResult Sanitize(const Program& program, const Options& options);
 
 /// Implementation class for HLSL generator
 class ASTPrinter : public tint::TextGenerator {
   public:
     /// Constructor
     /// @param program the program to generate
-    explicit ASTPrinter(const Program* program);
+    explicit ASTPrinter(const Program& program);
     ~ASTPrinter() override;
 
     /// @returns true on successful generation; false otherwise
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 cb81bf6..d6cdd1b 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,9 +24,9 @@
 TEST_F(HlslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(resolver::Resolve(*this));
-    ASSERT_FALSE(program->IsValid());
-    auto result = Generate(program.get(), Options{});
+    auto program = resolver::Resolve(*this);
+    ASSERT_FALSE(program.IsValid());
+    auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
     EXPECT_EQ(result.Failure(), "input program is not valid");
 }
diff --git a/src/tint/lang/hlsl/writer/ast_printer/helper_test.h b/src/tint/lang/hlsl/writer/ast_printer/helper_test.h
index a75aaed..55ab4cc 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/hlsl/writer/ast_printer/helper_test.h
@@ -56,7 +56,7 @@
         }();
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        gen_ = std::make_unique<ASTPrinter>(program.get());
+        gen_ = std::make_unique<ASTPrinter>(*program);
         return *gen_;
     }
 
@@ -76,7 +76,7 @@
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
-        auto sanitized_result = Sanitize(program.get(), options);
+        auto sanitized_result = Sanitize(*program, options);
         [&] {
             ASSERT_TRUE(sanitized_result.program.IsValid())
                 << sanitized_result.program.Diagnostics().str();
@@ -89,10 +89,10 @@
             ast::transform::Renamer::Target::kHlslKeywords,
             /* preserve_unicode */ true);
         transform_manager.Add<tint::ast::transform::Renamer>();
-        auto result = transform_manager.Run(&sanitized_result.program, transform_data, outputs);
+        auto result = transform_manager.Run(sanitized_result.program, transform_data, outputs);
         [&] { ASSERT_TRUE(result.IsValid()) << result.Diagnostics().str(); }();
         *program = std::move(result);
-        gen_ = std::make_unique<ASTPrinter>(program.get());
+        gen_ = std::make_unique<ASTPrinter>(*program);
         return *gen_;
     }
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
index 95c0661..5aa810e 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.cc
@@ -43,9 +43,9 @@
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
 
-bool ShouldRun(const Program* program) {
-    for (auto* fn : program->AST().Functions()) {
-        if (auto* sem_fn = program->Sem().Get(fn)) {
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
+        if (auto* sem_fn = program.Sem().Get(fn)) {
             for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
                 if (builtin->Fn() == core::BuiltinFn::kArrayLength) {
                     return true;
@@ -87,7 +87,7 @@
 CalculateArrayLength::CalculateArrayLength() = default;
 CalculateArrayLength::~CalculateArrayLength() = default;
 
-ast::transform::Transform::ApplyResult CalculateArrayLength::Apply(const Program* src,
+ast::transform::Transform::ApplyResult CalculateArrayLength::Apply(const Program& src,
                                                                    const ast::transform::DataMap&,
                                                                    ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -95,8 +95,8 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
-    auto& sem = src->Sem();
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+    auto& sem = src.Sem();
 
     // get_buffer_size_intrinsic() emits the function decorated with
     // BufferSizeIntrinsic that is transformed by the HLSL writer into a call to
@@ -126,7 +126,7 @@
     std::unordered_map<ArrayUsage, Symbol, ArrayUsage::Hasher> array_length_by_usage;
 
     // Find all the arrayLength() calls...
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         if (auto* call_expr = node->As<ast::CallExpression>()) {
             auto* call = sem.Get(call_expr)->UnwrapMaterialize()->As<sem::Call>();
             if (auto* builtin = call->Target()->As<sem::BuiltinFn>()) {
diff --git a/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
index e798a77..8d52ae7 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/calculate_array_length.h
@@ -56,7 +56,7 @@
     ~CalculateArrayLength() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
index fe6977e..ab0bfca 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.cc
@@ -52,9 +52,9 @@
 
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* decl : program->AST().GlobalDeclarations()) {
-        if (auto* var = program->Sem().Get<sem::Variable>(decl)) {
+bool ShouldRun(const Program& program) {
+    for (auto* decl : program.AST().GlobalDeclarations()) {
+        if (auto* var = program.Sem().Get<sem::Variable>(decl)) {
             if (var->AddressSpace() == core::AddressSpace::kStorage ||
                 var->AddressSpace() == core::AddressSpace::kUniform) {
                 return true;
@@ -795,16 +795,16 @@
 DecomposeMemoryAccess::~DecomposeMemoryAccess() = default;
 
 ast::transform::Transform::ApplyResult DecomposeMemoryAccess::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
         return SkipTransform;
     }
 
-    auto& sem = src->Sem();
+    auto& sem = src.Sem();
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     State state(ctx);
 
     // Scan the AST nodes for storage and uniform buffer accesses. Complex
@@ -815,7 +815,7 @@
     // Inner-most expression nodes are guaranteed to be visited first because AST
     // nodes are fully immutable and require their children to be constructed
     // first so their pointer can be passed to the parent's initializer.
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         if (auto* ident = node->As<ast::IdentifierExpression>()) {
             // X
             if (auto* sem_ident = sem.GetVal(ident)) {
diff --git a/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
index cea9dd9..92e5ec2 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/decompose_memory_access.h
@@ -117,7 +117,7 @@
     ~DecomposeMemoryAccess() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
index 883c911..3494440 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.cc
@@ -38,7 +38,7 @@
 struct LocalizeStructArrayAssignment::State {
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
@@ -51,7 +51,7 @@
 
         bool made_changes = false;
 
-        for (auto* node : ctx.src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             if (auto* assign_stmt = node->As<ast::AssignmentStatement>()) {
                 // Process if it's an assignment statement to a dynamically indexed array
                 // within a struct on a function or private storage variable. This
@@ -154,11 +154,11 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// Returns true if `expr` contains an index accessor expression to a
     /// structure member of array type.
@@ -166,12 +166,12 @@
         bool result = false;
         TraverseExpressions(expr, [&](const ast::IndexAccessorExpression* ia) {
             // Indexing using a runtime value?
-            auto* idx_sem = src->Sem().GetVal(ia->index);
+            auto* idx_sem = src.Sem().GetVal(ia->index);
             if (!idx_sem->ConstantValue()) {
                 // Indexing a member access expr?
                 if (auto* ma = ia->object->As<ast::MemberAccessorExpression>()) {
                     // That accesses an array?
-                    if (src->TypeOf(ma)->UnwrapRef()->Is<core::type::Array>()) {
+                    if (src.TypeOf(ma)->UnwrapRef()->Is<core::type::Array>()) {
                         result = true;
                         return ast::TraverseAction::Stop;
                     }
@@ -188,7 +188,7 @@
     // See https://www.w3.org/TR/WGSL/#originating-variable-section
     std::pair<const core::type::Type*, core::AddressSpace> GetOriginatingTypeAndAddressSpace(
         const ast::AssignmentStatement* assign_stmt) {
-        auto* root_ident = src->Sem().GetVal(assign_stmt->lhs)->RootIdentifier();
+        auto* root_ident = src.Sem().GetVal(assign_stmt->lhs)->RootIdentifier();
         if (TINT_UNLIKELY(!root_ident)) {
             TINT_ICE() << "Unable to determine originating variable for lhs of assignment "
                           "statement";
@@ -216,7 +216,7 @@
 LocalizeStructArrayAssignment::~LocalizeStructArrayAssignment() = default;
 
 ast::transform::Transform::ApplyResult LocalizeStructArrayAssignment::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     return State{src}.Run();
diff --git a/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
index d70e04e..0421116 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/localize_struct_array_assignment.h
@@ -37,7 +37,7 @@
     ~LocalizeStructArrayAssignment() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
index 3bb5a40..62356e1 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
@@ -36,10 +36,10 @@
 namespace tint::hlsl::writer {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
         if (auto* attr = node->As<ast::BuiltinAttribute>()) {
-            if (program->Sem().Get(attr)->Value() == core::BuiltinValue::kNumWorkgroups) {
+            if (program.Sem().Get(attr)->Value() == core::BuiltinValue::kNumWorkgroups) {
                 return true;
             }
         }
@@ -69,11 +69,11 @@
 NumWorkgroupsFromUniform::~NumWorkgroupsFromUniform() = default;
 
 ast::transform::Transform::ApplyResult NumWorkgroupsFromUniform::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap& inputs,
     ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
@@ -90,13 +90,13 @@
 
     // Find all entry point parameters that declare the num_workgroups builtin.
     std::unordered_set<Accessor, Accessor::Hasher> to_replace;
-    for (auto* func : src->AST().Functions()) {
+    for (auto* func : src.AST().Functions()) {
         // num_workgroups is only valid for compute stages.
         if (func->PipelineStage() != ast::PipelineStage::kCompute) {
             continue;
         }
 
-        for (auto* param : src->Sem().Get(func)->Parameters()) {
+        for (auto* param : src.Sem().Get(func)->Parameters()) {
             // Because the CanonicalizeEntryPointIO transform has been run, builtins
             // will only appear as struct members.
             auto* str = param->Type()->As<sem::Struct>();
@@ -124,7 +124,7 @@
                 // If this is the only member, remove the struct and parameter too.
                 if (str->Members().Length() == 1) {
                     ctx.Remove(func->params, param->Declaration());
-                    ctx.Remove(src->AST().GlobalDeclarations(), str->Declaration());
+                    ctx.Remove(src.AST().GlobalDeclarations(), str->Declaration());
                 }
             }
         }
@@ -150,8 +150,8 @@
                 // plus 1, or group 0 if no resource bound.
                 group = 0;
 
-                for (auto* global : src->AST().GlobalVariables()) {
-                    auto* global_sem = src->Sem().Get<sem::GlobalVariable>(global);
+                for (auto* global : src.AST().GlobalVariables()) {
+                    auto* global_sem = src.Sem().Get<sem::GlobalVariable>(global);
                     if (auto bp = global_sem->BindingPoint()) {
                         if (bp->group >= group) {
                             group = bp->group + 1;
@@ -171,7 +171,7 @@
 
     // Now replace all the places where the builtins are accessed with the value
     // loaded from the uniform buffer.
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         auto* accessor = node->As<ast::MemberAccessorExpression>();
         if (!accessor) {
             continue;
diff --git a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
index 4b26e33..a3beb78 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.h
@@ -69,7 +69,7 @@
     };
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
index 4f1808b..afe6e1f 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.cc
@@ -37,13 +37,13 @@
 struct RemoveContinueInSwitch::State {
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
         // First collect all switch statements within loops that contain a continue statement.
-        for (auto* node : src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             auto* cont = node->As<ast::ContinueStatement>();
             if (!cont) {
                 continue;
@@ -126,13 +126,13 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
-    /// Alias to src->sem
-    const sem::Info& sem = src->Sem();
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
+    /// Alias to src.sem
+    const sem::Info& sem = src.Sem();
 
     // Vector of switch statements within a loop that contains at least one continue statement.
     Vector<const ast::SwitchStatement*, 4> switch_stmts;
@@ -177,7 +177,7 @@
 RemoveContinueInSwitch::~RemoveContinueInSwitch() = default;
 
 ast::transform::Transform::ApplyResult RemoveContinueInSwitch::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     State state(src);
diff --git a/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
index 4ad747e..a4623a2 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/remove_continue_in_switch.h
@@ -33,7 +33,7 @@
     ~RemoveContinueInSwitch() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
index 240da0a..3a38837 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
@@ -49,11 +49,11 @@
 TruncateInterstageVariables::~TruncateInterstageVariables() = default;
 
 ast::transform::Transform::ApplyResult TruncateInterstageVariables::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap& config,
     ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     const auto* data = config.Get<Config>();
     if (data == nullptr) {
diff --git a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
index 8fe3250..d98deac 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.h
@@ -121,7 +121,7 @@
     ~TruncateInterstageVariables() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/hlsl/writer/writer.cc b/src/tint/lang/hlsl/writer/writer.cc
index fa88e21..67493be 100644
--- a/src/tint/lang/hlsl/writer/writer.cc
+++ b/src/tint/lang/hlsl/writer/writer.cc
@@ -21,8 +21,8 @@
 
 namespace tint::hlsl::writer {
 
-Result<Output, std::string> Generate(const Program* program, const Options& options) {
-    if (!program->IsValid()) {
+Result<Output, std::string> Generate(const Program& program, const Options& options) {
+    if (!program.IsValid()) {
         return std::string("input program is not valid");
     }
 
@@ -33,7 +33,7 @@
     }
 
     // Generate the HLSL code.
-    auto impl = std::make_unique<ASTPrinter>(&sanitized_result.program);
+    auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
     if (!impl->Generate()) {
         return impl->Diagnostics().str();
     }
diff --git a/src/tint/lang/hlsl/writer/writer.h b/src/tint/lang/hlsl/writer/writer.h
index 2d28d7f..68b5c3c 100644
--- a/src/tint/lang/hlsl/writer/writer.h
+++ b/src/tint/lang/hlsl/writer/writer.h
@@ -33,7 +33,7 @@
 /// @param program the program to translate to HLSL
 /// @param options the configuration options to use when generating HLSL
 /// @returns the resulting HLSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program* program, const Options& options);
+Result<Output, std::string> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::hlsl::writer
 
diff --git a/src/tint/lang/hlsl/writer/writer_bench.cc b/src/tint/lang/hlsl/writer/writer_bench.cc
index 16337e1..6250e0f 100644
--- a/src/tint/lang/hlsl/writer/writer_bench.cc
+++ b/src/tint/lang/hlsl/writer/writer_bench.cc
@@ -28,7 +28,7 @@
     }
     auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(&program, {});
+        auto res = Generate(program, {});
         if (!res) {
             state.SkipWithError(res.Failure().c_str());
         }
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index dc72ff3..2db793f 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -132,7 +132,7 @@
 SanitizedResult::~SanitizedResult() = default;
 SanitizedResult::SanitizedResult(SanitizedResult&&) = default;
 
-SanitizedResult Sanitize(const Program* in, const Options& options) {
+SanitizedResult Sanitize(const Program& in, const Options& options) {
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
@@ -249,7 +249,7 @@
     return result;
 }
 
-ASTPrinter::ASTPrinter(const Program* program) : builder_(ProgramBuilder::Wrap(program)) {}
+ASTPrinter::ASTPrinter(const Program& program) : builder_(ProgramBuilder::Wrap(program)) {}
 
 ASTPrinter::~ASTPrinter() = default;
 
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.h b/src/tint/lang/msl/writer/ast_printer/ast_printer.h
index b4796b0..43c36db 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.h
@@ -77,14 +77,14 @@
 /// @program The program to sanitize
 /// @param options The MSL generator options.
 /// @returns the sanitized program and any supplementary information
-SanitizedResult Sanitize(const Program* program, const Options& options);
+SanitizedResult Sanitize(const Program& program, const Options& options);
 
 /// Implementation class for MSL generator
 class ASTPrinter : public tint::TextGenerator {
   public:
     /// Constructor
     /// @param program the program to generate
-    explicit ASTPrinter(const Program* program);
+    explicit ASTPrinter(const Program& program);
     ~ASTPrinter() override;
 
     /// @returns true on successful generation; false otherwise
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 a6d092f..7087797 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
@@ -28,9 +28,9 @@
 TEST_F(MslASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(resolver::Resolve(*this));
-    ASSERT_FALSE(program->IsValid());
-    auto result = Generate(program.get(), Options{});
+    auto program = resolver::Resolve(*this);
+    ASSERT_FALSE(program.IsValid());
+    auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
     EXPECT_EQ(result.Failure(), "input program is not valid");
 }
diff --git a/src/tint/lang/msl/writer/ast_printer/helper_test.h b/src/tint/lang/msl/writer/ast_printer/helper_test.h
index aeb4982..aded390 100644
--- a/src/tint/lang/msl/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/msl/writer/ast_printer/helper_test.h
@@ -54,7 +54,7 @@
         }();
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        gen_ = std::make_unique<ASTPrinter>(program.get());
+        gen_ = std::make_unique<ASTPrinter>(*program);
         return *gen_;
     }
 
@@ -74,10 +74,10 @@
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
 
-        auto result = Sanitize(program.get(), options);
+        auto result = Sanitize(*program, options);
         [&] { ASSERT_TRUE(result.program.IsValid()) << result.program.Diagnostics().str(); }();
         *program = std::move(result.program);
-        gen_ = std::make_unique<ASTPrinter>(program.get());
+        gen_ = std::make_unique<ASTPrinter>(*program);
         return *gen_;
     }
 
diff --git a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
index ef07aa9..95ab089 100644
--- a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
@@ -43,8 +43,8 @@
 // The name of the struct member for arrays that are wrapped in structures.
 const char* kWrappedArrayMemberName = "arr";
 
-bool ShouldRun(const Program* program) {
-    for (auto* decl : program->AST().GlobalDeclarations()) {
+bool ShouldRun(const Program& program) {
+    for (auto* decl : program.AST().GlobalDeclarations()) {
         if (decl->Is<ast::Variable>()) {
             return true;
         }
@@ -587,7 +587,7 @@
 ModuleScopeVarToEntryPointParam::~ModuleScopeVarToEntryPointParam() = default;
 
 ast::transform::Transform::ApplyResult ModuleScopeVarToEntryPointParam::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -595,7 +595,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     State state{ctx};
     state.Process();
 
diff --git a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
index 034b22a..5d2ca0d 100644
--- a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.h
@@ -70,7 +70,7 @@
     ~ModuleScopeVarToEntryPointParam() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc b/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
index b0316a6..bfea334 100644
--- a/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
+++ b/src/tint/lang/msl/writer/ast_raise/packed_vec3.cc
@@ -49,7 +49,7 @@
 struct PackedVec3::State {
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// The name of the struct member used when wrapping packed vec3 types.
     static constexpr const char* kStructMemberName = "elements";
@@ -342,7 +342,7 @@
     bool ShouldRun() {
         // Check for vec3s in the types of all uniform and storage buffer variables to determine
         // if the transform is necessary.
-        for (auto* decl : src->AST().GlobalVariables()) {
+        for (auto* decl : src.AST().GlobalVariables()) {
             auto* var = sem.Get<sem::GlobalVariable>(decl);
             if (var && core::IsHostShareable(var->AddressSpace()) &&
                 ContainsVec3(var->Type()->UnwrapRef())) {
@@ -375,7 +375,7 @@
         // Replace vec3 types in host-shareable address spaces with `__packed_vec3` types, and
         // collect expressions that need to be converted to or from values that use the
         // `__packed_vec3` type.
-        for (auto* node : ctx.src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             Switch(
                 sem.Get(node),
                 [&](const sem::TypeExpression* type) {
@@ -508,19 +508,19 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in ctx.src
-    const sem::Info& sem = ctx.src->Sem();
+    const sem::Info& sem = src.Sem();
 };
 
 PackedVec3::PackedVec3() = default;
 PackedVec3::~PackedVec3() = default;
 
-ast::transform::Transform::ApplyResult PackedVec3::Apply(const Program* src,
+ast::transform::Transform::ApplyResult PackedVec3::Apply(const Program& src,
                                                          const ast::transform::DataMap&,
                                                          ast::transform::DataMap&) const {
     return State{src}.Run();
diff --git a/src/tint/lang/msl/writer/ast_raise/packed_vec3.h b/src/tint/lang/msl/writer/ast_raise/packed_vec3.h
index 4ef04a5..d557c41 100644
--- a/src/tint/lang/msl/writer/ast_raise/packed_vec3.h
+++ b/src/tint/lang/msl/writer/ast_raise/packed_vec3.h
@@ -46,7 +46,7 @@
     ~PackedVec3() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
index df67318..170f695 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
@@ -37,23 +37,23 @@
 /// PIMPL state for the transform
 struct PixelLocal::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// The transform config
     const Config& cfg;
 
     /// Constructor
     /// @param program the source program
     /// @param config the transform config
-    State(const Program* program, const Config& config) : src(program), cfg(config) {}
+    State(const Program& program, const Config& config) : src(program), cfg(config) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
         // If the pixel local extension isn't enabled, then there must be no use of pixel_local
         // variables, and so there's nothing for this transform to do.
@@ -67,7 +67,7 @@
         // Change all module scope `var<pixel_local>` variables to `var<private>`.
         // We need to do this even if the variable is not referenced by the entry point as later
         // stages do not understand the pixel_local address space.
-        for (auto* global : src->AST().GlobalVariables()) {
+        for (auto* global : src.AST().GlobalVariables()) {
             if (auto* var = global->As<ast::Var>()) {
                 if (sem.Get(var)->AddressSpace() == core::AddressSpace::kPixelLocal) {
                     // Change the 'var<pixel_local>' to 'var<private>'
@@ -79,7 +79,7 @@
 
         // Find the single entry point
         const sem::Function* entry_point = nullptr;
-        for (auto* fn : src->AST().Functions()) {
+        for (auto* fn : src.AST().Functions()) {
             if (fn->IsEntryPoint()) {
                 if (entry_point != nullptr) {
                     TINT_ICE() << "PixelLocal transform requires that the SingleEntryPoint "
@@ -251,7 +251,7 @@
 
 PixelLocal::~PixelLocal() = default;
 
-ast::transform::Transform::ApplyResult PixelLocal::Apply(const Program* src,
+ast::transform::Transform::ApplyResult PixelLocal::Apply(const Program& src,
                                                          const ast::transform::DataMap& inputs,
                                                          ast::transform::DataMap&) const {
     auto* cfg = inputs.Get<Config>();
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.h b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
index 4d1b51f..eb313c8 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local.h
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.h
@@ -83,7 +83,7 @@
     ~PixelLocal() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
index 3ed8cba..f977803 100644
--- a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.cc
@@ -35,11 +35,11 @@
 /// PIMPL state for the transform
 struct SubgroupBallot::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// The name of the `tint_subgroup_ballot` helper function.
     Symbol ballot_helper{};
@@ -52,12 +52,12 @@
 
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
         bool made_changes = false;
         for (auto* node : ctx.src->ASTNodes().Objects()) {
@@ -79,7 +79,7 @@
 
         // Set the subgroup size mask at the start of each entry point that transitively calls
         // `subgroupBallot()`.
-        for (auto* global : src->AST().GlobalDeclarations()) {
+        for (auto* global : src.AST().GlobalDeclarations()) {
             auto* func = global->As<ast::Function>();
             if (func && func->IsEntryPoint() && TransitvelyCallsSubgroupBallot(sem.Get(func))) {
                 SetSubgroupSizeMask(func);
@@ -145,7 +145,7 @@
         for (auto* param : ep->params) {
             auto* builtin = ast::GetAttribute<ast::BuiltinAttribute>(param->attributes);
             if (builtin &&
-                src->Sem()
+                src.Sem()
                         .Get<sem::BuiltinEnumExpression<core::BuiltinValue>>(builtin->builtin)
                         ->Value() == core::BuiltinValue::kSubgroupSize) {
                 subgroup_size = ctx.Clone(param->name->symbol);
@@ -183,7 +183,7 @@
 
 SubgroupBallot::~SubgroupBallot() = default;
 
-ast::transform::Transform::ApplyResult SubgroupBallot::Apply(const Program* src,
+ast::transform::Transform::ApplyResult SubgroupBallot::Apply(const Program& src,
                                                              const ast::transform::DataMap&,
                                                              ast::transform::DataMap&) const {
     return State(src).Run();
diff --git a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
index cde3203..cd13d4c 100644
--- a/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
+++ b/src/tint/lang/msl/writer/ast_raise/subgroup_ballot.h
@@ -36,7 +36,7 @@
     ~SubgroupBallot() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index fcfc910..088d81d 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -24,8 +24,8 @@
 
 namespace tint::msl::writer {
 
-Result<Output, std::string> Generate(const Program* program, const Options& options) {
-    if (!program->IsValid()) {
+Result<Output, std::string> Generate(const Program& program, const Options& options) {
+    if (!program.IsValid()) {
         return std::string("input program is not valid");
     }
 
@@ -64,7 +64,7 @@
             std::move(sanitized_result.used_array_length_from_uniform_indices);
 
         // Generate the MSL code.
-        auto impl = std::make_unique<ASTPrinter>(&sanitized_result.program);
+        auto impl = std::make_unique<ASTPrinter>(sanitized_result.program);
         if (!impl->Generate()) {
             return impl->Diagnostics().str();
         }
diff --git a/src/tint/lang/msl/writer/writer.h b/src/tint/lang/msl/writer/writer.h
index 1bfbc8f..c0ada69 100644
--- a/src/tint/lang/msl/writer/writer.h
+++ b/src/tint/lang/msl/writer/writer.h
@@ -33,7 +33,7 @@
 /// @param program the program to translate to MSL
 /// @param options the configuration options to use when generating MSL
 /// @returns the resulting MSL and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program* program, const Options& options);
+Result<Output, std::string> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::msl::writer
 
diff --git a/src/tint/lang/msl/writer/writer_bench.cc b/src/tint/lang/msl/writer/writer_bench.cc
index b10a350..beeec4d 100644
--- a/src/tint/lang/msl/writer/writer_bench.cc
+++ b/src/tint/lang/msl/writer/writer_bench.cc
@@ -61,7 +61,7 @@
         }
     }
     for (auto _ : state) {
-        auto res = Generate(&program, gen_options);
+        auto res = Generate(program, gen_options);
         if (!res) {
             state.SkipWithError(res.Failure().c_str());
         }
diff --git a/src/tint/lang/spirv/reader/ast_lower/atomics.cc b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
index c62df89..17ad8a2 100644
--- a/src/tint/lang/spirv/reader/ast_lower/atomics.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
@@ -53,11 +53,11 @@
     };
 
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     std::unordered_map<const core::type::Struct*, ForkedStruct> forked_structs;
     std::unordered_set<const sem::Variable*> atomic_variables;
     UniqueVector<const sem::ValueExpression*, 8> atomic_expressions;
@@ -65,7 +65,7 @@
   public:
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
@@ -289,7 +289,7 @@
                                                      builtin);
 }
 
-ast::transform::Transform::ApplyResult Atomics::Apply(const Program* src,
+ast::transform::Transform::ApplyResult Atomics::Apply(const Program& src,
                                                       const ast::transform::DataMap&,
                                                       ast::transform::DataMap&) const {
     return State{src}.Run();
diff --git a/src/tint/lang/spirv/reader/ast_lower/atomics.h b/src/tint/lang/spirv/reader/ast_lower/atomics.h
index d1902f6..c74d16d 100644
--- a/src/tint/lang/spirv/reader/ast_lower/atomics.h
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics.h
@@ -59,7 +59,7 @@
     };
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
index 1a2ac5e..586eb49 100644
--- a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.cc
@@ -40,8 +40,8 @@
 
 using DecomposedArrays = std::unordered_map<const core::type::Array*, Symbol>;
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
         if (auto* ident = node->As<ast::TemplatedIdentifier>()) {
             if (ast::GetAttribute<ast::StrideAttribute>(ident->attributes)) {
                 return true;
@@ -58,7 +58,7 @@
 DecomposeStridedArray::~DecomposeStridedArray() = default;
 
 ast::transform::Transform::ApplyResult DecomposeStridedArray::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -66,8 +66,8 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
-    const auto& sem = src->Sem();
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
+    const auto& sem = src.Sem();
 
     static constexpr const char* kMemberName = "el";
 
@@ -129,7 +129,7 @@
     // to insert an additional member accessor for the single structure field.
     // Example: `arr[i]` -> `arr[i].el`
     ctx.ReplaceAll([&](const ast::IndexAccessorExpression* idx) -> const ast::Expression* {
-        if (auto* ty = src->TypeOf(idx->object)) {
+        if (auto* ty = src.TypeOf(idx->object)) {
             if (auto* arr = ty->UnwrapRef()->As<core::type::Array>()) {
                 if (!arr->IsStrideImplicit()) {
                     auto* expr = ctx.CloneWithoutTransform(idx);
diff --git a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
index 1ab8894..80e5735 100644
--- a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_array.h
@@ -37,7 +37,7 @@
     ~DecomposeStridedArray() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
index 4ab6afc..c5c2827 100644
--- a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.cc
@@ -67,19 +67,19 @@
 DecomposeStridedMatrix::~DecomposeStridedMatrix() = default;
 
 ast::transform::Transform::ApplyResult DecomposeStridedMatrix::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     // Scan the program for all storage and uniform structure matrix members with
     // a custom stride attribute. Replace these matrices with an equivalent array,
     // and populate the `decomposed` map with the members that have been replaced.
     Hashmap<const core::type::StructMember*, MatrixInfo, 8> decomposed;
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         if (auto* str = node->As<ast::Struct>()) {
-            auto* str_ty = src->Sem().Get(str);
+            auto* str_ty = src.Sem().Get(str);
             if (!str_ty->UsedAs(core::AddressSpace::kUniform) &&
                 !str_ty->UsedAs(core::AddressSpace::kStorage)) {
                 continue;
@@ -119,7 +119,7 @@
     //   ssbo.mat[2] -> ssbo.mat[2]
     ctx.ReplaceAll(
         [&](const ast::IndexAccessorExpression* expr) -> const ast::IndexAccessorExpression* {
-            if (auto* access = src->Sem().Get<sem::StructMemberAccess>(expr->object)) {
+            if (auto* access = src.Sem().Get<sem::StructMemberAccess>(expr->object)) {
                 if (decomposed.Contains(access->Member())) {
                     auto* obj = ctx.CloneWithoutTransform(expr->object);
                     auto* idx = ctx.Clone(expr->index);
@@ -136,7 +136,7 @@
     //   ssbo.mat = mat_to_arr(m)
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> mat_to_arr;
     ctx.ReplaceAll([&](const ast::AssignmentStatement* stmt) -> const ast::Statement* {
-        if (auto* access = src->Sem().Get<sem::StructMemberAccess>(stmt->lhs)) {
+        if (auto* access = src.Sem().Get<sem::StructMemberAccess>(stmt->lhs)) {
             if (auto info = decomposed.Find(access->Member())) {
                 auto fn = tint::GetOrCreate(mat_to_arr, *info, [&] {
                     auto name =
@@ -175,7 +175,7 @@
     //   m = arr_to_mat(ssbo.mat)
     std::unordered_map<MatrixInfo, Symbol, MatrixInfo::Hasher> arr_to_mat;
     ctx.ReplaceAll([&](const ast::MemberAccessorExpression* expr) -> const ast::Expression* {
-        if (auto* access = src->Sem().Get(expr)->UnwrapLoad()->As<sem::StructMemberAccess>()) {
+        if (auto* access = src.Sem().Get(expr)->UnwrapLoad()->As<sem::StructMemberAccess>()) {
             if (auto info = decomposed.Find(access->Member())) {
                 auto fn = tint::GetOrCreate(arr_to_mat, *info, [&] {
                     auto name =
diff --git a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
index 58abe70..6ac7b09 100644
--- a/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
+++ b/src/tint/lang/spirv/reader/ast_lower/decompose_strided_matrix.h
@@ -37,7 +37,7 @@
     ~DecomposeStridedMatrix() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
index 4e65452..39cf8b0 100644
--- a/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.cc
@@ -30,17 +30,17 @@
 /// PIMPL state for the transform.
 struct FoldTrivialLets::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// The semantic info.
-    const sem::Info& sem = {ctx.src->Sem()};
+    const sem::Info& sem = src.Sem();
 
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Process a block.
     /// @param block the block
@@ -138,7 +138,7 @@
     /// @returns the new program
     ApplyResult Run() {
         // Process all blocks in the module.
-        for (auto* node : src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             if (auto* block = node->As<ast::BlockStatement>()) {
                 ProcessBlock(block);
             }
@@ -152,7 +152,7 @@
 
 FoldTrivialLets::~FoldTrivialLets() = default;
 
-ast::transform::Transform::ApplyResult FoldTrivialLets::Apply(const Program* src,
+ast::transform::Transform::ApplyResult FoldTrivialLets::Apply(const Program& src,
                                                               const ast::transform::DataMap&,
                                                               ast::transform::DataMap&) const {
     return State(src).Run();
diff --git a/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
index e11151e..fa6ff17 100644
--- a/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
+++ b/src/tint/lang/spirv/reader/ast_lower/fold_trivial_lets.h
@@ -31,7 +31,7 @@
     ~FoldTrivialLets() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/spirv/reader/ast_parser/helper_test.cc b/src/tint/lang/spirv/reader/ast_parser/helper_test.cc
index 7e5ed82..ab887b8 100644
--- a/src/tint/lang/spirv/reader/ast_parser/helper_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/helper_test.cc
@@ -36,7 +36,7 @@
 }
 
 std::string ToString(const Program& program) {
-    wgsl::writer::ASTPrinter writer(&program);
+    wgsl::writer::ASTPrinter writer(program);
     writer.Generate();
 
     if (!writer.Diagnostics().empty()) {
@@ -46,7 +46,7 @@
 }
 
 std::string ToString(const Program& program, VectorRef<const ast::Statement*> stmts) {
-    wgsl::writer::ASTPrinter writer(&program);
+    wgsl::writer::ASTPrinter writer(program);
     for (const auto* stmt : stmts) {
         writer.EmitStatement(stmt);
     }
@@ -57,7 +57,7 @@
 }
 
 std::string ToString(const Program& program, const ast::Node* node) {
-    wgsl::writer::ASTPrinter writer(&program);
+    wgsl::writer::ASTPrinter writer(program);
     return Switch(
         node,
         [&](const ast::Expression* expr) {
diff --git a/src/tint/lang/spirv/reader/ast_parser/parse.cc b/src/tint/lang/spirv/reader/ast_parser/parse.cc
index abef728..d0d147f 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parse.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parse.cc
@@ -95,7 +95,7 @@
     manager.Add<DecomposeStridedArray>();
     manager.Add<ast::transform::RemoveUnreachableStatements>();
     manager.Add<Atomics>();
-    return manager.Run(&program, {}, outputs);
+    return manager.Run(program, {}, outputs);
 }
 
 }  // namespace tint::spirv::reader::ast_parser
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 084c8f9..3b12896 100644
--- a/src/tint/lang/spirv/writer/ast_printer/assign_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/assign_test.cc
@@ -64,11 +64,11 @@
 
             pb.WrapInFunction(assign);
 
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateGlobalVariable(v);
-            b->GenerateAssignStatement(assign);
+            b.GenerateGlobalVariable(v);
+            b.GenerateAssignStatement(assign);
         },
         "trying to add SPIR-V instruction 62 outside a function");
 }
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 944240a..3637111 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,10 +60,10 @@
             auto* expr = pb.If(true, block);
             pb.WrapInFunction(expr);
 
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateIfStatement(expr);
+            b.GenerateIfStatement(expr);
         },
         "Internal error: trying to add SPIR-V instruction 247 outside a function");
 }
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
index c36f0bc..1affcf3 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.cc
@@ -47,7 +47,7 @@
 
 namespace tint::spirv::writer {
 
-SanitizedResult Sanitize(const Program* in, const Options& options) {
+SanitizedResult Sanitize(const Program& in, const Options& options) {
     ast::transform::Manager manager;
     ast::transform::DataMap data;
 
@@ -179,7 +179,7 @@
     return result;
 }
 
-ASTPrinter::ASTPrinter(const Program* program,
+ASTPrinter::ASTPrinter(const Program& program,
                        bool zero_initialize_workgroup_memory,
                        bool experimental_require_subgroup_uniform_control_flow)
     : builder_(program,
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer.h b/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
index 7c69ef6..8ad5859 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer.h
@@ -34,7 +34,7 @@
 /// Sanitize a program in preparation for generating SPIR-V.
 /// @program The program to sanitize
 /// @param options The SPIR-V generator options.
-SanitizedResult Sanitize(const Program* program, const Options& options);
+SanitizedResult Sanitize(const Program& program, const Options& options);
 
 /// Implementation class for SPIR-V generator
 class ASTPrinter {
@@ -46,7 +46,7 @@
     /// @param experimental_require_subgroup_uniform_control_flow `true` to require
     /// `SPV_KHR_subgroup_uniform_control_flow` extension and `SubgroupUniformControlFlowKHR`
     /// execution mode for compute stage entry points.
-    ASTPrinter(const Program* program,
+    ASTPrinter(const Program& program,
                bool zero_initialize_workgroup_memory,
                bool experimental_require_subgroup_uniform_control_flow);
 
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 bcf0633..da60198 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,9 +23,9 @@
 TEST_F(SpirvASTPrinterTest, InvalidProgram) {
     Diagnostics().add_error(diag::System::Writer, "make the program invalid");
     ASSERT_FALSE(IsValid());
-    auto program = std::make_unique<Program>(resolver::Resolve(*this));
-    ASSERT_FALSE(program->IsValid());
-    auto result = Generate(program.get(), Options{});
+    auto program = resolver::Resolve(*this);
+    ASSERT_FALSE(program.IsValid());
+    auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
     EXPECT_EQ(result.Failure(), "input program is not valid");
 }
@@ -33,8 +33,8 @@
 TEST_F(SpirvASTPrinterTest, UnsupportedExtension) {
     Enable(Source{{12, 34}}, wgsl::Extension::kUndefined);
 
-    auto program = std::make_unique<Program>(resolver::Resolve(*this));
-    auto result = Generate(program.get(), Options{});
+    auto program = resolver::Resolve(*this);
+    auto result = Generate(program, Options{});
     EXPECT_FALSE(result);
     EXPECT_EQ(result.Failure(),
               R"(12:34 error: SPIR-V backend does not support extension 'undefined')");
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.cc b/src/tint/lang/spirv/writer/ast_printer/builder.cc
index beb9b39..76bfba3 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.cc
@@ -248,7 +248,7 @@
 
 Builder::AccessorInfo::~AccessorInfo() {}
 
-Builder::Builder(const Program* program,
+Builder::Builder(const Program& program,
                  bool zero_initialize_workgroup_memory,
                  bool experimental_require_subgroup_uniform_control_flow)
     : builder_(ProgramBuilder::Wrap(program)),
diff --git a/src/tint/lang/spirv/writer/ast_printer/builder.h b/src/tint/lang/spirv/writer/ast_printer/builder.h
index ec5066e..ae4b639 100644
--- a/src/tint/lang/spirv/writer/ast_printer/builder.h
+++ b/src/tint/lang/spirv/writer/ast_printer/builder.h
@@ -85,7 +85,7 @@
     /// @param experimental_require_subgroup_uniform_control_flow `true` to require
     /// `SPV_KHR_subgroup_uniform_control_flow` extension and `SubgroupUniformControlFlowKHR`
     /// execution mode for compute stage entry points.
-    explicit Builder(const Program* program,
+    explicit Builder(const Program& program,
                      bool zero_initialize_workgroup_memory = false,
                      bool experimental_require_subgroup_uniform_control_flow = false);
     ~Builder();
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 55033a9..e86ce06 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,12 +3786,12 @@
                         pb.Stage(ast::PipelineStage::kFragment),
                     });
 
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateGlobalVariable(texture);
-            b->GenerateGlobalVariable(sampler);
-            b->GenerateExpression(call);
+            b.GenerateGlobalVariable(texture);
+            b.GenerateGlobalVariable(sampler);
+            b.GenerateExpression(call);
         },
         "Internal error: trying to add SPIR-V instruction ");
 }
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 d89bba8..aef2cf4 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,10 +164,10 @@
                                      pb.WorkgroupSize("width", "height", "depth"),
                                      pb.Stage(ast::PipelineStage::kCompute),
                                  });
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateExecutionModes(func, 3);
+            b.GenerateExecutionModes(func, 3);
         },
         "override-expressions should have been removed with the SubstituteOverride transform");
 }
@@ -185,10 +185,10 @@
                                      pb.Stage(ast::PipelineStage::kCompute),
                                  });
 
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateExecutionModes(func, 3);
+            b.GenerateExecutionModes(func, 3);
         },
         "override-expressions should have been removed with the SubstituteOverride transform");
 }
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 97ea5db..a29cf99 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 @@
 
     constexpr bool kZeroInitializeWorkgroupMemory = true;
     std::unique_ptr<Builder> b =
-        std::make_unique<Builder>(program.get(), kZeroInitializeWorkgroupMemory);
+        std::make_unique<Builder>(*program, kZeroInitializeWorkgroupMemory);
 
     EXPECT_TRUE(b->GenerateGlobalVariable(var_scalar)) << b->Diagnostics();
     EXPECT_TRUE(b->GenerateGlobalVariable(var_array)) << b->Diagnostics();
diff --git a/src/tint/lang/spirv/writer/ast_printer/helper_test.h b/src/tint/lang/spirv/writer/ast_printer/helper_test.h
index e97bb77..4580c28 100644
--- a/src/tint/lang/spirv/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/spirv/writer/ast_printer/helper_test.h
@@ -58,7 +58,7 @@
         }();
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        spirv_builder = std::make_unique<Builder>(program.get());
+        spirv_builder = std::make_unique<Builder>(*program);
         return *spirv_builder;
     }
 
@@ -77,14 +77,14 @@
         }();
         program = std::make_unique<Program>(resolver::Resolve(*this));
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        auto result = Sanitize(program.get(), options);
+        auto result = Sanitize(*program, options);
         [&] { ASSERT_TRUE(result.program.IsValid()) << result.program.Diagnostics().str(); }();
         *program = std::move(result.program);
         bool zero_initialize_workgroup_memory =
             !options.disable_workgroup_init &&
             options.use_zero_initialize_workgroup_memory_extension;
         spirv_builder =
-            std::make_unique<Builder>(program.get(), zero_initialize_workgroup_memory,
+            std::make_unique<Builder>(*program, zero_initialize_workgroup_memory,
                                       options.experimental_require_subgroup_uniform_control_flow);
         return *spirv_builder;
     }
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 fafc79b..6e3b3ca 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,11 +36,11 @@
             auto* expr = pb.Expr("c");
             pb.WrapInFunction(expr);
 
-            auto program = std::make_unique<Program>(resolver::Resolve(pb));
-            auto b = std::make_unique<Builder>(program.get());
+            auto program = resolver::Resolve(pb);
+            Builder b(program);
 
-            b->GenerateGlobalVariable(v);
-            b->GenerateIdentifierExpression(expr);
+            b.GenerateGlobalVariable(v);
+            b.GenerateIdentifierExpression(expr);
         },
         "internal compiler error: unable to find ID for variable: c");
 }
diff --git a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
index e49c951..f049300 100644
--- a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.cc
@@ -38,23 +38,23 @@
 /// PIMPL state for the transform
 struct ClampFragDepth::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b{};
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// The sem::Info of the program
-    const sem::Info& sem = src->Sem();
+    const sem::Info& sem = src.Sem();
     /// The symbols of the program
-    const SymbolTable& sym = src->Symbols();
+    const SymbolTable& sym = src.Symbols();
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     Transform::ApplyResult Run() {
         // Abort on any use of push constants in the module.
-        for (auto* global : src->AST().GlobalVariables()) {
+        for (auto* global : src.AST().GlobalVariables()) {
             if (auto* var = global->As<ast::Var>()) {
-                auto* v = src->Sem().Get(var);
+                auto* v = src.Sem().Get(var);
                 if (TINT_UNLIKELY(v->AddressSpace() == core::AddressSpace::kPushConstant)) {
                     TINT_ICE()
                         << "ClampFragDepth doesn't know how to handle module that already use push "
@@ -174,7 +174,7 @@
   private:
     /// @returns true if the transform should run
     bool ShouldRun() {
-        for (auto* fn : src->AST().Functions()) {
+        for (auto* fn : src.AST().Functions()) {
             if (fn->PipelineStage() == ast::PipelineStage::kFragment &&
                 (ReturnsFragDepthAsValue(fn) || ReturnsFragDepthInStruct(fn))) {
                 return true;
@@ -223,7 +223,7 @@
 ClampFragDepth::ClampFragDepth() = default;
 ClampFragDepth::~ClampFragDepth() = default;
 
-ast::transform::Transform::ApplyResult ClampFragDepth::Apply(const Program* src,
+ast::transform::Transform::ApplyResult ClampFragDepth::Apply(const Program& src,
                                                              const ast::transform::DataMap&,
                                                              ast::transform::DataMap&) const {
     return State{src}.Run();
diff --git a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
index cb3ce85..86b8ee2 100644
--- a/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
+++ b/src/tint/lang/spirv/writer/ast_raise/clamp_frag_depth.h
@@ -57,7 +57,7 @@
     ~ClampFragDepth() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 
diff --git a/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
index 36fc251..619b6dd 100644
--- a/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.cc
@@ -26,8 +26,8 @@
 namespace tint::spirv::writer {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
         if (node->Is<ast::ForLoopStatement>()) {
             return true;
         }
@@ -41,7 +41,7 @@
 
 ForLoopToLoop::~ForLoopToLoop() = default;
 
-ast::transform::Transform::ApplyResult ForLoopToLoop::Apply(const Program* src,
+ast::transform::Transform::ApplyResult ForLoopToLoop::Apply(const Program& src,
                                                             const ast::transform::DataMap&,
                                                             ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -49,7 +49,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     ctx.ReplaceAll([&](const ast::ForLoopStatement* for_loop) -> const ast::Statement* {
         tint::Vector<const ast::Statement*, 8> stmts;
diff --git a/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
index 776e1f3..7ca201a 100644
--- a/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
+++ b/src/tint/lang/spirv/writer/ast_raise/for_loop_to_loop.h
@@ -29,7 +29,7 @@
     ~ForLoopToLoop() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/writer/ast_raise/merge_return.cc b/src/tint/lang/spirv/writer/ast_raise/merge_return.cc
index 2d30235..7656750 100644
--- a/src/tint/lang/spirv/writer/ast_raise/merge_return.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/merge_return.cc
@@ -32,12 +32,12 @@
 namespace {
 
 /// Returns `true` if `stmt` has the behavior `behavior`.
-bool HasBehavior(const Program* program, const ast::Statement* stmt, sem::Behavior behavior) {
-    return program->Sem().Get(stmt)->Behaviors().Contains(behavior);
+bool HasBehavior(const Program& program, const ast::Statement* stmt, sem::Behavior behavior) {
+    return program.Sem().Get(stmt)->Behaviors().Contains(behavior);
 }
 
 /// Returns `true` if `func` needs to be transformed.
-bool NeedsTransform(const Program* program, const ast::Function* func) {
+bool NeedsTransform(const Program& program, const ast::Function* func) {
     // Entry points and intrinsic declarations never need transforming.
     if (func->IsEntryPoint() || func->body == nullptr) {
         return false;
@@ -99,7 +99,7 @@
 
     /// Process a statement (recursively).
     void ProcessStatement(const ast::Statement* stmt) {
-        if (stmt == nullptr || !HasBehavior(ctx.src, stmt, sem::Behavior::kReturn)) {
+        if (stmt == nullptr || !HasBehavior(*ctx.src, stmt, sem::Behavior::kReturn)) {
             return;
         }
 
@@ -171,7 +171,7 @@
             // Check if the statement is or contains a return statement.
             // We need to make sure any statements that follow this one do not get executed if the
             // return flag has been set.
-            if (HasBehavior(ctx.src, s, sem::Behavior::kReturn)) {
+            if (HasBehavior(*ctx.src, s, sem::Behavior::kReturn)) {
                 if (is_in_loop_or_switch) {
                     // We're in a loop/switch, and so we would have inserted a `break`.
                     // If we've just come out of a loop/switch statement, we need to `break` again.
@@ -179,7 +179,7 @@
                                    ast::SwitchStatement>()) {
                         // If the loop only has the 'Return' behavior, we can just unconditionally
                         // break. Otherwise check the return flag.
-                        if (HasBehavior(ctx.src, s, sem::Behavior::kNext)) {
+                        if (HasBehavior(*ctx.src, s, sem::Behavior::kNext)) {
                             new_stmts.Back().Push(b.If(b.Expr(flag), b.Block(Vector{b.Break()})));
                         } else {
                             new_stmts.Back().Push(b.Break());
@@ -216,16 +216,16 @@
 
 }  // namespace
 
-ast::transform::Transform::ApplyResult MergeReturn::Apply(const Program* src,
+ast::transform::Transform::ApplyResult MergeReturn::Apply(const Program& src,
                                                           const ast::transform::DataMap&,
                                                           ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
 
     for (auto* func : ctx.src->AST().Functions()) {
-        if (!NeedsTransform(ctx.src, func)) {
+        if (!NeedsTransform(src, func)) {
             continue;
         }
 
diff --git a/src/tint/lang/spirv/writer/ast_raise/merge_return.h b/src/tint/lang/spirv/writer/ast_raise/merge_return.h
index 4a5d324..124f07d 100644
--- a/src/tint/lang/spirv/writer/ast_raise/merge_return.h
+++ b/src/tint/lang/spirv/writer/ast_raise/merge_return.h
@@ -28,7 +28,7 @@
     ~MergeReturn() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
index e16172f..17fb1bd 100644
--- a/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.cc
@@ -29,11 +29,11 @@
 
 VarForDynamicIndex::~VarForDynamicIndex() = default;
 
-ast::transform::Transform::ApplyResult VarForDynamicIndex::Apply(const Program* src,
+ast::transform::Transform::ApplyResult VarForDynamicIndex::Apply(const Program& src,
                                                                  const ast::transform::DataMap&,
                                                                  ast::transform::DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     ast::transform::HoistToDeclBefore hoist_to_decl_before(ctx);
 
@@ -42,7 +42,7 @@
     auto dynamic_index_to_var = [&](const ast::IndexAccessorExpression* access_expr) {
         auto* index_expr = access_expr->index;
         auto* object_expr = access_expr->object;
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
         auto stage = sem.GetVal(index_expr)->Stage();
         if (stage == core::EvaluationStage::kConstant ||
@@ -66,7 +66,7 @@
     };
 
     bool index_accessor_found = false;
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         if (auto* access_expr = node->As<ast::IndexAccessorExpression>()) {
             if (!dynamic_index_to_var(access_expr)) {
                 return resolver::Resolve(b);
diff --git a/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
index 44b1409..effacc1 100644
--- a/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
+++ b/src/tint/lang/spirv/writer/ast_raise/var_for_dynamic_index.h
@@ -32,7 +32,7 @@
     ~VarForDynamicIndex() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
index 13ab74b..867b639 100644
--- a/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.cc
@@ -36,9 +36,9 @@
 namespace tint::spirv::writer {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
-        if (auto* sem = program->Sem().GetVal(node)) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
+        if (auto* sem = program.Sem().GetVal(node)) {
             if (auto* call = sem->UnwrapMaterialize()->As<sem::Call>()) {
                 if (call->Target()->Is<sem::ValueConversion>() &&
                     call->Type()->Is<core::type::Matrix>()) {
@@ -60,7 +60,7 @@
 VectorizeMatrixConversions::~VectorizeMatrixConversions() = default;
 
 ast::transform::Transform::ApplyResult VectorizeMatrixConversions::Apply(
-    const Program* src,
+    const Program& src,
     const ast::transform::DataMap&,
     ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -68,7 +68,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     using HelperFunctionKey =
         tint::UnorderedKeyWrapper<std::tuple<const core::type::Matrix*, const core::type::Matrix*>>;
@@ -76,7 +76,7 @@
     std::unordered_map<HelperFunctionKey, Symbol> matrix_convs;
 
     ctx.ReplaceAll([&](const ast::CallExpression* expr) -> const ast::CallExpression* {
-        auto* call = src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
+        auto* call = src.Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
         auto* ty_conv = call->Target()->As<sem::ValueConversion>();
         if (!ty_conv) {
             return nullptr;
diff --git a/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
index 8e4e795..f822417 100644
--- a/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
+++ b/src/tint/lang/spirv/writer/ast_raise/vectorize_matrix_conversions.h
@@ -30,7 +30,7 @@
     ~VectorizeMatrixConversions() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
index 942214f..c4be205 100644
--- a/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
+++ b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.cc
@@ -26,8 +26,8 @@
 namespace tint::spirv::writer {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
         if (node->Is<ast::WhileStatement>()) {
             return true;
         }
@@ -41,7 +41,7 @@
 
 WhileToLoop::~WhileToLoop() = default;
 
-ast::transform::Transform::ApplyResult WhileToLoop::Apply(const Program* src,
+ast::transform::Transform::ApplyResult WhileToLoop::Apply(const Program& src,
                                                           const ast::transform::DataMap&,
                                                           ast::transform::DataMap&) const {
     if (!ShouldRun(src)) {
@@ -49,7 +49,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     ctx.ReplaceAll([&](const ast::WhileStatement* w) -> const ast::Statement* {
         tint::Vector<const ast::Statement*, 16> stmts;
diff --git a/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
index e86fa6e..0e99378 100644
--- a/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
+++ b/src/tint/lang/spirv/writer/ast_raise/while_to_loop.h
@@ -30,7 +30,7 @@
     ~WhileToLoop() override;
 
     /// @copydoc ast::transform::Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const ast::transform::DataMap& inputs,
                       ast::transform::DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/spirv/writer/writer.cc b/src/tint/lang/spirv/writer/writer.cc
index 81acf02..c8181bc 100644
--- a/src/tint/lang/spirv/writer/writer.cc
+++ b/src/tint/lang/spirv/writer/writer.cc
@@ -32,8 +32,8 @@
 Output::~Output() = default;
 Output::Output(const Output&) = default;
 
-Result<Output, std::string> Generate(const Program* program, const Options& options) {
-    if (!program->IsValid()) {
+Result<Output, std::string> Generate(const Program& program, const Options& options) {
+    if (!program.IsValid()) {
         return std::string("input program is not valid");
     }
 
@@ -79,7 +79,7 @@
 
         // Generate the SPIR-V code.
         auto impl = std::make_unique<ASTPrinter>(
-            &sanitized_result.program, zero_initialize_workgroup_memory,
+            sanitized_result.program, zero_initialize_workgroup_memory,
             options.experimental_require_subgroup_uniform_control_flow);
         if (!impl->Generate()) {
             return impl->Diagnostics().str();
diff --git a/src/tint/lang/spirv/writer/writer.h b/src/tint/lang/spirv/writer/writer.h
index 98edb39..7845cb9 100644
--- a/src/tint/lang/spirv/writer/writer.h
+++ b/src/tint/lang/spirv/writer/writer.h
@@ -33,7 +33,7 @@
 /// @param program the program to translate to SPIR-V
 /// @param options the configuration options to use when generating SPIR-V
 /// @returns the resulting SPIR-V and supplementary information, or an error string
-Result<Output, std::string> Generate(const Program* program, const Options& options);
+Result<Output, std::string> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::spirv::writer
 
diff --git a/src/tint/lang/spirv/writer/writer_bench.cc b/src/tint/lang/spirv/writer/writer_bench.cc
index 3d74eb8..b514576 100644
--- a/src/tint/lang/spirv/writer/writer_bench.cc
+++ b/src/tint/lang/spirv/writer/writer_bench.cc
@@ -28,7 +28,7 @@
     }
     auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(&program, options);
+        auto res = Generate(program, options);
         if (!res) {
             state.SkipWithError(res.Failure());
         }
diff --git a/src/tint/lang/wgsl/ast/module_clone_test.cc b/src/tint/lang/wgsl/ast/module_clone_test.cc
index 9e3c7f9..2520091 100644
--- a/src/tint/lang/wgsl/ast/module_clone_test.cc
+++ b/src/tint/lang/wgsl/ast/module_clone_test.cc
@@ -132,7 +132,7 @@
     ASSERT_TRUE(dst.IsValid()) << dst.Diagnostics().str();
 
     // Expect the printed strings to match
-    EXPECT_EQ(Program::printer(&src), Program::printer(&dst));
+    EXPECT_EQ(Program::printer(src), Program::printer(dst));
 
     // Check that none of the AST nodes or type pointers in dst are found in src
     std::unordered_set<const Node*> src_nodes;
@@ -156,7 +156,7 @@
     wgsl::writer::Options options;
     std::string src_wgsl;
     {
-        auto result = wgsl::writer::Generate(&src, options);
+        auto result = wgsl::writer::Generate(src, options);
         ASSERT_TRUE(result) << result.Failure();
         src_wgsl = result->wgsl;
 
@@ -169,7 +169,7 @@
     }
 
     // Print the dst module, check it matches the original source
-    auto result = wgsl::writer::Generate(&dst, options);
+    auto result = wgsl::writer::Generate(dst, options);
     ASSERT_TRUE(result);
     auto dst_wgsl = result->wgsl;
     ASSERT_EQ(src_wgsl, dst_wgsl);
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 e9ad3ef..f1572a7 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.cc
@@ -33,13 +33,13 @@
 
 AddBlockAttribute::~AddBlockAttribute() = default;
 
-Transform::ApplyResult AddBlockAttribute::Apply(const Program* src,
+Transform::ApplyResult AddBlockAttribute::Apply(const Program& src,
                                                 const DataMap&,
                                                 DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
-    auto& sem = src->Sem();
+    auto& sem = src.Sem();
 
     // A map from a type in the source program to a block-decorated wrapper that contains it in the
     // destination program.
@@ -47,7 +47,7 @@
 
     // Process global 'var' declarations that are buffers.
     bool made_changes = false;
-    for (auto* global : src->AST().GlobalVariables()) {
+    for (auto* global : src.AST().GlobalVariables()) {
         auto* var = sem.Get(global);
         if (!core::IsHostShareable(var->AddressSpace())) {
             // Not declared in a host-sharable address space
@@ -77,7 +77,7 @@
                     b.create<Struct>(b.Ident(b.Symbols().New(wrapper_name)),
                                      tint::Vector{b.Member(kMemberName, CreateASTTypeFor(ctx, ty))},
                                      tint::Vector{block});
-                ctx.InsertBefore(src->AST().GlobalDeclarations(), global, ret);
+                ctx.InsertBefore(src.AST().GlobalDeclarations(), global, ret);
                 return ret;
             });
             ctx.Replace(global->type.expr, b.Expr(wrapper->name->symbol));
diff --git a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
index cd17977..3e31d08 100644
--- a/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
+++ b/src/tint/lang/wgsl/ast/transform/add_block_attribute.h
@@ -54,7 +54,7 @@
     ~AddBlockAttribute() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 015f4ae..86fb0ce 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
@@ -27,8 +27,8 @@
 namespace tint::ast::transform {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* func : program->AST().Functions()) {
+bool ShouldRun(const Program& program) {
+    for (auto* func : program.AST().Functions()) {
         if (func->IsEntryPoint()) {
             return false;
         }
@@ -42,7 +42,7 @@
 
 AddEmptyEntryPoint::~AddEmptyEntryPoint() = default;
 
-Transform::ApplyResult AddEmptyEntryPoint::Apply(const Program* src,
+Transform::ApplyResult AddEmptyEntryPoint::Apply(const Program& src,
                                                  const DataMap&,
                                                  DataMap&) const {
     if (!ShouldRun(src)) {
@@ -50,7 +50,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     b.Func(b.Symbols().New("unused_entry_point"), {}, b.ty.void_(), {},
            tint::Vector{
diff --git a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h
index f282d82..91767a9 100644
--- a/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h
+++ b/src/tint/lang/wgsl/ast/transform/add_empty_entry_point.h
@@ -28,7 +28,7 @@
     ~AddEmptyEntryPoint() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 49cc35c..c065eea 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
@@ -37,9 +37,9 @@
 namespace tint::ast::transform {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* fn : program->AST().Functions()) {
-        if (auto* sem_fn = program->Sem().Get(fn)) {
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
+        if (auto* sem_fn = program.Sem().Get(fn)) {
             for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
                 if (builtin->Fn() == core::BuiltinFn::kArrayLength) {
                     return true;
@@ -61,7 +61,7 @@
     /// @param program the source program
     /// @param in the input transform data
     /// @param out the output transform data
-    explicit State(const Program* program, const DataMap& in, DataMap& out)
+    explicit State(const Program& program, const DataMap& in, DataMap& out)
         : src(program), inputs(in), outputs(out) {}
 
     /// Runs the transform
@@ -76,7 +76,7 @@
             return resolver::Resolve(b);
         }
 
-        if (!ShouldRun(ctx.src)) {
+        if (!ShouldRun(src)) {
             return SkipTransform;
         }
 
@@ -177,7 +177,7 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The transform inputs
     const DataMap& inputs;
     /// The transform outputs
@@ -185,7 +185,7 @@
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// Iterate over all arrayLength() builtins that operate on
     /// storage buffer variables.
@@ -196,10 +196,10 @@
     /// sem::GlobalVariable for the storage buffer.
     template <typename F>
     void IterateArrayLengthOnStorageVar(F&& functor) {
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
         // Find all calls to the arrayLength() builtin.
-        for (auto* node : src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             auto* call_expr = node->As<CallExpression>();
             if (!call_expr) {
                 continue;
@@ -253,7 +253,7 @@
     }
 };
 
-Transform::ApplyResult ArrayLengthFromUniform::Apply(const Program* src,
+Transform::ApplyResult ArrayLengthFromUniform::Apply(const Program& src,
                                                      const DataMap& inputs,
                                                      DataMap& outputs) const {
     return State{src, inputs, outputs}.Run();
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
index 7f0acdb..67e9f56 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.h
@@ -96,7 +96,7 @@
     };
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
index 83a35bd..b77cc11 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
@@ -45,11 +45,11 @@
 BindingRemapper::BindingRemapper() = default;
 BindingRemapper::~BindingRemapper() = default;
 
-Transform::ApplyResult BindingRemapper::Apply(const Program* src,
+Transform::ApplyResult BindingRemapper::Apply(const Program& src,
                                               const DataMap& inputs,
                                               DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto* remappings = inputs.Get<Remappings>();
     if (!remappings) {
@@ -69,11 +69,11 @@
     if (remappings->allow_collisions) {
         // Scan for binding point collisions generated by this transform.
         // Populate all collisions in the `add_collision_attr` set.
-        for (auto* func_ast : src->AST().Functions()) {
+        for (auto* func_ast : src.AST().Functions()) {
             if (!func_ast->IsEntryPoint()) {
                 continue;
             }
-            auto* func = src->Sem().Get(func_ast);
+            auto* func = src.Sem().Get(func_ast);
             std::unordered_map<BindingPoint, int> binding_point_counts;
             for (auto* global : func->TransitivelyReferencedGlobals()) {
                 if (auto from = global->BindingPoint()) {
@@ -95,9 +95,9 @@
         }
     }
 
-    for (auto* var : src->AST().Globals<Var>()) {
+    for (auto* var : src.AST().Globals<Var>()) {
         if (var->HasBindingPoint()) {
-            auto* global_sem = src->Sem().Get<sem::GlobalVariable>(var);
+            auto* global_sem = src.Sem().Get<sem::GlobalVariable>(var);
 
             // The original binding point
             BindingPoint from = *global_sem->BindingPoint();
@@ -133,7 +133,7 @@
                                                   ")");
                     return resolver::Resolve(b);
                 }
-                auto* sem = src->Sem().Get(var);
+                auto* sem = src.Sem().Get(var);
                 if (sem->AddressSpace() != core::AddressSpace::kStorage) {
                     b.Diagnostics().add_error(
                         diag::System::Transform,
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.h b/src/tint/lang/wgsl/ast/transform/binding_remapper.h
index 62183b9..71a038b 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.h
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.h
@@ -68,7 +68,7 @@
     ~BindingRemapper() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
index c49d83b..2c92aa3 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.cc
@@ -48,9 +48,9 @@
     /// Constructor
     /// @param program the source program
     /// @param config the transform config
-    State(const Program* program, const Config& config) : src(program), cfg(config) {
+    State(const Program& program, const Config& config) : src(program), cfg(config) {
         has_full_ptr_params = false;
-        for (auto* enable : src->AST().Enables()) {
+        for (auto* enable : src.AST().Enables()) {
             if (enable->HasExtension(wgsl::Extension::kChromiumExperimentalFullPtrParameters)) {
                 has_full_ptr_params = true;
                 break;
@@ -61,12 +61,12 @@
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     Transform::ApplyResult Run() {
-        for (auto* node : src->ASTNodes().Objects()) {
+        for (auto* node : src.ASTNodes().Objects()) {
             Switch(
                 node,  //
                 [&](const CallExpression* expr) { Call(expr); },
                 [&](const BinaryExpression* bin_op) {
-                    if (auto* s = src->Sem().Get(bin_op);
+                    if (auto* s = src.Sem().Get(bin_op);
                         !s || s->Stage() == core::EvaluationStage::kConstant ||
                         s->Stage() == core::EvaluationStage::kNotEvaluated) {
                         return;  // Don't polyfill @const expressions
@@ -83,7 +83,7 @@
                         }
                         case core::BinaryOp::kDivide: {
                             if (cfg.builtins.int_div_mod) {
-                                auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
+                                auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
                                 if (lhs_ty->is_integer_scalar_or_vector()) {
                                     ctx.Replace(bin_op,
                                                 [this, bin_op] { return IntDivMod(bin_op); });
@@ -94,7 +94,7 @@
                         }
                         case core::BinaryOp::kModulo: {
                             if (cfg.builtins.int_div_mod) {
-                                auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
+                                auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
                                 if (lhs_ty->is_integer_scalar_or_vector()) {
                                     ctx.Replace(bin_op,
                                                 [this, bin_op] { return IntDivMod(bin_op); });
@@ -102,7 +102,7 @@
                                 }
                             }
                             if (cfg.builtins.precise_float_mod) {
-                                auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
+                                auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
                                 if (lhs_ty->is_float_scalar_or_vector()) {
                                     ctx.Replace(bin_op,
                                                 [this, bin_op] { return PreciseFloatMod(bin_op); });
@@ -117,7 +117,7 @@
                 },
                 [&](const Expression* expr) {
                     if (cfg.builtins.bgra8unorm) {
-                        if (auto* ty_expr = src->Sem().Get<sem::TypeExpression>(expr)) {
+                        if (auto* ty_expr = src.Sem().Get<sem::TypeExpression>(expr)) {
                             if (auto* tex = ty_expr->Type()->As<core::type::StorageTexture>()) {
                                 if (tex->texel_format() == core::TexelFormat::kBgra8Unorm) {
                                     ctx.Replace(expr, [this, tex] {
@@ -143,15 +143,15 @@
 
   private:
     /// The source program
-    Program const* const src;
+    const Program& src;
     /// The transform config
     const Config& cfg;
     /// The destination program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx{&b, src};
+    program::CloneContext ctx{&b, &src};
     /// The source clone context
-    const sem::Info& sem = src->Sem();
+    const sem::Info& sem = src.Sem();
     /// Polyfill functions for binary operators.
     Hashmap<BinaryOpSignature, Symbol, 8> binary_op_polyfills;
     /// Polyfill builtins.
@@ -884,8 +884,8 @@
     /// @param bin_op the original BinaryExpression
     /// @return the polyfill value for bitshift operation
     const Expression* BitshiftModulo(const BinaryExpression* bin_op) {
-        auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
-        auto* rhs_ty = src->TypeOf(bin_op->rhs)->UnwrapRef();
+        auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
+        auto* rhs_ty = src.TypeOf(bin_op->rhs)->UnwrapRef();
         auto* lhs_el_ty = lhs_ty->DeepestElement();
         const Expression* mask = b.Expr(AInt(lhs_el_ty->Size() * 8 - 1));
         if (rhs_ty->Is<core::type::Vector>()) {
@@ -901,8 +901,8 @@
     /// @param bin_op the original BinaryExpression
     /// @return the polyfill divide or modulo
     const Expression* IntDivMod(const BinaryExpression* bin_op) {
-        auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
-        auto* rhs_ty = src->TypeOf(bin_op->rhs)->UnwrapRef();
+        auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
+        auto* rhs_ty = src.TypeOf(bin_op->rhs)->UnwrapRef();
         BinaryOpSignature sig{bin_op->op, lhs_ty, rhs_ty};
         auto fn = binary_op_polyfills.GetOrCreate(sig, [&] {
             const bool is_div = bin_op->op == core::BinaryOp::kDivide;
@@ -994,8 +994,8 @@
     /// @param bin_op the original BinaryExpression
     /// @return the polyfill divide or modulo
     const Expression* PreciseFloatMod(const BinaryExpression* bin_op) {
-        auto* lhs_ty = src->TypeOf(bin_op->lhs)->UnwrapRef();
-        auto* rhs_ty = src->TypeOf(bin_op->rhs)->UnwrapRef();
+        auto* lhs_ty = src.TypeOf(bin_op->lhs)->UnwrapRef();
+        auto* rhs_ty = src.TypeOf(bin_op->rhs)->UnwrapRef();
         BinaryOpSignature sig{bin_op->op, lhs_ty, rhs_ty};
         auto fn = binary_op_polyfills.GetOrCreate(sig, [&] {
             const auto [lhs_el_ty, lhs_width] = lhs_ty->Elements(lhs_ty, 1);
@@ -1071,7 +1071,7 @@
 
     /// Examines the call expression @p expr, applying any necessary polyfill transforms
     void Call(const CallExpression* expr) {
-        auto* call = src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
+        auto* call = src.Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
         if (!call || call->Stage() == core::EvaluationStage::kConstant ||
             call->Stage() == core::EvaluationStage::kNotEvaluated) {
             return;  // Don't polyfill @const expressions
@@ -1289,7 +1289,7 @@
 
 BuiltinPolyfill::~BuiltinPolyfill() = default;
 
-Transform::ApplyResult BuiltinPolyfill::Apply(const Program* src,
+Transform::ApplyResult BuiltinPolyfill::Apply(const Program& src,
                                               const DataMap& data,
                                               DataMap&) const {
     auto* cfg = data.Get<Config>();
diff --git a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.h b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.h
index 8805996..cd1de52 100644
--- a/src/tint/lang/wgsl/ast/transform/builtin_polyfill.h
+++ b/src/tint/lang/wgsl/ast/transform/builtin_polyfill.h
@@ -105,7 +105,7 @@
     };
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 e9d244e..97425f5 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
@@ -884,11 +884,11 @@
     }
 };
 
-Transform::ApplyResult CanonicalizeEntryPointIO::Apply(const Program* src,
+Transform::ApplyResult CanonicalizeEntryPointIO::Apply(const Program& src,
                                                        const DataMap& inputs,
                                                        DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
@@ -899,7 +899,7 @@
 
     // Remove entry point IO attributes from struct declarations.
     // New structures will be created for each entry point, as necessary.
-    for (auto* ty : src->AST().TypeDecls()) {
+    for (auto* ty : src.AST().TypeDecls()) {
         if (auto* struct_ty = ty->As<Struct>()) {
             for (auto* member : struct_ty->members) {
                 for (auto* attr : member->attributes) {
@@ -911,7 +911,7 @@
         }
     }
 
-    for (auto* func_ast : src->AST().Functions()) {
+    for (auto* func_ast : src.AST().Functions()) {
         if (!func_ast->IsEntryPoint()) {
             continue;
         }
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
index a6b3715..d80a83f 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.h
@@ -161,7 +161,7 @@
     ~CanonicalizeEntryPointIO() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 021ac65..8c9d3e2 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper.cc
@@ -40,14 +40,14 @@
 
 DemoteToHelper::~DemoteToHelper() = default;
 
-Transform::ApplyResult DemoteToHelper::Apply(const Program* src, const DataMap&, DataMap&) const {
-    auto& sem = src->Sem();
+Transform::ApplyResult DemoteToHelper::Apply(const Program& src, const DataMap&, DataMap&) const {
+    auto& sem = src.Sem();
 
     // Collect the set of functions that need to be processed.
     // A function needs to be processed if it is reachable by a shader that contains a discard at
     // any point in its call hierarchy.
     std::unordered_set<const sem::Function*> functions_to_process;
-    for (auto* func : src->AST().Functions()) {
+    for (auto* func : src.AST().Functions()) {
         if (!func->IsEntryPoint()) {
             continue;
         }
@@ -80,7 +80,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     // Create a module-scope flag that indicates whether the current invocation has been discarded.
     auto flag = b.Symbols().New("tint_discarded");
@@ -107,7 +107,7 @@
     // We also insert a discard statement before all return statements in entry points for shaders
     // that discard.
     std::unordered_map<const core::type::Type*, Symbol> atomic_cmpxchg_result_types;
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         Switch(
             node,
 
diff --git a/src/tint/lang/wgsl/ast/transform/demote_to_helper.h b/src/tint/lang/wgsl/ast/transform/demote_to_helper.h
index 1fe3c59..7e18d7b 100644
--- a/src/tint/lang/wgsl/ast/transform/demote_to_helper.h
+++ b/src/tint/lang/wgsl/ast/transform/demote_to_helper.h
@@ -37,7 +37,7 @@
     ~DemoteToHelper() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 9cdc734..c10c787 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access.cc
@@ -179,8 +179,8 @@
     /// Constructor
     /// @param src the source Program
     /// @param options the transform options
-    State(const Program* src, const Options& options)
-        : ctx{&b, src, /* auto_clone_symbols */ true}, opts(options) {}
+    State(const Program& src, const Options& options)
+        : ctx{&b, &src, /* auto_clone_symbols */ true}, opts(options) {}
 
     /// The main function for the transform.
     /// @returns the ApplyResult
@@ -1188,7 +1188,7 @@
 
 DirectVariableAccess::~DirectVariableAccess() = default;
 
-Transform::ApplyResult DirectVariableAccess::Apply(const Program* program,
+Transform::ApplyResult DirectVariableAccess::Apply(const Program& program,
                                                    const DataMap& inputs,
                                                    DataMap&) const {
     Options options;
diff --git a/src/tint/lang/wgsl/ast/transform/direct_variable_access.h b/src/tint/lang/wgsl/ast/transform/direct_variable_access.h
index 2866383..28cbb5a 100644
--- a/src/tint/lang/wgsl/ast/transform/direct_variable_access.h
+++ b/src/tint/lang/wgsl/ast/transform/direct_variable_access.h
@@ -61,7 +61,7 @@
     };
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 34d212b..8dd0ce5 100644
--- a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
+++ b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.cc
@@ -29,16 +29,16 @@
 
 DisableUniformityAnalysis::~DisableUniformityAnalysis() = default;
 
-Transform::ApplyResult DisableUniformityAnalysis::Apply(const Program* src,
+Transform::ApplyResult DisableUniformityAnalysis::Apply(const Program& src,
                                                         const DataMap&,
                                                         DataMap&) const {
-    if (src->Sem().Module()->Extensions().Contains(
+    if (src.Sem().Module()->Extensions().Contains(
             wgsl::Extension::kChromiumDisableUniformityAnalysis)) {
         return SkipTransform;
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     b.Enable(wgsl::Extension::kChromiumDisableUniformityAnalysis);
 
     ctx.Clone();
diff --git a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h
index 6bf6dc1..3fbf877 100644
--- a/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h
+++ b/src/tint/lang/wgsl/ast/transform/disable_uniformity_analysis.h
@@ -28,7 +28,7 @@
     ~DisableUniformityAnalysis() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 a568c1d..ec847ea 100644
--- a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
+++ b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.cc
@@ -35,8 +35,8 @@
 
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
         if (node->IsAnyOf<CompoundAssignmentStatement, IncrementDecrementStatement>()) {
             return true;
         }
@@ -166,7 +166,7 @@
 
 ExpandCompoundAssignment::~ExpandCompoundAssignment() = default;
 
-Transform::ApplyResult ExpandCompoundAssignment::Apply(const Program* src,
+Transform::ApplyResult ExpandCompoundAssignment::Apply(const Program& src,
                                                        const DataMap&,
                                                        DataMap&) const {
     if (!ShouldRun(src)) {
@@ -174,9 +174,9 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     State state(ctx);
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         if (auto* assign = node->As<CompoundAssignmentStatement>()) {
             state.Expand(assign, assign->lhs, ctx.Clone(assign->rhs), assign->op);
         } else if (auto* inc_dec = node->As<IncrementDecrementStatement>()) {
diff --git a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h
index 33da512..ff04ffc 100644
--- a/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h
+++ b/src/tint/lang/wgsl/ast/transform/expand_compound_assignment.h
@@ -46,7 +46,7 @@
     ~ExpandCompoundAssignment() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 4deef70..170f063 100644
--- a/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
+++ b/src/tint/lang/wgsl/ast/transform/first_index_offset.cc
@@ -41,8 +41,8 @@
 constexpr char kFirstVertexName[] = "first_vertex_index";
 constexpr char kFirstInstanceName[] = "first_instance_index";
 
-bool ShouldRun(const Program* program) {
-    for (auto* fn : program->AST().Functions()) {
+bool ShouldRun(const Program& program) {
+    for (auto* fn : program.AST().Functions()) {
         if (fn->PipelineStage() == PipelineStage::kVertex) {
             return true;
         }
@@ -64,7 +64,7 @@
 FirstIndexOffset::FirstIndexOffset() = default;
 FirstIndexOffset::~FirstIndexOffset() = default;
 
-Transform::ApplyResult FirstIndexOffset::Apply(const Program* src,
+Transform::ApplyResult FirstIndexOffset::Apply(const Program& src,
                                                const DataMap& inputs,
                                                DataMap& outputs) const {
     if (!ShouldRun(src)) {
@@ -72,7 +72,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     // Get the uniform buffer binding point
     uint32_t ub_binding = binding_;
@@ -95,7 +95,7 @@
         if (auto* var = node->As<Variable>()) {
             for (auto* attr : var->attributes) {
                 if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
-                    core::BuiltinValue builtin = src->Sem().Get(builtin_attr)->Value();
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
                     if (builtin == core::BuiltinValue::kVertexIndex) {
                         auto* sem_var = ctx.src->Sem().Get(var);
                         builtin_vars.emplace(sem_var, kFirstVertexName);
@@ -112,7 +112,7 @@
         if (auto* member = node->As<StructMember>()) {
             for (auto* attr : member->attributes) {
                 if (auto* builtin_attr = attr->As<BuiltinAttribute>()) {
-                    core::BuiltinValue builtin = src->Sem().Get(builtin_attr)->Value();
+                    core::BuiltinValue builtin = src.Sem().Get(builtin_attr)->Value();
                     if (builtin == core::BuiltinValue::kVertexIndex) {
                         auto* sem_mem = ctx.src->Sem().Get(member);
                         builtin_members.emplace(sem_mem, kFirstVertexName);
diff --git a/src/tint/lang/wgsl/ast/transform/first_index_offset.h b/src/tint/lang/wgsl/ast/transform/first_index_offset.h
index d59713d..a37b55c 100644
--- a/src/tint/lang/wgsl/ast/transform/first_index_offset.h
+++ b/src/tint/lang/wgsl/ast/transform/first_index_offset.h
@@ -106,7 +106,7 @@
     ~FirstIndexOffset() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/ast/transform/helper_test.h b/src/tint/lang/wgsl/ast/transform/helper_test.h
index 0d185e4..68536ea 100644
--- a/src/tint/lang/wgsl/ast/transform/helper_test.h
+++ b/src/tint/lang/wgsl/ast/transform/helper_test.h
@@ -37,7 +37,7 @@
     }
 
     wgsl::writer::Options options;
-    auto result = wgsl::writer::Generate(&program, options);
+    auto result = wgsl::writer::Generate(program, options);
     if (!result) {
         return "WGSL writer failed:\n" + result.Failure();
     }
@@ -106,7 +106,7 @@
         for (auto* transform_ptr : std::initializer_list<Transform*>{new TRANSFORMS()...}) {
             manager.append(std::unique_ptr<Transform>(transform_ptr));
         }
-        auto result = manager.Run(&program, data, outputs);
+        auto result = manager.Run(program, data, outputs);
         return {std::move(result), std::move(outputs)};
     }
 
@@ -124,7 +124,7 @@
         const Transform& t = TRANSFORM();
 
         DataMap outputs;
-        auto result = t.Apply(&program, data, outputs);
+        auto result = t.Apply(program, data, outputs);
         if (!result) {
             return false;
         }
diff --git a/src/tint/lang/wgsl/ast/transform/manager.cc b/src/tint/lang/wgsl/ast/transform/manager.cc
index 2313bfe..46e24f7 100644
--- a/src/tint/lang/wgsl/ast/transform/manager.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager.cc
@@ -34,7 +34,9 @@
 Manager::Manager() = default;
 Manager::~Manager() = default;
 
-Program Manager::Run(const Program* program, const DataMap& inputs, DataMap& outputs) const {
+Program Manager::Run(const Program& program_in, const DataMap& inputs, DataMap& outputs) const {
+    const Program* program = &program_in;
+
 #if TINT_PRINT_PROGRAM_FOR_EACH_TRANSFORM
     auto print_program = [&](const char* msg, const Transform* transform) {
         auto wgsl = Program::printer(program);
@@ -44,9 +46,9 @@
                   << std::endl;
         std::cout << "=========================================================" << std::endl;
         std::cout << wgsl << std::endl;
-        if (!program->IsValid()) {
+        if (!program.IsValid()) {
             std::cout << "-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --" << std::endl;
-            std::cout << program->Diagnostics().str() << std::endl;
+            std::cout << program.Diagnostics().str() << std::endl;
         }
         std::cout << "=========================================================" << std::endl
                   << std::endl;
@@ -58,7 +60,7 @@
     TINT_IF_PRINT_PROGRAM(print_program("Input of", nullptr));
 
     for (const auto& transform : transforms_) {
-        if (auto result = transform->Apply(program, inputs, outputs)) {
+        if (auto result = transform->Apply(*program, inputs, outputs)) {
             output.emplace(std::move(result.value()));
             program = &output.value();
 
diff --git a/src/tint/lang/wgsl/ast/transform/manager.h b/src/tint/lang/wgsl/ast/transform/manager.h
index babc02f..a9d628f 100644
--- a/src/tint/lang/wgsl/ast/transform/manager.h
+++ b/src/tint/lang/wgsl/ast/transform/manager.h
@@ -54,7 +54,7 @@
     /// @param inputs optional extra transform-specific input data
     /// @param outputs optional extra transform-specific output data
     /// @returns the transformed program
-    Program Run(const Program* program, const DataMap& inputs, DataMap& outputs) const;
+    Program Run(const Program& program, const DataMap& inputs, DataMap& outputs) const;
 
   private:
     std::vector<std::unique_ptr<Transform>> transforms_;
diff --git a/src/tint/lang/wgsl/ast/transform/manager_test.cc b/src/tint/lang/wgsl/ast/transform/manager_test.cc
index 1d8b61a..d146055 100644
--- a/src/tint/lang/wgsl/ast/transform/manager_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/manager_test.cc
@@ -28,15 +28,15 @@
 using TransformManagerTest = testing::Test;
 
 class AST_NoOp final : public ast::transform::Transform {
-    ApplyResult Apply(const Program*, const DataMap&, DataMap&) const override {
+    ApplyResult Apply(const Program&, const DataMap&, DataMap&) const override {
         return SkipTransform;
     }
 };
 
 class AST_AddFunction final : public ast::transform::Transform {
-    ApplyResult Apply(const Program* src, const DataMap&, DataMap&) const override {
+    ApplyResult Apply(const Program& src, const DataMap&, DataMap&) const override {
         ProgramBuilder b;
-        program::CloneContext ctx{&b, src};
+        program::CloneContext ctx{&b, &src};
         b.Func(b.Sym("ast_func"), {}, b.ty.void_(), {});
         ctx.Clone();
         return resolver::Resolve(b);
@@ -57,7 +57,7 @@
     DataMap outputs;
     manager.Add<AST_NoOp>();
 
-    auto result = manager.Run(&ast, {}, outputs);
+    auto result = manager.Run(ast, {}, outputs);
     EXPECT_TRUE(result.IsValid()) << result.Diagnostics();
     EXPECT_NE(result.ID(), ast.ID());
     ASSERT_EQ(result.AST().Functions().Length(), 1u);
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 757ad37..21e5309 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
@@ -35,8 +35,8 @@
 using namespace tint::core::fluent_types;     // NOLINT
 using namespace tint::core::number_suffixes;  // NOLINT
 
-bool ShouldRun(const Program* program) {
-    auto ext = program->Types().Find<core::type::ExternalTexture>();
+bool ShouldRun(const Program& program) {
+    auto ext = program.Types().Find<core::type::ExternalTexture>();
     return ext != nullptr;
 }
 
@@ -511,7 +511,7 @@
 // buffer binding representing a struct of parameters. Calls to texture builtins that contain a
 // texture_external parameter will be transformed into a newly generated version of the function,
 // which can perform the desired operation on a single RGBA plane or on separate Y and UV planes.
-Transform::ApplyResult MultiplanarExternalTexture::Apply(const Program* src,
+Transform::ApplyResult MultiplanarExternalTexture::Apply(const Program& src,
                                                          const DataMap& inputs,
                                                          DataMap&) const {
     auto* new_binding_points = inputs.Get<NewBindingPoints>();
@@ -521,7 +521,7 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     if (!new_binding_points) {
         b.Diagnostics().add_error(diag::System::Transform, "missing new binding point data for " +
                                                                std::string(TypeInfo().name));
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h
index 1c429ce..25e1f9e 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.h
@@ -69,7 +69,7 @@
     ~MultiplanarExternalTexture() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
index 25cba78..9675b03 100644
--- a/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
+++ b/src/tint/lang/wgsl/ast/transform/preserve_padding.cc
@@ -41,7 +41,7 @@
 struct PreservePadding::State {
     /// Constructor
     /// @param src the source Program
-    explicit State(const Program* src) : ctx{&b, src, /* auto_clone_symbols */ true} {}
+    explicit State(const Program& src) : ctx{&b, &src, /* auto_clone_symbols */ true} {}
 
     /// The main function for the transform.
     /// @returns the ApplyResult
@@ -240,7 +240,7 @@
     Hashmap<const core::type::Type*, Symbol, 8> helpers;
 };
 
-Transform::ApplyResult PreservePadding::Apply(const Program* program,
+Transform::ApplyResult PreservePadding::Apply(const Program& program,
                                               const DataMap&,
                                               DataMap&) const {
     return State(program).Run();
diff --git a/src/tint/lang/wgsl/ast/transform/preserve_padding.h b/src/tint/lang/wgsl/ast/transform/preserve_padding.h
index 21c8131..6e1866e 100644
--- a/src/tint/lang/wgsl/ast/transform/preserve_padding.h
+++ b/src/tint/lang/wgsl/ast/transform/preserve_padding.h
@@ -34,7 +34,7 @@
     ~PreservePadding() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 9533281..5b02260 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
@@ -35,11 +35,11 @@
 
 PromoteInitializersToLet::~PromoteInitializersToLet() = default;
 
-Transform::ApplyResult PromoteInitializersToLet::Apply(const Program* src,
+Transform::ApplyResult PromoteInitializersToLet::Apply(const Program& src,
                                                        const DataMap&,
                                                        DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     // Returns true if the expression should be hoisted to a new let statement before the
     // expression's statement.
@@ -89,8 +89,8 @@
     Hashset<const Expression*, 32> const_chains;
 
     // Walk the AST nodes. This order guarantees that leaf-expressions are visited first.
-    for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* sem = src->Sem().GetVal(node)) {
+    for (auto* node : src.ASTNodes().Objects()) {
+        if (auto* sem = src.Sem().GetVal(node)) {
             auto* stmt = sem->Stmt();
             if (!stmt) {
                 // Expression is outside of a statement. This usually means the expression is part
@@ -123,7 +123,7 @@
     // After walking the full AST, const_chains only contains the outer-most constant expressions.
     // Check if any of these need hoisting, and append those to to_hoist.
     for (auto* expr : const_chains) {
-        if (auto* sem = src->Sem().GetVal(expr); should_hoist(sem)) {
+        if (auto* sem = src.Sem().GetVal(expr); should_hoist(sem)) {
             to_hoist.Push(sem);
         }
     }
diff --git a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h
index cb6ecba..2a85e17 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h
+++ b/src/tint/lang/wgsl/ast/transform/promote_initializers_to_let.h
@@ -34,7 +34,7 @@
     ~PromoteInitializersToLet() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 61dcccc..07003b4 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
@@ -56,20 +56,20 @@
 // to else {if}s so that the next transform, DecomposeSideEffects, can insert
 // hoisted expressions above their current location.
 struct SimplifySideEffectStatements : Castable<PromoteSideEffectsToDecl, Transform> {
-    ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override;
+    ApplyResult Apply(const Program& src, const DataMap& inputs, DataMap& outputs) const override;
 };
 
-Transform::ApplyResult SimplifySideEffectStatements::Apply(const Program* src,
+Transform::ApplyResult SimplifySideEffectStatements::Apply(const Program& src,
                                                            const DataMap&,
                                                            DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
 
     HoistToDeclBefore hoist_to_decl_before(ctx);
     for (auto* node : ctx.src->ASTNodes().Objects()) {
-        if (auto* sem_expr = src->Sem().GetVal(node)) {
+        if (auto* sem_expr = src.Sem().GetVal(node)) {
             if (!sem_expr->HasSideEffects()) {
                 continue;
             }
@@ -93,7 +93,7 @@
 struct DecomposeSideEffects : Castable<PromoteSideEffectsToDecl, Transform> {
     class CollectHoistsState;
     class DecomposeState;
-    ApplyResult Apply(const Program* src, const DataMap& inputs, DataMap& outputs) const override;
+    ApplyResult Apply(const Program& src, const DataMap& inputs, DataMap& outputs) const override;
 };
 
 // CollectHoistsState traverses the AST top-down, identifying which expressions
@@ -648,11 +648,11 @@
     }
 };
 
-Transform::ApplyResult DecomposeSideEffects::Apply(const Program* src,
+Transform::ApplyResult DecomposeSideEffects::Apply(const Program& src,
                                                    const DataMap&,
                                                    DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     // First collect side-effecting expressions to hoist
     CollectHoistsState collect_hoists_state{ctx};
@@ -671,7 +671,7 @@
 PromoteSideEffectsToDecl::PromoteSideEffectsToDecl() = default;
 PromoteSideEffectsToDecl::~PromoteSideEffectsToDecl() = default;
 
-Transform::ApplyResult PromoteSideEffectsToDecl::Apply(const Program* src,
+Transform::ApplyResult PromoteSideEffectsToDecl::Apply(const Program& src,
                                                        const DataMap& inputs,
                                                        DataMap& outputs) const {
     Manager manager;
diff --git a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h
index a8c7196..b5ee223 100644
--- a/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h
+++ b/src/tint/lang/wgsl/ast/transform/promote_side_effects_to_decl.h
@@ -32,7 +32,7 @@
     ~PromoteSideEffectsToDecl() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
index 7f7f7c6..5661d78 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_phonies.cc
@@ -43,16 +43,16 @@
 
 RemovePhonies::~RemovePhonies() = default;
 
-Transform::ApplyResult RemovePhonies::Apply(const Program* src, const DataMap&, DataMap&) const {
+Transform::ApplyResult RemovePhonies::Apply(const Program& src, const DataMap&, DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
-    auto& sem = src->Sem();
+    auto& sem = src.Sem();
 
     Hashmap<SinkSignature, Symbol, 8> sinks;
 
     bool made_changes = false;
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         Switch(
             node,
             [&](const AssignmentStatement* stmt) {
diff --git a/src/tint/lang/wgsl/ast/transform/remove_phonies.h b/src/tint/lang/wgsl/ast/transform/remove_phonies.h
index 5c3d04c..17b5657 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_phonies.h
+++ b/src/tint/lang/wgsl/ast/transform/remove_phonies.h
@@ -34,7 +34,7 @@
     ~RemovePhonies() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
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 82bd693..50cafba 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
+++ b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.cc
@@ -38,15 +38,15 @@
 
 RemoveUnreachableStatements::~RemoveUnreachableStatements() = default;
 
-Transform::ApplyResult RemoveUnreachableStatements::Apply(const Program* src,
+Transform::ApplyResult RemoveUnreachableStatements::Apply(const Program& src,
                                                           const DataMap&,
                                                           DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     bool made_changes = false;
-    for (auto* node : src->ASTNodes().Objects()) {
-        if (auto* stmt = src->Sem().Get<sem::Statement>(node)) {
+    for (auto* node : src.ASTNodes().Objects()) {
+        if (auto* stmt = src.Sem().Get<sem::Statement>(node)) {
             if (!stmt->IsReachable()) {
                 RemoveStatement(ctx, stmt->Declaration());
                 made_changes = true;
diff --git a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h
index 4469fc7..57b79e8 100644
--- a/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h
+++ b/src/tint/lang/wgsl/ast/transform/remove_unreachable_statements.h
@@ -33,7 +33,7 @@
     ~RemoveUnreachableStatements() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/renamer.cc b/src/tint/lang/wgsl/ast/transform/renamer.cc
index ea456df..f53c942 100644
--- a/src/tint/lang/wgsl/ast/transform/renamer.cc
+++ b/src/tint/lang/wgsl/ast/transform/renamer.cc
@@ -1258,18 +1258,18 @@
 Renamer::Renamer() = default;
 Renamer::~Renamer() = default;
 
-Transform::ApplyResult Renamer::Apply(const Program* src,
+Transform::ApplyResult Renamer::Apply(const Program& src,
                                       const DataMap& inputs,
                                       DataMap& outputs) const {
     Hashset<Symbol, 16> global_decls;
-    for (auto* decl : src->AST().TypeDecls()) {
+    for (auto* decl : src.AST().TypeDecls()) {
         global_decls.Add(decl->name->symbol);
     }
 
     // Identifiers that need to keep their symbols preserved.
     Hashset<const Identifier*, 16> preserved_identifiers;
 
-    for (auto* node : src->ASTNodes().Objects()) {
+    for (auto* node : src.ASTNodes().Objects()) {
         auto preserve_if_builtin_type = [&](const Identifier* ident) {
             if (!global_decls.Contains(ident->symbol)) {
                 preserved_identifiers.Add(ident);
@@ -1279,10 +1279,10 @@
         Switch(
             node,
             [&](const MemberAccessorExpression* accessor) {
-                auto* sem = src->Sem().Get(accessor)->UnwrapLoad();
+                auto* sem = src.Sem().Get(accessor)->UnwrapLoad();
                 if (sem->Is<sem::Swizzle>()) {
                     preserved_identifiers.Add(accessor->member);
-                } else if (auto* str_expr = src->Sem().GetVal(accessor->object)) {
+                } else if (auto* str_expr = src.Sem().GetVal(accessor->object)) {
                     if (auto* ty = str_expr->Type()->UnwrapRef()->As<core::type::Struct>()) {
                         if (!ty->Is<sem::Struct>()) {  // Builtin structure
                             preserved_identifiers.Add(accessor->member);
@@ -1304,7 +1304,7 @@
             },
             [&](const IdentifierExpression* expr) {
                 Switch(
-                    src->Sem().Get(expr),  //
+                    src.Sem().Get(expr),  //
                     [&](const sem::BuiltinEnumExpressionBase*) {
                         preserved_identifiers.Add(expr->identifier);
                     },
@@ -1314,7 +1314,7 @@
             },
             [&](const CallExpression* call) {
                 Switch(
-                    src->Sem().Get(call)->UnwrapMaterialize()->As<sem::Call>()->Target(),
+                    src.Sem().Get(call)->UnwrapMaterialize()->As<sem::Call>()->Target(),
                     [&](const sem::BuiltinFn*) {
                         preserved_identifiers.Add(call->target->identifier);
                     },
@@ -1372,7 +1372,7 @@
     Hashmap<Symbol, Symbol, 32> remappings;
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ false};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ false};
 
     ctx.ReplaceAll([&](const Identifier* ident) -> const Identifier* {
         const auto symbol = ident->symbol;
diff --git a/src/tint/lang/wgsl/ast/transform/renamer.h b/src/tint/lang/wgsl/ast/transform/renamer.h
index b2520a2..ec81454 100644
--- a/src/tint/lang/wgsl/ast/transform/renamer.h
+++ b/src/tint/lang/wgsl/ast/transform/renamer.h
@@ -86,7 +86,7 @@
     ~Renamer() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.cc b/src/tint/lang/wgsl/ast/transform/robustness.cc
index 89b442a..09b85fd 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness.cc
@@ -47,7 +47,7 @@
     /// Constructor
     /// @param p the source program
     /// @param c the transform config
-    State(const Program* p, Config&& c) : src(p), cfg(std::move(c)) {}
+    State(const Program& p, Config&& c) : src(p), cfg(std::move(c)) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
@@ -204,13 +204,13 @@
 
   private:
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The transform's config
     Config cfg;
     /// The target program builder
     ProgramBuilder b{};
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// Helper for hoisting declarations
     HoistToDeclBefore hoist{ctx};
     /// Alias to the source program's semantic info
@@ -272,7 +272,7 @@
     /// Transform the program to insert additional predicate parameters to all user functions that
     /// have a pointer parameter type in an address space that has predicate action.
     void AddPredicateParameters() {
-        for (auto* fn : src->AST().Functions()) {
+        for (auto* fn : src.AST().Functions()) {
             for (auto* param : fn->params) {
                 auto* sem_param = sem.Get(param);
                 if (auto* ptr = sem_param->Type()->As<core::type::Pointer>()) {
@@ -722,7 +722,7 @@
 Robustness::Robustness() = default;
 Robustness::~Robustness() = default;
 
-Transform::ApplyResult Robustness::Apply(const Program* src,
+Transform::ApplyResult Robustness::Apply(const Program& src,
                                          const DataMap& inputs,
                                          DataMap&) const {
     Config cfg;
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.h b/src/tint/lang/wgsl/ast/transform/robustness.h
index bf8d5be..5ca301f 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.h
+++ b/src/tint/lang/wgsl/ast/transform/robustness.h
@@ -94,7 +94,7 @@
     ~Robustness() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
index 98de628..b5a2181 100644
--- a/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
+++ b/src/tint/lang/wgsl/ast/transform/simplify_pointers.cc
@@ -51,15 +51,15 @@
 /// PIMPL state for the transform
 struct SimplifyPointers::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Traverses the expression `expr` looking for non-literal array indexing
     /// expressions that would affect the computed address of a pointer
@@ -259,7 +259,7 @@
 
 SimplifyPointers::~SimplifyPointers() = default;
 
-Transform::ApplyResult SimplifyPointers::Apply(const Program* src, const DataMap&, DataMap&) const {
+Transform::ApplyResult SimplifyPointers::Apply(const Program& src, const DataMap&, DataMap&) const {
     return State(src).Run();
 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/simplify_pointers.h b/src/tint/lang/wgsl/ast/transform/simplify_pointers.h
index df529c2..59e3a35 100644
--- a/src/tint/lang/wgsl/ast/transform/simplify_pointers.h
+++ b/src/tint/lang/wgsl/ast/transform/simplify_pointers.h
@@ -40,7 +40,7 @@
     ~SimplifyPointers() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 9976562..c61798d 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -33,11 +33,11 @@
 
 SingleEntryPoint::~SingleEntryPoint() = default;
 
-Transform::ApplyResult SingleEntryPoint::Apply(const Program* src,
+Transform::ApplyResult SingleEntryPoint::Apply(const Program& src,
                                                const DataMap& inputs,
                                                DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
@@ -48,7 +48,7 @@
 
     // Find the target entry point.
     const Function* entry_point = nullptr;
-    for (auto* f : src->AST().Functions()) {
+    for (auto* f : src.AST().Functions()) {
         if (!f->IsEntryPoint()) {
             continue;
         }
@@ -63,12 +63,12 @@
         return resolver::Resolve(b);
     }
 
-    auto& sem = src->Sem();
+    auto& sem = src.Sem();
     auto& referenced_vars = sem.Get(entry_point)->TransitivelyReferencedGlobals();
 
     // Clone any module-scope variables, types, and functions that are statically referenced by the
     // target entry point.
-    for (auto* decl : src->AST().GlobalDeclarations()) {
+    for (auto* decl : src.AST().GlobalDeclarations()) {
         Switch(
             decl,  //
             [&](const TypeDecl* ty) {
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.h b/src/tint/lang/wgsl/ast/transform/single_entry_point.h
index 97a99da..4d7f493 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.h
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.h
@@ -54,7 +54,7 @@
     ~SingleEntryPoint() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/std140.cc b/src/tint/lang/wgsl/ast/transform/std140.cc
index fd3193c..38fd81e 100644
--- a/src/tint/lang/wgsl/ast/transform/std140.cc
+++ b/src/tint/lang/wgsl/ast/transform/std140.cc
@@ -71,7 +71,7 @@
 struct Std140::State {
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
@@ -131,7 +131,7 @@
         };
 
         // Scan structures for members that need forking
-        for (auto* ty : src->Types()) {
+        for (auto* ty : src.Types()) {
             if (auto* str = ty->As<core::type::Struct>()) {
                 if (str->UsedAs(core::AddressSpace::kUniform)) {
                     for (auto* member : str->Members()) {
@@ -144,8 +144,8 @@
         }
 
         // Scan uniform variables that have types that need forking
-        for (auto* decl : src->AST().GlobalVariables()) {
-            auto* global = src->Sem().Get(decl);
+        for (auto* decl : src.AST().GlobalVariables()) {
+            auto* global = src.Sem().Get(decl);
             if (global->AddressSpace() == core::AddressSpace::kUniform) {
                 if (needs_fork(global->Type()->UnwrapRef())) {
                     return true;
@@ -194,15 +194,15 @@
     };
 
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in src
-    const sem::Info& sem = src->Sem();
+    const sem::Info& sem = src.Sem();
     /// Alias to the symbols in src
-    const SymbolTable& sym = src->Symbols();
+    const SymbolTable& sym = src.Symbols();
 
     /// Map of load function signature, to the generated function
     Hashmap<LoadFnKey, Symbol, 8, LoadFnKey::Hasher> load_fns;
@@ -266,7 +266,7 @@
     /// map (via Std140Type()).
     void ForkTypes() {
         // For each module scope declaration...
-        for (auto* global : src->Sem().Module()->DependencyOrderedDeclarations()) {
+        for (auto* global : src.Sem().Module()->DependencyOrderedDeclarations()) {
             // Check to see if this is a structure used by a uniform buffer...
             auto* str = sem.Get<sem::Struct>(global);
             if (str && str->UsedAs(core::AddressSpace::kUniform)) {
@@ -316,7 +316,7 @@
                 if (fork_std140) {
                     // Clone any members that have not already been cloned.
                     for (auto& member : members) {
-                        if (member->generation_id == src->ID()) {
+                        if (member->generation_id == src.ID()) {
                             member = ctx.Clone(member);
                         }
                     }
@@ -325,7 +325,7 @@
                     auto name = b.Symbols().New(str->Name().Name() + "_std140");
                     auto* std140 = b.create<Struct>(b.Ident(name), std::move(members),
                                                     ctx.Clone(str->Declaration()->attributes));
-                    ctx.InsertAfter(src->AST().GlobalDeclarations(), global, std140);
+                    ctx.InsertAfter(src.AST().GlobalDeclarations(), global, std140);
                     std140_structs.Add(str, name);
                 }
             }
@@ -336,7 +336,7 @@
     /// type that has been forked for std140-layout.
     /// Populates the #std140_uniforms set.
     void ReplaceUniformVarTypes() {
-        for (auto* global : src->AST().GlobalVariables()) {
+        for (auto* global : src.AST().GlobalVariables()) {
             if (auto* var = global->As<Var>()) {
                 auto* v = sem.Get(var);
                 if (v->AddressSpace() == core::AddressSpace::kUniform) {
@@ -1096,7 +1096,7 @@
                     for (auto el : *swizzle) {
                         rhs += xyzw[el];
                     }
-                    auto swizzle_ty = src->Types().Find<core::type::Vector>(
+                    auto swizzle_ty = src.Types().Find<core::type::Vector>(
                         vec->type(), static_cast<uint32_t>(swizzle->Length()));
                     auto* expr = b.MemberAccessor(lhs, rhs);
                     return {expr, swizzle_ty, rhs};
@@ -1140,7 +1140,7 @@
 
 Std140::~Std140() = default;
 
-Transform::ApplyResult Std140::Apply(const Program* src, const DataMap&, DataMap&) const {
+Transform::ApplyResult Std140::Apply(const Program& src, const DataMap&, DataMap&) const {
     return State(src).Run();
 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/std140.h b/src/tint/lang/wgsl/ast/transform/std140.h
index 8b60728..20e5a02 100644
--- a/src/tint/lang/wgsl/ast/transform/std140.h
+++ b/src/tint/lang/wgsl/ast/transform/std140.h
@@ -36,7 +36,7 @@
     ~Std140() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.cc b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
index 0ffcafb..9f41800 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.cc
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
@@ -35,8 +35,8 @@
 namespace tint::ast::transform {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->AST().GlobalVariables()) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.AST().GlobalVariables()) {
         if (node->Is<Override>()) {
             return true;
         }
@@ -50,11 +50,11 @@
 
 SubstituteOverride::~SubstituteOverride() = default;
 
-Transform::ApplyResult SubstituteOverride::Apply(const Program* src,
+Transform::ApplyResult SubstituteOverride::Apply(const Program& src,
                                                  const DataMap& config,
                                                  DataMap&) const {
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     const auto* data = config.Get<Config>();
     if (!data) {
@@ -62,7 +62,7 @@
         return resolver::Resolve(b);
     }
 
-    if (!ShouldRun(ctx.src)) {
+    if (!ShouldRun(src)) {
         return SkipTransform;
     }
 
@@ -107,7 +107,7 @@
     // If the object is not materialized, and the 'override' variable is turned to a 'const', the
     // resulting type of the index may change. See: crbug.com/tint/1697.
     ctx.ReplaceAll([&](const IndexAccessorExpression* expr) -> const IndexAccessorExpression* {
-        if (auto* sem = src->Sem().Get(expr)) {
+        if (auto* sem = src.Sem().Get(expr)) {
             if (auto* access = sem->UnwrapMaterialize()->As<sem::IndexAccessorExpression>()) {
                 if (access->Object()->UnwrapMaterialize()->Type()->HoldsAbstract() &&
                     access->Index()->Stage() == core::EvaluationStage::kOverride) {
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.h b/src/tint/lang/wgsl/ast/transform/substitute_override.h
index 2108dca..e234450 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.h
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.h
@@ -76,7 +76,7 @@
     ~SubstituteOverride() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/transform.cc b/src/tint/lang/wgsl/ast/transform/transform.cc
index 63d0cea..781f2f5 100644
--- a/src/tint/lang/wgsl/ast/transform/transform.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform.cc
@@ -41,13 +41,13 @@
 Transform::Transform() = default;
 Transform::~Transform() = default;
 
-Output Transform::Run(const Program* src, const DataMap& data /* = {} */) const {
+Output Transform::Run(const Program& src, const DataMap& data /* = {} */) const {
     Output output;
     if (auto program = Apply(src, data, output.data)) {
         output.program = std::move(program.value());
     } else {
         ProgramBuilder b;
-        program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+        program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
         ctx.Clone();
         output.program = resolver::Resolve(b);
     }
diff --git a/src/tint/lang/wgsl/ast/transform/transform.h b/src/tint/lang/wgsl/ast/transform/transform.h
index 8e7400c..e0c25e2 100644
--- a/src/tint/lang/wgsl/ast/transform/transform.h
+++ b/src/tint/lang/wgsl/ast/transform/transform.h
@@ -66,7 +66,7 @@
     /// @param program the source program to transform
     /// @param data optional extra transform-specific input data
     /// @returns the transformation result
-    Output Run(const Program* program, const DataMap& data = {}) const;
+    Output Run(const Program& program, const DataMap& data = {}) const;
 
     /// The return value of Apply().
     /// If SkipTransform (std::nullopt), then the transform is not needed to be run.
@@ -80,7 +80,7 @@
     /// @param inputs optional extra transform-specific input data
     /// @param outputs optional extra transform-specific output data
     /// @returns a transformed program, or std::nullopt if the transform didn't need to run.
-    virtual ApplyResult Apply(const Program* program,
+    virtual ApplyResult Apply(const Program& program,
                               const DataMap& inputs,
                               DataMap& outputs) const = 0;
 
diff --git a/src/tint/lang/wgsl/ast/transform/transform_test.cc b/src/tint/lang/wgsl/ast/transform/transform_test.cc
index 46c5f24..9efe65e 100644
--- a/src/tint/lang/wgsl/ast/transform/transform_test.cc
+++ b/src/tint/lang/wgsl/ast/transform/transform_test.cc
@@ -29,7 +29,7 @@
 
 // Inherit from Transform so we have access to protected methods
 struct CreateASTTypeForTest : public testing::Test, public Transform {
-    ApplyResult Apply(const Program*, const DataMap&, DataMap&) const override {
+    ApplyResult Apply(const Program&, const DataMap&, DataMap&) const override {
         return SkipTransform;
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/unshadow.cc b/src/tint/lang/wgsl/ast/transform/unshadow.cc
index e6655bb..4ff846d 100644
--- a/src/tint/lang/wgsl/ast/transform/unshadow.cc
+++ b/src/tint/lang/wgsl/ast/transform/unshadow.cc
@@ -34,20 +34,20 @@
 /// PIMPL state for the transform
 struct Unshadow::State {
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
 
     /// Constructor
     /// @param program the source program
-    explicit State(const Program* program) : src(program) {}
+    explicit State(const Program& program) : src(program) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     Transform::ApplyResult Run() {
-        auto& sem = src->Sem();
+        auto& sem = src.Sem();
 
         // Maps a variable to its new name.
         Hashmap<const sem::Variable*, Symbol, 8> renamed_to;
@@ -124,7 +124,7 @@
 
 Unshadow::~Unshadow() = default;
 
-Transform::ApplyResult Unshadow::Apply(const Program* src, const DataMap&, DataMap&) const {
+Transform::ApplyResult Unshadow::Apply(const Program& src, const DataMap&, DataMap&) const {
     return State(src).Run();
 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/unshadow.h b/src/tint/lang/wgsl/ast/transform/unshadow.h
index 77640cd..119c37c 100644
--- a/src/tint/lang/wgsl/ast/transform/unshadow.h
+++ b/src/tint/lang/wgsl/ast/transform/unshadow.h
@@ -29,7 +29,7 @@
     ~Unshadow() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 d670b91..2168218 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
@@ -31,9 +31,9 @@
 namespace tint::ast::transform {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* node : program->ASTNodes().Objects()) {
-        if (auto* call = program->Sem().Get<sem::Call>(node)) {
+bool ShouldRun(const Program& program) {
+    for (auto* node : program.ASTNodes().Objects()) {
+        if (auto* call = program.Sem().Get<sem::Call>(node)) {
             if (call->Target()->Is<sem::ValueConstructor>() &&
                 call->Type()->Is<core::type::Matrix>()) {
                 auto& args = call->Arguments();
@@ -52,7 +52,7 @@
 
 VectorizeScalarMatrixInitializers::~VectorizeScalarMatrixInitializers() = default;
 
-Transform::ApplyResult VectorizeScalarMatrixInitializers::Apply(const Program* src,
+Transform::ApplyResult VectorizeScalarMatrixInitializers::Apply(const Program& src,
                                                                 const DataMap&,
                                                                 DataMap&) const {
     if (!ShouldRun(src)) {
@@ -60,12 +60,12 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
     std::unordered_map<const core::type::Matrix*, Symbol> scalar_inits;
 
     ctx.ReplaceAll([&](const CallExpression* expr) -> const CallExpression* {
-        auto* call = src->Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
+        auto* call = src.Sem().Get(expr)->UnwrapMaterialize()->As<sem::Call>();
         auto* ty_init = call->Target()->As<sem::ValueConstructor>();
         if (!ty_init) {
             return nullptr;
diff --git a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h
index fc02609..4c49769 100644
--- a/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h
+++ b/src/tint/lang/wgsl/ast/transform/vectorize_scalar_matrix_initializers.h
@@ -30,7 +30,7 @@
     ~VectorizeScalarMatrixInitializers() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 };
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index e91dd35..cc747fb 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -235,14 +235,14 @@
     /// Constructor
     /// @param program the source program
     /// @param c the VertexPulling config
-    State(const Program* program, const VertexPulling::Config& c) : src(program), cfg(c) {}
+    State(const Program& program, const VertexPulling::Config& c) : src(program), cfg(c) {}
 
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
         // Find entry point
         const Function* func = nullptr;
-        for (auto* fn : src->AST().Functions()) {
+        for (auto* fn : src.AST().Functions()) {
             if (fn->PipelineStage() == PipelineStage::kVertex) {
                 if (func != nullptr) {
                     b.Diagnostics().add_error(
@@ -284,13 +284,13 @@
     };
 
     /// The source program
-    const Program* const src;
+    const Program& src;
     /// The transform config
     VertexPulling::Config const cfg;
     /// The target program builder
     ProgramBuilder b;
     /// The clone context
-    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx = {&b, &src, /* auto_clone_symbols */ true};
     std::unordered_map<uint32_t, LocationInfo> location_info;
     std::function<const Expression*()> vertex_index_expr = nullptr;
     std::function<const Expression*()> instance_index_expr = nullptr;
@@ -768,7 +768,7 @@
             LocationInfo info;
             info.expr = [this, func_var] { return b.Expr(func_var); };
 
-            auto* sem = src->Sem().Get<sem::Parameter>(param);
+            auto* sem = src.Sem().Get<sem::Parameter>(param);
             info.type = sem->Type();
 
             if (TINT_UNLIKELY(!sem->Location().has_value())) {
@@ -782,7 +782,7 @@
                 TINT_ICE() << "Invalid entry point parameter";
                 return;
             }
-            auto builtin = src->Sem().Get(builtin_attr)->Value();
+            auto builtin = src.Sem().Get(builtin_attr)->Value();
             // Check for existing vertex_index and instance_index builtins.
             if (builtin == core::BuiltinValue::kVertexIndex) {
                 vertex_index_expr = [this, param] {
@@ -824,7 +824,7 @@
                 LocationInfo info;
                 info.expr = member_expr;
 
-                auto* sem = src->Sem().Get(member);
+                auto* sem = src.Sem().Get(member);
                 info.type = sem->Type();
 
                 TINT_ASSERT(sem->Attributes().location.has_value());
@@ -836,7 +836,7 @@
                     TINT_ICE() << "Invalid entry point parameter";
                     return;
                 }
-                auto builtin = src->Sem().Get(builtin_attr)->Value();
+                auto builtin = src.Sem().Get(builtin_attr)->Value();
                 // Check for existing vertex_index and instance_index builtins.
                 if (builtin == core::BuiltinValue::kVertexIndex) {
                     vertex_index_expr = member_expr;
@@ -891,7 +891,7 @@
 
         // Process entry point parameters.
         for (auto* param : func->params) {
-            auto* sem = src->Sem().Get(param);
+            auto* sem = src.Sem().Get(param);
             if (auto* str = sem->Type()->As<sem::Struct>()) {
                 ProcessStructParameter(func, param, str->Declaration());
             } else {
@@ -946,7 +946,7 @@
 VertexPulling::VertexPulling() = default;
 VertexPulling::~VertexPulling() = default;
 
-Transform::ApplyResult VertexPulling::Apply(const Program* src,
+Transform::ApplyResult VertexPulling::Apply(const Program& src,
                                             const DataMap& inputs,
                                             DataMap&) const {
     auto cfg = cfg_;
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.h b/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
index d18bb0f..deaed5a 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.h
@@ -172,7 +172,7 @@
     ~VertexPulling() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
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 a14d218..39ea897 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
@@ -39,10 +39,10 @@
 namespace tint::ast::transform {
 namespace {
 
-bool ShouldRun(const Program* program) {
-    for (auto* global : program->AST().GlobalVariables()) {
+bool ShouldRun(const Program& program) {
+    for (auto* global : program.AST().GlobalVariables()) {
         if (auto* var = global->As<Var>()) {
-            auto* v = program->Sem().Get(var);
+            auto* v = program.Sem().Get(var);
             if (v->AddressSpace() == core::AddressSpace::kWorkgroup) {
                 return true;
             }
@@ -461,7 +461,7 @@
 
 ZeroInitWorkgroupMemory::~ZeroInitWorkgroupMemory() = default;
 
-Transform::ApplyResult ZeroInitWorkgroupMemory::Apply(const Program* src,
+Transform::ApplyResult ZeroInitWorkgroupMemory::Apply(const Program& src,
                                                       const DataMap&,
                                                       DataMap&) const {
     if (!ShouldRun(src)) {
@@ -469,9 +469,9 @@
     }
 
     ProgramBuilder b;
-    program::CloneContext ctx{&b, src, /* auto_clone_symbols */ true};
+    program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
 
-    for (auto* fn : src->AST().Functions()) {
+    for (auto* fn : src.AST().Functions()) {
         if (fn->PipelineStage() == PipelineStage::kCompute) {
             State{ctx}.Run(fn);
         }
diff --git a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h
index cb3de9a..3d12132 100644
--- a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h
+++ b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h
@@ -31,7 +31,7 @@
     ~ZeroInitWorkgroupMemory() override;
 
     /// @copydoc Transform::Apply
-    ApplyResult Apply(const Program* program,
+    ApplyResult Apply(const Program& program,
                       const DataMap& inputs,
                       DataMap& outputs) const override;
 
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings.cc b/src/tint/lang/wgsl/helpers/flatten_bindings.cc
index 7e6d6c5..3400dd9 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings.cc
@@ -23,7 +23,7 @@
 
 namespace tint::writer {
 
-std::optional<Program> FlattenBindings(const Program* program) {
+std::optional<Program> FlattenBindings(const Program& program) {
     // TODO(crbug.com/tint/1101): Make this more robust for multiple entry points.
     tint::ast::transform::BindingRemapper::BindingPoints binding_points;
     uint32_t next_buffer_idx = 0;
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings.h b/src/tint/lang/wgsl/helpers/flatten_bindings.h
index 99d75e0..be2ca44 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings.h
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings.h
@@ -24,7 +24,7 @@
 /// group 0 within unique binding numbers.
 /// @param program A valid program
 /// @return A new program with bindings remapped if needed
-std::optional<Program> FlattenBindings(const Program* program);
+std::optional<Program> FlattenBindings(const Program& program);
 
 }  // namespace tint::writer
 
diff --git a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
index 201e254..a9b43ca 100644
--- a/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
+++ b/src/tint/lang/wgsl/helpers/flatten_bindings_test.cc
@@ -34,7 +34,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
-    auto flattened = tint::writer::FlattenBindings(&program);
+    auto flattened = tint::writer::FlattenBindings(program);
     EXPECT_FALSE(flattened);
 }
 
@@ -47,7 +47,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
-    auto flattened = tint::writer::FlattenBindings(&program);
+    auto flattened = tint::writer::FlattenBindings(program);
     EXPECT_FALSE(flattened);
 }
 
@@ -61,7 +61,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
-    auto flattened = tint::writer::FlattenBindings(&program);
+    auto flattened = tint::writer::FlattenBindings(program);
     EXPECT_TRUE(flattened);
 
     auto& vars = flattened->AST().GlobalVariables();
@@ -123,7 +123,7 @@
     Program program(resolver::Resolve(b));
     ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
 
-    auto flattened = tint::writer::FlattenBindings(&program);
+    auto flattened = tint::writer::FlattenBindings(program);
     EXPECT_TRUE(flattened);
 
     auto& vars = flattened->AST().GlobalVariables();
diff --git a/src/tint/lang/wgsl/helpers/ir_program_test.h b/src/tint/lang/wgsl/helpers/ir_program_test.h
index d1b96dd..63bdd7f 100644
--- a/src/tint/lang/wgsl/helpers/ir_program_test.h
+++ b/src/tint/lang/wgsl/helpers/ir_program_test.h
@@ -46,7 +46,7 @@
             return program.Diagnostics().str();
         }
 
-        auto result = wgsl::reader::ProgramToIR(&program);
+        auto result = wgsl::reader::ProgramToIR(program);
         if (result) {
             auto validated = core::ir::Validate(result.Get());
             if (!validated) {
@@ -67,7 +67,7 @@
             return program.Diagnostics().str();
         }
 
-        auto result = wgsl::reader::ProgramToIR(&program);
+        auto result = wgsl::reader::ProgramToIR(program);
         if (result) {
             auto validated = core::ir::Validate(result.Get());
             if (!validated) {
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index ee3dd29..dd12745 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -122,7 +122,7 @@
 
 }  // namespace
 
-Inspector::Inspector(const Program* program) : program_(program) {}
+Inspector::Inspector(const Program& program) : program_(program) {}
 
 Inspector::~Inspector() = default;
 
@@ -131,7 +131,7 @@
     TINT_ASSERT(func != nullptr);
     TINT_ASSERT(func->IsEntryPoint());
 
-    auto* sem = program_->Sem().Get(func);
+    auto* sem = program_.Sem().Get(func);
 
     entry_point.name = func->name->symbol.Name();
     entry_point.remapped_name = func->name->symbol.Name();
@@ -245,7 +245,7 @@
 std::vector<EntryPoint> Inspector::GetEntryPoints() {
     std::vector<EntryPoint> result;
 
-    for (auto* func : program_->AST().Functions()) {
+    for (auto* func : program_.AST().Functions()) {
         if (!func->IsEntryPoint()) {
             continue;
         }
@@ -258,8 +258,8 @@
 
 std::map<OverrideId, Scalar> Inspector::GetOverrideDefaultValues() {
     std::map<OverrideId, Scalar> result;
-    for (auto* var : program_->AST().GlobalVariables()) {
-        auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    for (auto* var : program_.AST().GlobalVariables()) {
+        auto* global = program_.Sem().Get<sem::GlobalVariable>(var);
         if (!global || !global->Declaration()->Is<ast::Override>()) {
             continue;
         }
@@ -298,8 +298,8 @@
 
 std::map<std::string, OverrideId> Inspector::GetNamedOverrideIds() {
     std::map<std::string, OverrideId> result;
-    for (auto* var : program_->AST().GlobalVariables()) {
-        auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    for (auto* var : program_.AST().GlobalVariables()) {
+        auto* global = program_.Sem().Get<sem::GlobalVariable>(var);
         if (global && global->Declaration()->Is<ast::Override>()) {
             auto name = var->name->symbol.Name();
             result[name] = global->OverrideId();
@@ -342,7 +342,7 @@
 
     std::vector<ResourceBinding> result;
 
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (auto& ruv : func_sem->TransitivelyReferencedUniformVariables()) {
         auto* var = ruv.first;
         auto binding_info = ruv.second;
@@ -385,7 +385,7 @@
 
     std::vector<ResourceBinding> result;
 
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (auto& rs : func_sem->TransitivelyReferencedSamplerVariables()) {
         auto binding_info = rs.second;
 
@@ -409,7 +409,7 @@
 
     std::vector<ResourceBinding> result;
 
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (auto& rcs : func_sem->TransitivelyReferencedComparisonSamplerVariables()) {
         auto binding_info = rcs.second;
 
@@ -449,7 +449,7 @@
     }
 
     std::vector<ResourceBinding> result;
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (auto& ref : func_sem->TransitivelyReferencedVariablesOfType(texture_type)) {
         auto* var = ref.first;
         auto binding_info = ref.second;
@@ -509,7 +509,7 @@
     if (!func) {
         return {};
     }
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
 
     std::vector<SamplerTexturePair> new_pairs;
     for (auto pair : func_sem->TextureSamplerPairs()) {
@@ -524,7 +524,7 @@
 }
 
 std::vector<std::string> Inspector::GetUsedExtensionNames() {
-    auto& extensions = program_->Sem().Module()->Extensions();
+    auto& extensions = program_.Sem().Module()->Extensions();
     std::vector<std::string> out;
     out.reserve(extensions.Length());
     for (auto ext : extensions) {
@@ -537,7 +537,7 @@
     std::vector<std::pair<std::string, Source>> result;
 
     // Ast nodes for enable directive are stored within global declarations list
-    auto global_decls = program_->AST().GlobalDeclarations();
+    auto global_decls = program_.AST().GlobalDeclarations();
     for (auto* node : global_decls) {
         if (auto* enable = node->As<ast::Enable>()) {
             for (auto* ext : enable->extensions) {
@@ -550,7 +550,7 @@
 }
 
 const ast::Function* Inspector::FindEntryPointByName(const std::string& name) {
-    auto* func = program_->AST().Functions().Find(program_->Symbols().Get(name));
+    auto* func = program_.AST().Functions().Find(program_.Symbols().Get(name));
     if (!func) {
         diagnostics_.add_error(diag::System::Inspector, name + " was not found!");
         return nullptr;
@@ -623,7 +623,7 @@
     if (!builtin_declaration) {
         return false;
     }
-    return program_->Sem().Get(builtin_declaration)->Value() == builtin;
+    return program_.Sem().Get(builtin_declaration)->Value() == builtin;
 }
 
 std::vector<ResourceBinding> Inspector::GetStorageBufferResourceBindingsImpl(
@@ -634,7 +634,7 @@
         return {};
     }
 
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     std::vector<ResourceBinding> result;
     for (auto& rsv : func_sem->TransitivelyReferencedStorageBufferVariables()) {
         auto* var = rsv.first;
@@ -673,7 +673,7 @@
     }
 
     std::vector<ResourceBinding> result;
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     auto referenced_variables = multisampled_only
                                     ? func_sem->TransitivelyReferencedMultisampledTextureVariables()
                                     : func_sem->TransitivelyReferencedSampledTextureVariables();
@@ -712,7 +712,7 @@
         return {};
     }
 
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     std::vector<ResourceBinding> result;
     for (auto& ref :
          func_sem->TransitivelyReferencedVariablesOfType<core::type::StorageTexture>()) {
@@ -761,9 +761,9 @@
     sampler_targets_ =
         std::make_unique<std::unordered_map<std::string, UniqueVector<SamplerTexturePair, 4>>>();
 
-    auto& sem = program_->Sem();
+    auto& sem = program_.Sem();
 
-    for (auto* node : program_->ASTNodes().Objects()) {
+    for (auto* node : program_.ASTNodes().Objects()) {
         auto* c = node->As<ast::CallExpression>();
         if (!c) {
             continue;
@@ -832,7 +832,7 @@
         return {InterpolationType::kPerspective, InterpolationSampling::kCenter};
     }
 
-    auto& sem = program_->Sem();
+    auto& sem = program_.Sem();
 
     auto ast_interpolation_type =
         sem.Get<sem::BuiltinEnumExpression<core::InterpolationType>>(interpolation_attribute->type)
@@ -886,7 +886,7 @@
 
 uint32_t Inspector::ComputeWorkgroupStorageSize(const ast::Function* func) const {
     uint32_t total_size = 0;
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
         if (var->AddressSpace() == core::AddressSpace::kWorkgroup) {
             auto* ty = var->Type()->UnwrapRef();
@@ -906,7 +906,7 @@
 
 std::vector<PixelLocalMemberType> Inspector::ComputePixelLocalMemberTypes(
     const ast::Function* func) const {
-    auto* func_sem = program_->Sem().Get(func);
+    auto* func_sem = program_.Sem().Get(func);
     for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
         if (var->AddressSpace() != core::AddressSpace::kPixelLocal) {
             continue;
@@ -937,12 +937,12 @@
 
 template <size_t N, typename F>
 void Inspector::GetOriginatingResources(std::array<const ast::Expression*, N> exprs, F&& callback) {
-    if (TINT_UNLIKELY(!program_->IsValid())) {
+    if (TINT_UNLIKELY(!program_.IsValid())) {
         TINT_ICE() << "attempting to get originating resources in invalid program";
         return;
     }
 
-    auto& sem = program_->Sem();
+    auto& sem = program_.Sem();
 
     std::array<const sem::GlobalVariable*, N> globals{};
     std::array<const sem::Parameter*, N> parameters{};
diff --git a/src/tint/lang/wgsl/inspector/inspector.h b/src/tint/lang/wgsl/inspector/inspector.h
index e1f6d6e..e4b78e2 100644
--- a/src/tint/lang/wgsl/inspector/inspector.h
+++ b/src/tint/lang/wgsl/inspector/inspector.h
@@ -43,7 +43,7 @@
   public:
     /// Constructor
     /// @param program Shader program to extract information from.
-    explicit Inspector(const Program* program);
+    explicit Inspector(const Program& program);
 
     /// Destructor
     ~Inspector();
@@ -144,7 +144,7 @@
     std::vector<std::pair<std::string, Source>> GetEnableDirectives();
 
   private:
-    const Program* program_;
+    const Program& program_;
     diag::List diagnostics_;
     std::unique_ptr<std::unordered_map<std::string, UniqueVector<SamplerTexturePair, 4>>>
         sampler_targets_;
diff --git a/src/tint/lang/wgsl/inspector/inspector_builder_test.cc b/src/tint/lang/wgsl/inspector/inspector_builder_test.cc
index 6e960a4..58f781f 100644
--- a/src/tint/lang/wgsl/inspector/inspector_builder_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_builder_test.cc
@@ -357,7 +357,7 @@
     }
     program_ = std::make_unique<Program>(resolver::Resolve(*this));
     [&] { ASSERT_TRUE(program_->IsValid()) << program_->Diagnostics().str(); }();
-    inspector_ = std::make_unique<Inspector>(program_.get());
+    inspector_ = std::make_unique<Inspector>(*program_);
     return *inspector_;
 }
 
diff --git a/src/tint/lang/wgsl/inspector/inspector_runner_test.cc b/src/tint/lang/wgsl/inspector/inspector_runner_test.cc
index 8993320..b249358 100644
--- a/src/tint/lang/wgsl/inspector/inspector_runner_test.cc
+++ b/src/tint/lang/wgsl/inspector/inspector_runner_test.cc
@@ -28,7 +28,7 @@
     file_ = std::make_unique<Source::File>("test", shader);
     program_ = std::make_unique<Program>(wgsl::reader::Parse(file_.get()));
     [&] { ASSERT_TRUE(program_->IsValid()) << program_->Diagnostics().str(); }();
-    inspector_ = std::make_unique<Inspector>(program_.get());
+    inspector_ = std::make_unique<Inspector>(*program_);
     return *inspector_;
 }
 
diff --git a/src/tint/lang/wgsl/ir_roundtrip_test.cc b/src/tint/lang/wgsl/ir_roundtrip_test.cc
index 8f08426..2ca1090 100644
--- a/src/tint/lang/wgsl/ir_roundtrip_test.cc
+++ b/src/tint/lang/wgsl/ir_roundtrip_test.cc
@@ -34,7 +34,7 @@
         auto input_program = wgsl::reader::Parse(&file);
         ASSERT_TRUE(input_program.IsValid()) << input_program.Diagnostics().str();
 
-        auto ir_module = wgsl::reader::ProgramToIR(&input_program);
+        auto ir_module = wgsl::reader::ProgramToIR(input_program);
         ASSERT_TRUE(ir_module) << (ir_module ? "" : ir_module.Failure());
 
         tint::core::ir::Disassembler d{ir_module.Get()};
@@ -46,12 +46,12 @@
                    << "IR:" << std::endl                               //
                    << disassembly << std::endl                         //
                    << "AST:" << std::endl                              //
-                   << Program::printer(&output_program) << std::endl;
+                   << Program::printer(output_program) << std::endl;
         }
 
         ASSERT_TRUE(output_program.IsValid()) << output_program.Diagnostics().str();
 
-        auto output = wgsl::writer::Generate(&output_program, {});
+        auto output = wgsl::writer::Generate(output_program, {});
         ASSERT_TRUE(output) << output.Failure();
 
         auto expected = expected_wgsl.empty() ? input : tint::TrimSpace(expected_wgsl);
diff --git a/src/tint/lang/wgsl/program/program.cc b/src/tint/lang/wgsl/program/program.cc
index 5c058c9..cb1ce95 100644
--- a/src/tint/lang/wgsl/program/program.cc
+++ b/src/tint/lang/wgsl/program/program.cc
@@ -24,7 +24,7 @@
 namespace tint {
 namespace {
 
-std::string DefaultPrinter(const Program*) {
+std::string DefaultPrinter(const Program&) {
     return "<no program printer assigned>";
 }
 
diff --git a/src/tint/lang/wgsl/program/program.h b/src/tint/lang/wgsl/program/program.h
index a367412..2674f58 100644
--- a/src/tint/lang/wgsl/program/program.h
+++ b/src/tint/lang/wgsl/program/program.h
@@ -152,7 +152,7 @@
     const core::type::Type* TypeOf(const ast::TypeDecl* type_decl) const;
 
     /// A function that can be used to print a program
-    using Printer = std::string (*)(const Program*);
+    using Printer = std::string (*)(const Program&);
 
     /// The Program printer used for testing and debugging.
     static Printer printer;
@@ -178,8 +178,8 @@
 
 /// @param program the Program
 /// @returns the GenerationID of the Program
-inline GenerationID GenerationIDOf(const Program* program) {
-    return program->ID();
+inline GenerationID GenerationIDOf(const Program& program) {
+    return program.ID();
 }
 
 }  // namespace tint
diff --git a/src/tint/lang/wgsl/program/program_builder.cc b/src/tint/lang/wgsl/program/program_builder.cc
index ff7b469..53cd098 100644
--- a/src/tint/lang/wgsl/program/program_builder.cc
+++ b/src/tint/lang/wgsl/program/program_builder.cc
@@ -46,16 +46,16 @@
     return *this;
 }
 
-ProgramBuilder ProgramBuilder::Wrap(const Program* program) {
+ProgramBuilder ProgramBuilder::Wrap(const Program& program) {
     ProgramBuilder builder;
-    builder.id_ = program->ID();
-    builder.last_ast_node_id_ = program->HighestASTNodeID();
-    builder.constants = core::constant::Manager::Wrap(program->Constants());
+    builder.id_ = program.ID();
+    builder.last_ast_node_id_ = program.HighestASTNodeID();
+    builder.constants = core::constant::Manager::Wrap(program.Constants());
     builder.ast_ =
-        builder.create<ast::Module>(program->AST().source, program->AST().GlobalDeclarations());
-    builder.sem_ = sem::Info::Wrap(program->Sem());
-    builder.symbols_.Wrap(program->Symbols());
-    builder.diagnostics_ = program->Diagnostics();
+        builder.create<ast::Module>(program.AST().source, program.AST().GlobalDeclarations());
+    builder.sem_ = sem::Info::Wrap(program.Sem());
+    builder.symbols_.Wrap(program.Symbols());
+    builder.diagnostics_ = program.Diagnostics();
     return builder;
 }
 
diff --git a/src/tint/lang/wgsl/program/program_builder.h b/src/tint/lang/wgsl/program/program_builder.h
index 81fdfc7..3b17967 100644
--- a/src/tint/lang/wgsl/program/program_builder.h
+++ b/src/tint/lang/wgsl/program/program_builder.h
@@ -90,7 +90,7 @@
     /// function. See crbug.com/tint/460.
     /// @param program the immutable Program to wrap
     /// @return the ProgramBuilder that wraps `program`
-    static ProgramBuilder Wrap(const Program* program);
+    static ProgramBuilder Wrap(const Program& program);
 
     /// @returns a reference to the program's types
     core::type::Manager& Types() {
diff --git a/src/tint/lang/wgsl/program/program_builder_test.cc b/src/tint/lang/wgsl/program/program_builder_test.cc
index b887ff6..c1c3099 100644
--- a/src/tint/lang/wgsl/program/program_builder_test.cc
+++ b/src/tint/lang/wgsl/program/program_builder_test.cc
@@ -42,7 +42,7 @@
     ASSERT_TRUE(inner.Symbols().Get("a").IsValid());
     ASSERT_FALSE(inner.Symbols().Get("b").IsValid());
 
-    ProgramBuilder outer = ProgramBuilder::Wrap(&inner);
+    ProgramBuilder outer = ProgramBuilder::Wrap(inner);
 
     ASSERT_EQ(inner.AST().Functions().Length(), 1u);
     ASSERT_EQ(outer.AST().Functions().Length(), 1u);
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 49babb3..fa464bb 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -118,7 +118,7 @@
   public:
     /// Constructor
     /// @param program the program to convert to IR
-    explicit Impl(const Program* program) : program_(program) {}
+    explicit Impl(const Program& program) : program_(program) {}
 
     /// Builds an IR module from the program passed to the constructor.
     /// @return the IR module or an error.
@@ -128,7 +128,7 @@
     enum class ControlFlags { kNone, kExcludeSwitch };
 
     // The input Program
-    const Program* program_ = nullptr;
+    const Program& program_;
 
     /// The IR module being built
     core::ir::Module mod;
@@ -139,7 +139,7 @@
     // The clone context used to clone data from #program_
     core::constant::CloneContext clone_ctx_{
         /* type_ctx */ core::type::CloneContext{
-            /* src */ {&program_->Symbols()},
+            /* src */ {&program_.Symbols()},
             /* dst */ {&builder_.ir.symbols, &builder_.ir.Types()},
         },
         /* dst */ {builder_.ir.constant_values},
@@ -216,7 +216,7 @@
     }
 
     ResultType EmitModule() {
-        auto* sem = program_->Sem().Module();
+        auto* sem = program_.Sem().Module();
 
         for (auto* decl : sem->DependencyOrderedDeclarations()) {
             tint::Switch(
@@ -258,7 +258,7 @@
     }
 
     core::Interpolation ExtractInterpolation(const ast::InterpolateAttribute* interp) {
-        auto type = program_->Sem()
+        auto type = program_.Sem()
                         .Get(interp->type)
                         ->As<sem::BuiltinEnumExpression<core::InterpolationType>>();
         core::InterpolationType interpolation_type = type->Value();
@@ -266,7 +266,7 @@
         core::InterpolationSampling interpolation_sampling =
             core::InterpolationSampling::kUndefined;
         if (interp->sampling) {
-            auto sampling = program_->Sem()
+            auto sampling = program_.Sem()
                                 .Get(interp->sampling)
                                 ->As<sem::BuiltinEnumExpression<core::InterpolationSampling>>();
             interpolation_sampling = sampling->Value();
@@ -279,7 +279,7 @@
         // The flow stack should have been emptied when the previous function finished building.
         TINT_ASSERT(control_stack_.IsEmpty());
 
-        const auto* sem = program_->Sem().Get(ast_func);
+        const auto* sem = program_.Sem().Get(ast_func);
 
         auto* ir_func = builder_.Function(ast_func->name->symbol.NameView(),
                                           sem->ReturnType()->Clone(clone_ctx_.type_ctx));
@@ -321,7 +321,7 @@
                     [&](const ast::InvariantAttribute*) { ir_func->SetReturnInvariant(true); },
                     [&](const ast::BuiltinAttribute* b) {
                         if (auto* ident_sem =
-                                program_->Sem()
+                                program_.Sem()
                                     .Get(b)
                                     ->As<sem::BuiltinEnumExpression<core::BuiltinValue>>()) {
                             switch (ident_sem->Value()) {
@@ -358,7 +358,7 @@
 
         Vector<core::ir::FunctionParam*, 1> params;
         for (auto* p : ast_func->params) {
-            const auto* param_sem = program_->Sem().Get(p)->As<sem::Parameter>();
+            const auto* param_sem = program_.Sem().Get(p)->As<sem::Parameter>();
             auto* ty = param_sem->Type()->Clone(clone_ctx_.type_ctx);
             auto* param = builder_.FunctionParam(p->name->symbol.NameView(), ty);
 
@@ -374,7 +374,7 @@
                     [&](const ast::InvariantAttribute*) { param->SetInvariant(true); },
                     [&](const ast::BuiltinAttribute* b) {
                         if (auto* ident_sem =
-                                program_->Sem()
+                                program_.Sem()
                                     .Get(b)
                                     ->As<sem::BuiltinEnumExpression<core::BuiltinValue>>()) {
                             switch (ident_sem->Value()) {
@@ -459,7 +459,7 @@
 
         // Add a terminator if one was not already created.
         if (NeedTerminator()) {
-            if (!program_->Sem().Get(ast_func->body)->Behaviors().Contains(sem::Behavior::kNext)) {
+            if (!program_.Sem().Get(ast_func->body)->Behaviors().Contains(sem::Behavior::kNext)) {
                 SetTerminator(builder_.Unreachable());
             } else {
                 SetTerminator(builder_.Return(current_function_));
@@ -475,7 +475,7 @@
         for (auto* s : stmts) {
             EmitStatement(s);
 
-            if (auto* sem = program_->Sem().Get(s);
+            if (auto* sem = program_.Sem().Get(s);
                 sem && !sem->Behaviors().Contains(sem::Behavior::kNext)) {
                 break;  // Unreachable statement.
             }
@@ -539,7 +539,7 @@
     void EmitIncrementDecrement(const ast::IncrementDecrementStatement* stmt) {
         auto lhs = EmitExpression(stmt->lhs);
 
-        auto* one = program_->TypeOf(stmt->lhs)->UnwrapRef()->is_signed_integer_scalar()
+        auto* one = program_.TypeOf(stmt->lhs)->UnwrapRef()->is_signed_integer_scalar()
                         ? builder_.Constant(1_i)
                         : builder_.Constant(1_u);
 
@@ -757,7 +757,7 @@
 
         ControlStackScope scope(this, switch_inst);
 
-        const auto* sem = program_->Sem().Get(stmt);
+        const auto* sem = program_.Sem().Get(stmt);
         for (const auto* c : sem->Cases()) {
             Vector<core::ir::Switch::CaseSelector, 4> selectors;
             for (const auto* selector : c->Selectors()) {
@@ -863,7 +863,7 @@
 
             void Bind(const ast::Expression* expr, core::ir::Value* value) {
                 // If this expression maps to sem::Load, insert a load instruction to get the result
-                if (impl.program_->Sem().Get<sem::Load>(expr)) {
+                if (impl.program_.Sem().Get<sem::Load>(expr)) {
                     auto* load = impl.builder_.Load(value);
                     impl.current_block_->Append(load);
                     value = load->Result();
@@ -873,7 +873,7 @@
 
             void Bind(const ast::Expression* expr, const VectorRefElementAccess& access) {
                 // If this expression maps to sem::Load, insert a load instruction to get the result
-                if (impl.program_->Sem().Get<sem::Load>(expr)) {
+                if (impl.program_.Sem().Get<sem::Load>(expr)) {
                     auto* load = impl.builder_.LoadVectorElement(access.vector, access.index);
                     impl.current_block_->Append(load);
                     bindings_.Add(expr, load->Result());
@@ -907,7 +907,7 @@
             void PopBlock() { impl.current_block_ = blocks.Pop(); }
 
             core::ir::Value* EmitConstant(const ast::Expression* expr) {
-                if (auto* sem = impl.program_->Sem().GetVal(expr)) {
+                if (auto* sem = impl.program_.Sem().GetVal(expr)) {
                     if (auto* v = sem->ConstantValue()) {
                         if (auto* cv = v->Clone(impl.clone_ctx_)) {
                             auto* val = impl.builder_.Constant(cv);
@@ -931,7 +931,7 @@
                     return;
                 }
 
-                auto* sem = impl.program_->Sem().Get(expr)->Unwrap();
+                auto* sem = impl.program_.Sem().Get(expr)->Unwrap();
 
                 // The access result type should match the source result type. If the source is a
                 // pointer, we generate a pointer.
@@ -1004,7 +1004,7 @@
             }
 
             void EmitBinary(const ast::BinaryExpression* b) {
-                auto* b_sem = impl.program_->Sem().Get(b);
+                auto* b_sem = impl.program_.Sem().Get(b);
                 auto* ty = b_sem->Type()->Clone(impl.clone_ctx_.type_ctx);
                 auto lhs = GetValue(b->lhs);
                 if (!lhs) {
@@ -1027,7 +1027,7 @@
                 if (!val) {
                     return;
                 }
-                auto* sem = impl.program_->Sem().Get(expr);
+                auto* sem = impl.program_.Sem().Get(expr);
                 auto* ty = sem->Type()->Clone(impl.clone_ctx_.type_ctx);
                 core::ir::Instruction* inst = nullptr;
                 switch (expr->op) {
@@ -1056,7 +1056,7 @@
                 if (!val) {
                     return;
                 }
-                auto* sem = impl.program_->Sem().Get(b);
+                auto* sem = impl.program_.Sem().Get(b);
                 auto* ty = sem->Type()->Clone(impl.clone_ctx_.type_ctx);
                 auto* inst = impl.builder_.Bitcast(ty, val);
                 impl.current_block_->Append(inst);
@@ -1065,7 +1065,7 @@
 
             void EmitCall(const ast::CallExpression* expr) {
                 // If this is a materialized semantic node, just use the constant value.
-                if (auto* mat = impl.program_->Sem().Get(expr)) {
+                if (auto* mat = impl.program_.Sem().Get(expr)) {
                     if (mat->ConstantValue()) {
                         auto* cv = mat->ConstantValue()->Clone(impl.clone_ctx_);
                         if (!cv) {
@@ -1088,7 +1088,7 @@
                     }
                     args.Push(value);
                 }
-                auto* sem = impl.program_->Sem().Get<sem::Call>(expr);
+                auto* sem = impl.program_.Sem().Get<sem::Call>(expr);
                 if (!sem) {
                     impl.add_error(expr->source, "failed to get semantic information for call " +
                                                      std::string(expr->TypeInfo().name));
@@ -1131,7 +1131,7 @@
             }
 
             void EmitLiteral(const ast::LiteralExpression* lit) {
-                auto* sem = impl.program_->Sem().Get(lit);
+                auto* sem = impl.program_.Sem().Get(lit);
                 if (!sem) {
                     impl.add_error(lit->source, "failed to get semantic information for node " +
                                                     std::string(lit->TypeInfo().name));
@@ -1150,7 +1150,7 @@
             std::optional<VectorRefElementAccess> AsVectorRefElementAccess(
                 const ast::Expression* expr) {
                 return AsVectorRefElementAccess(
-                    impl.program_->Sem().Get<sem::ValueExpression>(expr)->UnwrapLoad());
+                    impl.program_.Sem().Get<sem::ValueExpression>(expr)->UnwrapLoad());
             }
 
             std::optional<VectorRefElementAccess> AsVectorRefElementAccess(
@@ -1294,7 +1294,7 @@
     void EmitCall(const ast::CallStatement* stmt) { (void)EmitValueExpression(stmt->expr); }
 
     void EmitVariable(const ast::Variable* var) {
-        auto* sem = program_->Sem().Get(var);
+        auto* sem = program_.Sem().Get(var);
 
         return tint::Switch(  //
             var,
@@ -1417,8 +1417,8 @@
 
 }  // namespace
 
-tint::Result<core::ir::Module, std::string> ProgramToIR(const Program* program) {
-    if (!program->IsValid()) {
+tint::Result<core::ir::Module, std::string> ProgramToIR(const Program& program) {
+    if (!program.IsValid()) {
         return std::string("input program is not valid");
     }
 
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
index c09549b..d51f338 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h
@@ -35,7 +35,7 @@
 /// @note this assumes the `program.IsValid()`, and has had const-eval done so
 /// any abstract values have been calculated and converted into the relevant
 /// concrete types.
-tint::Result<core::ir::Module, std::string> ProgramToIR(const Program* program);
+tint::Result<core::ir::Module, std::string> ProgramToIR(const Program& program);
 
 }  // namespace tint::wgsl::reader
 
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 815442a..f3b8c5d 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -78,18 +78,18 @@
 
 namespace tint::wgsl::writer {
 
-ASTPrinter::ASTPrinter(const Program* program) : program_(program) {}
+ASTPrinter::ASTPrinter(const Program& program) : program_(program) {}
 
 ASTPrinter::~ASTPrinter() = default;
 
 bool ASTPrinter::Generate() {
     // Generate directives before any other global declarations.
     bool has_directives = false;
-    for (auto enable : program_->AST().Enables()) {
+    for (auto enable : program_.AST().Enables()) {
         EmitEnable(enable);
         has_directives = true;
     }
-    for (auto diagnostic : program_->AST().DiagnosticDirectives()) {
+    for (auto diagnostic : program_.AST().DiagnosticDirectives()) {
         auto out = Line();
         EmitDiagnosticControl(out, diagnostic->control);
         out << ";";
@@ -99,7 +99,7 @@
         Line();
     }
     // Generate global declarations in the order they appear in the module.
-    for (auto* decl : program_->AST().GlobalDeclarations()) {
+    for (auto* decl : program_.AST().GlobalDeclarations()) {
         if (decl->IsAnyOf<ast::DiagnosticDirective, ast::Enable>()) {
             continue;
         }
@@ -110,7 +110,7 @@
             [&](const ast::Variable* var) { return EmitVariable(Line(), var); },
             [&](const ast::ConstAssert* ca) { return EmitConstAssert(ca); },
             [&](Default) { TINT_UNREACHABLE(); });
-        if (decl != program_->AST().GlobalDeclarations().Back()) {
+        if (decl != program_.AST().GlobalDeclarations().Back()) {
             Line();
         }
     }
@@ -361,7 +361,7 @@
     for (auto* mem : str->members) {
         // TODO(crbug.com/tint/798) move the @offset attribute handling to the transform::Wgsl
         // sanitizer.
-        if (auto* mem_sem = program_->Sem().Get(mem)) {
+        if (auto* mem_sem = program_.Sem().Get(mem)) {
             offset = tint::RoundUp(mem_sem->Align(), offset);
             if (uint32_t padding = mem_sem->Offset() - offset) {
                 add_padding(padding);
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
index 525b683..4b3dffe 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.h
@@ -77,7 +77,7 @@
   public:
     /// Constructor
     /// @param program the program
-    explicit ASTPrinter(const Program* program);
+    explicit ASTPrinter(const Program& program);
     ~ASTPrinter() override;
 
     /// Generates the result data
@@ -219,7 +219,7 @@
     void EmitAttributes(StringStream& out, VectorRef<const ast::Attribute*> attrs);
 
   private:
-    Program const* const program_;
+    const Program& program_;
 };
 
 }  // namespace tint::wgsl::writer
diff --git a/src/tint/lang/wgsl/writer/ast_printer/helper_test.h b/src/tint/lang/wgsl/writer/ast_printer/helper_test.h
index 861f42c..008a38b 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/helper_test.h
+++ b/src/tint/lang/wgsl/writer/ast_printer/helper_test.h
@@ -48,7 +48,7 @@
             program = std::make_unique<Program>(std::move(*this));
         }
         [&] { ASSERT_TRUE(program->IsValid()) << program->Diagnostics().str(); }();
-        gen_ = std::make_unique<ASTPrinter>(program.get());
+        gen_ = std::make_unique<ASTPrinter>(*program);
         return *gen_;
     }
 
diff --git a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
index d4d758b..44ffdd2 100644
--- a/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
+++ b/src/tint/lang/wgsl/writer/ir_to_program/ir_to_program_test.cc
@@ -35,11 +35,11 @@
     auto output_program = IRToProgram(mod);
     if (!output_program.IsValid()) {
         result.err = output_program.Diagnostics().str();
-        result.ast = Program::printer(&output_program);
+        result.ast = Program::printer(output_program);
         return result;
     }
 
-    auto output = wgsl::writer::Generate(&output_program, {});
+    auto output = wgsl::writer::Generate(output_program, {});
     if (!output) {
         std::stringstream ss;
         ss << "wgsl::Generate() errored: " << output.Failure();
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
index 16735f2..85e88b8 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.cc
@@ -76,13 +76,13 @@
 
 namespace tint::wgsl::writer {
 
-SyntaxTreePrinter::SyntaxTreePrinter(const Program* program) : program_(program) {}
+SyntaxTreePrinter::SyntaxTreePrinter(const Program& program) : program_(program) {}
 
 SyntaxTreePrinter::~SyntaxTreePrinter() = default;
 
 bool SyntaxTreePrinter::Generate() {
     // Generate global declarations in the order they appear in the module.
-    for (auto* decl : program_->AST().GlobalDeclarations()) {
+    for (auto* decl : program_.AST().GlobalDeclarations()) {
         Switch(
             decl,  //
             [&](const ast::DiagnosticDirective* dd) { EmitDiagnosticControl(dd->control); },
@@ -93,7 +93,7 @@
             [&](const ast::ConstAssert* ca) { EmitConstAssert(ca); },
             [&](Default) { TINT_UNREACHABLE(); });
 
-        if (decl != program_->AST().GlobalDeclarations().Back()) {
+        if (decl != program_.AST().GlobalDeclarations().Back()) {
             Line();
         }
     }
diff --git a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
index 4ef93a1..aff7fca 100644
--- a/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
+++ b/src/tint/lang/wgsl/writer/syntax_tree_printer/syntax_tree_printer.h
@@ -76,7 +76,7 @@
   public:
     /// Constructor
     /// @param program the program
-    explicit SyntaxTreePrinter(const Program* program);
+    explicit SyntaxTreePrinter(const Program& program);
     ~SyntaxTreePrinter() override;
 
     /// Generates the result data
@@ -202,7 +202,7 @@
     void EmitAttributes(VectorRef<const ast::Attribute*> attrs);
 
   private:
-    Program const* const program_;
+    const Program& program_;
 };
 
 }  // namespace tint::wgsl::writer
diff --git a/src/tint/lang/wgsl/writer/writer.cc b/src/tint/lang/wgsl/writer/writer.cc
index 1a7204a..48cdca0 100644
--- a/src/tint/lang/wgsl/writer/writer.cc
+++ b/src/tint/lang/wgsl/writer/writer.cc
@@ -26,7 +26,7 @@
 
 namespace tint::wgsl::writer {
 
-Result<Output, std::string> Generate(const Program* program, const Options& options) {
+Result<Output, std::string> Generate(const Program& program, const Options& options) {
     (void)options;
 
     Output output;
diff --git a/src/tint/lang/wgsl/writer/writer.h b/src/tint/lang/wgsl/writer/writer.h
index 135388e..f1e6988 100644
--- a/src/tint/lang/wgsl/writer/writer.h
+++ b/src/tint/lang/wgsl/writer/writer.h
@@ -33,7 +33,7 @@
 /// @param program the program to translate to WGSL
 /// @param options the configuration options to use when generating WGSL
 /// @returns the resulting WGSL, or an error string
-Result<Output, std::string> Generate(const Program* program, const Options& options);
+Result<Output, std::string> Generate(const Program& program, const Options& options);
 
 }  // namespace tint::wgsl::writer
 
diff --git a/src/tint/lang/wgsl/writer/writer_bench.cc b/src/tint/lang/wgsl/writer/writer_bench.cc
index 8996be2..c3fd5d4 100644
--- a/src/tint/lang/wgsl/writer/writer_bench.cc
+++ b/src/tint/lang/wgsl/writer/writer_bench.cc
@@ -28,7 +28,7 @@
     }
     auto& program = std::get<bench::ProgramAndFile>(res).program;
     for (auto _ : state) {
-        auto res = Generate(&program, {});
+        auto res = Generate(program, {});
         if (!res) {
             state.SkipWithError(res.Failure().c_str());
         }