[tint][msl] Make the MSL printer PIMPL

This is done, so:
• The API is cleaner - one stateless Print() function instead of a constructor, Generate() and Result()
• The compiler / linker does a better job inlining - binary size is reduced by 592 bytes, and gives the compiler more room for optimization.
• You don't have to forward-declare or #include a bunch of implementation details in the header.
• You don't have to duplicate the methods signatures in two files

Change-Id: I211422d7e72d131cbc40ed032185615f2a933178
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/157241
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/msl/writer/printer/BUILD.bazel b/src/tint/lang/msl/writer/printer/BUILD.bazel
index 806a71d..c72bc54 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.bazel
+++ b/src/tint/lang/msl/writer/printer/BUILD.bazel
@@ -97,7 +97,6 @@
     "//src/tint/lang/msl/writer/raise",
     "//src/tint/utils/containers",
     "//src/tint/utils/diagnostic",
-    "//src/tint/utils/generator",
     "//src/tint/utils/ice",
     "//src/tint/utils/id",
     "//src/tint/utils/macros",
diff --git a/src/tint/lang/msl/writer/printer/BUILD.cmake b/src/tint/lang/msl/writer/printer/BUILD.cmake
index 3141c6e..8992d6c 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.cmake
+++ b/src/tint/lang/msl/writer/printer/BUILD.cmake
@@ -102,7 +102,6 @@
   tint_lang_msl_writer_raise
   tint_utils_containers
   tint_utils_diagnostic
-  tint_utils_generator
   tint_utils_ice
   tint_utils_id
   tint_utils_macros
diff --git a/src/tint/lang/msl/writer/printer/BUILD.gn b/src/tint/lang/msl/writer/printer/BUILD.gn
index e2d37b2..bfd0975 100644
--- a/src/tint/lang/msl/writer/printer/BUILD.gn
+++ b/src/tint/lang/msl/writer/printer/BUILD.gn
@@ -99,7 +99,6 @@
         "${tint_src_dir}/lang/msl/writer/raise",
         "${tint_src_dir}/utils/containers",
         "${tint_src_dir}/utils/diagnostic",
-        "${tint_src_dir}/utils/generator",
         "${tint_src_dir}/utils/ice",
         "${tint_src_dir}/utils/id",
         "${tint_src_dir}/utils/macros",
diff --git a/src/tint/lang/msl/writer/printer/helper_test.h b/src/tint/lang/msl/writer/printer/helper_test.h
index e6fe363..e0ecbf8 100644
--- a/src/tint/lang/msl/writer/printer/helper_test.h
+++ b/src/tint/lang/msl/writer/printer/helper_test.h
@@ -61,8 +61,6 @@
 template <typename BASE>
 class MslPrinterTestHelperBase : public BASE {
   public:
-    MslPrinterTestHelperBase() : writer_(mod) {}
-
     /// The test module.
     core::ir::Module mod;
     /// The test builder.
@@ -71,9 +69,6 @@
     core::type::Manager& ty{mod.Types()};
 
   protected:
-    /// The MSL writer.
-    Printer writer_;
-
     /// Validation errors
     std::string err_;
 
@@ -83,18 +78,17 @@
     /// Run the writer on the IR module and validate the result.
     /// @returns true if generation and validation succeeded
     bool Generate() {
-        auto raised = raise::Raise(mod);
-        if (!raised) {
+        if (auto raised = raise::Raise(mod); !raised) {
             err_ = raised.Failure().reason.str();
             return false;
         }
 
-        auto result = writer_.Generate();
+        auto result = Print(mod);
         if (!result) {
             err_ = result.Failure().reason.str();
             return false;
         }
-        output_ = writer_.Result();
+        output_ = result.Get();
 
         return true;
     }
diff --git a/src/tint/lang/msl/writer/printer/printer.cc b/src/tint/lang/msl/writer/printer/printer.cc
index 45ea4d1..adaa72f 100644
--- a/src/tint/lang/msl/writer/printer/printer.cc
+++ b/src/tint/lang/msl/writer/printer/printer.cc
@@ -27,6 +27,8 @@
 
 #include "src/tint/lang/msl/writer/printer/printer.h"
 
+#include <unordered_map>
+#include <unordered_set>
 #include <utility>
 
 #include "src/tint/lang/core/constant/composite.h"
@@ -38,6 +40,7 @@
 #include "src/tint/lang/core/ir/if.h"
 #include "src/tint/lang/core/ir/let.h"
 #include "src/tint/lang/core/ir/load.h"
+#include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/core/ir/multi_in_block.h"
 #include "src/tint/lang/core/ir/return.h"
 #include "src/tint/lang/core/ir/unreachable.h"
@@ -63,6 +66,7 @@
 #include "src/tint/lang/core/type/void.h"
 #include "src/tint/lang/msl/writer/common/printer_support.h"
 #include "src/tint/utils/containers/map.h"
+#include "src/tint/utils/generator/text_generator.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/rtti/switch.h"
 #include "src/tint/utils/text/string.h"
@@ -70,836 +74,988 @@
 using namespace tint::core::fluent_types;  // NOLINT
 
 namespace tint::msl::writer {
+namespace {
 
 // Helper for calling TINT_UNIMPLEMENTED() from a Switch(object_ptr) default case.
 #define UNHANDLED_CASE(object_ptr)                         \
     TINT_UNIMPLEMENTED() << "unhandled case in Switch(): " \
                          << (object_ptr ? object_ptr->TypeInfo().name : "<null>")
 
-Printer::Printer(core::ir::Module& module) : ir_(module) {}
+/// PIMPL class for the MSL generator
+class Printer : public tint::TextGenerator {
+  public:
+    /// Constructor
+    /// @param module the Tint IR module to generate
+    explicit Printer(core::ir::Module& module) : ir_(module) {}
 
-Printer::~Printer() = default;
+    /// @returns the generated MSL shader
+    tint::Result<std::string> Generate() {
+        auto valid = core::ir::ValidateAndDumpIfNeeded(ir_, "MSL writer");
+        if (!valid) {
+            return std::move(valid.Failure());
+        }
 
-tint::Result<SuccessType> Printer::Generate() {
-    auto valid = core::ir::ValidateAndDumpIfNeeded(ir_, "MSL writer");
-    if (!valid) {
-        return std::move(valid.Failure());
+        {
+            TINT_SCOPED_ASSIGNMENT(current_buffer_, &preamble_buffer_);
+            Line() << "#include <metal_stdlib>";
+            Line() << "using namespace metal;";
+        }
+
+        // Emit module-scope declarations.
+        EmitBlockInstructions(ir_.root_block);
+
+        // Emit functions.
+        for (auto* func : ir_.functions) {
+            EmitFunction(func);
+        }
+
+        StringStream ss;
+        ss << preamble_buffer_.String() << std::endl << main_buffer_.String();
+        return ss.str();
     }
 
-    {
+  private:
+    /// Map of builtin structure to unique generated name
+    std::unordered_map<const core::type::Struct*, std::string> builtin_struct_names_;
+
+    core::ir::Module& ir_;
+
+    /// The buffer holding preamble text
+    TextBuffer preamble_buffer_;
+
+    /// Unique name of the 'TINT_INVARIANT' preprocessor define.
+    /// Non-empty only if an invariant attribute has been generated.
+    std::string invariant_define_name_;
+
+    std::unordered_set<const core::type::Struct*> emitted_structs_;
+
+    /// The current function being emitted
+    core::ir::Function* current_function_ = nullptr;
+    /// The current block being emitted
+    core::ir::Block* current_block_ = nullptr;
+
+    /// Unique name of the tint_array<T, N> template.
+    /// Non-empty only if the template has been generated.
+    std::string array_template_name_;
+
+    /// The representation for an IR pointer type
+    enum class PtrKind {
+        kPtr,  // IR pointer is represented in a pointer
+        kRef,  // IR pointer is represented in a reference
+    };
+
+    /// The structure for a value held by a 'let', 'var' or parameter.
+    struct VariableValue {
+        Symbol name;  // Name of the variable
+        PtrKind ptr_kind = PtrKind::kRef;
+    };
+
+    /// The structure for an inlined value
+    struct InlinedValue {
+        std::string expr;
+        PtrKind ptr_kind = PtrKind::kRef;
+    };
+
+    /// Empty struct used as a sentinel value to indicate that an string expression has been
+    /// consumed by its single place of usage. Attempting to use this value a second time should
+    /// result in an ICE.
+    struct ConsumedValue {};
+
+    using ValueBinding = std::variant<VariableValue, InlinedValue, ConsumedValue>;
+
+    /// IR values to their representation
+    Hashmap<core::ir::Value*, ValueBinding, 32> bindings_;
+
+    /// Values that can be inlined.
+    Hashset<core::ir::Value*, 64> can_inline_;
+
+    /// @returns the name of the templated `tint_array` helper type, generating it if needed
+    const std::string& ArrayTemplateName() {
+        if (!array_template_name_.empty()) {
+            return array_template_name_;
+        }
+
+        array_template_name_ = UniqueIdentifier("tint_array");
+
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &preamble_buffer_);
-        Line() << "#include <metal_stdlib>";
-        Line() << "using namespace metal;";
-    }
+        Line() << "template<typename T, size_t N>";
+        Line() << "struct " << array_template_name_ << " {";
 
-    // Emit module-scope declarations.
-    EmitBlockInstructions(ir_.root_block);
+        {
+            ScopedIndent si(current_buffer_);
+            Line()
+                << "const constant T& operator[](size_t i) const constant { return elements[i]; }";
+            for (auto* space : {"device", "thread", "threadgroup"}) {
+                Line() << space << " T& operator[](size_t i) " << space
+                       << " { return elements[i]; }";
+                Line() << "const " << space << " T& operator[](size_t i) const " << space
+                       << " { return elements[i]; }";
+            }
+            Line() << "T elements[N];";
+        }
+        Line() << "};";
+        Line();
 
-    // Emit functions.
-    for (auto* func : ir_.functions) {
-        EmitFunction(func);
-    }
-
-    return Success;
-}
-
-std::string Printer::Result() const {
-    StringStream ss;
-    ss << preamble_buffer_.String() << std::endl << main_buffer_.String();
-    return ss.str();
-}
-
-const std::string& Printer::ArrayTemplateName() {
-    if (!array_template_name_.empty()) {
         return array_template_name_;
     }
 
-    array_template_name_ = UniqueIdentifier("tint_array");
+    /// Emit the function
+    /// @param func the function to emit
+    void EmitFunction(core::ir::Function* func) {
+        TINT_SCOPED_ASSIGNMENT(current_function_, func);
 
-    TINT_SCOPED_ASSIGNMENT(current_buffer_, &preamble_buffer_);
-    Line() << "template<typename T, size_t N>";
-    Line() << "struct " << array_template_name_ << " {";
+        {
+            auto out = Line();
 
-    {
-        ScopedIndent si(current_buffer_);
-        Line() << "const constant T& operator[](size_t i) const constant { return elements[i]; }";
-        for (auto* space : {"device", "thread", "threadgroup"}) {
-            Line() << space << " T& operator[](size_t i) " << space << " { return elements[i]; }";
-            Line() << "const " << space << " T& operator[](size_t i) const " << space
-                   << " { return elements[i]; }";
+            // TODO(dsinclair): Emit function stage if any
+            // TODO(dsinclair): Handle return type attributes
+
+            EmitType(out, func->ReturnType());
+            out << " " << ir_.NameOf(func).Name() << "() {";
+
+            // TODO(dsinclair): Emit Function parameters
         }
-        Line() << "T elements[N];";
+        {
+            ScopedIndent si(current_buffer_);
+            EmitBlock(func->Block());
+        }
+
+        Line() << "}";
     }
-    Line() << "};";
-    Line();
 
-    return array_template_name_;
-}
+    /// Emit a block
+    /// @param block the block to emit
+    void EmitBlock(core::ir::Block* block) {
+        MarkInlinable(block);
 
-void Printer::EmitFunction(core::ir::Function* func) {
-    TINT_SCOPED_ASSIGNMENT(current_function_, func);
+        EmitBlockInstructions(block);
+    }
 
-    {
+    /// Emit the instructions in a block
+    /// @param block the block with the instructions to emit
+    void EmitBlockInstructions(core::ir::Block* block) {
+        TINT_SCOPED_ASSIGNMENT(current_block_, block);
+
+        for (auto* inst : *block) {
+            Switch(
+                inst,                                                //
+                [&](core::ir::Binary* b) { EmitBinary(b); },         //
+                [&](core::ir::ExitIf* e) { EmitExitIf(e); },         //
+                [&](core::ir::If* if_) { EmitIf(if_); },             //
+                [&](core::ir::Let* l) { EmitLet(l); },               //
+                [&](core::ir::Load* l) { EmitLoad(l); },             //
+                [&](core::ir::Return* r) { EmitReturn(r); },         //
+                [&](core::ir::Unreachable*) { EmitUnreachable(); },  //
+                [&](core::ir::Var* v) { EmitVar(v); },               //
+                [&](Default) {
+                    TINT_ICE() << "unimplemented instruction: " << inst->TypeInfo().name;
+                });
+        }
+    }
+
+    /// Emit a binary instruction
+    /// @param b the binary instruction
+    void EmitBinary(core::ir::Binary* b) {
+        if (b->Op() == core::ir::BinaryOp::kEqual) {
+            auto* rhs = b->RHS()->As<core::ir::Constant>();
+            if (rhs && rhs->Type()->Is<core::type::Bool>() &&
+                rhs->Value()->ValueAs<bool>() == false) {
+                // expr == false
+                Bind(b->Result(), "!(" + Expr(b->LHS()) + ")");
+                return;
+            }
+        }
+
+        auto kind = [&] {
+            switch (b->Op()) {
+                case core::ir::BinaryOp::kAdd:
+                    return "+";
+                case core::ir::BinaryOp::kSubtract:
+                    return "-";
+                case core::ir::BinaryOp::kMultiply:
+                    return "*";
+                case core::ir::BinaryOp::kDivide:
+                    return "/";
+                case core::ir::BinaryOp::kModulo:
+                    return "%";
+                case core::ir::BinaryOp::kAnd:
+                    return "&";
+                case core::ir::BinaryOp::kOr:
+                    return "|";
+                case core::ir::BinaryOp::kXor:
+                    return "^";
+                case core::ir::BinaryOp::kEqual:
+                    return "==";
+                case core::ir::BinaryOp::kNotEqual:
+                    return "!=";
+                case core::ir::BinaryOp::kLessThan:
+                    return "<";
+                case core::ir::BinaryOp::kGreaterThan:
+                    return ">";
+                case core::ir::BinaryOp::kLessThanEqual:
+                    return "<=";
+                case core::ir::BinaryOp::kGreaterThanEqual:
+                    return ">=";
+                case core::ir::BinaryOp::kShiftLeft:
+                    return "<<";
+                case core::ir::BinaryOp::kShiftRight:
+                    return ">>";
+            }
+            return "<error>";
+        };
+
+        StringStream str;
+        str << "(" << Expr(b->LHS()) << " " << kind() << " " + Expr(b->RHS()) << ")";
+
+        Bind(b->Result(), str.str());
+    }
+
+    /// Emit a load instruction
+    /// @param l the load instruction
+    void EmitLoad(core::ir::Load* l) {
+        // Force loads to be bound as inlines
+        bindings_.Add(l->Result(), InlinedValue{Expr(l->From()), PtrKind::kRef});
+    }
+
+    /// Emit a var instruction
+    /// @param v the var instruction
+    void EmitVar(core::ir::Var* v) {
         auto out = Line();
 
-        // TODO(dsinclair): Emit function stage if any
-        // TODO(dsinclair): Handle return type attributes
+        auto* ptr = v->Result()->Type()->As<core::type::Pointer>();
+        TINT_ASSERT_OR_RETURN(ptr);
 
-        EmitType(out, func->ReturnType());
-        out << " " << ir_.NameOf(func).Name() << "() {";
-
-        // TODO(dsinclair): Emit Function parameters
-    }
-    {
-        ScopedIndent si(current_buffer_);
-        EmitBlock(func->Block());
-    }
-
-    Line() << "}";
-}
-
-void Printer::EmitBlock(core::ir::Block* block) {
-    MarkInlinable(block);
-
-    EmitBlockInstructions(block);
-}
-
-void Printer::EmitBlockInstructions(core::ir::Block* block) {
-    TINT_SCOPED_ASSIGNMENT(current_block_, block);
-
-    for (auto* inst : *block) {
-        Switch(
-            inst,                                                //
-            [&](core::ir::Binary* b) { EmitBinary(b); },         //
-            [&](core::ir::ExitIf* e) { EmitExitIf(e); },         //
-            [&](core::ir::If* if_) { EmitIf(if_); },             //
-            [&](core::ir::Let* l) { EmitLet(l); },               //
-            [&](core::ir::Load* l) { EmitLoad(l); },             //
-            [&](core::ir::Return* r) { EmitReturn(r); },         //
-            [&](core::ir::Unreachable*) { EmitUnreachable(); },  //
-            [&](core::ir::Var* v) { EmitVar(v); },               //
-            [&](Default) { TINT_ICE() << "unimplemented instruction: " << inst->TypeInfo().name; });
-    }
-}
-
-void Printer::EmitBinary(core::ir::Binary* b) {
-    if (b->Op() == core::ir::BinaryOp::kEqual) {
-        auto* rhs = b->RHS()->As<core::ir::Constant>();
-        if (rhs && rhs->Type()->Is<core::type::Bool>() && rhs->Value()->ValueAs<bool>() == false) {
-            // expr == false
-            Bind(b->Result(), "!(" + Expr(b->LHS()) + ")");
-            return;
-        }
-    }
-
-    auto kind = [&] {
-        switch (b->Op()) {
-            case core::ir::BinaryOp::kAdd:
-                return "+";
-            case core::ir::BinaryOp::kSubtract:
-                return "-";
-            case core::ir::BinaryOp::kMultiply:
-                return "*";
-            case core::ir::BinaryOp::kDivide:
-                return "/";
-            case core::ir::BinaryOp::kModulo:
-                return "%";
-            case core::ir::BinaryOp::kAnd:
-                return "&";
-            case core::ir::BinaryOp::kOr:
-                return "|";
-            case core::ir::BinaryOp::kXor:
-                return "^";
-            case core::ir::BinaryOp::kEqual:
-                return "==";
-            case core::ir::BinaryOp::kNotEqual:
-                return "!=";
-            case core::ir::BinaryOp::kLessThan:
-                return "<";
-            case core::ir::BinaryOp::kGreaterThan:
-                return ">";
-            case core::ir::BinaryOp::kLessThanEqual:
-                return "<=";
-            case core::ir::BinaryOp::kGreaterThanEqual:
-                return ">=";
-            case core::ir::BinaryOp::kShiftLeft:
-                return "<<";
-            case core::ir::BinaryOp::kShiftRight:
-                return ">>";
-        }
-        return "<error>";
-    };
-
-    StringStream str;
-    str << "(" << Expr(b->LHS()) << " " << kind() << " " + Expr(b->RHS()) << ")";
-
-    Bind(b->Result(), str.str());
-}
-
-void Printer::EmitLoad(core::ir::Load* l) {
-    // Force loads to be bound as inlines
-    bindings_.Add(l->Result(), InlinedValue{Expr(l->From()), PtrKind::kRef});
-}
-
-void Printer::EmitVar(core::ir::Var* v) {
-    auto out = Line();
-
-    auto* ptr = v->Result()->Type()->As<core::type::Pointer>();
-    TINT_ASSERT_OR_RETURN(ptr);
-
-    auto space = ptr->AddressSpace();
-    switch (space) {
-        case core::AddressSpace::kFunction:
-        case core::AddressSpace::kHandle:
-            break;
-        case core::AddressSpace::kPrivate:
-            out << "thread ";
-            break;
-        case core::AddressSpace::kWorkgroup:
-            out << "threadgroup ";
-            break;
-        default:
-            TINT_ICE() << "unhandled variable address space";
-            return;
-    }
-
-    auto name = ir_.NameOf(v);
-
-    EmitType(out, ptr->UnwrapPtr());
-    out << " " << name.Name();
-
-    if (v->Initializer()) {
-        out << " = " << Expr(v->Initializer());
-    } else if (space == core::AddressSpace::kPrivate || space == core::AddressSpace::kFunction ||
-               space == core::AddressSpace::kUndefined) {
-        out << " = ";
-        EmitZeroValue(out, ptr->UnwrapPtr());
-    }
-    out << ";";
-
-    Bind(v->Result(), name, PtrKind::kRef);
-}
-
-void Printer::EmitLet(core::ir::Let* l) {
-    Bind(l->Result(), Expr(l->Value(), PtrKind::kPtr), PtrKind::kPtr);
-}
-
-void Printer::EmitIf(core::ir::If* if_) {
-    // Emit any nodes that need to be used as PHI nodes
-    for (auto* phi : if_->Results()) {
-        if (!ir_.NameOf(phi).IsValid()) {
-            ir_.SetName(phi, ir_.symbols.New());
-        }
-
-        auto name = ir_.NameOf(phi);
-
-        auto out = Line();
-        EmitType(out, phi->Type());
-        out << " " << name.Name() << ";";
-
-        Bind(phi, name);
-    }
-
-    Line() << "if (" << Expr(if_->Condition()) << ") {";
-
-    {
-        ScopedIndent si(current_buffer_);
-        EmitBlockInstructions(if_->True());
-    }
-
-    if (if_->False() && !if_->False()->IsEmpty()) {
-        Line() << "} else {";
-
-        ScopedIndent si(current_buffer_);
-        EmitBlockInstructions(if_->False());
-    }
-
-    Line() << "}";
-}
-
-void Printer::EmitExitIf(core::ir::ExitIf* e) {
-    auto results = e->If()->Results();
-    auto args = e->Args();
-    for (size_t i = 0; i < e->Args().Length(); ++i) {
-        auto* phi = results[i];
-        auto* val = args[i];
-
-        Line() << ir_.NameOf(phi).Name() << " = " << Expr(val) << ";";
-    }
-}
-
-void Printer::EmitReturn(core::ir::Return* r) {
-    // If this return has no arguments and the current block is for the function which is
-    // being returned, skip the return.
-    if (current_block_ == current_function_->Block() && r->Args().IsEmpty()) {
-        return;
-    }
-
-    auto out = Line();
-    out << "return";
-    if (!r->Args().IsEmpty()) {
-        out << " " << Expr(r->Args().Front());
-    }
-    out << ";";
-}
-
-void Printer::EmitUnreachable() {
-    Line() << "/* unreachable */";
-}
-
-void Printer::EmitAddressSpace(StringStream& out, core::AddressSpace sc) {
-    switch (sc) {
-        case core::AddressSpace::kFunction:
-        case core::AddressSpace::kPrivate:
-        case core::AddressSpace::kHandle:
-            out << "thread";
-            break;
-        case core::AddressSpace::kWorkgroup:
-            out << "threadgroup";
-            break;
-        case core::AddressSpace::kStorage:
-            out << "device";
-            break;
-        case core::AddressSpace::kUniform:
-            out << "constant";
-            break;
-        default:
-            TINT_ICE() << "unhandled address space: " << sc;
-            break;
-    }
-}
-
-void Printer::EmitType(StringStream& out, const core::type::Type* ty) {
-    tint::Switch(
-        ty,                                               //
-        [&](const core::type::Bool*) { out << "bool"; },  //
-        [&](const core::type::Void*) { out << "void"; },  //
-        [&](const core::type::F32*) { out << "float"; },  //
-        [&](const core::type::F16*) { out << "half"; },   //
-        [&](const core::type::I32*) { out << "int"; },    //
-        [&](const core::type::U32*) { out << "uint"; },   //
-        [&](const core::type::Array* arr) { EmitArrayType(out, arr); },
-        [&](const core::type::Vector* vec) { EmitVectorType(out, vec); },
-        [&](const core::type::Matrix* mat) { EmitMatrixType(out, mat); },
-        [&](const core::type::Atomic* atomic) { EmitAtomicType(out, atomic); },
-        [&](const core::type::Pointer* ptr) { EmitPointerType(out, ptr); },
-        [&](const core::type::Sampler*) { out << "sampler"; },  //
-        [&](const core::type::Texture* tex) { EmitTextureType(out, tex); },
-        [&](const core::type::Struct* str) {
-            out << StructName(str);
-
-            TINT_SCOPED_ASSIGNMENT(current_buffer_, &preamble_buffer_);
-            EmitStructType(str);
-        },
-        [&](Default) { UNHANDLED_CASE(ty); });
-}
-
-void Printer::EmitPointerType(StringStream& out, const core::type::Pointer* ptr) {
-    if (ptr->Access() == core::Access::kRead) {
-        out << "const ";
-    }
-    EmitAddressSpace(out, ptr->AddressSpace());
-    out << " ";
-    EmitType(out, ptr->StoreType());
-    out << "*";
-}
-
-void Printer::EmitAtomicType(StringStream& out, const core::type::Atomic* atomic) {
-    if (atomic->Type()->Is<core::type::I32>()) {
-        out << "atomic_int";
-        return;
-    }
-    if (TINT_LIKELY(atomic->Type()->Is<core::type::U32>())) {
-        out << "atomic_uint";
-        return;
-    }
-    TINT_ICE() << "unhandled atomic type " << atomic->Type()->FriendlyName();
-}
-
-void Printer::EmitArrayType(StringStream& out, const core::type::Array* arr) {
-    out << ArrayTemplateName() << "<";
-    EmitType(out, arr->ElemType());
-    out << ", ";
-    if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
-        out << "1";
-    } else {
-        auto count = arr->ConstantCount();
-        if (!count) {
-            TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
-            return;
-        }
-        out << count.value();
-    }
-    out << ">";
-}
-
-void Printer::EmitVectorType(StringStream& out, const core::type::Vector* vec) {
-    if (vec->Packed()) {
-        out << "packed_";
-    }
-    EmitType(out, vec->type());
-    out << vec->Width();
-}
-
-void Printer::EmitMatrixType(StringStream& out, const core::type::Matrix* mat) {
-    EmitType(out, mat->type());
-    out << mat->columns() << "x" << mat->rows();
-}
-
-void Printer::EmitTextureType(StringStream& out, const core::type::Texture* tex) {
-    if (TINT_UNLIKELY(tex->Is<core::type::ExternalTexture>())) {
-        TINT_ICE() << "Multiplanar external texture transform was not run.";
-        return;
-    }
-
-    if (tex->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
-        out << "depth";
-    } else {
-        out << "texture";
-    }
-
-    switch (tex->dim()) {
-        case core::type::TextureDimension::k1d:
-            out << "1d";
-            break;
-        case core::type::TextureDimension::k2d:
-            out << "2d";
-            break;
-        case core::type::TextureDimension::k2dArray:
-            out << "2d_array";
-            break;
-        case core::type::TextureDimension::k3d:
-            out << "3d";
-            break;
-        case core::type::TextureDimension::kCube:
-            out << "cube";
-            break;
-        case core::type::TextureDimension::kCubeArray:
-            out << "cube_array";
-            break;
-        default:
-            TINT_ICE() << "invalid texture dimensions";
-            return;
-    }
-    if (tex->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture>()) {
-        out << "_ms";
-    }
-    out << "<";
-    TINT_DEFER(out << ">");
-
-    tint::Switch(
-        tex,  //
-        [&](const core::type::DepthTexture*) { out << "float, access::sample"; },
-        [&](const core::type::DepthMultisampledTexture*) { out << "float, access::read"; },
-        [&](const core::type::StorageTexture* storage) {
-            EmitType(out, storage->type());
-            out << ", ";
-
-            std::string access_str;
-            if (storage->access() == core::Access::kRead) {
-                out << "access::read";
-            } else if (storage->access() == core::Access::kWrite) {
-                out << "access::write";
-            } else {
-                TINT_ICE() << "invalid access control for storage texture";
+        auto space = ptr->AddressSpace();
+        switch (space) {
+            case core::AddressSpace::kFunction:
+            case core::AddressSpace::kHandle:
+                break;
+            case core::AddressSpace::kPrivate:
+                out << "thread ";
+                break;
+            case core::AddressSpace::kWorkgroup:
+                out << "threadgroup ";
+                break;
+            default:
+                TINT_ICE() << "unhandled variable address space";
                 return;
-            }
-        },
-        [&](const core::type::MultisampledTexture* ms) {
-            EmitType(out, ms->type());
-            out << ", access::read";
-        },
-        [&](const core::type::SampledTexture* sampled) {
-            EmitType(out, sampled->type());
-            out << ", access::sample";
-        },
-        [&](Default) { TINT_ICE() << "invalid texture type"; });
-}
-
-void Printer::EmitStructType(const core::type::Struct* str) {
-    auto it = emitted_structs_.emplace(str);
-    if (!it.second) {
-        return;
-    }
-
-    // This does not append directly to the preamble because a struct may require other
-    // structs, or the array template, to get emitted before it. So, the struct emits into a
-    // temporary text buffer, then anything it depends on will emit to the preamble first,
-    // and then it copies the text buffer into the preamble.
-    TextBuffer str_buf;
-    Line(&str_buf) << "struct " << StructName(str) << " {";
-
-    bool is_host_shareable = str->IsHostShareable();
-
-    // Emits a `/* 0xnnnn */` byte offset comment for a struct member.
-    auto add_byte_offset_comment = [&](StringStream& out, uint32_t offset) {
-        std::ios_base::fmtflags saved_flag_state(out.flags());
-        out << "/* 0x" << std::hex << std::setfill('0') << std::setw(4) << offset << " */ ";
-        out.flags(saved_flag_state);
-    };
-
-    auto add_padding = [&](uint32_t size, uint32_t msl_offset) {
-        std::string name;
-        do {
-            name = UniqueIdentifier("tint_pad");
-        } while (str->FindMember(ir_.symbols.Get(name)));
-
-        auto out = Line(&str_buf);
-        add_byte_offset_comment(out, msl_offset);
-        out << ArrayTemplateName() << "<int8_t, " << size << "> " << name << ";";
-    };
-
-    str_buf.IncrementIndent();
-
-    uint32_t msl_offset = 0;
-    for (auto* mem : str->Members()) {
-        auto out = Line(&str_buf);
-        auto mem_name = mem->Name().Name();
-        auto ir_offset = mem->Offset();
-
-        if (is_host_shareable) {
-            if (TINT_UNLIKELY(ir_offset < msl_offset)) {
-                // Unimplementable layout
-                TINT_ICE() << "Structure member offset (" << ir_offset << ") is behind MSL offset ("
-                           << msl_offset << ")";
-                return;
-            }
-
-            // Generate padding if required
-            if (auto padding = ir_offset - msl_offset) {
-                add_padding(padding, msl_offset);
-                msl_offset += padding;
-            }
-
-            add_byte_offset_comment(out, msl_offset);
         }
 
-        auto* ty = mem->Type();
+        auto name = ir_.NameOf(v);
 
-        EmitType(out, ty);
-        out << " " << mem_name;
+        EmitType(out, ptr->UnwrapPtr());
+        out << " " << name.Name();
 
-        // Emit attributes
-        auto& attributes = mem->Attributes();
-
-        if (auto builtin = attributes.builtin) {
-            auto name = BuiltinToAttribute(builtin.value());
-            if (name.empty()) {
-                TINT_ICE() << "unknown builtin";
-                return;
-            }
-            out << " [[" << name << "]]";
+        if (v->Initializer()) {
+            out << " = " << Expr(v->Initializer());
+        } else if (space == core::AddressSpace::kPrivate ||
+                   space == core::AddressSpace::kFunction ||
+                   space == core::AddressSpace::kUndefined) {
+            out << " = ";
+            EmitZeroValue(out, ptr->UnwrapPtr());
         }
-
-        if (auto location = attributes.location) {
-            auto& pipeline_stage_uses = str->PipelineStageUses();
-            if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
-                TINT_ICE() << "invalid entry point IO struct uses";
-                return;
-            }
-
-            if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexInput)) {
-                out << " [[attribute(" + std::to_string(location.value()) + ")]]";
-            } else if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexOutput)) {
-                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-            } else if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kFragmentInput)) {
-                out << " [[user(locn" + std::to_string(location.value()) + ")]]";
-            } else if (TINT_LIKELY(pipeline_stage_uses.count(
-                           core::type::PipelineStageUsage::kFragmentOutput))) {
-                out << " [[color(" + std::to_string(location.value()) + ")]]";
-            } else {
-                TINT_ICE() << "invalid use of location decoration";
-                return;
-            }
-        }
-
-        if (auto interpolation = attributes.interpolation) {
-            auto name = InterpolationToAttribute(interpolation->type, interpolation->sampling);
-            if (name.empty()) {
-                TINT_ICE() << "unknown interpolation attribute";
-                return;
-            }
-            out << " [[" << name << "]]";
-        }
-
-        if (attributes.invariant) {
-            invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
-            out << " " << invariant_define_name_;
-        }
-
         out << ";";
 
-        if (is_host_shareable) {
-            // Calculate new MSL offset
-            auto size_align = MslPackedTypeSizeAndAlign(ty);
-            if (TINT_UNLIKELY(msl_offset % size_align.align)) {
-                TINT_ICE() << "Misaligned MSL structure member " << mem_name << " : "
-                           << ty->FriendlyName() << " offset: " << msl_offset
-                           << " align: " << size_align.align;
-                return;
+        Bind(v->Result(), name, PtrKind::kRef);
+    }
+
+    /// Emit a let instruction
+    /// @param l the let instruction
+    void EmitLet(core::ir::Let* l) {
+        Bind(l->Result(), Expr(l->Value(), PtrKind::kPtr), PtrKind::kPtr);
+    }
+
+    /// Emit an if instruction
+    /// @param if_ the if instruction
+    void EmitIf(core::ir::If* if_) {
+        // Emit any nodes that need to be used as PHI nodes
+        for (auto* phi : if_->Results()) {
+            if (!ir_.NameOf(phi).IsValid()) {
+                ir_.SetName(phi, ir_.symbols.New());
             }
-            msl_offset += size_align.size;
+
+            auto name = ir_.NameOf(phi);
+
+            auto out = Line();
+            EmitType(out, phi->Type());
+            out << " " << name.Name() << ";";
+
+            Bind(phi, name);
+        }
+
+        Line() << "if (" << Expr(if_->Condition()) << ") {";
+
+        {
+            ScopedIndent si(current_buffer_);
+            EmitBlockInstructions(if_->True());
+        }
+
+        if (if_->False() && !if_->False()->IsEmpty()) {
+            Line() << "} else {";
+
+            ScopedIndent si(current_buffer_);
+            EmitBlockInstructions(if_->False());
+        }
+
+        Line() << "}";
+    }
+
+    /// Emit an exit-if instruction
+    /// @param e the exit-if instruction
+    void EmitExitIf(core::ir::ExitIf* e) {
+        auto results = e->If()->Results();
+        auto args = e->Args();
+        for (size_t i = 0; i < e->Args().Length(); ++i) {
+            auto* phi = results[i];
+            auto* val = args[i];
+
+            Line() << ir_.NameOf(phi).Name() << " = " << Expr(val) << ";";
         }
     }
 
-    if (is_host_shareable && str->Size() != msl_offset) {
-        add_padding(str->Size() - msl_offset, msl_offset);
+    /// Emit a return instruction
+    /// @param r the return instruction
+    void EmitReturn(core::ir::Return* r) {
+        // If this return has no arguments and the current block is for the function which is
+        // being returned, skip the return.
+        if (current_block_ == current_function_->Block() && r->Args().IsEmpty()) {
+            return;
+        }
+
+        auto out = Line();
+        out << "return";
+        if (!r->Args().IsEmpty()) {
+            out << " " << Expr(r->Args().Front());
+        }
+        out << ";";
     }
 
-    str_buf.DecrementIndent();
-    Line(&str_buf) << "};";
+    /// Emit an unreachable instruction
+    void EmitUnreachable() { Line() << "/* unreachable */"; }
 
-    preamble_buffer_.Append(str_buf);
-}
-
-void Printer::EmitConstant(StringStream& out, core::ir::Constant* c) {
-    EmitConstant(out, c->Value());
-}
-
-void Printer::EmitConstant(StringStream& out, const core::constant::Value* c) {
-    auto emit_values = [&](uint32_t count) {
-        for (size_t i = 0; i < count; i++) {
-            if (i > 0) {
-                out << ", ";
-            }
-            EmitConstant(out, c->Index(i));
+    /// Handles generating a address space
+    /// @param out the output of the type stream
+    /// @param sc the address space to generate
+    void EmitAddressSpace(StringStream& out, core::AddressSpace sc) {
+        switch (sc) {
+            case core::AddressSpace::kFunction:
+            case core::AddressSpace::kPrivate:
+            case core::AddressSpace::kHandle:
+                out << "thread";
+                break;
+            case core::AddressSpace::kWorkgroup:
+                out << "threadgroup";
+                break;
+            case core::AddressSpace::kStorage:
+                out << "device";
+                break;
+            case core::AddressSpace::kUniform:
+                out << "constant";
+                break;
+            default:
+                TINT_ICE() << "unhandled address space: " << sc;
+                break;
         }
-    };
+    }
 
-    tint::Switch(
-        c->Type(),  //
-        [&](const core::type::Bool*) { out << (c->ValueAs<bool>() ? "true" : "false"); },
-        [&](const core::type::I32*) { PrintI32(out, c->ValueAs<i32>()); },
-        [&](const core::type::U32*) { out << c->ValueAs<u32>() << "u"; },
-        [&](const core::type::F32*) { PrintF32(out, c->ValueAs<f32>()); },
-        [&](const core::type::F16*) { PrintF16(out, c->ValueAs<f16>()); },
-        [&](const core::type::Vector* v) {
-            EmitType(out, v);
+    /// Emit a type
+    /// @param out the stream to emit too
+    /// @param ty the type to emit
+    void EmitType(StringStream& out, const core::type::Type* ty) {
+        tint::Switch(
+            ty,                                               //
+            [&](const core::type::Bool*) { out << "bool"; },  //
+            [&](const core::type::Void*) { out << "void"; },  //
+            [&](const core::type::F32*) { out << "float"; },  //
+            [&](const core::type::F16*) { out << "half"; },   //
+            [&](const core::type::I32*) { out << "int"; },    //
+            [&](const core::type::U32*) { out << "uint"; },   //
+            [&](const core::type::Array* arr) { EmitArrayType(out, arr); },
+            [&](const core::type::Vector* vec) { EmitVectorType(out, vec); },
+            [&](const core::type::Matrix* mat) { EmitMatrixType(out, mat); },
+            [&](const core::type::Atomic* atomic) { EmitAtomicType(out, atomic); },
+            [&](const core::type::Pointer* ptr) { EmitPointerType(out, ptr); },
+            [&](const core::type::Sampler*) { out << "sampler"; },  //
+            [&](const core::type::Texture* tex) { EmitTextureType(out, tex); },
+            [&](const core::type::Struct* str) {
+                out << StructName(str);
 
-            ScopedParen sp(out);
-            if (auto* splat = c->As<core::constant::Splat>()) {
-                EmitConstant(out, splat->el);
-                return;
-            }
-            emit_values(v->Width());
-        },
-        [&](const core::type::Matrix* m) {
-            EmitType(out, m);
-            ScopedParen sp(out);
-            emit_values(m->columns());
-        },
-        [&](const core::type::Array* a) {
-            EmitType(out, a);
-            out << "{";
-            TINT_DEFER(out << "}");
+                TINT_SCOPED_ASSIGNMENT(current_buffer_, &preamble_buffer_);
+                EmitStructType(str);
+            },
+            [&](Default) { UNHANDLED_CASE(ty); });
+    }
 
-            if (c->AllZero()) {
-                return;
-            }
+    /// Handles generating a pointer declaration
+    /// @param out the output stream
+    /// @param ptr the pointer to emit
+    void EmitPointerType(StringStream& out, const core::type::Pointer* ptr) {
+        if (ptr->Access() == core::Access::kRead) {
+            out << "const ";
+        }
+        EmitAddressSpace(out, ptr->AddressSpace());
+        out << " ";
+        EmitType(out, ptr->StoreType());
+        out << "*";
+    }
 
-            auto count = a->ConstantCount();
+    /// Handles generating an atomic declaration
+    /// @param out the output stream
+    /// @param atomic the atomic to emit
+    void EmitAtomicType(StringStream& out, const core::type::Atomic* atomic) {
+        if (atomic->Type()->Is<core::type::I32>()) {
+            out << "atomic_int";
+            return;
+        }
+        if (TINT_LIKELY(atomic->Type()->Is<core::type::U32>())) {
+            out << "atomic_uint";
+            return;
+        }
+        TINT_ICE() << "unhandled atomic type " << atomic->Type()->FriendlyName();
+    }
+
+    /// Handles generating an array declaration
+    /// @param out the output stream
+    /// @param arr the array to emit
+    void EmitArrayType(StringStream& out, const core::type::Array* arr) {
+        out << ArrayTemplateName() << "<";
+        EmitType(out, arr->ElemType());
+        out << ", ";
+        if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
+            out << "1";
+        } else {
+            auto count = arr->ConstantCount();
             if (!count) {
                 TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
                 return;
             }
-            emit_values(*count);
-        },
-        [&](const core::type::Struct* s) {
-            EmitStructType(s);
-            out << StructName(s) << "{";
-            TINT_DEFER(out << "}");
+            out << count.value();
+        }
+        out << ">";
+    }
 
-            if (c->AllZero()) {
+    /// Handles generating a vector declaration
+    /// @param out the output stream
+    /// @param vec the vector to emit
+    void EmitVectorType(StringStream& out, const core::type::Vector* vec) {
+        if (vec->Packed()) {
+            out << "packed_";
+        }
+        EmitType(out, vec->type());
+        out << vec->Width();
+    }
+
+    /// Handles generating a matrix declaration
+    /// @param out the output stream
+    /// @param mat the matrix to emit
+    void EmitMatrixType(StringStream& out, const core::type::Matrix* mat) {
+        EmitType(out, mat->type());
+        out << mat->columns() << "x" << mat->rows();
+    }
+
+    /// Handles generating a texture declaration
+    /// @param out the output stream
+    /// @param tex the texture to emit
+    void EmitTextureType(StringStream& out, const core::type::Texture* tex) {
+        if (TINT_UNLIKELY(tex->Is<core::type::ExternalTexture>())) {
+            TINT_ICE() << "Multiplanar external texture transform was not run.";
+            return;
+        }
+
+        if (tex->IsAnyOf<core::type::DepthTexture, core::type::DepthMultisampledTexture>()) {
+            out << "depth";
+        } else {
+            out << "texture";
+        }
+
+        switch (tex->dim()) {
+            case core::type::TextureDimension::k1d:
+                out << "1d";
+                break;
+            case core::type::TextureDimension::k2d:
+                out << "2d";
+                break;
+            case core::type::TextureDimension::k2dArray:
+                out << "2d_array";
+                break;
+            case core::type::TextureDimension::k3d:
+                out << "3d";
+                break;
+            case core::type::TextureDimension::kCube:
+                out << "cube";
+                break;
+            case core::type::TextureDimension::kCubeArray:
+                out << "cube_array";
+                break;
+            default:
+                TINT_ICE() << "invalid texture dimensions";
                 return;
+        }
+        if (tex->IsAnyOf<core::type::MultisampledTexture, core::type::DepthMultisampledTexture>()) {
+            out << "_ms";
+        }
+        out << "<";
+        TINT_DEFER(out << ">");
+
+        tint::Switch(
+            tex,  //
+            [&](const core::type::DepthTexture*) { out << "float, access::sample"; },
+            [&](const core::type::DepthMultisampledTexture*) { out << "float, access::read"; },
+            [&](const core::type::StorageTexture* storage) {
+                EmitType(out, storage->type());
+                out << ", ";
+
+                std::string access_str;
+                if (storage->access() == core::Access::kRead) {
+                    out << "access::read";
+                } else if (storage->access() == core::Access::kWrite) {
+                    out << "access::write";
+                } else {
+                    TINT_ICE() << "invalid access control for storage texture";
+                    return;
+                }
+            },
+            [&](const core::type::MultisampledTexture* ms) {
+                EmitType(out, ms->type());
+                out << ", access::read";
+            },
+            [&](const core::type::SampledTexture* sampled) {
+                EmitType(out, sampled->type());
+                out << ", access::sample";
+            },
+            [&](Default) { TINT_ICE() << "invalid texture type"; });
+    }
+
+    /// Handles generating a struct declaration. If the structure has already been emitted, then
+    /// this function will simply return without emitting anything.
+    /// @param str the struct to generate
+    void EmitStructType(const core::type::Struct* str) {
+        auto it = emitted_structs_.emplace(str);
+        if (!it.second) {
+            return;
+        }
+
+        // This does not append directly to the preamble because a struct may require other
+        // structs, or the array template, to get emitted before it. So, the struct emits into a
+        // temporary text buffer, then anything it depends on will emit to the preamble first,
+        // and then it copies the text buffer into the preamble.
+        TextBuffer str_buf;
+        Line(&str_buf) << "struct " << StructName(str) << " {";
+
+        bool is_host_shareable = str->IsHostShareable();
+
+        // Emits a `/* 0xnnnn */` byte offset comment for a struct member.
+        auto add_byte_offset_comment = [&](StringStream& out, uint32_t offset) {
+            std::ios_base::fmtflags saved_flag_state(out.flags());
+            out << "/* 0x" << std::hex << std::setfill('0') << std::setw(4) << offset << " */ ";
+            out.flags(saved_flag_state);
+        };
+
+        auto add_padding = [&](uint32_t size, uint32_t msl_offset) {
+            std::string name;
+            do {
+                name = UniqueIdentifier("tint_pad");
+            } while (str->FindMember(ir_.symbols.Get(name)));
+
+            auto out = Line(&str_buf);
+            add_byte_offset_comment(out, msl_offset);
+            out << ArrayTemplateName() << "<int8_t, " << size << "> " << name << ";";
+        };
+
+        str_buf.IncrementIndent();
+
+        uint32_t msl_offset = 0;
+        for (auto* mem : str->Members()) {
+            auto out = Line(&str_buf);
+            auto mem_name = mem->Name().Name();
+            auto ir_offset = mem->Offset();
+
+            if (is_host_shareable) {
+                if (TINT_UNLIKELY(ir_offset < msl_offset)) {
+                    // Unimplementable layout
+                    TINT_ICE() << "Structure member offset (" << ir_offset
+                               << ") is behind MSL offset (" << msl_offset << ")";
+                    return;
+                }
+
+                // Generate padding if required
+                if (auto padding = ir_offset - msl_offset) {
+                    add_padding(padding, msl_offset);
+                    msl_offset += padding;
+                }
+
+                add_byte_offset_comment(out, msl_offset);
             }
 
-            auto members = s->Members();
-            for (size_t i = 0; i < members.Length(); i++) {
+            auto* ty = mem->Type();
+
+            EmitType(out, ty);
+            out << " " << mem_name;
+
+            // Emit attributes
+            auto& attributes = mem->Attributes();
+
+            if (auto builtin = attributes.builtin) {
+                auto name = BuiltinToAttribute(builtin.value());
+                if (name.empty()) {
+                    TINT_ICE() << "unknown builtin";
+                    return;
+                }
+                out << " [[" << name << "]]";
+            }
+
+            if (auto location = attributes.location) {
+                auto& pipeline_stage_uses = str->PipelineStageUses();
+                if (TINT_UNLIKELY(pipeline_stage_uses.size() != 1)) {
+                    TINT_ICE() << "invalid entry point IO struct uses";
+                    return;
+                }
+
+                if (pipeline_stage_uses.count(core::type::PipelineStageUsage::kVertexInput)) {
+                    out << " [[attribute(" + std::to_string(location.value()) + ")]]";
+                } else if (pipeline_stage_uses.count(
+                               core::type::PipelineStageUsage::kVertexOutput)) {
+                    out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+                } else if (pipeline_stage_uses.count(
+                               core::type::PipelineStageUsage::kFragmentInput)) {
+                    out << " [[user(locn" + std::to_string(location.value()) + ")]]";
+                } else if (TINT_LIKELY(pipeline_stage_uses.count(
+                               core::type::PipelineStageUsage::kFragmentOutput))) {
+                    out << " [[color(" + std::to_string(location.value()) + ")]]";
+                } else {
+                    TINT_ICE() << "invalid use of location decoration";
+                    return;
+                }
+            }
+
+            if (auto interpolation = attributes.interpolation) {
+                auto name = InterpolationToAttribute(interpolation->type, interpolation->sampling);
+                if (name.empty()) {
+                    TINT_ICE() << "unknown interpolation attribute";
+                    return;
+                }
+                out << " [[" << name << "]]";
+            }
+
+            if (attributes.invariant) {
+                invariant_define_name_ = UniqueIdentifier("TINT_INVARIANT");
+                out << " " << invariant_define_name_;
+            }
+
+            out << ";";
+
+            if (is_host_shareable) {
+                // Calculate new MSL offset
+                auto size_align = MslPackedTypeSizeAndAlign(ty);
+                if (TINT_UNLIKELY(msl_offset % size_align.align)) {
+                    TINT_ICE() << "Misaligned MSL structure member " << mem_name << " : "
+                               << ty->FriendlyName() << " offset: " << msl_offset
+                               << " align: " << size_align.align;
+                    return;
+                }
+                msl_offset += size_align.size;
+            }
+        }
+
+        if (is_host_shareable && str->Size() != msl_offset) {
+            add_padding(str->Size() - msl_offset, msl_offset);
+        }
+
+        str_buf.DecrementIndent();
+        Line(&str_buf) << "};";
+
+        preamble_buffer_.Append(str_buf);
+    }
+
+    /// Handles core::ir::Constant values
+    /// @param out the stream to write the constant too
+    /// @param c the constant to emit
+    void EmitConstant(StringStream& out, core::ir::Constant* c) { EmitConstant(out, c->Value()); }
+
+    /// Handles core::constant::Value values
+    /// @param out the stream to write the constant too
+    /// @param c the constant to emit
+    void EmitConstant(StringStream& out, const core::constant::Value* c) {
+        auto emit_values = [&](uint32_t count) {
+            for (size_t i = 0; i < count; i++) {
                 if (i > 0) {
                     out << ", ";
                 }
-                out << "." << members[i]->Name().Name() << "=";
                 EmitConstant(out, c->Index(i));
             }
-        },
-        [&](Default) { UNHANDLED_CASE(c->Type()); });
-}
+        };
 
-void Printer::EmitZeroValue(StringStream& out, const core::type::Type* ty) {
-    Switch(
-        ty, [&](const core::type::Bool*) { out << "false"; },                     //
-        [&](const core::type::F16*) { out << "0.0h"; },                           //
-        [&](const core::type::F32*) { out << "0.0f"; },                           //
-        [&](const core::type::I32*) { out << "0"; },                              //
-        [&](const core::type::U32*) { out << "0u"; },                             //
-        [&](const core::type::Vector* vec) { EmitZeroValue(out, vec->type()); },  //
-        [&](const core::type::Matrix* mat) {
-            EmitType(out, mat);
+        tint::Switch(
+            c->Type(),  //
+            [&](const core::type::Bool*) { out << (c->ValueAs<bool>() ? "true" : "false"); },
+            [&](const core::type::I32*) { PrintI32(out, c->ValueAs<i32>()); },
+            [&](const core::type::U32*) { out << c->ValueAs<u32>() << "u"; },
+            [&](const core::type::F32*) { PrintF32(out, c->ValueAs<f32>()); },
+            [&](const core::type::F16*) { PrintF16(out, c->ValueAs<f16>()); },
+            [&](const core::type::Vector* v) {
+                EmitType(out, v);
 
-            ScopedParen sp(out);
-            EmitZeroValue(out, mat->type());
-        },
-        [&](const core::type::Array*) { out << "{}"; },   //
-        [&](const core::type::Struct*) { out << "{}"; },  //
-        [&](Default) { TINT_ICE() << "Invalid type for zero emission: " << ty->FriendlyName(); });
-}
+                ScopedParen sp(out);
+                if (auto* splat = c->As<core::constant::Splat>()) {
+                    EmitConstant(out, splat->el);
+                    return;
+                }
+                emit_values(v->Width());
+            },
+            [&](const core::type::Matrix* m) {
+                EmitType(out, m);
+                ScopedParen sp(out);
+                emit_values(m->columns());
+            },
+            [&](const core::type::Array* a) {
+                EmitType(out, a);
+                out << "{";
+                TINT_DEFER(out << "}");
 
-std::string Printer::StructName(const core::type::Struct* s) {
-    auto name = s->Name().Name();
-    if (HasPrefix(name, "__")) {
-        name = tint::GetOrCreate(builtin_struct_names_, s,
-                                 [&] { return UniqueIdentifier(name.substr(2)); });
+                if (c->AllZero()) {
+                    return;
+                }
+
+                auto count = a->ConstantCount();
+                if (!count) {
+                    TINT_ICE() << core::type::Array::kErrExpectedConstantCount;
+                    return;
+                }
+                emit_values(*count);
+            },
+            [&](const core::type::Struct* s) {
+                EmitStructType(s);
+                out << StructName(s) << "{";
+                TINT_DEFER(out << "}");
+
+                if (c->AllZero()) {
+                    return;
+                }
+
+                auto members = s->Members();
+                for (size_t i = 0; i < members.Length(); i++) {
+                    if (i > 0) {
+                        out << ", ";
+                    }
+                    out << "." << members[i]->Name().Name() << "=";
+                    EmitConstant(out, c->Index(i));
+                }
+            },
+            [&](Default) { UNHANDLED_CASE(c->Type()); });
     }
-    return name;
-}
 
-std::string Printer::UniqueIdentifier(const std::string& prefix /* = "" */) {
-    return ir_.symbols.New(prefix).Name();
-}
+    /// Emits the zero value for the given type
+    /// @param out the stream to emit too
+    /// @param ty the type
+    void EmitZeroValue(StringStream& out, const core::type::Type* ty) {
+        Switch(
+            ty, [&](const core::type::Bool*) { out << "false"; },                     //
+            [&](const core::type::F16*) { out << "0.0h"; },                           //
+            [&](const core::type::F32*) { out << "0.0f"; },                           //
+            [&](const core::type::I32*) { out << "0"; },                              //
+            [&](const core::type::U32*) { out << "0u"; },                             //
+            [&](const core::type::Vector* vec) { EmitZeroValue(out, vec->type()); },  //
+            [&](const core::type::Matrix* mat) {
+                EmitType(out, mat);
 
-TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
+                ScopedParen sp(out);
+                EmitZeroValue(out, mat->type());
+            },
+            [&](const core::type::Array*) { out << "{}"; },   //
+            [&](const core::type::Struct*) { out << "{}"; },  //
+            [&](Default) {
+                TINT_ICE() << "Invalid type for zero emission: " << ty->FriendlyName();
+            });
+    }
 
-std::string Printer::Expr(core::ir::Value* value, PtrKind want_ptr_kind) {
-    using ExprAndPtrKind = std::pair<std::string, PtrKind>;
+    /// @param s the structure
+    /// @returns the name of the structure, taking special care of builtin structures that start
+    /// with double underscores. If the structure is a builtin, then the returned name will be a
+    /// unique name without the leading underscores.
+    std::string StructName(const core::type::Struct* s) {
+        auto name = s->Name().Name();
+        if (HasPrefix(name, "__")) {
+            name = tint::GetOrCreate(builtin_struct_names_, s,
+                                     [&] { return UniqueIdentifier(name.substr(2)); });
+        }
+        return name;
+    }
 
-    auto [expr, got_ptr_kind] = tint::Switch(
-        value,
-        [&](core::ir::Constant* c) -> ExprAndPtrKind {
-            StringStream str;
-            EmitConstant(str, c);
-            return {str.str(), PtrKind::kRef};
-        },
-        [&](Default) -> ExprAndPtrKind {
-            auto lookup = bindings_.Find(value);
-            if (TINT_UNLIKELY(!lookup)) {
-                TINT_ICE() << "Expr(" << (value ? value->TypeInfo().name : "null")
-                           << ") value has no expression";
-                return {};
-            }
+    /// @return a new, unique identifier with the given prefix.
+    /// @param prefix optional prefix to apply to the generated identifier. If empty "tint_symbol"
+    /// will be used.
+    std::string UniqueIdentifier(const std::string& prefix /* = "" */) {
+        return ir_.symbols.New(prefix).Name();
+    }
 
-            return std::visit(
-                [&](auto&& got) -> ExprAndPtrKind {
-                    using T = std::decay_t<decltype(got)>;
+    TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
 
-                    if constexpr (std::is_same_v<T, VariableValue>) {
-                        return {got.name.Name(), got.ptr_kind};
-                    }
+    /// Returns the expression for the given value
+    /// @param value the value to lookup
+    /// @param want_ptr_kind the pointer information for the return
+    /// @returns the string expression
+    std::string Expr(core::ir::Value* value, PtrKind want_ptr_kind = PtrKind::kRef) {
+        using ExprAndPtrKind = std::pair<std::string, PtrKind>;
 
-                    if constexpr (std::is_same_v<T, InlinedValue>) {
-                        auto result = ExprAndPtrKind{got.expr, got.ptr_kind};
-
-                        // Single use (inlined) expression.
-                        // Mark the bindings_ map entry as consumed.
-                        *lookup = ConsumedValue{};
-                        return result;
-                    }
-
-                    if constexpr (std::is_same_v<T, ConsumedValue>) {
-                        TINT_ICE() << "Expr(" << value->TypeInfo().name
-                                   << ") called twice on the same value";
-                    } else {
-                        TINT_ICE() << "Expr(" << value->TypeInfo().name << ") has unhandled value";
-                    }
+        auto [expr, got_ptr_kind] = tint::Switch(
+            value,
+            [&](core::ir::Constant* c) -> ExprAndPtrKind {
+                StringStream str;
+                EmitConstant(str, c);
+                return {str.str(), PtrKind::kRef};
+            },
+            [&](Default) -> ExprAndPtrKind {
+                auto lookup = bindings_.Find(value);
+                if (TINT_UNLIKELY(!lookup)) {
+                    TINT_ICE() << "Expr(" << (value ? value->TypeInfo().name : "null")
+                               << ") value has no expression";
                     return {};
-                },
-                *lookup);
-        });
-    if (expr.empty()) {
-        return "<error>";
+                }
+
+                return std::visit(
+                    [&](auto&& got) -> ExprAndPtrKind {
+                        using T = std::decay_t<decltype(got)>;
+
+                        if constexpr (std::is_same_v<T, VariableValue>) {
+                            return {got.name.Name(), got.ptr_kind};
+                        }
+
+                        if constexpr (std::is_same_v<T, InlinedValue>) {
+                            auto result = ExprAndPtrKind{got.expr, got.ptr_kind};
+
+                            // Single use (inlined) expression.
+                            // Mark the bindings_ map entry as consumed.
+                            *lookup = ConsumedValue{};
+                            return result;
+                        }
+
+                        if constexpr (std::is_same_v<T, ConsumedValue>) {
+                            TINT_ICE() << "Expr(" << value->TypeInfo().name
+                                       << ") called twice on the same value";
+                        } else {
+                            TINT_ICE()
+                                << "Expr(" << value->TypeInfo().name << ") has unhandled value";
+                        }
+                        return {};
+                    },
+                    *lookup);
+            });
+        if (expr.empty()) {
+            return "<error>";
+        }
+
+        if (value->Type()->Is<core::type::Pointer>()) {
+            return ToPtrKind(expr, got_ptr_kind, want_ptr_kind);
+        }
+
+        return expr;
     }
 
-    if (value->Type()->Is<core::type::Pointer>()) {
-        return ToPtrKind(expr, got_ptr_kind, want_ptr_kind);
+    TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+
+    /// Returns the given expression converted to the given pointer kind
+    /// @param in the input expression
+    /// @param got the pointer kind we have
+    /// @param want the pointer kind we want
+    std::string ToPtrKind(const std::string& in, PtrKind got, PtrKind want) {
+        if (want == PtrKind::kRef && got == PtrKind::kPtr) {
+            return "*(" + in + ")";
+        }
+        if (want == PtrKind::kPtr && got == PtrKind::kRef) {
+            return "&(" + in + ")";
+        }
+        return in;
     }
 
-    return expr;
-}
+    /// Associates an IR value with a result expression
+    /// @param value the IR value
+    /// @param expr the result expression
+    /// @param ptr_kind defines how pointer values are represented by the expression
+    void Bind(core::ir::Value* value, const std::string& expr, PtrKind ptr_kind = PtrKind::kRef) {
+        TINT_ASSERT(value);
 
-TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
+        if (can_inline_.Remove(value)) {
+            // Value will be inlined at its place of usage.
+            if (TINT_LIKELY(bindings_.Add(value, InlinedValue{expr, ptr_kind}))) {
+                return;
+            }
+        } else {
+            auto mod_name = ir_.NameOf(value);
+            if (value->Usages().IsEmpty() && !mod_name.IsValid()) {
+                // Drop phonies.
+            } else {
+                if (mod_name.Name().empty()) {
+                    mod_name = ir_.symbols.New("v");
+                }
 
-std::string Printer::ToPtrKind(const std::string& in, PtrKind got, PtrKind want) {
-    if (want == PtrKind::kRef && got == PtrKind::kPtr) {
-        return "*(" + in + ")";
-    }
-    if (want == PtrKind::kPtr && got == PtrKind::kRef) {
-        return "&(" + in + ")";
-    }
-    return in;
-}
+                auto out = Line();
+                EmitType(out, value->Type());
+                out << " const " << mod_name.Name() << " = ";
+                if (value->Type()->Is<core::type::Pointer>()) {
+                    out << ToPtrKind(expr, ptr_kind, PtrKind::kPtr);
+                } else {
+                    out << expr;
+                }
+                out << ";";
 
-void Printer::Bind(core::ir::Value* value, const std::string& expr, PtrKind ptr_kind) {
-    TINT_ASSERT(value);
-
-    if (can_inline_.Remove(value)) {
-        // Value will be inlined at its place of usage.
-        if (TINT_LIKELY(bindings_.Add(value, InlinedValue{expr, ptr_kind}))) {
+                Bind(value, mod_name, PtrKind::kPtr);
+            }
             return;
         }
-    } else {
-        auto mod_name = ir_.NameOf(value);
-        if (value->Usages().IsEmpty() && !mod_name.IsValid()) {
-            // Drop phonies.
-        } else {
-            if (mod_name.Name().empty()) {
-                mod_name = ir_.symbols.New("v");
-            }
 
-            auto out = Line();
-            EmitType(out, value->Type());
-            out << " const " << mod_name.Name() << " = ";
-            if (value->Type()->Is<core::type::Pointer>()) {
-                out << ToPtrKind(expr, ptr_kind, PtrKind::kPtr);
-            } else {
-                out << expr;
-            }
-            out << ";";
-
-            Bind(value, mod_name, PtrKind::kPtr);
-        }
-        return;
-    }
-
-    TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
-}
-
-void Printer::Bind(core::ir::Value* value, Symbol name, PtrKind ptr_kind) {
-    TINT_ASSERT(value);
-
-    bool added = bindings_.Add(value, VariableValue{name, ptr_kind});
-    if (TINT_UNLIKELY(!added)) {
         TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
     }
-}
 
-void Printer::MarkInlinable(core::ir::Block* block) {
-    // An ordered list of possibly-inlinable values returned by sequenced instructions that have
-    // not yet been marked-for or ruled-out-for inlining.
-    UniqueVector<core::ir::Value*, 32> pending_resolution;
+    /// Associates an IR value the 'var', 'let' or parameter of the given name
+    /// @param value the IR value
+    /// @param name the name for the value
+    /// @param ptr_kind defines how pointer values are represented by @p expr.
+    void Bind(core::ir::Value* value, Symbol name, PtrKind ptr_kind = PtrKind::kRef) {
+        TINT_ASSERT(value);
 
-    // Walk the instructions of the block starting with the first.
-    for (auto* inst : *block) {
-        // Is the instruction sequenced?
-        bool sequenced = inst->Sequenced();
-
-        if (inst->Results().Length() != 1) {
-            continue;
-        }
-
-        // Instruction has a single result value.
-        // Check to see if the result of this instruction is a candidate for inlining.
-        auto* result = inst->Result();
-        // Only values with a single usage can be inlined.
-        // Named values are not inlined, as we want to emit the name for a let.
-        if (result->Usages().Count() == 1 && !ir_.NameOf(result).IsValid()) {
-            if (sequenced) {
-                // The value comes from a sequenced instruction.  Don't inline.
-            } else {
-                // The value comes from an unsequenced instruction. Just inline.
-                can_inline_.Add(result);
-            }
-            continue;
+        bool added = bindings_.Add(value, VariableValue{name, ptr_kind});
+        if (TINT_UNLIKELY(!added)) {
+            TINT_ICE() << "Bind(" << value->TypeInfo().name << ") called twice for same value";
         }
     }
+
+    /// Marks instructions in a block for inlineability
+    /// @param block the block
+    void MarkInlinable(core::ir::Block* block) {
+        // An ordered list of possibly-inlinable values returned by sequenced instructions that have
+        // not yet been marked-for or ruled-out-for inlining.
+        UniqueVector<core::ir::Value*, 32> pending_resolution;
+
+        // Walk the instructions of the block starting with the first.
+        for (auto* inst : *block) {
+            // Is the instruction sequenced?
+            bool sequenced = inst->Sequenced();
+
+            if (inst->Results().Length() != 1) {
+                continue;
+            }
+
+            // Instruction has a single result value.
+            // Check to see if the result of this instruction is a candidate for inlining.
+            auto* result = inst->Result();
+            // Only values with a single usage can be inlined.
+            // Named values are not inlined, as we want to emit the name for a let.
+            if (result->Usages().Count() == 1 && !ir_.NameOf(result).IsValid()) {
+                if (sequenced) {
+                    // The value comes from a sequenced instruction.  Don't inline.
+                } else {
+                    // The value comes from an unsequenced instruction. Just inline.
+                    can_inline_.Add(result);
+                }
+                continue;
+            }
+        }
+    }
+};
+}  // namespace
+
+Result<std::string> Print(core::ir::Module& module) {
+    return Printer{module}.Generate();
 }
 
 }  // namespace tint::msl::writer
diff --git a/src/tint/lang/msl/writer/printer/printer.h b/src/tint/lang/msl/writer/printer/printer.h
index 09f111c..a5218c3 100644
--- a/src/tint/lang/msl/writer/printer/printer.h
+++ b/src/tint/lang/msl/writer/printer/printer.h
@@ -29,231 +29,19 @@
 #define SRC_TINT_LANG_MSL_WRITER_PRINTER_PRINTER_H_
 
 #include <string>
-#include <unordered_map>
-#include <unordered_set>
 
-#include "src/tint/lang/core/ir/module.h"
-#include "src/tint/lang/core/type/texture.h"
-#include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/generator/text_generator.h"
-#include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/result/result.h"
 
 // Forward declarations
 namespace tint::core::ir {
-class Binary;
-class ExitIf;
-class If;
-class Let;
-class Load;
-class Return;
-class Unreachable;
-class Var;
+class Module;
 }  // namespace tint::core::ir
 
 namespace tint::msl::writer {
 
-/// Implementation class for the MSL generator
-class Printer : public tint::TextGenerator {
-  public:
-    /// Constructor
-    /// @param module the Tint IR module to generate
-    explicit Printer(core::ir::Module& module);
-    ~Printer() override;
-
-    /// @returns success or failure
-    tint::Result<SuccessType> Generate();
-
-    /// @copydoc tint::TextGenerator::Result
-    std::string Result() const override;
-
-  private:
-    /// Emit the function
-    /// @param func the function to emit
-    void EmitFunction(core::ir::Function* func);
-
-    /// Emit a block
-    /// @param block the block to emit
-    void EmitBlock(core::ir::Block* block);
-    /// Emit the instructions in a block
-    /// @param block the block with the instructions to emit
-    void EmitBlockInstructions(core::ir::Block* block);
-
-    /// Emit an if instruction
-    /// @param if_ the if instruction
-    void EmitIf(core::ir::If* if_);
-    /// Emit an exit-if instruction
-    /// @param e the exit-if instruction
-    void EmitExitIf(core::ir::ExitIf* e);
-
-    /// Emit a let instruction
-    /// @param l the let instruction
-    void EmitLet(core::ir::Let* l);
-    /// Emit a var instruction
-    /// @param v the var instruction
-    void EmitVar(core::ir::Var* v);
-    /// Emit a load instruction
-    /// @param l the load instruction
-    void EmitLoad(core::ir::Load* l);
-
-    /// Emit a return instruction
-    /// @param r the return instruction
-    void EmitReturn(core::ir::Return* r);
-    /// Emit an unreachable instruction
-    void EmitUnreachable();
-
-    /// Emit a binary instruction
-    /// @param b the binary instruction
-    void EmitBinary(core::ir::Binary* b);
-
-    /// Emit a type
-    /// @param out the stream to emit too
-    /// @param ty the type to emit
-    void EmitType(StringStream& out, const core::type::Type* ty);
-
-    /// Handles generating an array declaration
-    /// @param out the output stream
-    /// @param arr the array to emit
-    void EmitArrayType(StringStream& out, const core::type::Array* arr);
-    /// Handles generating an atomic declaration
-    /// @param out the output stream
-    /// @param atomic the atomic to emit
-    void EmitAtomicType(StringStream& out, const core::type::Atomic* atomic);
-    /// Handles generating a pointer declaration
-    /// @param out the output stream
-    /// @param ptr the pointer to emit
-    void EmitPointerType(StringStream& out, const core::type::Pointer* ptr);
-    /// Handles generating a vector declaration
-    /// @param out the output stream
-    /// @param vec the vector to emit
-    void EmitVectorType(StringStream& out, const core::type::Vector* vec);
-    /// Handles generating a matrix declaration
-    /// @param out the output stream
-    /// @param mat the matrix to emit
-    void EmitMatrixType(StringStream& out, const core::type::Matrix* mat);
-    /// Handles generating a texture declaration
-    /// @param out the output stream
-    /// @param tex the texture to emit
-    void EmitTextureType(StringStream& out, const core::type::Texture* tex);
-    /// Handles generating a struct declaration. If the structure has already been emitted, then
-    /// this function will simply return without emitting anything.
-    /// @param str the struct to generate
-    void EmitStructType(const core::type::Struct* str);
-
-    /// Handles generating a address space
-    /// @param out the output of the type stream
-    /// @param sc the address space to generate
-    void EmitAddressSpace(StringStream& out, core::AddressSpace sc);
-
-    /// Handles core::ir::Constant values
-    /// @param out the stream to write the constant too
-    /// @param c the constant to emit
-    void EmitConstant(StringStream& out, core::ir::Constant* c);
-    /// Handles core::constant::Value values
-    /// @param out the stream to write the constant too
-    /// @param c the constant to emit
-    void EmitConstant(StringStream& out, const core::constant::Value* c);
-
-    /// Emits the zero value for the given type
-    /// @param out the stream to emit too
-    /// @param ty the type
-    void EmitZeroValue(StringStream& out, const core::type::Type* ty);
-
-    /// @returns the name of the templated `tint_array` helper type, generating it if needed
-    const std::string& ArrayTemplateName();
-
-    /// @param s the structure
-    /// @returns the name of the structure, taking special care of builtin structures that start
-    /// with double underscores. If the structure is a builtin, then the returned name will be a
-    /// unique name without the leading underscores.
-    std::string StructName(const core::type::Struct* s);
-
-    /// @return a new, unique identifier with the given prefix.
-    /// @param prefix optional prefix to apply to the generated identifier. If empty "tint_symbol"
-    /// will be used.
-    std::string UniqueIdentifier(const std::string& prefix = "");
-
-    /// Map of builtin structure to unique generated name
-    std::unordered_map<const core::type::Struct*, std::string> builtin_struct_names_;
-
-    core::ir::Module& ir_;
-
-    /// The buffer holding preamble text
-    TextBuffer preamble_buffer_;
-
-    /// Unique name of the 'TINT_INVARIANT' preprocessor define.
-    /// Non-empty only if an invariant attribute has been generated.
-    std::string invariant_define_name_;
-
-    std::unordered_set<const core::type::Struct*> emitted_structs_;
-
-    /// The current function being emitted
-    core::ir::Function* current_function_ = nullptr;
-    /// The current block being emitted
-    core::ir::Block* current_block_ = nullptr;
-
-    /// Unique name of the tint_array<T, N> template.
-    /// Non-empty only if the template has been generated.
-    std::string array_template_name_;
-
-    /// The representation for an IR pointer type
-    enum class PtrKind {
-        kPtr,  // IR pointer is represented in a pointer
-        kRef,  // IR pointer is represented in a reference
-    };
-
-    /// The structure for a value held by a 'let', 'var' or parameter.
-    struct VariableValue {
-        Symbol name;  // Name of the variable
-        PtrKind ptr_kind = PtrKind::kRef;
-    };
-
-    /// The structure for an inlined value
-    struct InlinedValue {
-        std::string expr;
-        PtrKind ptr_kind = PtrKind::kRef;
-    };
-
-    /// Empty struct used as a sentinel value to indicate that an string expression has been
-    /// consumed by its single place of usage. Attempting to use this value a second time should
-    /// result in an ICE.
-    struct ConsumedValue {};
-
-    using ValueBinding = std::variant<VariableValue, InlinedValue, ConsumedValue>;
-
-    /// IR values to their representation
-    Hashmap<core::ir::Value*, ValueBinding, 32> bindings_;
-
-    /// Values that can be inlined.
-    Hashset<core::ir::Value*, 64> can_inline_;
-
-    /// Returns the expression for the given value
-    /// @param value the value to lookup
-    /// @param want_ptr_kind the pointer information for the return
-    /// @returns the string expression
-    std::string Expr(core::ir::Value* value, PtrKind want_ptr_kind = PtrKind::kRef);
-
-    /// Returns the given expression converted to the given pointer kind
-    /// @param in the input expression
-    /// @param got the pointer kind we have
-    /// @param want the pointer kind we want
-    std::string ToPtrKind(const std::string& in, PtrKind got, PtrKind want);
-
-    /// Associates an IR value with a result expression
-    /// @param value the IR value
-    /// @param expr the result expression
-    /// @param ptr_kind defines how pointer values are represented by the expression
-    void Bind(core::ir::Value* value, const std::string& expr, PtrKind ptr_kind = PtrKind::kRef);
-
-    /// Associates an IR value the 'var', 'let' or parameter of the given name
-    /// @param value the IR value
-    /// @param name the name for the value
-    /// @param ptr_kind defines how pointer values are represented by @p expr.
-    void Bind(core::ir::Value* value, Symbol name, PtrKind ptr_kind = PtrKind::kRef);
-
-    /// Marks instructions in a block for inlineability
-    /// @param block the block
-    void MarkInlinable(core::ir::Block* block);
-};
+/// @returns the generated MSL shader on success, or failure
+/// @param module the Tint IR module to generate
+Result<std::string> Print(core::ir::Module& module);
 
 }  // namespace tint::msl::writer
 
diff --git a/src/tint/lang/msl/writer/writer.cc b/src/tint/lang/msl/writer/writer.cc
index 8f854df..78f3040 100644
--- a/src/tint/lang/msl/writer/writer.cc
+++ b/src/tint/lang/msl/writer/writer.cc
@@ -69,12 +69,11 @@
         }
 
         // Generate the MSL code.
-        auto impl = std::make_unique<Printer>(ir);
-        auto result = impl->Generate();
+        auto result = Print(ir);
         if (!result) {
             return result.Failure();
         }
-        output.msl = impl->Result();
+        output.msl = result.Get();
 #else
         return Failure{"use_tint_ir requires building with TINT_BUILD_WGSL_READER"};
 #endif