writer: Move sanitizers into the backends

Adds a new single-function API for the generators, which applies the
sanitizing transform and performs the generation in one step, and
returns a result object which contains the generated code and success
status/diagnostics.

The new APIs take an `Option` structure to control backend-specific
generation details (e.g. MSL fixed sample mask). The result objects
also provide backend-specific feedback (e.g. whether a UBO of buffer
lengths was generated).

HLSL needs a list of entry points to validate, and it's the HLSL
sanitizer that generates an entry point for programs that do not have
one. This change makes the HLSL generator return the list of
post-sanitize entry points so that the Tint executable can forward
them to the validation code.

Change-Id: I2d5aa27fda95d7c50c5bef41e206aee38f2fd2eb
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/57101
Auto-Submit: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: James Price <jrprice@google.com>
Reviewed-by: Ben Clayton <bclayton@chromium.org>
diff --git a/samples/main.cc b/samples/main.cc
index 3e8de72..e44fbfa 100644
--- a/samples/main.cc
+++ b/samples/main.cc
@@ -589,12 +589,182 @@
 /// @param program the program
 void PrintWGSL(std::ostream& out, const tint::Program& program) {
 #if TINT_BUILD_WGSL_WRITER
-  tint::writer::wgsl::Generator writer(&program);
-  writer.Generate();
-  out << std::endl << writer.result() << std::endl;
+  tint::writer::wgsl::Options options;
+  auto result = tint::writer::wgsl::Generate(&program, options);
+  out << std::endl << result.wgsl << std::endl;
 #endif
 }
 
+/// Generate SPIR-V code for a program.
+/// @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) {
+#if TINT_BUILD_SPV_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::spirv::Options gen_options;
+  auto result = tint::writer::spirv::Generate(program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (options.format == Format::kSpvAsm) {
+    if (!WriteFile(options.output_file, "w", Disassemble(result.spirv))) {
+      return false;
+    }
+  } else {
+    if (!WriteFile(options.output_file, "wb", result.spirv)) {
+      return false;
+    }
+  }
+
+  if (options.validate) {
+    // Use Vulkan 1.1, since this is what Tint, internally, uses.
+    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;
+    });
+    if (!tools.Validate(result.spirv.data(), result.spirv.size(),
+                        spvtools::ValidatorOptions())) {
+      return false;
+    }
+  }
+
+  return true;
+#else
+  std::cerr << "SPIR-V writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_SPV_WRITER
+}
+
+/// Generate WGSL code for a program.
+/// @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) {
+#if TINT_BUILD_WGSL_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::wgsl::Options gen_options;
+  auto result = tint::writer::wgsl::Generate(program, gen_options);
+  if (!result.success) {
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  return WriteFile(options.output_file, "w", result.wgsl);
+#else
+  std::cerr << "WGSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_WGSL_WRITER
+}
+
+/// Generate MSL code for a program.
+/// @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) {
+#if TINT_BUILD_MSL_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::msl::Options gen_options;
+  auto result = tint::writer::msl::Generate(program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (!WriteFile(options.output_file, "w", result.msl)) {
+    return false;
+  }
+
+  if (options.validate) {
+    tint::val::Result res;
+#ifdef TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
+    res = tint::val::MslUsingMetalAPI(result.msl);
+#else
+#ifdef _WIN32
+    const char* default_xcrun_exe = "metal.exe";
+#else
+    const char* default_xcrun_exe = "xcrun";
+#endif
+    auto xcrun = tint::utils::Command::LookPath(
+        options.xcrun_path.empty() ? default_xcrun_exe : options.xcrun_path);
+    if (xcrun.Found()) {
+      res = tint::val::Msl(xcrun.Path(), result.msl);
+    } else {
+      res.output = "xcrun executable not found. Cannot validate.";
+      res.failed = true;
+    }
+#endif  // TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
+    if (res.failed) {
+      std::cerr << res.output << std::endl;
+      return false;
+    }
+  }
+
+  return true;
+#else
+  std::cerr << "MSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_MSL_WRITER
+}
+
+/// Generate HLSL code for a program.
+/// @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) {
+#if TINT_BUILD_HLSL_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::hlsl::Options gen_options;
+  auto result = tint::writer::hlsl::Generate(program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (!WriteFile(options.output_file, "w", result.hlsl)) {
+    return false;
+  }
+
+  if (options.validate) {
+    tint::val::Result res;
+    if (options.use_fxc) {
+#ifdef _WIN32
+      res = tint::val::HlslUsingFXC(result.hlsl, result.entry_points);
+#else
+      res.failed = true;
+      res.output = "FXC can only be used on Windows. Sorry :X";
+#endif  // _WIN32
+    } else {
+      auto dxc = tint::utils::Command::LookPath(
+          options.dxc_path.empty() ? "dxc" : options.dxc_path);
+      if (dxc.Found()) {
+        res = tint::val::HlslUsingDXC(dxc.Path(), result.hlsl,
+                                      result.entry_points);
+      } else {
+        res.failed = true;
+        res.output = "DXC executable not found. Cannot validate";
+      }
+    }
+    if (res.failed) {
+      std::cerr << res.output << std::endl;
+      return false;
+    }
+  }
+
+  return true;
+#else
+  std::cerr << "MSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_HLSL_WRITER
+}
+
 }  // namespace
 
 int main(int argc, const char** argv) {
@@ -729,19 +899,11 @@
   }
 
   switch (options.format) {
-#if TINT_BUILD_SPV_WRITER
-    case Format::kSpirv:
-    case Format::kSpvAsm:
-      transform_manager.Add<tint::transform::Spirv>();
-      transform_inputs.Add<tint::transform::Spirv::Config>(true);
-      break;
-#endif  // TINT_BUILD_SPV_WRITER
 #if TINT_BUILD_MSL_WRITER
     case Format::kMsl: {
       transform_inputs.Add<tint::transform::Renamer::Config>(
           tint::transform::Renamer::Target::kMslKeywords);
       transform_manager.Add<tint::transform::Renamer>();
-      transform_manager.Add<tint::transform::Msl>();
       break;
     }
 #endif  // TINT_BUILD_MSL_WRITER
@@ -750,7 +912,6 @@
       transform_inputs.Add<tint::transform::Renamer::Config>(
           tint::transform::Renamer::Target::kHlslKeywords);
       transform_manager.Add<tint::transform::Renamer>();
-      transform_manager.Add<tint::transform::Hlsl>();
       break;
     }
 #endif  // TINT_BUILD_HLSL_WRITER
@@ -801,185 +962,28 @@
     std::cout << std::string(80, '-') << std::endl;
   }
 
-  std::unique_ptr<tint::writer::Writer> writer;
-
+  bool success = false;
   switch (options.format) {
     case Format::kSpirv:
     case Format::kSpvAsm:
-#if TINT_BUILD_SPV_WRITER
-      writer = std::make_unique<tint::writer::spirv::Generator>(program.get());
+      success = GenerateSpirv(program.get(), options);
       break;
-#else
-      std::cerr << "SPIR-V writer not enabled in tint build" << std::endl;
-      return 1;
-#endif  // TINT_BUILD_SPV_WRITER
-
     case Format::kWgsl:
-#if TINT_BUILD_WGSL_WRITER
-      writer = std::make_unique<tint::writer::wgsl::Generator>(program.get());
+      success = GenerateWgsl(program.get(), options);
       break;
-#else
-      std::cerr << "WGSL writer not enabled in tint build" << std::endl;
-      return 1;
-#endif  // TINT_BUILD_WGSL_WRITER
-
     case Format::kMsl:
-#if TINT_BUILD_MSL_WRITER
-      writer = std::make_unique<tint::writer::msl::Generator>(program.get());
+      success = GenerateMsl(program.get(), options);
       break;
-#else
-      std::cerr << "MSL writer not enabled in tint build" << std::endl;
-      return 1;
-#endif  // TINT_BUILD_MSL_WRITER
-
     case Format::kHlsl:
-#if TINT_BUILD_HLSL_WRITER
-      writer = std::make_unique<tint::writer::hlsl::Generator>(program.get());
+      success = GenerateHlsl(program.get(), options);
       break;
-#else
-      std::cerr << "HLSL writer not enabled in tint build" << std::endl;
-      return 1;
-#endif  // TINT_BUILD_HLSL_WRITER
-
     default:
       std::cerr << "Unknown output format specified" << std::endl;
       return 1;
   }
-
-  if (!writer->Generate()) {
-    PrintWGSL(std::cerr, *program);
-    std::cerr << "Failed to generate: " << writer->error() << std::endl;
+  if (!success) {
     return 1;
   }
 
-  bool validation_failed = false;
-  std::ostringstream validation_msgs;
-
-  if (options.validate) {
-    switch (options.format) {
-#if TINT_BUILD_SPV_WRITER
-      case Format::kSpirv:
-      case Format::kSpvAsm: {
-        // Use Vulkan 1.1, since this is what Tint, internally, uses.
-        spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
-        tools.SetMessageConsumer(
-            [&validation_msgs](spv_message_level_t, const char*,
-                               const spv_position_t& pos, const char* msg) {
-              validation_msgs << (pos.line + 1) << ":" << (pos.column + 1)
-                              << ": " << msg << std::endl;
-            });
-        auto* w = static_cast<tint::writer::spirv::Generator*>(writer.get());
-        if (!tools.Validate(w->result().data(), w->result().size(),
-                            spvtools::ValidatorOptions())) {
-          validation_failed = true;
-        }
-        break;
-      }
-#endif
-#if TINT_BUILD_HLSL_WRITER
-      case Format::kHlsl: {
-        auto* w = static_cast<tint::writer::Text*>(writer.get());
-        auto hlsl = w->result();
-
-        tint::val::Result res;
-        if (options.use_fxc) {
-#ifdef _WIN32
-          res = tint::val::HlslUsingFXC(hlsl, program.get());
-#else
-          res.failed = true;
-          res.output = "FXC can only be used on Windows. Sorry :X";
-#endif  // _WIN32
-        } else {
-          auto dxc = tint::utils::Command::LookPath(
-              options.dxc_path.empty() ? "dxc" : options.dxc_path);
-          if (dxc.Found()) {
-            res = tint::val::HlslUsingDXC(dxc.Path(), hlsl, program.get());
-          } else {
-            res.failed = true;
-            res.output = "DXC executable not found. Cannot validate";
-          }
-        }
-        if (res.failed) {
-          validation_failed = true;
-          validation_msgs << res.source << std::endl;
-          validation_msgs << res.output;
-        }
-        break;
-      }
-#endif  // TINT_BUILD_HLSL_WRITER
-#if TINT_BUILD_MSL_WRITER
-      case Format::kMsl: {
-        auto* w = static_cast<tint::writer::Text*>(writer.get());
-        auto msl = w->result();
-#ifdef TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
-        auto res = tint::val::MslUsingMetalAPI(msl);
-        if (res.failed) {
-          validation_failed = true;
-          validation_msgs << res.source << std::endl;
-          validation_msgs << res.output;
-        }
-#else
-#ifdef _WIN32
-        const char* default_xcrun_exe = "metal.exe";
-#else
-        const char* default_xcrun_exe = "xcrun";
-#endif
-        auto xcrun = tint::utils::Command::LookPath(options.xcrun_path.empty()
-                                                        ? default_xcrun_exe
-                                                        : options.xcrun_path);
-        if (xcrun.Found()) {
-          auto res = tint::val::Msl(xcrun.Path(), msl);
-          if (res.failed) {
-            validation_failed = true;
-            validation_msgs << res.source << std::endl;
-            validation_msgs << res.output;
-          }
-        } else {
-          validation_failed = true;
-          validation_msgs << "xcrun executable not found. Cannot validate";
-        }
-#endif  // TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
-        break;
-      }
-
-#endif  // TINT_BUILD_MSL_WRITER
-      default:
-        break;
-    }
-  }
-
-#if TINT_BUILD_SPV_WRITER
-  if (options.format == Format::kSpvAsm) {
-    auto* w = static_cast<tint::writer::spirv::Generator*>(writer.get());
-    auto str = Disassemble(w->result());
-    if (!WriteFile(options.output_file, "w", str)) {
-      return 1;
-    }
-  }
-  if (options.format == Format::kSpirv) {
-    auto* w = static_cast<tint::writer::spirv::Generator*>(writer.get());
-    if (!WriteFile(options.output_file, "wb", w->result())) {
-      return 1;
-    }
-  }
-#endif  // TINT_BUILD_SPV_WRITER
-
-  if (validation_failed) {
-    std::cerr << std::endl << std::endl << "Validation Failure:" << std::endl;
-    std::cerr << validation_msgs.str();
-    return 1;
-  }
-
-  if (options.format != Format::kSpvAsm && options.format != Format::kSpirv) {
-    auto* w = static_cast<tint::writer::Text*>(writer.get());
-    auto output = w->result();
-    if (options.demangle) {
-      output = tint::Demangler().Demangle(program->Symbols(), output);
-    }
-    if (!WriteFile(options.output_file, "w", output)) {
-      return 1;
-    }
-  }
-
   return 0;
 }
diff --git a/src/ast/module_clone_test.cc b/src/ast/module_clone_test.cc
index 0dc6eb9..2a78bb5 100644
--- a/src/ast/module_clone_test.cc
+++ b/src/ast/module_clone_test.cc
@@ -144,11 +144,12 @@
   // Regenerate the wgsl for the src program. We use this instead of the
   // original source so that reformatting doesn't impact the final wgsl
   // comparison.
+  writer::wgsl::Options options;
   std::string src_wgsl;
   {
-    writer::wgsl::Generator src_gen(&src);
-    ASSERT_TRUE(src_gen.Generate()) << src_gen.error();
-    src_wgsl = src_gen.result();
+    auto result = writer::wgsl::Generate(&src, options);
+    ASSERT_TRUE(result.success) << result.error;
+    src_wgsl = result.wgsl;
 
     // Move the src program to a temporary that'll be dropped, so that the src
     // program is released before we attempt to print the dst program. This
@@ -159,9 +160,9 @@
   }
 
   // Print the dst module, check it matches the original source
-  writer::wgsl::Generator dst_gen(&dst);
-  ASSERT_TRUE(dst_gen.Generate());
-  auto dst_wgsl = dst_gen.result();
+  auto result = writer::wgsl::Generate(&dst, options);
+  ASSERT_TRUE(result.success);
+  auto dst_wgsl = result.wgsl;
   ASSERT_EQ(src_wgsl, dst_wgsl);
 
 #else  // #if TINT_BUILD_WGSL_READER && TINT_BUILD_WGSL_WRITER
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index 3c9b935..2aa1c49 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -1549,9 +1549,6 @@
 
   Inspector& inspector = Build();
 
-  tint::writer::wgsl::Generator writer(program_.get());
-  writer.Generate();
-
   auto result = inspector.GetEntryPoints();
 
   ASSERT_EQ(1u, result.size());
diff --git a/src/transform/test_helper.h b/src/transform/test_helper.h
index ed21a60..4b35a71 100644
--- a/src/transform/test_helper.h
+++ b/src/transform/test_helper.h
@@ -82,12 +82,13 @@
       return diag::Formatter(style).format(output.program.Diagnostics());
     }
 
-    writer::wgsl::Generator generator(&output.program);
-    if (!generator.Generate()) {
-      return "WGSL writer failed:\n" + generator.error();
+    writer::wgsl::Options options;
+    auto result = writer::wgsl::Generate(&output.program, options);
+    if (!result.success) {
+      return "WGSL writer failed:\n" + result.error;
     }
 
-    auto res = generator.result();
+    auto res = result.wgsl;
     if (res.empty()) {
       return res;
     }
diff --git a/src/val/hlsl.cc b/src/val/hlsl.cc
index e7e3aea..2a02f29 100644
--- a/src/val/hlsl.cc
+++ b/src/val/hlsl.cc
@@ -14,8 +14,6 @@
 
 #include "src/val/val.h"
 
-#include "src/ast/module.h"
-#include "src/program.h"
 #include "src/utils/io/command.h"
 #include "src/utils/io/tmpfile.h"
 
@@ -33,7 +31,7 @@
 
 Result HlslUsingDXC(const std::string& dxc_path,
                     const std::string& source,
-                    Program* program) {
+                    const EntryPointList& entry_points) {
   Result result;
 
   auto dxc = utils::Command(dxc_path);
@@ -48,48 +46,42 @@
   utils::TmpFile file;
   file << source;
 
-  bool found_an_entrypoint = false;
-  for (auto* func : program->AST().Functions()) {
-    if (func->IsEntryPoint()) {
-      found_an_entrypoint = true;
+  for (auto ep : entry_points) {
+    const char* profile = "";
 
-      const char* profile = "";
-
-      switch (func->pipeline_stage()) {
-        case ast::PipelineStage::kNone:
-          result.output = "Invalid PipelineStage";
-          result.failed = true;
-          return result;
-        case ast::PipelineStage::kVertex:
-          profile = "-T vs_6_0";
-          break;
-        case ast::PipelineStage::kFragment:
-          profile = "-T ps_6_0";
-          break;
-        case ast::PipelineStage::kCompute:
-          profile = "-T cs_6_0";
-          break;
-      }
-
-      auto name = program->Symbols().NameFor(func->symbol());
-      auto res = dxc(profile, "-E " + name, file.Path());
-      if (!res.out.empty()) {
-        if (!result.output.empty()) {
-          result.output += "\n";
-        }
-        result.output += res.out;
-      }
-      if (!res.err.empty()) {
-        if (!result.output.empty()) {
-          result.output += "\n";
-        }
-        result.output += res.err;
-      }
-      result.failed = (res.error_code != 0);
+    switch (ep.second) {
+      case ast::PipelineStage::kNone:
+        result.output = "Invalid PipelineStage";
+        result.failed = true;
+        return result;
+      case ast::PipelineStage::kVertex:
+        profile = "-T vs_6_0";
+        break;
+      case ast::PipelineStage::kFragment:
+        profile = "-T ps_6_0";
+        break;
+      case ast::PipelineStage::kCompute:
+        profile = "-T cs_6_0";
+        break;
     }
+
+    auto res = dxc(profile, "-E " + ep.first, file.Path());
+    if (!res.out.empty()) {
+      if (!result.output.empty()) {
+        result.output += "\n";
+      }
+      result.output += res.out;
+    }
+    if (!res.err.empty()) {
+      if (!result.output.empty()) {
+        result.output += "\n";
+      }
+      result.output += res.err;
+    }
+    result.failed = (res.error_code != 0);
   }
 
-  if (!found_an_entrypoint) {
+  if (entry_points.empty()) {
     result.output = "No entrypoint found";
     result.failed = true;
     return result;
@@ -99,7 +91,8 @@
 }
 
 #ifdef _WIN32
-Result HlslUsingFXC(const std::string& source, Program* program) {
+Result HlslUsingFXC(const std::string& source,
+                    const EntryPointList& entry_points) {
   Result result;
 
   // This library leaks if an error happens in this function, but it is ok
@@ -122,45 +115,38 @@
 
   result.source = source;
 
-  bool found_an_entrypoint = false;
-  for (auto* func : program->AST().Functions()) {
-    if (func->IsEntryPoint()) {
-      found_an_entrypoint = true;
-
-      const char* profile = "";
-      switch (func->pipeline_stage()) {
-        case ast::PipelineStage::kNone:
-          result.output = "Invalid PipelineStage";
-          result.failed = true;
-          return result;
-        case ast::PipelineStage::kVertex:
-          profile = "vs_5_1";
-          break;
-        case ast::PipelineStage::kFragment:
-          profile = "ps_5_1";
-          break;
-        case ast::PipelineStage::kCompute:
-          profile = "cs_5_1";
-          break;
-      }
-
-      auto name = program->Symbols().NameFor(func->symbol());
-
-      ComPtr<ID3DBlob> compiledShader;
-      ComPtr<ID3DBlob> errors;
-      if (FAILED(d3dCompile(source.c_str(), source.length(), nullptr, nullptr,
-                            nullptr, name.c_str(), profile, 0, 0,
-                            &compiledShader, &errors))) {
-        result.output = static_cast<char*>(errors->GetBufferPointer());
+  for (auto ep : entry_points) {
+    const char* profile = "";
+    switch (ep.second) {
+      case ast::PipelineStage::kNone:
+        result.output = "Invalid PipelineStage";
         result.failed = true;
         return result;
-      }
+      case ast::PipelineStage::kVertex:
+        profile = "vs_5_1";
+        break;
+      case ast::PipelineStage::kFragment:
+        profile = "ps_5_1";
+        break;
+      case ast::PipelineStage::kCompute:
+        profile = "cs_5_1";
+        break;
+    }
+
+    ComPtr<ID3DBlob> compiledShader;
+    ComPtr<ID3DBlob> errors;
+    if (FAILED(d3dCompile(source.c_str(), source.length(), nullptr, nullptr,
+                          nullptr, ep.first.c_str(), profile, 0, 0,
+                          &compiledShader, &errors))) {
+      result.output = static_cast<char*>(errors->GetBufferPointer());
+      result.failed = true;
+      return result;
     }
   }
 
   FreeLibrary(fxcLib);
 
-  if (!found_an_entrypoint) {
+  if (entry_points.empty()) {
     result.output = "No entrypoint found";
     result.failed = true;
     return result;
diff --git a/src/val/val.h b/src/val/val.h
index fd3ae0a..233a868 100644
--- a/src/val/val.h
+++ b/src/val/val.h
@@ -16,6 +16,10 @@
 #define SRC_VAL_VAL_H_
 
 #include <string>
+#include <utility>
+#include <vector>
+
+#include "src/ast/pipeline_stage.h"
 
 // Forward declarations
 namespace tint {
@@ -25,6 +29,8 @@
 namespace tint {
 namespace val {
 
+using EntryPointList = std::vector<std::pair<std::string, ast::PipelineStage>>;
+
 /// The return structure of Validate()
 struct Result {
   /// True if validation passed
@@ -39,19 +45,20 @@
 /// compiles successfully.
 /// @param dxc_path path to DXC
 /// @param source the generated HLSL source
-/// @param program the HLSL program
+/// @param entry_points the list of entry points to validate
 /// @return the result of the compile
 Result HlslUsingDXC(const std::string& dxc_path,
                     const std::string& source,
-                    Program* program);
+                    const EntryPointList& entry_points);
 
 #ifdef _WIN32
 /// Hlsl attempts to compile the shader with FXC, verifying that the shader
 /// compiles successfully.
 /// @param source the generated HLSL source
-/// @param program the HLSL program
+/// @param entry_points the list of entry points to validate
 /// @return the result of the compile
-Result HlslUsingFXC(const std::string& source, Program* program);
+Result HlslUsingFXC(const std::string& source,
+                    const EntryPointList& entry_points);
 #endif  // _WIN32
 
 /// Msl attempts to compile the shader with the Metal Shader Compiler,
diff --git a/src/writer/hlsl/generator.cc b/src/writer/hlsl/generator.cc
index 297788e..d6815a3 100644
--- a/src/writer/hlsl/generator.cc
+++ b/src/writer/hlsl/generator.cc
@@ -14,12 +14,46 @@
 
 #include "src/writer/hlsl/generator.h"
 
+#include "src/transform/hlsl.h"
 #include "src/writer/hlsl/generator_impl.h"
 
 namespace tint {
 namespace writer {
 namespace hlsl {
 
+Result::Result() = default;
+Result::~Result() = default;
+Result::Result(const Result&) = default;
+
+Result Generate(const Program* program, const Options&) {
+  Result result;
+
+  // Run the HLSL sanitizer.
+  transform::Hlsl sanitizer;
+  auto output = sanitizer.Run(program);
+  if (!output.program.IsValid()) {
+    result.success = false;
+    result.error = output.program.Diagnostics().str();
+    return result;
+  }
+
+  // Generate the HLSL code.
+  auto impl = std::make_unique<GeneratorImpl>(&output.program);
+  result.success = impl->Generate();
+  result.error = impl->error();
+  result.hlsl = impl->result();
+
+  // Collect the list of entry points in the sanitized program.
+  for (auto* func : output.program.AST().Functions()) {
+    if (func->IsEntryPoint()) {
+      auto name = output.program.Symbols().NameFor(func->symbol());
+      result.entry_points.push_back({name, func->pipeline_stage()});
+    }
+  }
+
+  return result;
+}
+
 Generator::Generator(const Program* program)
     : impl_(std::make_unique<GeneratorImpl>(program)) {}
 
diff --git a/src/writer/hlsl/generator.h b/src/writer/hlsl/generator.h
index a3bef00..75d47d8 100644
--- a/src/writer/hlsl/generator.h
+++ b/src/writer/hlsl/generator.h
@@ -17,16 +17,59 @@
 
 #include <memory>
 #include <string>
+#include <utility>
+#include <vector>
 
+#include "src/ast/pipeline_stage.h"
 #include "src/writer/text.h"
 
 namespace tint {
+
+// Forward declarations
+class Program;
+
 namespace writer {
 namespace hlsl {
 
 // Forward declarations
 class GeneratorImpl;
 
+/// Configuration options used for generating HLSL.
+struct Options {};
+
+/// The result produced when generating HLSL.
+struct Result {
+  /// Constructor
+  Result();
+
+  /// Destructor
+  ~Result();
+
+  /// Copy constructor
+  Result(const Result&);
+
+  /// True if generation was successful.
+  bool success = false;
+
+  /// The errors generated during code generation, if any.
+  std::string error;
+
+  /// The generated HLSL.
+  std::string hlsl = "";
+
+  /// The list of entry points in the generated HLSL.
+  std::vector<std::pair<std::string, ast::PipelineStage>> entry_points;
+};
+
+/// Generate HLSL for a program, according to a set of configuration options.
+/// The result will contain the HLSL, as well as success status and diagnostic
+/// information.
+/// @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
+Result Generate(const Program* program, const Options& options);
+
+// TODO(jrprice): Remove this once Dawn is using the new interface.
 /// Class to generate HLSL source
 class Generator : public Text {
  public:
diff --git a/src/writer/msl/generator.cc b/src/writer/msl/generator.cc
index 3c2190f..45287fe 100644
--- a/src/writer/msl/generator.cc
+++ b/src/writer/msl/generator.cc
@@ -13,12 +13,45 @@
 // limitations under the License.
 
 #include "src/writer/msl/generator.h"
+
+#include "src/transform/msl.h"
 #include "src/writer/msl/generator_impl.h"
 
 namespace tint {
 namespace writer {
 namespace msl {
 
+Result::Result() = default;
+Result::~Result() = default;
+Result::Result(const Result&) = default;
+
+Result Generate(const Program* program, const Options& options) {
+  Result result;
+
+  // Run the MSL sanitizer.
+  transform::Msl sanitizer;
+  transform::DataMap transform_input;
+  transform_input.Add<transform::Msl::Config>(options.buffer_size_ubo_index,
+                                              options.fixed_sample_mask);
+  auto output = sanitizer.Run(program, transform_input);
+  if (!output.program.IsValid()) {
+    result.success = false;
+    result.error = output.program.Diagnostics().str();
+    return result;
+  }
+  auto* transform_output = output.data.Get<transform::Msl::Result>();
+  result.needs_storage_buffer_sizes =
+      transform_output->needs_storage_buffer_sizes;
+
+  // Generate the MSL code.
+  auto impl = std::make_unique<GeneratorImpl>(&output.program);
+  result.success = impl->Generate();
+  result.error = impl->error();
+  result.msl = impl->result();
+
+  return result;
+}
+
 Generator::Generator(const Program* program)
     : impl_(std::make_unique<GeneratorImpl>(program)) {}
 
diff --git a/src/writer/msl/generator.h b/src/writer/msl/generator.h
index d63f755..b4891c9 100644
--- a/src/writer/msl/generator.h
+++ b/src/writer/msl/generator.h
@@ -21,11 +21,59 @@
 #include "src/writer/text.h"
 
 namespace tint {
+
+// Forward declarations
+class Program;
+
 namespace writer {
 namespace msl {
 
 class GeneratorImpl;
 
+/// Configuration options used for generating MSL.
+struct Options {
+  /// The index to use when generating a UBO to receive storage buffer sizes.
+  /// Defaults to 30, which is the last valid buffer slot.
+  uint32_t buffer_size_ubo_index = 30;
+
+  /// The fixed sample mask to combine with fragment shader outputs.
+  /// Defaults to 0xFFFFFFFF.
+  uint32_t fixed_sample_mask = 0xFFFFFFFF;
+};
+
+/// The result produced when generating MSL.
+struct Result {
+  /// Constructor
+  Result();
+
+  /// Destructor
+  ~Result();
+
+  /// Copy constructor
+  Result(const Result&);
+
+  /// True if generation was successful.
+  bool success = false;
+
+  /// The errors generated during code generation, if any.
+  std::string error;
+
+  /// The generated MSL.
+  std::string msl = "";
+
+  /// True if the shader needs a UBO of buffer sizes.
+  bool needs_storage_buffer_sizes = false;
+};
+
+/// Generate MSL for a program, according to a set of configuration options. The
+/// result will contain the MSL, as well as success status and diagnostic
+/// information.
+/// @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
+Result Generate(const Program* program, const Options& options);
+
+// TODO(jrprice): Remove this once Dawn is using the new interface.
 /// Class to generate MSL source
 class Generator : public Text {
  public:
diff --git a/src/writer/spirv/generator.cc b/src/writer/spirv/generator.cc
index 8a1e598..8fae66c 100644
--- a/src/writer/spirv/generator.cc
+++ b/src/writer/spirv/generator.cc
@@ -14,12 +14,49 @@
 
 #include "src/writer/spirv/generator.h"
 
+#include "src/transform/spirv.h"
 #include "src/writer/spirv/binary_writer.h"
 
 namespace tint {
 namespace writer {
 namespace spirv {
 
+Result::Result() = default;
+Result::~Result() = default;
+Result::Result(const Result&) = default;
+
+Result Generate(const Program* program, const Options& options) {
+  Result result;
+
+  // Run the SPIR-V sanitizer.
+  transform::Spirv sanitizer;
+  transform::DataMap transform_input;
+  transform_input.Add<transform::Spirv::Config>(options.emit_vertex_point_size);
+  auto output = sanitizer.Run(program, transform_input);
+  if (!output.program.IsValid()) {
+    result.success = false;
+    result.error = output.program.Diagnostics().str();
+    return result;
+  }
+
+  // Generate the SPIR-V code.
+  auto builder = std::make_unique<Builder>(&output.program);
+  auto writer = std::make_unique<BinaryWriter>();
+  if (!builder->Build()) {
+    result.success = false;
+    result.error = builder->error();
+    return result;
+  }
+
+  writer->WriteHeader(builder->id_bound());
+  writer->WriteBuilder(builder.get());
+
+  result.success = true;
+  result.spirv = writer->result();
+
+  return result;
+}
+
 Generator::Generator(const Program* program)
     : builder_(std::make_unique<Builder>(program)),
       writer_(std::make_unique<BinaryWriter>()) {}
diff --git a/src/writer/spirv/generator.h b/src/writer/spirv/generator.h
index 5227a3f..122f12b 100644
--- a/src/writer/spirv/generator.h
+++ b/src/writer/spirv/generator.h
@@ -22,6 +22,10 @@
 #include "src/writer/writer.h"
 
 namespace tint {
+
+// Forward declarations
+class Program;
+
 namespace writer {
 namespace spirv {
 
@@ -29,6 +33,43 @@
 class Builder;
 class BinaryWriter;
 
+/// Configuration options used for generating SPIR-V.
+struct Options {
+  /// Set to `true` to generate a PointSize builtin and have it set to 1.0
+  /// from all vertex shaders in the module.
+  bool emit_vertex_point_size = true;
+};
+
+/// The result produced when generating SPIR-V.
+struct Result {
+  /// Constructor
+  Result();
+
+  /// Destructor
+  ~Result();
+
+  /// Copy constructor
+  Result(const Result&);
+
+  /// True if generation was successful.
+  bool success = false;
+
+  /// The errors generated during code generation, if any.
+  std::string error;
+
+  /// The generated SPIR-V.
+  std::vector<uint32_t> spirv;
+};
+
+/// Generate SPIR-V for a program, according to a set of configuration options.
+/// The result will contain the SPIR-V, as well as success status and diagnostic
+/// information.
+/// @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
+Result Generate(const Program* program, const Options& options);
+
+// TODO(jrprice): Remove this once Dawn is using the new interface.
 /// Class to generate SPIR-V from a Tint program
 class Generator : public writer::Writer {
  public:
diff --git a/src/writer/wgsl/generator.cc b/src/writer/wgsl/generator.cc
index 36eeabc..48e1ceb 100644
--- a/src/writer/wgsl/generator.cc
+++ b/src/writer/wgsl/generator.cc
@@ -19,6 +19,22 @@
 namespace writer {
 namespace wgsl {
 
+Result::Result() = default;
+Result::~Result() = default;
+Result::Result(const Result&) = default;
+
+Result Generate(const Program* program, const Options&) {
+  Result result;
+
+  // Generate the WGSL code.
+  auto impl = std::make_unique<GeneratorImpl>(program);
+  result.success = impl->Generate();
+  result.error = impl->error();
+  result.wgsl = impl->result();
+
+  return result;
+}
+
 Generator::Generator(const Program* program)
     : impl_(std::make_unique<GeneratorImpl>(program)) {}
 
diff --git a/src/writer/wgsl/generator.h b/src/writer/wgsl/generator.h
index 6b0b358..35f194f 100644
--- a/src/writer/wgsl/generator.h
+++ b/src/writer/wgsl/generator.h
@@ -21,11 +21,48 @@
 #include "src/writer/text.h"
 
 namespace tint {
+
+// Forward declarations
+class Program;
+
 namespace writer {
 namespace wgsl {
 
 class GeneratorImpl;
 
+/// Configuration options used for generating WGSL.
+struct Options {};
+
+/// The result produced when generating WGSL.
+struct Result {
+  /// Constructor
+  Result();
+
+  /// Destructor
+  ~Result();
+
+  /// Copy constructor
+  Result(const Result&);
+
+  /// True if generation was successful.
+  bool success = false;
+
+  /// The errors generated during code generation, if any.
+  std::string error;
+
+  /// The generated WGSL.
+  std::string wgsl = "";
+};
+
+/// Generate WGSL for a program, according to a set of configuration options.
+/// The result will contain the WGSL, as well as success status and diagnostic
+/// information.
+/// @param program the program to translate to WGSL
+/// @param options the configuration options to use when generating WGSL
+/// @returns the resulting WGSL and supplementary information
+Result Generate(const Program* program, const Options& options);
+
+// TODO(jrprice): Remove this once Dawn is using the new interface.
 /// Class to generate WGSL source
 class Generator : public Text {
  public:
diff --git a/src/writer/writer.h b/src/writer/writer.h
index 3e5737c..fdbb569 100644
--- a/src/writer/writer.h
+++ b/src/writer/writer.h
@@ -17,8 +17,6 @@
 
 #include <string>
 
-#include "src/program.h"
-
 namespace tint {
 namespace writer {