Import Tint changes from Dawn

Changes:
  - 710b62fdf1227d0da9c890b4d43f3f1c447f7fc9 [tint][wgsl] Migrate more resolver diagnostics over to St... by Ben Clayton <bclayton@google.com>
  - c27315a48c5cd49e525c14d3bdee3f18bf1dc28e [tint] Use StyledText for all diagnostics by Ben Clayton <bclayton@google.com>
  - 89549b14a2200f128a3025b317efff3efae194cb [tint] Add new StyledText utilities by Ben Clayton <bclayton@google.com>
  - e4d210d69171910312fedb84f836653b4a9869a2 [tint][wgsl] Add 'chromium_internal_graphite' extension by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 710b62fdf1227d0da9c890b4d43f3f1c447f7fc9
Change-Id: Ib3929ed9cabc3f6b3069ce4c1c7b2bbec96aa4e0
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/176760
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/include/tint/tint.h b/include/tint/tint.h
index 492f11b..23d1d3e 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -51,7 +51,7 @@
 #include "src/tint/lang/wgsl/helpers/flatten_bindings.h"
 #include "src/tint/lang/wgsl/inspector/inspector.h"
 #include "src/tint/utils/diagnostic/formatter.h"
-#include "src/tint/utils/diagnostic/printer.h"
+#include "src/tint/utils/text/styled_text.h"
 
 #if TINT_BUILD_SPV_READER
 #include "src/tint/lang/spirv/reader/reader.h"
diff --git a/src/tint/cmd/common/helper.cc b/src/tint/cmd/common/helper.cc
index 4dd0ea1..c043588 100644
--- a/src/tint/cmd/common/helper.cc
+++ b/src/tint/cmd/common/helper.cc
@@ -46,8 +46,10 @@
 #endif
 
 #include "src/tint/utils/diagnostic/formatter.h"
-#include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/text/string.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/text_style.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint::cmd {
@@ -145,10 +147,10 @@
 }  // namespace
 
 [[noreturn]] void TintInternalCompilerErrorReporter(const InternalCompilerError& err) {
-    auto printer = diag::Printer::Create(stderr, true);
-    diag::Style bold_red{diag::Color::kRed, true};
-    printer->Write(err.Error(), bold_red);
-    constexpr const char* please_file_bug = R"(
+    auto printer = StyledTextPrinter::Create(stderr);
+    StyledText msg;
+    msg << (style::Error + style::Bold) << err.Error();
+    msg << R"(
 ********************************************************************
 *  The tint shader compiler has encountered an unexpected error.   *
 *                                                                  *
@@ -156,7 +158,7 @@
 *  crbug.com/tint with the source program that triggered the bug.  *
 ********************************************************************
 )";
-    printer->Write(please_file_bug, bold_red);
+    printer->Print(msg);
     exit(1);
 }
 
@@ -267,9 +269,9 @@
             PrintWGSL(std::cout, info.program);
         }
 
-        auto diag_printer = tint::diag::Printer::Create(stderr, true);
-        tint::diag::Formatter diag_formatter;
-        diag_formatter.Format(info.program.Diagnostics(), diag_printer.get());
+        auto printer = tint::StyledTextPrinter::Create(stderr);
+        tint::diag::Formatter formatter;
+        printer->Print(formatter.Format(info.program.Diagnostics()));
     }
 
     if (!info.program.IsValid()) {
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 8db9a64..947355e 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -56,10 +56,11 @@
 #include "src/tint/utils/command/command.h"
 #include "src/tint/utils/containers/transform.h"
 #include "src/tint/utils/diagnostic/formatter.h"
-#include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/macros/defer.h"
 #include "src/tint/utils/text/string.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
 
 #if TINT_BUILD_WGSL_READER
 #include "src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.h"
@@ -789,9 +790,9 @@
         auto source = std::make_unique<tint::Source::File>(options.input_filename, result->wgsl);
         auto reparsed_program = tint::wgsl::reader::Parse(source.get(), parser_options);
         if (!reparsed_program.IsValid()) {
-            auto diag_printer = tint::diag::Printer::Create(stderr, true);
+            auto printer = tint::StyledTextPrinter::Create(stderr);
             tint::diag::Formatter diag_formatter;
-            diag_formatter.Format(reparsed_program.Diagnostics(), diag_printer.get());
+            printer->Print(diag_formatter.Format(reparsed_program.Diagnostics()));
             return false;
         }
     }
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index bf83f01..3809954 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -101,6 +101,7 @@
       "${tint_src_dir}/lang/wgsl/writer",
       "${tint_src_dir}/utils/diagnostic",
       "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/text",
     ]
 
     sources = [
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index a50ca8e..be3a673 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -51,8 +51,10 @@
 #include "src/tint/lang/wgsl/program/program.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/diagnostic/formatter.h"
-#include "src/tint/utils/diagnostic/printer.h"
 #include "src/tint/utils/math/hash.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/text_style.h"
 
 #if TINT_BUILD_SPV_WRITER
 #include "src/tint/lang/spirv/writer/helpers/ast_generate_bindings.h"
@@ -74,15 +76,14 @@
 // to better de-duplication of bug reports, because ClusterFuzz only uses the
 // top few stack frames for de-duplication, and a FATAL_ERROR stack frame
 // provides no useful information.
-#define FATAL_ERROR(diags, msg_string)                             \
-    do {                                                           \
-        std::string msg = msg_string;                              \
-        auto printer = tint::diag::Printer::Create(stderr, true);  \
-        if (!msg.empty()) {                                        \
-            printer->Write(msg + "\n", {diag::Color::kRed, true}); \
-        }                                                          \
-        tint::diag::Formatter().Format(diags, printer.get());      \
-        __builtin_trap();                                          \
+#define FATAL_ERROR(diags, msg_string)                          \
+    do {                                                        \
+        StyledText msg;                                         \
+        msg << (style::Error + style::Bold) << msg_string;      \
+        auto printer = tint::StyledTextPrinter::Create(stderr); \
+        printer->Print(msg);                                    \
+        printer->Print(tint::diag::Formatter().Format(diags));  \
+        __builtin_trap();                                       \
     } while (false)
 
 [[noreturn]] void TintInternalCompilerErrorReporter(const InternalCompilerError& err) {
@@ -117,13 +118,13 @@
     const tint::diag::List& diags = program.Diagnostics();
     tools.SetMessageConsumer(
         [diags](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
-            std::stringstream out;
+            StyledText out;
             out << "Unexpected spirv-val error:\n"
-                << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl;
+                << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg;
 
-            auto printer = tint::diag::Printer::Create(stderr, true);
-            printer->Write(out.str(), {diag::Color::kYellow, false});
-            tint::diag::Formatter().Format(diags, printer.get());
+            auto printer = tint::StyledTextPrinter::Create(stderr);
+            printer->Print(out);
+            printer->Print(tint::diag::Formatter().Format(diags));
         });
 
     return tools.Validate(spirv.data(), spirv.size(), spvtools::ValidatorOptions());
diff --git a/src/tint/lang/core/constant/eval.cc b/src/tint/lang/core/constant/eval.cc
index 74b9022..0b78386 100644
--- a/src/tint/lang/core/constant/eval.cc
+++ b/src/tint/lang/core/constant/eval.cc
@@ -281,7 +281,7 @@
             // [abstract-numeric -> x] - materialization failure
             auto msg = OverflowErrorMessage(scalar->value, target_ty->FriendlyName());
             if (ctx.use_runtime_semantics) {
-                ctx.diags.AddWarning(tint::diag::System::Resolver, msg, ctx.source);
+                ctx.diags.AddWarning(tint::diag::System::Resolver, ctx.source) << msg;
                 switch (conv.Failure()) {
                     case ConversionFailure::kExceedsNegativeLimit:
                         return ctx.mgr.Get<Scalar<TO>>(target_ty, TO::Lowest());
@@ -289,7 +289,7 @@
                         return ctx.mgr.Get<Scalar<TO>>(target_ty, TO::Highest());
                 }
             } else {
-                ctx.diags.AddError(tint::diag::System::Resolver, msg, ctx.source);
+                ctx.diags.AddError(tint::diag::System::Resolver, ctx.source) << msg;
                 return nullptr;
             }
         } else if constexpr (IsFloatingPoint<TO>) {
@@ -297,7 +297,7 @@
             // https://www.w3.org/TR/WGSL/#floating-point-conversion
             auto msg = OverflowErrorMessage(scalar->value, target_ty->FriendlyName());
             if (ctx.use_runtime_semantics) {
-                ctx.diags.AddWarning(tint::diag::System::Resolver, msg, ctx.source);
+                ctx.diags.AddWarning(tint::diag::System::Resolver, ctx.source) << msg;
                 switch (conv.Failure()) {
                     case ConversionFailure::kExceedsNegativeLimit:
                         return ctx.mgr.Get<Scalar<TO>>(target_ty, TO::Lowest());
@@ -305,7 +305,7 @@
                         return ctx.mgr.Get<Scalar<TO>>(target_ty, TO::Highest());
                 }
             } else {
-                ctx.diags.AddError(tint::diag::System::Resolver, msg, ctx.source);
+                ctx.diags.AddError(tint::diag::System::Resolver, ctx.source) << msg;
                 return nullptr;
             }
         } else if constexpr (IsFloatingPoint<FROM>) {
@@ -648,7 +648,7 @@
 
     if constexpr (IsFloatingPoint<T>) {
         if (!std::isfinite(v.value)) {
-            AddError(OverflowErrorMessage(v, t->FriendlyName()), source);
+            AddError(source) << OverflowErrorMessage(v, t->FriendlyName());
             if (use_runtime_semantics_) {
                 return mgr.Zero(t);
             } else {
@@ -666,7 +666,7 @@
         if (auto r = CheckedAdd(a, b)) {
             result = r->value;
         } else {
-            AddError(OverflowErrorMessage(a, "+", b), source);
+            AddError(source) << OverflowErrorMessage(a, "+", b);
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
@@ -696,7 +696,7 @@
         if (auto r = CheckedSub(a, b)) {
             result = r->value;
         } else {
-            AddError(OverflowErrorMessage(a, "-", b), source);
+            AddError(source) << OverflowErrorMessage(a, "-", b);
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
@@ -727,7 +727,7 @@
         if (auto r = CheckedMul(a, b)) {
             result = r->value;
         } else {
-            AddError(OverflowErrorMessage(a, "*", b), source);
+            AddError(source) << OverflowErrorMessage(a, "*", b);
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
@@ -756,7 +756,7 @@
         if (auto r = CheckedDiv(a, b)) {
             result = r->value;
         } else {
-            AddError(OverflowErrorMessage(a, "/", b), source);
+            AddError(source) << OverflowErrorMessage(a, "/", b);
             if (use_runtime_semantics_) {
                 return a;
             } else {
@@ -769,7 +769,7 @@
         auto rhs = b.value;
         if (rhs == 0) {
             // For integers (as for floats), lhs / 0 is an error
-            AddError(OverflowErrorMessage(a, "/", b), source);
+            AddError(source) << OverflowErrorMessage(a, "/", b);
             if (use_runtime_semantics_) {
                 return a;
             } else {
@@ -780,7 +780,7 @@
             // For signed integers, lhs / -1 where lhs is the
             // most negative value is an error
             if (rhs == -1 && lhs == std::numeric_limits<T>::min()) {
-                AddError(OverflowErrorMessage(a, "/", b), source);
+                AddError(source) << OverflowErrorMessage(a, "/", b);
                 if (use_runtime_semantics_) {
                     return a;
                 } else {
@@ -800,7 +800,7 @@
         if (auto r = CheckedMod(a, b)) {
             result = r->value;
         } else {
-            AddError(OverflowErrorMessage(a, "%", b), source);
+            AddError(source) << OverflowErrorMessage(a, "%", b);
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
@@ -813,7 +813,7 @@
         auto rhs = b.value;
         if (rhs == 0) {
             // lhs % 0 is an error
-            AddError(OverflowErrorMessage(a, "%", b), source);
+            AddError(source) << OverflowErrorMessage(a, "%", b);
             if (use_runtime_semantics_) {
                 return NumberT{0};
             } else {
@@ -824,7 +824,7 @@
             // For signed integers, lhs % -1 where lhs is the
             // most negative value is an error
             if (rhs == -1 && lhs == std::numeric_limits<T>::min()) {
-                AddError(OverflowErrorMessage(a, "%", b), source);
+                AddError(source) << OverflowErrorMessage(a, "%", b);
                 if (use_runtime_semantics_) {
                     return NumberT{0};
                 } else {
@@ -1084,7 +1084,7 @@
 template <typename NumberT>
 tint::Result<NumberT, Eval::Error> Eval::Sqrt(const Source& source, NumberT v) {
     if (v < NumberT(0)) {
-        AddError("sqrt must be called with a value >= 0", source);
+        AddError(source) << "sqrt must be called with a value >= 0";
         if (use_runtime_semantics_) {
             return NumberT{0};
         } else {
@@ -1109,9 +1109,8 @@
                                                NumberT low,
                                                NumberT high) {
     if (low > high) {
-        StringStream ss;
-        ss << "clamp called with 'low' (" << low << ") greater than 'high' (" << high << ")";
-        AddError(ss.str(), source);
+        AddError(source) << "clamp called with 'low' (" << low << ") greater than 'high' (" << high
+                         << ")";
         if (!use_runtime_semantics_) {
             return error;
         }
@@ -1403,11 +1402,11 @@
 
     AInt idx = idx_val->ValueAs<AInt>();
     if (idx < 0 || (el.count > 0 && idx >= el.count)) {
-        std::string range;
+        auto& err = AddError(idx_source) << "index " << idx << " out of bounds";
         if (el.count > 0) {
-            range = " [0.." + std::to_string(el.count - 1) + "]";
+            err << " [0.." + std::to_string(el.count - 1) + "]";
         }
-        AddError("index " + std::to_string(idx) + " out of bounds" + range, idx_source);
+
         if (use_runtime_semantics_) {
             return mgr.Zero(el.type);
         } else {
@@ -1998,7 +1997,7 @@
                     UT must_match_msb = e2u + 1;
                     UT mask = ~UT{0} << (bit_width - must_match_msb);
                     if ((e1u & mask) != 0 && (e1u & mask) != mask) {
-                        AddError("shift left operation results in sign change", source);
+                        AddError(source) << "shift left operation results in sign change";
                         if (!use_runtime_semantics_) {
                             return error;
                         }
@@ -2006,7 +2005,7 @@
                 } else {
                     // If shift value >= bit_width, then any non-zero value would overflow
                     if (e1 != 0) {
-                        AddError(OverflowErrorMessage(e1, "<<", e2), source);
+                        AddError(source) << OverflowErrorMessage(e1, "<<", e2);
                         if (!use_runtime_semantics_) {
                             return error;
                         }
@@ -2021,10 +2020,9 @@
                     // At shader/pipeline-creation time, it is an error to shift by the bit width of
                     // the lhs or greater.
                     // NOTE: At runtime, we shift by e2 % (bit width of e1).
-                    AddError(
-                        "shift left value must be less than the bit width of the lhs, which is " +
-                            std::to_string(bit_width),
-                        source);
+                    AddError(source)
+                        << "shift left value must be less than the bit width of the lhs, which is "
+                        << bit_width;
                     if (use_runtime_semantics_) {
                         e2u = e2u % bit_width;
                     } else {
@@ -2038,7 +2036,7 @@
                     size_t must_match_msb = e2u + 1;
                     UT mask = ~UT{0} << (bit_width - must_match_msb);
                     if ((e1u & mask) != 0 && (e1u & mask) != mask) {
-                        AddError("shift left operation results in sign change", source);
+                        AddError(source) << "shift left operation results in sign change";
                         if (!use_runtime_semantics_) {
                             return error;
                         }
@@ -2050,7 +2048,7 @@
                         size_t must_be_zero_msb = e2u;
                         UT mask = ~UT{0} << (bit_width - must_be_zero_msb);
                         if ((e1u & mask) != 0) {
-                            AddError(OverflowErrorMessage(e1, "<<", e2), source);
+                            AddError(source) << OverflowErrorMessage(e1, "<<", e2);
                             if (!use_runtime_semantics_) {
                                 return error;
                             }
@@ -2111,10 +2109,9 @@
                 if (static_cast<size_t>(e2) >= bit_width) {
                     // At shader/pipeline-creation time, it is an error to shift by the bit width of
                     // the lhs or greater. NOTE: At runtime, we shift by e2 % (bit width of e1).
-                    AddError(
-                        "shift right value must be less than the bit width of the lhs, which is " +
-                            std::to_string(bit_width),
-                        source);
+                    AddError(source)
+                        << "shift right value must be less than the bit width of the lhs, which is "
+                        << bit_width;
                     if (use_runtime_semantics_) {
                         e2u = e2u % bit_width;
                     } else {
@@ -2169,22 +2166,23 @@
 Eval::Result Eval::acos(const core::type::Type* ty,
                         VectorRef<const Value*> args,
                         const Source& source) {
-    auto transform = [&](const Value* c0) {
-        auto create = [&](auto i) -> Eval::Result {
-            using NumberT = decltype(i);
-            if (i < NumberT(-1.0) || i > NumberT(1.0)) {
-                AddError("acos must be called with a value in the range [-1 .. 1] (inclusive)",
-                         source);
-                if (use_runtime_semantics_) {
-                    return mgr.Zero(c0->Type());
-                } else {
-                    return error;
+    auto transform =
+        [&](const Value* c0) {
+            auto create = [&](auto i) -> Eval::Result {
+                using NumberT = decltype(i);
+                if (i < NumberT(-1.0) || i > NumberT(1.0)) {
+                    AddError(source)
+                        << "acos must be called with a value in the range [-1 .. 1] (inclusive)";
+                    if (use_runtime_semantics_) {
+                        return mgr.Zero(c0->Type());
+                    } else {
+                        return error;
+                    }
                 }
-            }
-            return CreateScalar(source, c0->Type(), NumberT(std::acos(i.value)));
+                return CreateScalar(source, c0->Type(), NumberT(std::acos(i.value)));
+            };
+            return Dispatch_fa_f32_f16(create, c0);
         };
-        return Dispatch_fa_f32_f16(create, c0);
-    };
     return TransformUnaryElements(mgr, ty, transform, args[0]);
 }
 
@@ -2195,7 +2193,7 @@
         auto create = [&](auto i) -> Eval::Result {
             using NumberT = decltype(i);
             if (i < NumberT(1.0)) {
-                AddError("acosh must be called with a value >= 1.0", source);
+                AddError(source) << "acosh must be called with a value >= 1.0";
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -2225,22 +2223,23 @@
 Eval::Result Eval::asin(const core::type::Type* ty,
                         VectorRef<const Value*> args,
                         const Source& source) {
-    auto transform = [&](const Value* c0) {
-        auto create = [&](auto i) -> Eval::Result {
-            using NumberT = decltype(i);
-            if (i < NumberT(-1.0) || i > NumberT(1.0)) {
-                AddError("asin must be called with a value in the range [-1 .. 1] (inclusive)",
-                         source);
-                if (use_runtime_semantics_) {
-                    return mgr.Zero(c0->Type());
-                } else {
-                    return error;
+    auto transform =
+        [&](const Value* c0) {
+            auto create = [&](auto i) -> Eval::Result {
+                using NumberT = decltype(i);
+                if (i < NumberT(-1.0) || i > NumberT(1.0)) {
+                    AddError(source)
+                        << "asin must be called with a value in the range [-1 .. 1] (inclusive)";
+                    if (use_runtime_semantics_) {
+                        return mgr.Zero(c0->Type());
+                    } else {
+                        return error;
+                    }
                 }
-            }
-            return CreateScalar(source, c0->Type(), NumberT(std::asin(i.value)));
+                return CreateScalar(source, c0->Type(), NumberT(std::asin(i.value)));
+            };
+            return Dispatch_fa_f32_f16(create, c0);
         };
-        return Dispatch_fa_f32_f16(create, c0);
-    };
     return TransformUnaryElements(mgr, ty, transform, args[0]);
 }
 
@@ -2272,22 +2271,23 @@
 Eval::Result Eval::atanh(const core::type::Type* ty,
                          VectorRef<const Value*> args,
                          const Source& source) {
-    auto transform = [&](const Value* c0) {
-        auto create = [&](auto i) -> Eval::Result {
-            using NumberT = decltype(i);
-            if (i <= NumberT(-1.0) || i >= NumberT(1.0)) {
-                AddError("atanh must be called with a value in the range (-1 .. 1) (exclusive)",
-                         source);
-                if (use_runtime_semantics_) {
-                    return mgr.Zero(c0->Type());
-                } else {
-                    return error;
+    auto transform =
+        [&](const Value* c0) {
+            auto create = [&](auto i) -> Eval::Result {
+                using NumberT = decltype(i);
+                if (i <= NumberT(-1.0) || i >= NumberT(1.0)) {
+                    AddError(source)
+                        << "atanh must be called with a value in the range (-1 .. 1) (exclusive)";
+                    if (use_runtime_semantics_) {
+                        return mgr.Zero(c0->Type());
+                    } else {
+                        return error;
+                    }
                 }
-            }
-            return CreateScalar(source, c0->Type(), NumberT(std::atanh(i.value)));
+                return CreateScalar(source, c0->Type(), NumberT(std::atanh(i.value)));
+            };
+            return Dispatch_fa_f32_f16(create, c0);
         };
-        return Dispatch_fa_f32_f16(create, c0);
-    };
 
     return TransformUnaryElements(mgr, ty, transform, args[0]);
 }
@@ -2458,12 +2458,12 @@
             auto pi = kPi<T>;
             auto scale = Div(source, NumberT(180), NumberT(pi));
             if (scale != Success) {
-                AddNote("when calculating degrees", source);
+                AddNote(source) << "when calculating degrees";
                 return error;
             }
             auto result = Mul(source, e, scale.Get());
             if (result != Success) {
-                AddNote("when calculating degrees", source);
+                AddNote(source) << "when calculating degrees";
                 return error;
             }
             return CreateScalar(source, c0->Type(), result.Get());
@@ -2504,7 +2504,7 @@
     };
     auto r = calculate();
     if (r != Success) {
-        AddNote("when calculating determinant", source);
+        AddNote(source) << "when calculating determinant";
     }
     return r;
 }
@@ -2513,7 +2513,7 @@
                             VectorRef<const Value*> args,
                             const Source& source) {
     auto err = [&]() -> Eval::Result {
-        AddNote("when calculating distance", source);
+        AddNote(source) << "when calculating distance";
         return error;
     };
 
@@ -2534,7 +2534,7 @@
                        const Source& source) {
     auto r = Dot(source, args[0], args[1]);
     if (r != Success) {
-        AddNote("when calculating dot", source);
+        AddNote(source) << "when calculating dot";
     }
     return r;
 }
@@ -2577,7 +2577,7 @@
             using NumberT = decltype(e0);
             auto val = NumberT(std::exp(e0));
             if (!std::isfinite(val.value)) {
-                AddError(OverflowExpErrorMessage("e", e0), source);
+                AddError(source) << OverflowExpErrorMessage("e", e0);
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -2599,7 +2599,7 @@
             using NumberT = decltype(e0);
             auto val = NumberT(std::exp2(e0));
             if (!std::isfinite(val.value)) {
-                AddError(OverflowExpErrorMessage("2", e0), source);
+                AddError(source) << OverflowExpErrorMessage("2", e0);
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -2616,60 +2616,62 @@
 Eval::Result Eval::extractBits(const core::type::Type* ty,
                                VectorRef<const Value*> args,
                                const Source& source) {
-    auto transform = [&](const Value* c0) {
-        auto create = [&](auto in_e) -> Eval::Result {
-            using NumberT = decltype(in_e);
-            using T = UnwrapNumber<NumberT>;
-            using UT = std::make_unsigned_t<T>;
-            using NumberUT = Number<UT>;
+    auto transform =
+        [&](const Value* c0) {
+            auto create = [&](auto in_e) -> Eval::Result {
+                using NumberT = decltype(in_e);
+                using T = UnwrapNumber<NumberT>;
+                using UT = std::make_unsigned_t<T>;
+                using NumberUT = Number<UT>;
 
-            // Read args that are always scalar
-            NumberUT in_offset = args[1]->ValueAs<NumberUT>();
-            NumberUT in_count = args[2]->ValueAs<NumberUT>();
+                // Read args that are always scalar
+                NumberUT in_offset = args[1]->ValueAs<NumberUT>();
+                NumberUT in_count = args[2]->ValueAs<NumberUT>();
 
-            // Cast all to unsigned
-            UT e = static_cast<UT>(in_e);
-            UT o = static_cast<UT>(in_offset);
-            UT c = static_cast<UT>(in_count);
+                // Cast all to unsigned
+                UT e = static_cast<UT>(in_e);
+                UT o = static_cast<UT>(in_offset);
+                UT c = static_cast<UT>(in_count);
 
-            constexpr UT w = sizeof(UT) * 8;
-            if (o > w || c > w || (o + c) > w) {
-                AddError("'offset + 'count' must be less than or equal to the bit width of 'e'",
-                         source);
-                if (use_runtime_semantics_) {
-                    o = std::min(o, w);
-                    c = std::min(c, w - o);
-                } else {
-                    return error;
-                }
-            }
-
-            NumberT result;
-            if (c == UT{0}) {
-                // The result is 0 if c is 0
-                result = NumberT{0};
-            } else if (c == w) {
-                // The result is e if c is w
-                result = NumberT{e};
-            } else {
-                // Otherwise, bits 0..c - 1 of the result are copied from bits o..o + c - 1 of e.
-                UT src_mask = ((UT{1} << c) - UT{1}) << o;
-                UT r = (e & src_mask) >> o;
-                if constexpr (IsSignedIntegral<NumberT>) {
-                    // Other bits of the result are the same as bit c - 1 of the result.
-                    // Only need to set other bits if bit at c - 1 of result is 1
-                    if ((r & (UT{1} << (c - UT{1}))) != UT{0}) {
-                        UT dst_mask = src_mask >> o;
-                        r |= (~UT{0} & ~dst_mask);
+                constexpr UT w = sizeof(UT) * 8;
+                if (o > w || c > w || (o + c) > w) {
+                    AddError(source)
+                        << "'offset + 'count' must be less than or equal to the bit width of 'e'";
+                    if (use_runtime_semantics_) {
+                        o = std::min(o, w);
+                        c = std::min(c, w - o);
+                    } else {
+                        return error;
                     }
                 }
 
-                result = NumberT{r};
-            }
-            return CreateScalar(source, c0->Type(), result);
+                NumberT result;
+                if (c == UT{0}) {
+                    // The result is 0 if c is 0
+                    result = NumberT{0};
+                } else if (c == w) {
+                    // The result is e if c is w
+                    result = NumberT{e};
+                } else {
+                    // Otherwise, bits 0..c - 1 of the result are copied from bits o..o + c - 1 of
+                    // e.
+                    UT src_mask = ((UT{1} << c) - UT{1}) << o;
+                    UT r = (e & src_mask) >> o;
+                    if constexpr (IsSignedIntegral<NumberT>) {
+                        // Other bits of the result are the same as bit c - 1 of the result.
+                        // Only need to set other bits if bit at c - 1 of result is 1
+                        if ((r & (UT{1} << (c - UT{1}))) != UT{0}) {
+                            UT dst_mask = src_mask >> o;
+                            r |= (~UT{0} & ~dst_mask);
+                        }
+                    }
+
+                    result = NumberT{r};
+                }
+                return CreateScalar(source, c0->Type(), result);
+            };
+            return Dispatch_iu32(create, c0);
         };
-        return Dispatch_iu32(create, c0);
-    };
     return TransformUnaryElements(mgr, ty, transform, args[0]);
 }
 
@@ -2682,7 +2684,7 @@
     auto* e3 = args[2];
     auto r = Dot(source, e2, e3);
     if (r != Success) {
-        AddNote("when calculating faceForward", source);
+        AddNote(source) << "when calculating faceForward";
         return error;
     }
     auto is_negative = [](auto v) { return v < 0; };
@@ -2780,7 +2782,7 @@
     auto transform = [&](const Value* c1, const Value* c2, const Value* c3) {
         auto create = [&](auto e1, auto e2, auto e3) -> Eval::Result {
             auto err_msg = [&] {
-                AddNote("when calculating fma", source);
+                AddNote(source) << "when calculating fma";
                 return error;
             };
 
@@ -2882,57 +2884,58 @@
 Eval::Result Eval::insertBits(const core::type::Type* ty,
                               VectorRef<const Value*> args,
                               const Source& source) {
-    auto transform = [&](const Value* c0, const Value* c1) {
-        auto create = [&](auto in_e, auto in_newbits) -> Eval::Result {
-            using NumberT = decltype(in_e);
-            using T = UnwrapNumber<NumberT>;
-            using UT = std::make_unsigned_t<T>;
-            using NumberUT = Number<UT>;
+    auto transform =
+        [&](const Value* c0, const Value* c1) {
+            auto create = [&](auto in_e, auto in_newbits) -> Eval::Result {
+                using NumberT = decltype(in_e);
+                using T = UnwrapNumber<NumberT>;
+                using UT = std::make_unsigned_t<T>;
+                using NumberUT = Number<UT>;
 
-            // Read args that are always scalar
-            NumberUT in_offset = args[2]->ValueAs<NumberUT>();
-            NumberUT in_count = args[3]->ValueAs<NumberUT>();
+                // Read args that are always scalar
+                NumberUT in_offset = args[2]->ValueAs<NumberUT>();
+                NumberUT in_count = args[3]->ValueAs<NumberUT>();
 
-            // Cast all to unsigned
-            UT e = static_cast<UT>(in_e);
-            UT newbits = static_cast<UT>(in_newbits);
-            UT o = static_cast<UT>(in_offset);
-            UT c = static_cast<UT>(in_count);
+                // Cast all to unsigned
+                UT e = static_cast<UT>(in_e);
+                UT newbits = static_cast<UT>(in_newbits);
+                UT o = static_cast<UT>(in_offset);
+                UT c = static_cast<UT>(in_count);
 
-            constexpr UT w = sizeof(UT) * 8;
-            if (o > w || c > w || (o + c) > w) {
-                AddError("'offset + 'count' must be less than or equal to the bit width of 'e'",
-                         source);
-                if (use_runtime_semantics_) {
-                    o = std::min(o, w);
-                    c = std::min(c, w - o);
-                } else {
-                    return error;
+                constexpr UT w = sizeof(UT) * 8;
+                if (o > w || c > w || (o + c) > w) {
+                    AddError(source)
+                        << "'offset + 'count' must be less than or equal to the bit width of 'e'";
+                    if (use_runtime_semantics_) {
+                        o = std::min(o, w);
+                        c = std::min(c, w - o);
+                    } else {
+                        return error;
+                    }
                 }
-            }
 
-            NumberT result;
-            if (c == UT{0}) {
-                // The result is e if c is 0
-                result = NumberT{e};
-            } else if (c == w) {
-                // The result is newbits if c is w
-                result = NumberT{newbits};
-            } else {
-                // Otherwise, bits o..o + c - 1 of the result are copied from bits 0..c - 1 of
-                // newbits. Other bits of the result are copied from e.
-                UT from = newbits << o;
-                UT mask = ((UT{1} << c) - UT{1}) << UT{o};
-                auto r = e;          // Start with 'e' as the result
-                r &= ~mask;          // Zero the bits in 'e' we're overwriting
-                r |= (from & mask);  // Overwrite from 'newbits' (shifted into position)
-                result = NumberT{r};
-            }
+                NumberT result;
+                if (c == UT{0}) {
+                    // The result is e if c is 0
+                    result = NumberT{e};
+                } else if (c == w) {
+                    // The result is newbits if c is w
+                    result = NumberT{newbits};
+                } else {
+                    // Otherwise, bits o..o + c - 1 of the result are copied from bits 0..c - 1 of
+                    // newbits. Other bits of the result are copied from e.
+                    UT from = newbits << o;
+                    UT mask = ((UT{1} << c) - UT{1}) << UT{o};
+                    auto r = e;          // Start with 'e' as the result
+                    r &= ~mask;          // Zero the bits in 'e' we're overwriting
+                    r |= (from & mask);  // Overwrite from 'newbits' (shifted into position)
+                    result = NumberT{r};
+                }
 
-            return CreateScalar(source, c0->Type(), result);
+                return CreateScalar(source, c0->Type(), result);
+            };
+            return Dispatch_iu32(create, c0, c1);
         };
-        return Dispatch_iu32(create, c0, c1);
-    };
     return TransformBinaryElements(mgr, ty, transform, args[0], args[1]);
 }
 
@@ -2944,7 +2947,7 @@
             using NumberT = decltype(e);
 
             if (e <= NumberT(0)) {
-                AddError("inverseSqrt must be called with a value > 0", source);
+                AddError(source) << "inverseSqrt must be called with a value > 0";
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -2953,7 +2956,7 @@
             }
 
             auto err = [&] {
-                AddNote("when calculating inverseSqrt", source);
+                AddNote(source) << "when calculating inverseSqrt";
                 return error;
             };
 
@@ -3001,7 +3004,7 @@
             }
 
             if (e2 > bias + 1) {
-                AddError("e2 must be less than or equal to " + std::to_string(bias + 1), source);
+                AddError(source) << "e2 must be less than or equal to " << (bias + 1);
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c1->Type());
                 } else {
@@ -3025,7 +3028,7 @@
                           const Source& source) {
     auto r = Length(source, ty, args[0]);
     if (r != Success) {
-        AddNote("when calculating length", source);
+        AddNote(source) << "when calculating length";
     }
     return r;
 }
@@ -3037,7 +3040,7 @@
         auto create = [&](auto v) -> Eval::Result {
             using NumberT = decltype(v);
             if (v <= NumberT(0)) {
-                AddError("log must be called with a value > 0", source);
+                AddError(source) << "log must be called with a value > 0";
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -3058,7 +3061,7 @@
         auto create = [&](auto v) -> Eval::Result {
             using NumberT = decltype(v);
             if (v <= NumberT(0)) {
-                AddError("log2 must be called with a value > 0", source);
+                AddError(source) << "log2 must be called with a value > 0";
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -3134,7 +3137,7 @@
     };
     auto r = TransformElements(mgr, ty, transform, 0, args[0], args[1]);
     if (r != Success) {
-        AddNote("when calculating mix", source);
+        AddNote(source) << "when calculating mix";
     }
     return r;
 }
@@ -3180,12 +3183,12 @@
     auto* len_ty = ty->DeepestElement();
     auto len = Length(source, len_ty, args[0]);
     if (len != Success) {
-        AddNote("when calculating normalize", source);
+        AddNote(source) << "when calculating normalize";
         return error;
     }
     auto* v = len.Get();
     if (v->AllZero()) {
-        AddError("zero length vector can not be normalized", source);
+        AddError(source) << "zero length vector can not be normalized";
         if (use_runtime_semantics_) {
             return mgr.Zero(ty);
         } else {
@@ -3201,7 +3204,7 @@
     auto convert = [&](f32 val) -> tint::Result<uint32_t, Error> {
         auto conv = CheckedConvert<f16>(val);
         if (conv != Success) {
-            AddError(OverflowErrorMessage(val, "f16"), source);
+            AddError(source) << OverflowErrorMessage(val, "f16");
             if (use_runtime_semantics_) {
                 return 0;
             } else {
@@ -3365,7 +3368,7 @@
         auto create = [&](auto e1, auto e2) -> Eval::Result {
             auto r = CheckedPow(e1, e2);
             if (!r) {
-                AddError(OverflowErrorMessage(e1, "^", e2), source);
+                AddError(source) << OverflowErrorMessage(e1, "^", e2);
                 if (use_runtime_semantics_) {
                     return mgr.Zero(c0->Type());
                 } else {
@@ -3390,12 +3393,12 @@
             auto pi = kPi<T>;
             auto scale = Div(source, NumberT(pi), NumberT(180));
             if (scale != Success) {
-                AddNote("when calculating radians", source);
+                AddNote(source) << "when calculating radians";
                 return error;
             }
             auto result = Mul(source, e, scale.Get());
             if (result != Success) {
-                AddNote("when calculating radians", source);
+                AddNote(source) << "when calculating radians";
                 return error;
             }
             return CreateScalar(source, c0->Type(), result.Get());
@@ -3443,7 +3446,7 @@
     };
     auto r = calculate();
     if (r != Success) {
-        AddNote("when calculating reflect", source);
+        AddNote(source) << "when calculating reflect";
     }
     return r;
 }
@@ -3541,7 +3544,7 @@
     };
     auto r = calculate();
     if (r != Success) {
-        AddNote("when calculating refract", source);
+        AddNote(source) << "when calculating refract";
     }
     return r;
 }
@@ -3708,7 +3711,7 @@
             using NumberT = decltype(low);
 
             auto err = [&] {
-                AddNote("when calculating smoothstep", source);
+                AddNote(source) << "when calculating smoothstep";
                 return error;
             };
 
@@ -3844,7 +3847,7 @@
         auto in = f16::FromBits(uint16_t((e >> (16 * i)) & 0x0000'ffff));
         auto val = CheckedConvert<f32>(in);
         if (val != Success) {
-            AddError(OverflowErrorMessage(in, "f32"), source);
+            AddError(source) << OverflowErrorMessage(in, "f32");
             if (use_runtime_semantics_) {
                 val = f32(0.f);
             } else {
@@ -3986,7 +3989,7 @@
         auto value = c->ValueAs<f32>();
         auto conv = CheckedConvert<f32>(f16(value));
         if (conv != Success) {
-            AddError(OverflowErrorMessage(value, "f16"), source);
+            AddError(source) << OverflowErrorMessage(value, "f16");
             if (use_runtime_semantics_) {
                 return mgr.Zero(c->Type());
             } else {
@@ -4009,20 +4012,20 @@
     return converted ? Result(converted) : Result(error);
 }
 
-void Eval::AddError(const std::string& msg, const Source& source) const {
+diag::Diagnostic& Eval::AddError(const Source& source) const {
     if (use_runtime_semantics_) {
-        diags.AddWarning(diag::System::Constant, msg, source);
+        return diags.AddWarning(diag::System::Constant, source);
     } else {
-        diags.AddError(diag::System::Constant, msg, source);
+        return diags.AddError(diag::System::Constant, source);
     }
 }
 
-void Eval::AddWarning(const std::string& msg, const Source& source) const {
-    diags.AddWarning(diag::System::Constant, msg, source);
+diag::Diagnostic& Eval::AddWarning(const Source& source) const {
+    return diags.AddWarning(diag::System::Constant, source);
 }
 
-void Eval::AddNote(const std::string& msg, const Source& source) const {
-    diags.AddNote(diag::System::Constant, msg, source);
+diag::Diagnostic& Eval::AddNote(const Source& source) const {
+    return diags.AddNote(diag::System::Constant, source);
 }
 
 }  // namespace tint::core::constant
diff --git a/src/tint/lang/core/constant/eval.h b/src/tint/lang/core/constant/eval.h
index de6624a..f3c4e6e 100644
--- a/src/tint/lang/core/constant/eval.h
+++ b/src/tint/lang/core/constant/eval.h
@@ -1025,14 +1025,14 @@
                          const Source& source);
 
   private:
-    /// Adds the given error message to the diagnostics
-    void AddError(const std::string& msg, const Source& source) const;
+    /// @returns a new error diagnostic
+    diag::Diagnostic& AddError(const Source& source) const;
 
-    /// Adds the given warning message to the diagnostics
-    void AddWarning(const std::string& msg, const Source& source) const;
+    /// @returns a new warning diagnostic
+    diag::Diagnostic& AddWarning(const Source& source) const;
 
-    /// Adds the given note message to the diagnostics
-    void AddNote(const std::string& msg, const Source& source) const;
+    /// @returns a new note diagnostic
+    diag::Diagnostic& AddNote(const Source& source) const;
 
     /// CreateScalar constructs and returns a constant::Scalar<T>.
     /// @param source the source location
diff --git a/src/tint/lang/core/constant/eval_binary_op_test.cc b/src/tint/lang/core/constant/eval_binary_op_test.cc
index 1a1fbe2..7859908 100644
--- a/src/tint/lang/core/constant/eval_binary_op_test.cc
+++ b/src/tint/lang/core/constant/eval_binary_op_test.cc
@@ -1508,11 +1508,11 @@
     GlobalConst("result", LogicalAnd(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for operator ! (abstract-int)
+    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for 'operator ! (abstract-int)'
 
 2 candidate operators:
-  operator ! (bool) -> bool
-  operator ! (vecN<bool>) -> vecN<bool>
+  'operator ! (bool) -> bool'
+  'operator ! (vecN<bool>) -> vecN<bool>'
 )");
 }
 
@@ -1525,11 +1525,11 @@
     GlobalConst("result", LogicalOr(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for operator ! (abstract-int)
+    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for 'operator ! (abstract-int)'
 
 2 candidate operators:
-  operator ! (bool) -> bool
-  operator ! (vecN<bool>) -> vecN<bool>
+  'operator ! (bool) -> bool'
+  'operator ! (vecN<bool>) -> vecN<bool>'
 )");
 }
 
@@ -1574,10 +1574,10 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator && (bool, abstract-int)
+              R"(12:34 error: no matching overload for 'operator && (bool, abstract-int)'
 
 1 candidate operator:
-  operator && (bool, bool) -> bool
+  'operator && (bool, bool) -> bool'
 )");
 }
 
@@ -1618,10 +1618,10 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator || (bool, abstract-int)
+              R"(12:34 error: no matching overload for 'operator || (bool, abstract-int)'
 
 1 candidate operator:
-  operator || (bool, bool) -> bool
+  'operator || (bool, bool) -> bool'
 )");
 }
 
@@ -1672,11 +1672,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (abstract-float, i32)
+              R"(12:34 error: no matching overload for 'operator == (abstract-float, i32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1723,11 +1723,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (abstract-float, i32)
+              R"(12:34 error: no matching overload for 'operator == (abstract-float, i32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1784,11 +1784,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (i32, f32)
+              R"(12:34 error: no matching overload for 'operator == (i32, f32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1841,11 +1841,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (i32, f32)
+              R"(12:34 error: no matching overload for 'operator == (i32, f32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1895,11 +1895,11 @@
     GlobalConst("result", binary);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for operator == (f32, i32)
+    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for 'operator == (f32, i32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1945,11 +1945,11 @@
     GlobalConst("result", binary);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for operator == (f32, i32)
+    EXPECT_EQ(r()->error(), R"(12:34 error: no matching overload for 'operator == (f32, i32)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -1971,24 +1971,24 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching constructor for vec2<f32>(abstract-float, bool)
+              R"(12:34 error: no matching constructor for 'vec2<f32>(abstract-float, bool)'
 
 8 candidate constructors:
-  vec2<T>(x: T, y: T) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2<T>(T) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(T) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec2<T>(vec2<T>) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(vec2<T>) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec2() -> vec2<abstract-int>
-  vec2<T>() -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(x: T, y: T) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'vec2<T>(x: T, y: T) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2<T>(T) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(T) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2<T>(vec2<T>) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(vec2<T>) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2() -> vec2<abstract-int>'
+  'vec2<T>() -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(x: T, y: T) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'f32', 'U' is 'abstract-int', 'abstract-float', 'i32', 'f16', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'f16', 'U' is 'abstract-int', 'abstract-float', 'f32', 'i32', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'i32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'u32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'bool', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -2003,24 +2003,24 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching constructor for vec2<f32>(abstract-float, bool)
+              R"(12:34 error: no matching constructor for 'vec2<f32>(abstract-float, bool)'
 
 8 candidate constructors:
-  vec2<T>(x: T, y: T) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2<T>(T) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(T) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec2<T>(vec2<T>) -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(vec2<T>) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec2() -> vec2<abstract-int>
-  vec2<T>() -> vec2<T>  where: T is f32, f16, i32, u32 or bool
-  vec2(x: T, y: T) -> vec2<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'vec2<T>(x: T, y: T) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2<T>(T) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(T) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2<T>(vec2<T>) -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(vec2<T>) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2() -> vec2<abstract-int>'
+  'vec2<T>() -> vec2<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec2(x: T, y: T) -> vec2<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
-  vec2<T>(vec2<U>) -> vec2<T>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'f32', 'U' is 'abstract-int', 'abstract-float', 'i32', 'f16', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'f16', 'U' is 'abstract-int', 'abstract-float', 'f32', 'i32', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'i32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'u32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'bool'
+  'vec2<T>(vec2<U>) -> vec2<T>'  where: 'T' is 'bool', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -2078,12 +2078,13 @@
     GlobalConst("result", LogicalAnd(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching overload for operator == (array<abstract-int, 1>, abstract-int)
+    EXPECT_EQ(
+        r()->error(),
+        R"(error: no matching overload for 'operator == (array<abstract-int, 1>, abstract-int)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -2096,12 +2097,13 @@
     GlobalConst("result", LogicalOr(lhs, rhs));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching overload for operator == (array<abstract-int, 1>, abstract-int)
+    EXPECT_EQ(
+        r()->error(),
+        R"(error: no matching overload for 'operator == (array<abstract-int, 1>, abstract-int)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -2147,11 +2149,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (i32, abstract-float)
+              R"(12:34 error: no matching overload for 'operator == (i32, abstract-float)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -2193,11 +2195,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching overload for operator == (i32, abstract-float)
+              R"(12:34 error: no matching overload for 'operator == (i32, abstract-float)'
 
 2 candidate operators:
-  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'operator == (T, T) -> bool'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'operator == (vecN<T>, vecN<T>) -> vecN<bool>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
diff --git a/src/tint/lang/core/intrinsic/data.cc b/src/tint/lang/core/intrinsic/data.cc
index d5db81e..46637b1 100644
--- a/src/tint/lang/core/intrinsic/data.cc
+++ b/src/tint/lang/core/intrinsic/data.cc
@@ -80,8 +80,8 @@
     }
     return BuildBool(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bool";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "bool";
   }
 };
 
@@ -94,8 +94,8 @@
     }
     return BuildI32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "i32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "i32";
   }
 };
 
@@ -108,8 +108,8 @@
     }
     return BuildU32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "u32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "u32";
   }
 };
 
@@ -122,8 +122,8 @@
     }
     return BuildF32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f32";
   }
 };
 
@@ -136,8 +136,8 @@
     }
     return BuildF16(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f16";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f16";
   }
 };
 
@@ -155,9 +155,9 @@
     }
     return BuildVec2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -175,9 +175,9 @@
     }
     return BuildVec3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -195,9 +195,9 @@
     }
     return BuildVec4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -215,9 +215,9 @@
     }
     return BuildMat2X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -235,9 +235,9 @@
     }
     return BuildMat2X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -255,9 +255,9 @@
     }
     return BuildMat2X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -275,9 +275,9 @@
     }
     return BuildMat3X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -295,9 +295,9 @@
     }
     return BuildMat3X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -315,9 +315,9 @@
     }
     return BuildMat3X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -335,9 +335,9 @@
     }
     return BuildMat4X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -355,9 +355,9 @@
     }
     return BuildMat4X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -375,9 +375,9 @@
     }
     return BuildMat4X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -400,12 +400,10 @@
     }
     return BuildVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "vec" << N << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "vec" << style::Type << N << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -433,13 +431,11 @@
     }
     return BuildMat(state, ty, N, M, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string M = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "mat" << N << "x" << M << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText M;
+  state->PrintNum(M);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "mat" << style::Type << N << style::Type << "x" << style::Type << M << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -467,11 +463,11 @@
     }
     return BuildPtr(state, ty, S, T, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string S = state->NumName();
-  const std::string T = state->TypeName();
-  const std::string A = state->NumName();
-    return "ptr<" + S + ", " + T + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText S;
+  state->PrintNum(S);StyledText T;
+  state->PrintType(T);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "ptr" << style::Code << "<" << style::Type << S << style::Code << ", " << style::Type << T << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -489,9 +485,9 @@
     }
     return BuildAtomic(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "atomic<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "atomic" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -509,9 +505,9 @@
     }
     return BuildArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -524,8 +520,8 @@
     }
     return BuildSampler(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler";
   }
 };
 
@@ -538,8 +534,8 @@
     }
     return BuildSamplerComparison(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler_comparison";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler_comparison";
   }
 };
 
@@ -557,9 +553,9 @@
     }
     return BuildTexture1D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_1d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_1d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -577,9 +573,9 @@
     }
     return BuildTexture2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -597,9 +593,9 @@
     }
     return BuildTexture2DArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -617,9 +613,9 @@
     }
     return BuildTexture3D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_3d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_3d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -637,9 +633,9 @@
     }
     return BuildTextureCube(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -657,9 +653,9 @@
     }
     return BuildTextureCubeArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -677,9 +673,9 @@
     }
     return BuildTextureMultisampled2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_multisampled_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_multisampled_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -692,8 +688,8 @@
     }
     return BuildTextureDepth2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d";
   }
 };
 
@@ -706,8 +702,8 @@
     }
     return BuildTextureDepth2DArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d_array";
   }
 };
 
@@ -720,8 +716,8 @@
     }
     return BuildTextureDepthCube(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube";
   }
 };
 
@@ -734,8 +730,8 @@
     }
     return BuildTextureDepthCubeArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube_array";
   }
 };
 
@@ -748,8 +744,8 @@
     }
     return BuildTextureDepthMultisampled2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_multisampled_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_multisampled_2d";
   }
 };
 
@@ -772,10 +768,10 @@
     }
     return BuildTextureStorage1D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_1d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_1d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -798,10 +794,10 @@
     }
     return BuildTextureStorage2D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -824,10 +820,10 @@
     }
     return BuildTextureStorage2DArray(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d_array<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d_array" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -850,10 +846,10 @@
     }
     return BuildTextureStorage3D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_3d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_3d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -866,8 +862,8 @@
     }
     return BuildTextureExternal(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_external";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_external";
   }
 };
 
@@ -885,11 +881,9 @@
     }
     return BuildModfResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__modf_result_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__modf_result_" << style::Type << T;
   }
 };
 
@@ -912,12 +906,10 @@
     }
     return BuildModfResultVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__modf_result_vec" << N << "_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__modf_result_vec" << style::Type << N << style::Type << "_" << style::Type << T;
   }
 };
 
@@ -935,11 +927,9 @@
     }
     return BuildFrexpResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__frexp_result_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__frexp_result_" << style::Type << T;
   }
 };
 
@@ -962,12 +952,10 @@
     }
     return BuildFrexpResultVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__frexp_result_vec" << N << "_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__frexp_result_vec" << style::Type << N << style::Type << "_" << style::Type << T;
   }
 };
 
@@ -985,9 +973,9 @@
     }
     return BuildAtomicCompareExchangeResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "__atomic_compare_exchange_result<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "__atomic_compare_exchange_result" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -1012,13 +1000,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_f32'
@@ -1038,13 +1023,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kI32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_f16'
@@ -1064,13 +1046,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_i32'
@@ -1090,13 +1069,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_u32'
@@ -1116,13 +1092,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_bool'
@@ -1142,13 +1115,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fiu32_f16'
@@ -1168,13 +1138,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fiu32'
@@ -1191,13 +1158,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fi32_f16'
@@ -1214,13 +1178,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fi32'
@@ -1234,13 +1195,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << " or " << kI32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kI32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match f32_f16'
@@ -1254,13 +1212,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match iu32'
@@ -1274,13 +1229,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// EnumMatcher for 'match f32_texel_format'
@@ -1299,8 +1251,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bgra8unorm, rgba8unorm, rgba8snorm, rgba16float, r32float, rg32float or rgba32float";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "bgra8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8snorm"<< TextStyle{} << ", " << style::Enum << "rgba16float"<< TextStyle{} << ", " << style::Enum << "r32float"<< TextStyle{} << ", " << style::Enum << "rg32float"<< TextStyle{} << " or " << style::Enum << "rgba32float";
   }
 };
 
@@ -1318,8 +1270,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8sint, rgba16sint, r32sint, rg32sint or rgba32sint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8sint"<< TextStyle{} << ", " << style::Enum << "rgba16sint"<< TextStyle{} << ", " << style::Enum << "r32sint"<< TextStyle{} << ", " << style::Enum << "rg32sint"<< TextStyle{} << " or " << style::Enum << "rgba32sint";
   }
 };
 
@@ -1337,8 +1289,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8uint, rgba16uint, r32uint, rg32uint or rgba32uint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8uint"<< TextStyle{} << ", " << style::Enum << "rgba16uint"<< TextStyle{} << ", " << style::Enum << "r32uint"<< TextStyle{} << ", " << style::Enum << "rg32uint"<< TextStyle{} << " or " << style::Enum << "rgba32uint";
   }
 };
 
@@ -1350,8 +1302,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "write";
   }
 };
 
@@ -1363,8 +1315,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read_write";
   }
 };
 
@@ -1379,8 +1331,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
@@ -1395,8 +1347,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "write or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "write"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
@@ -1412,8 +1364,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "function, private or workgroup";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "function"<< TextStyle{} << ", " << style::Enum << "private"<< TextStyle{} << " or " << style::Enum << "workgroup";
   }
 };
 
@@ -1428,8 +1380,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "workgroup or storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "workgroup"<< TextStyle{} << " or " << style::Enum << "storage";
   }
 };
 
@@ -1441,8 +1393,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "storage";
   }
 };
 
@@ -1454,8 +1406,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "workgroup";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "workgroup";
   }
 };
 
diff --git a/src/tint/lang/core/intrinsic/table.cc b/src/tint/lang/core/intrinsic/table.cc
index d7c080e..b888362 100644
--- a/src/tint/lang/core/intrinsic/table.cc
+++ b/src/tint/lang/core/intrinsic/table.cc
@@ -37,7 +37,10 @@
 #include "src/tint/lang/core/type/manager.h"
 #include "src/tint/lang/core/type/void.h"
 #include "src/tint/utils/ice/ice.h"
+#include "src/tint/utils/macros/defer.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/text_style.h"
 
 namespace tint::core::intrinsic {
 
@@ -86,7 +89,7 @@
 using Candidates = Vector<Candidate, kNumFixedCandidates>;
 
 /// Callback function when no overloads match.
-using OnNoMatch = std::function<std::string(VectorRef<Candidate>)>;
+using OnNoMatch = std::function<StyledText(VectorRef<Candidate>)>;
 
 /// Sorts the candidates based on their score, with the lowest (best-ranking) scores first.
 static inline void SortCandidates(Candidates& candidates) {
@@ -94,15 +97,17 @@
                      [&](const Candidate& a, const Candidate& b) { return a.score < b.score; });
 }
 
-static void PrintTypeList(StringStream& ss, VectorRef<const core::type::Type*> types) {
+/// Prints a list of types. Ends with the style of @p ss set to style::Code.
+static void PrintTypeList(StyledText& ss, VectorRef<const core::type::Type*> types) {
     bool first = true;
     for (auto* arg : types) {
         if (!first) {
-            ss << ", ";
+            ss << style::Code << ", ";
         }
         first = false;
-        ss << arg->FriendlyName();
+        ss << style::Type << arg->FriendlyName();
     }
+    ss << style::Code;
 }
 
 /// Attempts to find a single intrinsic overload that matches the provided argument types.
@@ -114,13 +119,13 @@
 /// @param on_no_match an error callback when no intrinsic overloads matched the provided
 ///                    arguments.
 /// @returns the matched intrinsic
-Result<Overload, std::string> MatchIntrinsic(Context& context,
-                                             const IntrinsicInfo& intrinsic,
-                                             std::string_view intrinsic_name,
-                                             VectorRef<const core::type::Type*> template_args,
-                                             VectorRef<const core::type::Type*> args,
-                                             EvaluationStage earliest_eval_stage,
-                                             const OnNoMatch& on_no_match);
+Result<Overload, StyledText> MatchIntrinsic(Context& context,
+                                            const IntrinsicInfo& intrinsic,
+                                            std::string_view intrinsic_name,
+                                            VectorRef<const core::type::Type*> template_args,
+                                            VectorRef<const core::type::Type*> args,
+                                            EvaluationStage earliest_eval_stage,
+                                            const OnNoMatch& on_no_match);
 
 /// The scoring mode for ScoreOverload()
 enum class ScoreMode {
@@ -156,11 +161,11 @@
 /// @param args the argument types
 /// @see https://www.w3.org/TR/WGSL/#overload-resolution-section
 /// @returns the resolved Candidate.
-Result<Candidate, std::string> ResolveCandidate(Context& context,
-                                                Candidates&& candidates,
-                                                std::string_view intrinsic_name,
-                                                VectorRef<const core::type::Type*> template_args,
-                                                VectorRef<const core::type::Type*> args);
+Result<Candidate, StyledText> ResolveCandidate(Context& context,
+                                               Candidates&& candidates,
+                                               std::string_view intrinsic_name,
+                                               VectorRef<const core::type::Type*> template_args,
+                                               VectorRef<const core::type::Type*> args);
 
 /// Match constructs a new MatchState
 /// @param context the intrinsic context
@@ -174,43 +179,44 @@
                  EvaluationStage earliest_eval_stage);
 
 // Prints the list of candidates for emitting diagnostics
-void PrintCandidates(StringStream& ss,
+void PrintCandidates(StyledText& err,
                      Context& context,
                      VectorRef<Candidate> candidates,
                      std::string_view intrinsic_name);
 
 /// Raises an ICE when no overload is a clear winner of overload resolution
-std::string ErrAmbiguousOverload(Context& context,
-                                 std::string_view intrinsic_name,
-                                 VectorRef<const core::type::Type*> template_args,
-                                 VectorRef<const core::type::Type*> args,
-                                 VectorRef<Candidate> candidates);
+StyledText ErrAmbiguousOverload(Context& context,
+                                std::string_view intrinsic_name,
+                                VectorRef<const core::type::Type*> template_args,
+                                VectorRef<const core::type::Type*> args,
+                                VectorRef<Candidate> candidates);
 
 /// @return a string representing a call to a builtin with the given argument
 /// types.
-std::string CallSignature(std::string_view intrinsic_name,
-                          VectorRef<const core::type::Type*> template_args,
-                          VectorRef<const core::type::Type*> args) {
-    StringStream ss;
-    ss << intrinsic_name;
+StyledText CallSignature(std::string_view intrinsic_name,
+                         VectorRef<const core::type::Type*> template_args,
+                         VectorRef<const core::type::Type*> args) {
+    StyledText out;
+    out << style::Function << intrinsic_name << style::Code;
     if (!template_args.IsEmpty()) {
-        ss << "<";
-        PrintTypeList(ss, template_args);
-        ss << ">";
+        out << "<";
+        PrintTypeList(out, template_args);
+        out << ">";
     }
-    ss << "(";
-    PrintTypeList(ss, args);
-    ss << ")";
-    return ss.str();
+    out << "(";
+    PrintTypeList(out, args);
+    out << ")";
+
+    return out << style::Plain;
 }
 
-Result<Overload, std::string> MatchIntrinsic(Context& context,
-                                             const IntrinsicInfo& intrinsic,
-                                             std::string_view intrinsic_name,
-                                             VectorRef<const core::type::Type*> template_args,
-                                             VectorRef<const core::type::Type*> args,
-                                             EvaluationStage earliest_eval_stage,
-                                             const OnNoMatch& on_no_match) {
+Result<Overload, StyledText> MatchIntrinsic(Context& context,
+                                            const IntrinsicInfo& intrinsic,
+                                            std::string_view intrinsic_name,
+                                            VectorRef<const core::type::Type*> template_args,
+                                            VectorRef<const core::type::Type*> args,
+                                            EvaluationStage earliest_eval_stage,
+                                            const OnNoMatch& on_no_match) {
     const size_t num_overloads = static_cast<size_t>(intrinsic.num_overloads);
     size_t num_matched = 0;
     size_t match_idx = 0;
@@ -261,8 +267,9 @@
             Match(context, match.templates, *match.overload, matcher_indices, earliest_eval_stage)
                 .Type(&any);
         if (TINT_UNLIKELY(!return_type)) {
-            std::string err = "MatchState.Match() returned null";
-            TINT_ICE() << err;
+            StyledText err;
+            err << "MatchState.Match() returned null";
+            TINT_ICE() << err.Plain();
             return err;
         }
     } else {
@@ -423,11 +430,11 @@
 #undef MATCH_FAILURE
 }
 
-Result<Candidate, std::string> ResolveCandidate(Context& context,
-                                                Candidates&& candidates,
-                                                std::string_view intrinsic_name,
-                                                VectorRef<const core::type::Type*> template_args,
-                                                VectorRef<const core::type::Type*> args) {
+Result<Candidate, StyledText> ResolveCandidate(Context& context,
+                                               Candidates&& candidates,
+                                               std::string_view intrinsic_name,
+                                               VectorRef<const core::type::Type*> template_args,
+                                               VectorRef<const core::type::Type*> args) {
     Vector<uint32_t, kNumFixedParams> best_ranks;
     best_ranks.Resize(args.Length(), 0xffffffff);
     size_t num_matched = 0;
@@ -497,57 +504,59 @@
                       overload,      matcher_indices, earliest_eval_stage};
 }
 
-void PrintCandidates(StringStream& ss,
+void PrintCandidates(StyledText& ss,
                      Context& context,
                      VectorRef<Candidate> candidates,
                      std::string_view intrinsic_name) {
     for (auto& candidate : candidates) {
         ss << "  ";
         PrintOverload(ss, context, *candidate.overload, intrinsic_name);
-        ss << std::endl;
+        ss << "\n";
     }
 }
 
-std::string ErrAmbiguousOverload(Context& context,
-                                 std::string_view intrinsic_name,
-                                 VectorRef<const core::type::Type*> template_args,
-                                 VectorRef<const core::type::Type*> args,
-                                 VectorRef<Candidate> candidates) {
-    StringStream ss;
-    ss << "ambiguous overload while attempting to match "
-       << CallSignature(intrinsic_name, template_args, args) << "\n";
+StyledText ErrAmbiguousOverload(Context& context,
+                                std::string_view intrinsic_name,
+                                VectorRef<const core::type::Type*> template_args,
+                                VectorRef<const core::type::Type*> args,
+                                VectorRef<Candidate> candidates) {
+    StyledText err;
+    err << "ambiguous overload while attempting to match "
+        << CallSignature(intrinsic_name, template_args, args) << "\n";
 
     for (auto& candidate : candidates) {
         if (candidate.score == 0) {
-            ss << "  ";
-            PrintOverload(ss, context, *candidate.overload, intrinsic_name);
-            ss << "\n";
+            err << "  ";
+            PrintOverload(err, context, *candidate.overload, intrinsic_name);
+            err << "\n";
         }
     }
-    TINT_ICE() << ss.str();
-    return ss.str();
+    TINT_ICE() << err.Plain();
+    return err;
 }
 
 }  // namespace
 
-void PrintOverload(StringStream& ss,
+void PrintOverload(StyledText& ss,
                    Context& context,
                    const OverloadInfo& overload,
                    std::string_view intrinsic_name) {
+    TINT_DEFER(ss << style::Plain);
+
     TemplateState templates;
 
     // TODO(crbug.com/tint/1730): Use input evaluation stage to output only relevant overloads.
     auto earliest_eval_stage = EvaluationStage::kConstant;
 
-    ss << intrinsic_name;
+    ss << style::Function << intrinsic_name << style::Code;
 
     if (overload.num_explicit_templates > 0) {
         ss << "<";
         for (size_t i = 0; i < overload.num_explicit_templates; i++) {
             if (i > 0) {
-                ss << ", ";
+                ss << style::Code << ", ";
             }
-            ss << context.data[overload.templates + i].name;
+            ss << style::Type << context.data[overload.templates + i].name;
         }
         ss << ">";
     }
@@ -555,24 +564,24 @@
     for (size_t p = 0; p < overload.num_parameters; p++) {
         auto& parameter = context.data[overload.parameters + p];
         if (p > 0) {
-            ss << ", ";
+            ss << style::Code << ", ";
         }
         if (parameter.usage != ParameterUsage::kNone) {
-            ss << ToString(parameter.usage) << ": ";
+            ss << style::Variable << ToString(parameter.usage) << style::Code << ": ";
         }
         auto* matcher_indices = context.data[parameter.matcher_indices];
-        ss << Match(context, templates, overload, matcher_indices, earliest_eval_stage).TypeName();
+        Match(context, templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
     }
-    ss << ")";
+    ss << style::Code << ")";
     if (overload.return_matcher_indices.IsValid()) {
         ss << " -> ";
         auto* matcher_indices = context.data[overload.return_matcher_indices];
-        ss << Match(context, templates, overload, matcher_indices, earliest_eval_stage).TypeName();
+        Match(context, templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
     }
 
     bool first = true;
     auto separator = [&] {
-        ss << (first ? "  where: " : ", ");
+        ss << style::Plain << (first ? "  where: " : ", ");
         first = false;
     };
 
@@ -583,35 +592,33 @@
                 Match(context, templates, overload, matcher_indices, earliest_eval_stage);
 
             separator();
-            ss << tmpl.name;
-            ss << " is ";
+            ss << style::Type << tmpl.name << style::Plain << " is ";
             if (tmpl.kind == TemplateInfo::Kind::kType) {
-                ss << matcher.TypeName();
+                matcher.PrintType(ss);
             } else {
-                ss << matcher.NumName();
+                matcher.PrintNum(ss);
             }
         }
     }
 }
 
-Result<Overload, std::string> LookupFn(Context& context,
-                                       std::string_view intrinsic_name,
-                                       size_t function_id,
-                                       VectorRef<const core::type::Type*> template_args,
-                                       VectorRef<const core::type::Type*> args,
-                                       EvaluationStage earliest_eval_stage) {
+Result<Overload, StyledText> LookupFn(Context& context,
+                                      std::string_view intrinsic_name,
+                                      size_t function_id,
+                                      VectorRef<const core::type::Type*> template_args,
+                                      VectorRef<const core::type::Type*> args,
+                                      EvaluationStage earliest_eval_stage) {
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching call to " << CallSignature(intrinsic_name, template_args, args)
-           << std::endl;
+        StyledText err;
+        err << "no matching call to " << CallSignature(intrinsic_name, template_args, args) << "\n";
         if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate function"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, context, candidates, intrinsic_name);
+            err << "\n"
+                << candidates.Length() << " candidate function"
+                << (candidates.Length() > 1 ? "s:" : ":") << "\n";
+            PrintCandidates(err, context, candidates, intrinsic_name);
         }
-        return ss.str();
+        return err;
     };
 
     // Resolve the intrinsic overload
@@ -619,10 +626,10 @@
                           template_args, args, earliest_eval_stage, on_no_match);
 }
 
-Result<Overload, std::string> LookupUnary(Context& context,
-                                          core::UnaryOp op,
-                                          const core::type::Type* arg,
-                                          EvaluationStage earliest_eval_stage) {
+Result<Overload, StyledText> LookupUnary(Context& context,
+                                         core::UnaryOp op,
+                                         const core::type::Type* arg,
+                                         EvaluationStage earliest_eval_stage) {
     const IntrinsicInfo* intrinsic_info = nullptr;
     std::string_view intrinsic_name;
     switch (op) {
@@ -652,15 +659,15 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching overload for " << CallSignature(name, Empty, args) << std::endl;
+        StyledText err;
+        err << "no matching overload for " << CallSignature(name, Empty, args) << "\n";
         if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate operator"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, context, candidates, name);
+            err << "\n"
+                << candidates.Length() << " candidate operator"
+                << (candidates.Length() > 1 ? "s:" : ":") << "\n";
+            PrintCandidates(err, context, candidates, name);
         }
-        return ss.str();
+        return err;
     };
 
     // Resolve the intrinsic overload
@@ -668,12 +675,12 @@
                           earliest_eval_stage, on_no_match);
 }
 
-Result<Overload, std::string> LookupBinary(Context& context,
-                                           core::BinaryOp op,
-                                           const core::type::Type* lhs,
-                                           const core::type::Type* rhs,
-                                           EvaluationStage earliest_eval_stage,
-                                           bool is_compound) {
+Result<Overload, StyledText> LookupBinary(Context& context,
+                                          core::BinaryOp op,
+                                          const core::type::Type* lhs,
+                                          const core::type::Type* rhs,
+                                          EvaluationStage earliest_eval_stage,
+                                          bool is_compound) {
     const IntrinsicInfo* intrinsic_info = nullptr;
     std::string_view intrinsic_name;
     switch (op) {
@@ -755,15 +762,15 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&, name = intrinsic_name](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching overload for " << CallSignature(name, Empty, args) << std::endl;
+        StyledText err;
+        err << "no matching overload for " << CallSignature(name, Empty, args) << "\n";
         if (!candidates.IsEmpty()) {
-            ss << std::endl
-               << candidates.Length() << " candidate operator"
-               << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
-            PrintCandidates(ss, context, candidates, name);
+            err << "\n"
+                << candidates.Length() << " candidate operator"
+                << (candidates.Length() > 1 ? "s:" : ":") << "\n";
+            PrintCandidates(err, context, candidates, name);
         }
-        return ss.str();
+        return err;
     };
 
     // Resolve the intrinsic overload
@@ -771,17 +778,17 @@
                           earliest_eval_stage, on_no_match);
 }
 
-Result<Overload, std::string> LookupCtorConv(Context& context,
-                                             std::string_view type_name,
-                                             size_t type_id,
-                                             VectorRef<const core::type::Type*> template_args,
-                                             VectorRef<const core::type::Type*> args,
-                                             EvaluationStage earliest_eval_stage) {
+Result<Overload, StyledText> LookupCtorConv(Context& context,
+                                            std::string_view type_name,
+                                            size_t type_id,
+                                            VectorRef<const core::type::Type*> template_args,
+                                            VectorRef<const core::type::Type*> args,
+                                            EvaluationStage earliest_eval_stage) {
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&](VectorRef<Candidate> candidates) {
-        StringStream ss;
-        ss << "no matching constructor for " << CallSignature(type_name, template_args, args)
-           << std::endl;
+        StyledText err;
+        err << "no matching constructor for " << CallSignature(type_name, template_args, args)
+            << "\n";
         Candidates ctor, conv;
         for (auto candidate : candidates) {
             if (candidate.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
@@ -791,18 +798,18 @@
             }
         }
         if (!ctor.IsEmpty()) {
-            ss << std::endl
-               << ctor.Length() << " candidate constructor" << (ctor.Length() > 1 ? "s:" : ":")
-               << std::endl;
-            PrintCandidates(ss, context, ctor, type_name);
+            err << "\n"
+                << ctor.Length() << " candidate constructor" << (ctor.Length() > 1 ? "s:" : ":")
+                << "\n";
+            PrintCandidates(err, context, ctor, type_name);
         }
         if (!conv.IsEmpty()) {
-            ss << std::endl
-               << conv.Length() << " candidate conversion" << (conv.Length() > 1 ? "s:" : ":")
-               << std::endl;
-            PrintCandidates(ss, context, conv, type_name);
+            err << "\n"
+                << conv.Length() << " candidate conversion" << (conv.Length() > 1 ? "s:" : ":")
+                << "\n";
+            PrintCandidates(err, context, conv, type_name);
         }
-        return ss.str();
+        return err;
     };
 
     // Resolve the intrinsic overload
diff --git a/src/tint/lang/core/intrinsic/table.h b/src/tint/lang/core/intrinsic/table.h
index c77e61b..56f590a 100644
--- a/src/tint/lang/core/intrinsic/table.h
+++ b/src/tint/lang/core/intrinsic/table.h
@@ -39,7 +39,9 @@
 #include "src/tint/lang/core/parameter_usage.h"
 #include "src/tint/lang/core/unary_op.h"
 #include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/text/string.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
 
 // Forward declarations
 namespace tint::diag {
@@ -107,7 +109,7 @@
 };
 
 // Prints the overload for emitting diagnostics
-void PrintOverload(StringStream& ss,
+void PrintOverload(StyledText& ss,
                    Context& context,
                    const OverloadInfo& overload,
                    std::string_view intrinsic_name);
@@ -126,12 +128,12 @@
 ///        abstract-numerics will have been materialized after shader creation time
 ///        (EvaluationStage::kConstant).
 /// @return the resolved builtin function overload
-Result<Overload, std::string> LookupFn(Context& context,
-                                       std::string_view function_name,
-                                       size_t function_id,
-                                       VectorRef<const core::type::Type*> template_args,
-                                       VectorRef<const core::type::Type*> args,
-                                       EvaluationStage earliest_eval_stage);
+Result<Overload, StyledText> LookupFn(Context& context,
+                                      std::string_view function_name,
+                                      size_t function_id,
+                                      VectorRef<const core::type::Type*> template_args,
+                                      VectorRef<const core::type::Type*> args,
+                                      EvaluationStage earliest_eval_stage);
 
 /// Lookup looks for the unary op overload with the given signature, raising an error
 /// diagnostic if the operator was not found.
@@ -145,10 +147,10 @@
 ///        will be considered, as all abstract-numerics will have been materialized
 ///        after shader creation time (EvaluationStage::kConstant).
 /// @return the resolved unary operator overload
-Result<Overload, std::string> LookupUnary(Context& context,
-                                          core::UnaryOp op,
-                                          const core::type::Type* arg,
-                                          EvaluationStage earliest_eval_stage);
+Result<Overload, StyledText> LookupUnary(Context& context,
+                                         core::UnaryOp op,
+                                         const core::type::Type* arg,
+                                         EvaluationStage earliest_eval_stage);
 
 /// Lookup looks for the binary op overload with the given signature, raising an error
 /// diagnostic if the operator was not found.
@@ -164,12 +166,12 @@
 ///        after shader creation time (EvaluationStage::kConstant).
 /// @param is_compound true if the binary operator is being used as a compound assignment
 /// @return the resolved binary operator overload
-Result<Overload, std::string> LookupBinary(Context& context,
-                                           core::BinaryOp op,
-                                           const core::type::Type* lhs,
-                                           const core::type::Type* rhs,
-                                           EvaluationStage earliest_eval_stage,
-                                           bool is_compound);
+Result<Overload, StyledText> LookupBinary(Context& context,
+                                          core::BinaryOp op,
+                                          const core::type::Type* lhs,
+                                          const core::type::Type* rhs,
+                                          EvaluationStage earliest_eval_stage,
+                                          bool is_compound);
 
 /// Lookup looks for the value constructor or conversion overload for the given CtorConv.
 /// @param context the intrinsic context
@@ -184,12 +186,12 @@
 ///        will be considered, as all abstract-numerics will have been materialized
 ///        after shader creation time (EvaluationStage::kConstant).
 /// @return the resolved type constructor or conversion function overload
-Result<Overload, std::string> LookupCtorConv(Context& context,
-                                             std::string_view type_name,
-                                             size_t type_id,
-                                             VectorRef<const core::type::Type*> template_args,
-                                             VectorRef<const core::type::Type*> args,
-                                             EvaluationStage earliest_eval_stage);
+Result<Overload, StyledText> LookupCtorConv(Context& context,
+                                            std::string_view type_name,
+                                            size_t type_id,
+                                            VectorRef<const core::type::Type*> template_args,
+                                            VectorRef<const core::type::Type*> args,
+                                            EvaluationStage earliest_eval_stage);
 
 /// Table is a wrapper around a dialect to provide type-safe interface to the intrinsic table.
 template <typename DIALECT>
@@ -219,12 +221,11 @@
     ///        only overloads with concrete argument types will be considered, as all
     ///        abstract-numerics will have been materialized after shader creation time
     ///        (EvaluationStage::kConstant).
-
     /// @return the resolved builtin function overload
-    Result<Overload, std::string> Lookup(BuiltinFn builtin_fn,
-                                         VectorRef<const core::type::Type*> template_args,
-                                         VectorRef<const core::type::Type*> args,
-                                         EvaluationStage earliest_eval_stage) {
+    Result<Overload, StyledText> Lookup(BuiltinFn builtin_fn,
+                                        VectorRef<const core::type::Type*> template_args,
+                                        VectorRef<const core::type::Type*> args,
+                                        EvaluationStage earliest_eval_stage) {
         std::string_view name = DIALECT::ToString(builtin_fn);
         size_t id = static_cast<size_t>(builtin_fn);
         return LookupFn(context, name, id, std::move(template_args), std::move(args),
@@ -243,9 +244,9 @@
     ///        after shader creation time (EvaluationStage::kConstant).
 
     /// @return the resolved unary operator overload
-    Result<Overload, std::string> Lookup(core::UnaryOp op,
-                                         const core::type::Type* arg,
-                                         EvaluationStage earliest_eval_stage) {
+    Result<Overload, StyledText> Lookup(core::UnaryOp op,
+                                        const core::type::Type* arg,
+                                        EvaluationStage earliest_eval_stage) {
         return LookupUnary(context, op, arg, earliest_eval_stage);
     }
 
@@ -263,11 +264,11 @@
 
     /// @param is_compound true if the binary operator is being used as a compound assignment
     /// @return the resolved binary operator overload
-    Result<Overload, std::string> Lookup(core::BinaryOp op,
-                                         const core::type::Type* lhs,
-                                         const core::type::Type* rhs,
-                                         EvaluationStage earliest_eval_stage,
-                                         bool is_compound) {
+    Result<Overload, StyledText> Lookup(core::BinaryOp op,
+                                        const core::type::Type* lhs,
+                                        const core::type::Type* rhs,
+                                        EvaluationStage earliest_eval_stage,
+                                        bool is_compound) {
         return LookupBinary(context, op, lhs, rhs, earliest_eval_stage, is_compound);
     }
 
@@ -281,12 +282,11 @@
     ///        `EvaluationStage::kRuntime`, then only overloads with concrete argument types
     ///        will be considered, as all abstract-numerics will have been materialized
     ///        after shader creation time (EvaluationStage::kConstant).
-
     /// @return the resolved type constructor or conversion function overload
-    Result<Overload, std::string> Lookup(CtorConv type,
-                                         VectorRef<const core::type::Type*> template_args,
-                                         VectorRef<const core::type::Type*> args,
-                                         EvaluationStage earliest_eval_stage) {
+    Result<Overload, StyledText> Lookup(CtorConv type,
+                                        VectorRef<const core::type::Type*> template_args,
+                                        VectorRef<const core::type::Type*> args,
+                                        EvaluationStage earliest_eval_stage) {
         std::string_view name = DIALECT::ToString(type);
         size_t id = static_cast<size_t>(type);
         return LookupCtorConv(context, name, id, std::move(template_args), std::move(args),
diff --git a/src/tint/lang/core/intrinsic/table_data.h b/src/tint/lang/core/intrinsic/table_data.h
index b2d86c0..1ed619d 100644
--- a/src/tint/lang/core/intrinsic/table_data.h
+++ b/src/tint/lang/core/intrinsic/table_data.h
@@ -37,6 +37,8 @@
 #include "src/tint/lang/core/parameter_usage.h"
 #include "src/tint/utils/containers/enum_set.h"
 #include "src/tint/utils/containers/slice.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/text_style.h"
 
 /// Forward declaration
 namespace tint::core::intrinsic {
@@ -414,13 +416,13 @@
     /// @note: The matcher indices are progressed on calling.
     inline Number Num(Number number);
 
-    /// @returns a string representation of the next TypeMatcher from the matcher indices.
+    /// Prints the type matcher representation to @p out
     /// @note: The matcher indices are progressed on calling.
-    inline std::string TypeName();
+    inline void PrintType(StyledText& out);
 
-    /// @returns a string representation of the next NumberMatcher from the matcher indices.
+    /// Prints the number matcher representation to @p out
     /// @note: The matcher indices are progressed on calling.
-    inline std::string NumName();
+    inline void PrintNum(StyledText& out);
 
   private:
     const MatcherIndex* matcher_indices_ = nullptr;
@@ -439,12 +441,12 @@
     /// @see #MatchFn
     MatchFn* const match;
 
-    /// Returns a string representation of the matcher.
+    /// Prints the representation of the matcher.
     /// Used for printing error messages when no overload is found.
-    using StringFn = std::string(MatchState* state);
+    using PrintFn = void(MatchState* state, StyledText& out);
 
-    /// @see #StringFn
-    StringFn* const string;
+    /// @see #PrintFn
+    PrintFn* const print;
 };
 
 /// A NumberMatcher is the interface used to match a number or enumerator used
@@ -459,12 +461,12 @@
     /// @see #MatchFn
     MatchFn* const match;
 
-    /// Returns a string representation of the matcher.
+    /// Prints the representation of the matcher.
     /// Used for printing error messages when no overload is found.
-    using StringFn = std::string(MatchState* state);
+    using PrintFn = void(MatchState* state, StyledText& out);
 
-    /// @see #StringFn
-    StringFn* const string;
+    /// @see #PrintFn
+    PrintFn* const print;
 };
 
 /// TableData holds the immutable data that holds the intrinsic data for a language.
@@ -600,16 +602,16 @@
     return matcher.match(*this, number);
 }
 
-std::string MatchState::TypeName() {
+void MatchState::PrintType(StyledText& out) {
     TypeMatcherIndex matcher_index{(*matcher_indices_++).value};
     auto& matcher = data[matcher_index];
-    return matcher.string(this);
+    matcher.print(this, out);
 }
 
-std::string MatchState::NumName() {
+void MatchState::PrintNum(StyledText& out) {
     NumberMatcherIndex matcher_index{(*matcher_indices_++).value};
     auto& matcher = data[matcher_index];
-    return matcher.string(this);
+    matcher.print(this, out);
 }
 
 /// TemplateTypeMatcher is a Matcher for a template type.
@@ -630,9 +632,9 @@
             }
             return nullptr;
         },
-        /* string */
-        [](MatchState* state) -> std::string {
-            return state->data[state->overload.templates + INDEX].name;
+        /* print */
+        [](MatchState* state, StyledText& out) {
+            out << style::Type << state->data[state->overload.templates + INDEX].name;
         },
     };
 };
@@ -651,9 +653,9 @@
             }
             return state.templates.Num(INDEX, number) ? number : Number::invalid;
         },
-        /* string */
-        [](MatchState* state) -> std::string {
-            return state->data[state->overload.templates + INDEX].name;
+        /* print */
+        [](MatchState* state, StyledText& out) {
+            out << style::Variable << state->data[state->overload.templates + INDEX].name;
         },
     };
 };
diff --git a/src/tint/lang/core/intrinsic/table_test.cc b/src/tint/lang/core/intrinsic/table_test.cc
index ddf205f..52b25f4 100644
--- a/src/tint/lang/core/intrinsic/table_test.cc
+++ b/src/tint/lang/core/intrinsic/table_test.cc
@@ -65,7 +65,7 @@
     auto* i32 = create<type::I32>();
     auto result = table.Lookup(BuiltinFn::kCos, Empty, Vector{i32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchU32) {
@@ -85,7 +85,7 @@
     auto result =
         table.Lookup(BuiltinFn::kUnpack2X16Float, Empty, Vector{f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchI32) {
@@ -112,7 +112,7 @@
     auto result =
         table.Lookup(BuiltinFn::kTextureLoad, Empty, Vector{tex, f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchIU32AsI32) {
@@ -140,7 +140,7 @@
     auto result =
         table.Lookup(BuiltinFn::kCountOneBits, Empty, Vector{f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchFIU32AsI32) {
@@ -184,7 +184,7 @@
     auto result = table.Lookup(BuiltinFn::kClamp, Empty, Vector{bool_, bool_, bool_},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchBool) {
@@ -205,7 +205,7 @@
     auto result =
         table.Lookup(BuiltinFn::kSelect, Empty, Vector{f32, f32, f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchPointer) {
@@ -226,7 +226,7 @@
     auto result =
         table.Lookup(BuiltinFn::kAtomicLoad, Empty, Vector{atomic_i32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchArray) {
@@ -248,7 +248,7 @@
     auto result =
         table.Lookup(BuiltinFn::kArrayLength, Empty, Vector{f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchSampler) {
@@ -277,7 +277,7 @@
     auto result = table.Lookup(BuiltinFn::kTextureSample, Empty, Vector{tex, f32, vec2f},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchSampledTexture) {
@@ -400,7 +400,7 @@
     auto result = table.Lookup(BuiltinFn::kTextureLoad, Empty, Vector{f32, vec2i},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchTemplateType) {
@@ -420,7 +420,7 @@
     auto result =
         table.Lookup(BuiltinFn::kClamp, Empty, Vector{f32, u32, f32}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchOpenSizeVector) {
@@ -443,7 +443,7 @@
     auto result = table.Lookup(BuiltinFn::kClamp, Empty, Vector{vec2f, u32, vec2f},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchOpenSizeMatrix) {
@@ -465,7 +465,7 @@
     auto result =
         table.Lookup(BuiltinFn::kDeterminant, Empty, Vector{mat3x2f}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(CoreIntrinsicTableTest, MatchDifferentArgsElementType_Builtin_ConstantEval) {
@@ -524,37 +524,37 @@
     auto result = table.Lookup(BuiltinFn::kTextureDimensions, Empty, Vector{bool_, bool_},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_EQ(result.Failure(),
-              R"(no matching call to textureDimensions(bool, bool)
+    ASSERT_EQ(result.Failure().Plain(),
+              R"(no matching call to 'textureDimensions(bool, bool)'
 
 27 candidate functions:
-  textureDimensions(texture: texture_1d<T>, level: L) -> u32  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_1d<T>) -> u32  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_3d<T>) -> vec3<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_depth_2d) -> vec2<u32>
-  textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>
-  textureDimensions(texture: texture_storage_1d<F, A>) -> u32
-  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>
-  textureDimensions(texture: texture_external) -> vec2<u32>
+  'textureDimensions(texture: texture_1d<T>, level: L) -> u32'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_1d<T>) -> u32'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_1d<F, A>) -> u32'
+  'textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>'
+  'textureDimensions(texture: texture_external) -> vec2<u32>'
 )");
 }
 
@@ -564,37 +564,37 @@
     auto result = table.Lookup(BuiltinFn::kTextureDimensions, Empty, Vector{tex, bool_},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_EQ(result.Failure(),
-              R"(no matching call to textureDimensions(texture_depth_2d, bool)
+    ASSERT_EQ(result.Failure().Plain(),
+              R"(no matching call to 'textureDimensions(texture_depth_2d, bool)'
 
 27 candidate functions:
-  textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_1d<T>, level: L) -> u32  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_2d) -> vec2<u32>
-  textureDimensions(texture: texture_1d<T>) -> u32  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_3d<T>) -> vec3<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>
-  textureDimensions(texture: texture_storage_1d<F, A>) -> u32
-  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>
-  textureDimensions(texture: texture_external) -> vec2<u32>
+  'textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_1d<T>, level: L) -> u32'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_1d<T>) -> u32'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_1d<F, A>) -> u32'
+  'textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>'
+  'textureDimensions(texture: texture_external) -> vec2<u32>'
 )");
 }
 
@@ -610,11 +610,11 @@
     auto* bool_ = create<type::Bool>();
     auto result = table.Lookup(UnaryOp::kNegation, bool_, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator - (bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator - (bool)'
 
 2 candidate operators:
-  operator - (T) -> T  where: T is f32, i32 or f16
-  operator - (vecN<T>) -> vecN<T>  where: T is f32, i32 or f16
+  'operator - (T) -> T'  where: 'T' is 'f32', 'i32' or 'f16'
+  'operator - (vecN<T>) -> vecN<T>'  where: 'T' is 'f32', 'i32' or 'f16'
 )");
 }
 
@@ -649,18 +649,18 @@
     auto result = table.Lookup(BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
                                /* is_compound */ false);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator * (f32, bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator * (f32, bool)'
 
 9 candidate operators:
-  operator * (T, T) -> T  where: T is f32, i32, u32 or f16
-  operator * (vecN<T>, T) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator * (T, vecN<T>) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator * (T, matNxM<T>) -> matNxM<T>  where: T is f32 or f16
-  operator * (matNxM<T>, T) -> matNxM<T>  where: T is f32 or f16
-  operator * (vecN<T>, vecN<T>) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator * (matCxR<T>, vecC<T>) -> vecR<T>  where: T is f32 or f16
-  operator * (vecR<T>, matCxR<T>) -> vecC<T>  where: T is f32 or f16
-  operator * (matKxR<T>, matCxK<T>) -> matCxR<T>  where: T is f32 or f16
+  'operator * (T, T) -> T'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator * (vecN<T>, T) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator * (T, vecN<T>) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator * (T, matNxM<T>) -> matNxM<T>'  where: 'T' is 'f32' or 'f16'
+  'operator * (matNxM<T>, T) -> matNxM<T>'  where: 'T' is 'f32' or 'f16'
+  'operator * (vecN<T>, vecN<T>) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator * (matCxR<T>, vecC<T>) -> vecR<T>'  where: 'T' is 'f32' or 'f16'
+  'operator * (vecR<T>, matCxR<T>) -> vecC<T>'  where: 'T' is 'f32' or 'f16'
+  'operator * (matKxR<T>, matCxK<T>) -> matCxR<T>'  where: 'T' is 'f32' or 'f16'
 )");
 }
 
@@ -681,18 +681,18 @@
     auto result = table.Lookup(BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
                                /* is_compound */ true);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator *= (f32, bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator *= (f32, bool)'
 
 9 candidate operators:
-  operator *= (T, T) -> T  where: T is f32, i32, u32 or f16
-  operator *= (vecN<T>, T) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator *= (T, vecN<T>) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator *= (T, matNxM<T>) -> matNxM<T>  where: T is f32 or f16
-  operator *= (matNxM<T>, T) -> matNxM<T>  where: T is f32 or f16
-  operator *= (vecN<T>, vecN<T>) -> vecN<T>  where: T is f32, i32, u32 or f16
-  operator *= (matCxR<T>, vecC<T>) -> vecR<T>  where: T is f32 or f16
-  operator *= (vecR<T>, matCxR<T>) -> vecC<T>  where: T is f32 or f16
-  operator *= (matKxR<T>, matCxK<T>) -> matCxR<T>  where: T is f32 or f16
+  'operator *= (T, T) -> T'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (vecN<T>, T) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (T, vecN<T>) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (T, matNxM<T>) -> matNxM<T>'  where: 'T' is 'f32' or 'f16'
+  'operator *= (matNxM<T>, T) -> matNxM<T>'  where: 'T' is 'f32' or 'f16'
+  'operator *= (vecN<T>, vecN<T>) -> vecN<T>'  where: 'T' is 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (matCxR<T>, vecC<T>) -> vecR<T>'  where: 'T' is 'f32' or 'f16'
+  'operator *= (vecR<T>, matCxR<T>) -> vecC<T>'  where: 'T' is 'f32' or 'f16'
+  'operator *= (matKxR<T>, matCxK<T>) -> matCxR<T>'  where: 'T' is 'f32' or 'f16'
 )");
 }
 
@@ -717,23 +717,23 @@
     auto result = table.Lookup(CtorConv::kVec3, Vector{i32}, Vector{i32, f32, i32},
                                EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(),
-              R"(no matching constructor for vec3<i32>(i32, f32, i32)
+    EXPECT_EQ(result.Failure().Plain(),
+              R"(no matching constructor for 'vec3<i32>(i32, f32, i32)'
 
 6 candidate constructors:
-  vec3<T>(x: T, y: T, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(xy: vec2<T>, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(x: T, yz: vec2<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(vec3<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
+  'vec3<T>(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>() -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f32, U is i32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f16, U is f32, i32, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is i32, U is f32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is u32, U is f32, f16, i32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is bool, U is f32, f16, i32 or u32
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f32', 'U' is 'i32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f16', 'U' is 'f32', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'i32', 'U' is 'f32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'u32', 'U' is 'f32', 'f16', 'i32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'bool', 'U' is 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -790,23 +790,23 @@
     auto result =
         table.Lookup(CtorConv::kVec3, Vector{f32}, Vector{arr}, EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(),
-              R"(no matching constructor for vec3<f32>(array<u32>)
+    EXPECT_EQ(result.Failure().Plain(),
+              R"(no matching constructor for 'vec3<f32>(array<u32>)'
 
 6 candidate constructors:
-  vec3<T>(vec3<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(x: T, yz: vec2<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(xy: vec2<T>, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(x: T, y: T, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
+  'vec3<T>(vec3<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>() -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f32, U is i32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f16, U is f32, i32, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is i32, U is f32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is u32, U is f32, f16, i32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is bool, U is f32, f16, i32 or u32
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f32', 'U' is 'i32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f16', 'U' is 'f32', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'i32', 'U' is 'f32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'u32', 'U' is 'f32', 'f16', 'i32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'bool', 'U' is 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -848,7 +848,7 @@
     auto result =
         table.Lookup(BuiltinFn::kAbs, Empty, std::move(arg_tys), EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 }  // namespace
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index afc234f..3fd6ae4 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -72,6 +72,7 @@
 #include "src/tint/utils/containers/transform.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/text_style.h"
 
 /// If set to 1 then the Tint will dump the IR when validating.
 #define TINT_DUMP_IR_WHEN_VALIDATING 0
@@ -101,60 +102,51 @@
     Result<SuccessType> Run();
 
   protected:
-    /// @param inst the instruction
-    /// @param err the error message
-    /// @returns a string with the instruction name name and error message formatted
-    std::string InstError(const Instruction* inst, std::string err);
-
     /// Adds an error for the @p inst and highlights the instruction in the disassembly
     /// @param inst the instruction
-    /// @param err the error string
-    void AddError(const Instruction* inst, std::string err);
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const Instruction* inst);
 
     /// Adds an error for the @p inst operand at @p idx and highlights the operand in the
     /// disassembly
     /// @param inst the instaruction
     /// @param idx the operand index
-    /// @param err the error string
-    void AddError(const Instruction* inst, size_t idx, std::string err);
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const Instruction* inst, size_t idx);
 
     /// Adds an error for the @p inst result at @p idx and highlgihts the result in the disassembly
     /// @param inst the instruction
     /// @param idx the result index
-    /// @param err the error string
-    void AddResultError(const Instruction* inst, size_t idx, std::string err);
+    /// @returns the diagnostic
+    diag::Diagnostic& AddResultError(const Instruction* inst, size_t idx);
 
     /// Adds an error the @p block and highlights the block header in the disassembly
     /// @param blk the block
-    /// @param err the error string
-    void AddError(const Block* blk, std::string err);
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(const Block* blk);
+
+    /// Adds an error the @p block and highlights the block header in the disassembly
+    /// @param src the source lines to highlight
+    /// @returns the diagnostic
+    diag::Diagnostic& AddError(Source src);
 
     /// Adds a note to @p inst and highlights the instruction in the disassembly
     /// @param inst the instruction
-    /// @param err the message to emit
-    void AddNote(const Instruction* inst, std::string err);
+    diag::Diagnostic& AddNote(const Instruction* inst);
 
     /// Adds a note to @p inst for operand @p idx and highlights the operand in the
     /// disassembly
     /// @param inst the instruction
     /// @param idx the operand index
-    /// @param err the message string
-    void AddNote(const Instruction* inst, size_t idx, std::string err);
+    diag::Diagnostic& AddNote(const Instruction* inst, size_t idx);
 
     /// Adds a note to @p blk and highlights the block in the disassembly
     /// @param blk the block
-    /// @param err the message to emit
-    void AddNote(const Block* blk, std::string err);
-
-    /// Adds an error to the diagnostics
-    /// @param err the message to emit
-    /// @param src the source lines to highlight
-    void AddError(std::string err, Source src = {});
+    diag::Diagnostic& AddNote(const Block* blk);
 
     /// Adds a note to the diagnostics
-    /// @param note the note to emit
     /// @param src the source lines to highlight
-    void AddNote(std::string note, Source src = {});
+    diag::Diagnostic& AddNote(Source src = {});
 
     /// @param v the value to get the name for
     /// @returns the name for the given value
@@ -316,7 +308,8 @@
 
     for (auto& func : mod_.functions) {
         if (!all_functions_.Add(func.Get())) {
-            AddError("function '" + Name(func.Get()) + "' added to module multiple times");
+            AddError(Source{}) << "function " << style::Function << Name(func.Get()) << style::Plain
+                               << " added to module multiple times";
         }
     }
 
@@ -328,92 +321,94 @@
         // Check for orphaned instructions.
         for (auto* inst : mod_.instructions.Objects()) {
             if (inst->Alive() && !visited_instructions_.Contains(inst)) {
-                AddError("orphaned instruction: " + inst->FriendlyName());
+                AddError(inst) << "orphaned instruction: " << inst->FriendlyName();
             }
         }
     }
 
     if (diagnostics_.ContainsErrors()) {
         DisassembleIfNeeded();
-        diagnostics_.AddNote(tint::diag::System::IR,
-                             "# Disassembly\n" + disassembly_file->content.data, {});
+        diagnostics_.AddNote(tint::diag::System::IR, Source{}) << "# Disassembly\n"
+                                                               << disassembly_file->content.data;
         return Failure{std::move(diagnostics_)};
     }
     return Success;
 }
 
-std::string Validator::InstError(const Instruction* inst, std::string err) {
-    return std::string(inst->FriendlyName()) + ": " + err;
-}
-
-void Validator::AddError(const Instruction* inst, std::string err) {
+diag::Diagnostic& Validator::AddError(const Instruction* inst) {
     DisassembleIfNeeded();
     auto src = dis_.InstructionSource(inst);
-    AddError(std::move(err), src);
+    auto& diag = AddError(src) << inst->FriendlyName() << ": ";
 
     if (current_block_) {
-        AddNote(current_block_, "In block");
+        AddNote(current_block_) << "In block";
     }
+    return diag;
 }
 
-void Validator::AddError(const Instruction* inst, size_t idx, std::string err) {
+diag::Diagnostic& Validator::AddError(const Instruction* inst, size_t idx) {
     DisassembleIfNeeded();
     auto src = dis_.OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
-    AddError(std::move(err), src);
+    auto& diag = AddError(src) << inst->FriendlyName() << ": ";
 
     if (current_block_) {
-        AddNote(current_block_, "In block");
+        AddNote(current_block_) << "In block";
     }
+
+    return diag;
 }
 
-void Validator::AddResultError(const Instruction* inst, size_t idx, std::string err) {
+diag::Diagnostic& Validator::AddResultError(const Instruction* inst, size_t idx) {
     DisassembleIfNeeded();
     auto src = dis_.ResultSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
-    AddError(std::move(err), src);
+    auto& diag = AddError(src) << inst->FriendlyName() << ": ";
 
     if (current_block_) {
-        AddNote(current_block_, "In block");
+        AddNote(current_block_) << "In block";
     }
+    return diag;
 }
 
-void Validator::AddError(const Block* blk, std::string err) {
+diag::Diagnostic& Validator::AddError(const Block* blk) {
     DisassembleIfNeeded();
     auto src = dis_.BlockSource(blk);
-    AddError(std::move(err), src);
+    return AddError(src);
 }
 
-void Validator::AddNote(const Instruction* inst, std::string err) {
+diag::Diagnostic& Validator::AddNote(const Instruction* inst) {
     DisassembleIfNeeded();
     auto src = dis_.InstructionSource(inst);
-    AddNote(std::move(err), src);
+    return AddNote(src);
 }
 
-void Validator::AddNote(const Instruction* inst, size_t idx, std::string err) {
+diag::Diagnostic& Validator::AddNote(const Instruction* inst, size_t idx) {
     DisassembleIfNeeded();
     auto src = dis_.OperandSource(Disassembler::IndexedValue{inst, static_cast<uint32_t>(idx)});
-    AddNote(std::move(err), src);
+    return AddNote(src);
 }
 
-void Validator::AddNote(const Block* blk, std::string err) {
+diag::Diagnostic& Validator::AddNote(const Block* blk) {
     DisassembleIfNeeded();
     auto src = dis_.BlockSource(blk);
-    AddNote(std::move(err), src);
+    return AddNote(src);
 }
 
-void Validator::AddError(std::string err, Source src) {
-    auto& diag = diagnostics_.AddError(tint::diag::System::IR, std::move(err), src);
+diag::Diagnostic& Validator::AddError(Source src) {
+    auto& diag = diagnostics_.AddError(tint::diag::System::IR, src);
     if (src.range != Source::Range{{}}) {
         diag.source.file = disassembly_file.get();
         diag.owned_file = disassembly_file;
     }
+    return diag;
 }
 
-void Validator::AddNote(std::string note, Source src) {
-    auto& diag = diagnostics_.AddNote(tint::diag::System::IR, std::move(note), src);
+diag::Diagnostic& Validator::AddNote(Source src) {
+    auto& diag = diagnostics_.AddNote(tint::diag::System::IR, src);
     if (src.range != Source::Range{{}}) {
         diag.source.file = disassembly_file.get();
         diag.owned_file = disassembly_file;
     }
+    return diag;
 }
 
 std::string Validator::Name(const Value* v) {
@@ -422,7 +417,7 @@
 
 void Validator::CheckOperandNotNull(const Instruction* inst, const ir::Value* operand, size_t idx) {
     if (operand == nullptr) {
-        AddError(inst, idx, InstError(inst, "operand is undefined"));
+        AddError(inst, idx) << "operand is undefined";
     }
 }
 
@@ -440,15 +435,12 @@
 
     for (auto* inst : *blk) {
         if (inst->Block() != blk) {
-            AddError(
-                inst,
-                InstError(inst, "instruction in root block does not have root block as parent"));
+            AddError(inst) << "instruction in root block does not have root block as parent";
             continue;
         }
         auto* var = inst->As<ir::Var>();
         if (!var) {
-            AddError(inst,
-                     std::string("root block: invalid instruction: ") + inst->TypeInfo().name);
+            AddError(inst) << "root block: invalid instruction: " << inst->TypeInfo().name;
             continue;
         }
         CheckInstruction(var);
@@ -463,17 +455,17 @@
     TINT_SCOPED_ASSIGNMENT(current_block_, blk);
 
     if (!blk->Terminator()) {
-        AddError(blk, "block: does not end in a terminator instruction");
+        AddError(blk) << "block: does not end in a terminator instruction";
     }
 
     for (auto* inst : *blk) {
         if (inst->Block() != blk) {
-            AddError(inst, InstError(inst, "block instruction does not have same block as parent"));
-            AddNote(current_block_, "In block");
+            AddError(inst) << "block instruction does not have same block as parent";
+            AddNote(current_block_) << "In block";
             continue;
         }
         if (inst->Is<ir::Terminator>() && inst != blk->Terminator()) {
-            AddError(inst, "block: terminator which isn't the final instruction");
+            AddError(inst) << "block: terminator which isn't the final instruction";
             continue;
         }
 
@@ -484,22 +476,21 @@
 void Validator::CheckInstruction(const Instruction* inst) {
     visited_instructions_.Add(inst);
     if (!inst->Alive()) {
-        AddError(inst, InstError(inst, "destroyed instruction found in instruction list"));
+        AddError(inst) << "destroyed instruction found in instruction list";
         return;
     }
     auto results = inst->Results();
     for (size_t i = 0; i < results.Length(); ++i) {
         auto* res = results[i];
         if (!res) {
-            AddResultError(inst, i, InstError(inst, "result is undefined"));
+            AddResultError(inst, i) << "result is undefined";
             continue;
         }
 
         if (res->Instruction() == nullptr) {
-            AddResultError(inst, i, InstError(inst, "instruction of result is undefined"));
+            AddResultError(inst, i) << "instruction of result is undefined";
         } else if (res->Instruction() != inst) {
-            AddResultError(inst, i,
-                           InstError(inst, "instruction of result is a different instruction"));
+            AddResultError(inst, i) << "instruction of result is a different instruction";
         }
     }
 
@@ -513,11 +504,11 @@
         // Note, a `nullptr` is a valid operand in some cases, like `var` so we can't just check
         // for `nullptr` here.
         if (!op->Alive()) {
-            AddError(inst, i, InstError(inst, "operand is not alive"));
+            AddError(inst, i) << "operand is not alive";
         }
 
         if (!op->HasUsage(inst, i)) {
-            AddError(inst, i, InstError(inst, "operand missing usage"));
+            AddError(inst, i) << "operand missing usage";
         }
     }
 
@@ -538,13 +529,13 @@
         [&](const Terminator* b) { CheckTerminator(b); },                  //
         [&](const Unary* u) { CheckUnary(u); },                            //
         [&](const Var* var) { CheckVar(var); },                            //
-        [&](const Default) { AddError(inst, InstError(inst, "missing validation")); });
+        [&](const Default) { AddError(inst) << "missing validation"; });
 }
 
 void Validator::CheckVar(const Var* var) {
     if (var->Result(0) && var->Initializer()) {
         if (var->Initializer()->Type() != var->Result(0)->Type()->UnwrapPtr()) {
-            AddError(var, InstError(var, "initializer has incorrect type"));
+            AddError(var) << "initializer has incorrect type";
         }
     }
 }
@@ -554,7 +545,7 @@
 
     if (let->Result(0) && let->Value()) {
         if (let->Result(0)->Type() != let->Value()->Type()) {
-            AddError(let, InstError(let, "result type does not match value type"));
+            AddError(let) << "result type does not match value type";
         }
     }
 }
@@ -587,42 +578,39 @@
     auto result = core::intrinsic::LookupFn(context, call->FriendlyName().c_str(), call->FuncId(),
                                             Empty, args, core::EvaluationStage::kRuntime);
     if (result != Success) {
-        AddError(call, InstError(call, result.Failure()));
+        AddError(call) << result.Failure();
         return;
     }
 
     if (result->return_type != call->Result(0)->Type()) {
-        AddError(call, InstError(call, "call result type does not match builtin return type"));
+        AddError(call) << "call result type does not match builtin return type";
     }
 }
 
 void Validator::CheckUserCall(const UserCall* call) {
     if (!all_functions_.Contains(call->Target())) {
-        AddError(call, UserCall::kFunctionOperandOffset,
-                 InstError(call, "call target is not part of the module"));
+        AddError(call, UserCall::kFunctionOperandOffset) << "call target is not part of the module";
     }
 
     if (call->Target()->Stage() != Function::PipelineStage::kUndefined) {
-        AddError(call, UserCall::kFunctionOperandOffset,
-                 InstError(call, "call target must not have a pipeline stage"));
+        AddError(call, UserCall::kFunctionOperandOffset)
+            << "call target must not have a pipeline stage";
     }
 
     auto args = call->Args();
     auto params = call->Target()->Params();
     if (args.Length() != params.Length()) {
-        StringStream err;
-        err << "function has " << params.Length() << " parameters, but call provides "
+        AddError(call, UserCall::kFunctionOperandOffset)
+            << "function has " << params.Length() << " parameters, but call provides "
             << args.Length() << " arguments";
-        AddError(call, UserCall::kFunctionOperandOffset, InstError(call, err.str()));
         return;
     }
 
     for (size_t i = 0; i < args.Length(); i++) {
         if (args[i]->Type() != params[i]->Type()) {
-            StringStream err;
-            err << "function parameter " << i << " is of type " << params[i]->Type()->FriendlyName()
+            AddError(call, UserCall::kArgsOperandOffset + i)
+                << "function parameter " << i << " is of type " << params[i]->Type()->FriendlyName()
                 << ", but argument is of type " << args[i]->Type()->FriendlyName();
-            AddError(call, UserCall::kArgsOperandOffset + i, InstError(call, err.str()));
         }
     }
 }
@@ -643,10 +631,8 @@
     };
 
     for (size_t i = 0; i < a->Indices().Length(); i++) {
-        auto err = [&](std::string msg) {
-            AddError(a, i + Access::kIndicesOperandOffset, InstError(a, msg));
-        };
-        auto note = [&](std::string msg) { AddNote(a, i + Access::kIndicesOperandOffset, msg); };
+        auto err = [&](std::string msg) { AddError(a, i + Access::kIndicesOperandOffset) << msg; };
+        auto note = [&](std::string msg) { AddNote(a, i + Access::kIndicesOperandOffset) << msg; };
 
         auto* index = a->Indices()[i];
         if (TINT_UNLIKELY(!index->Type()->is_integer_scalar())) {
@@ -705,8 +691,8 @@
     }
 
     if (TINT_UNLIKELY(!ok)) {
-        AddError(a, InstError(a, "result of access chain is type " + current() +
-                                     " but instruction type is " + want->FriendlyName()));
+        AddError(a) << "result of access chain is type " << current() << " but instruction type is "
+                    << want->FriendlyName();
     }
 }
 
@@ -725,7 +711,7 @@
             core::intrinsic::LookupBinary(context, b->Op(), b->LHS()->Type(), b->RHS()->Type(),
                                           core::EvaluationStage::kRuntime, /* is_compound */ false);
         if (overload != Success) {
-            AddError(b, InstError(b, overload.Failure()));
+            AddError(b) << overload.Failure();
             return;
         }
 
@@ -735,7 +721,7 @@
                 err << "binary instruction result type (" << result->Type()->FriendlyName()
                     << ") does not match overload result type ("
                     << overload->return_type->FriendlyName() << ")";
-                AddError(b, InstError(b, err.str()));
+                AddError(b) << err.str();
             }
         }
     }
@@ -755,7 +741,7 @@
         auto overload = core::intrinsic::LookupUnary(context, u->Op(), u->Val()->Type(),
                                                      core::EvaluationStage::kRuntime);
         if (overload != Success) {
-            AddError(u, InstError(u, overload.Failure()));
+            AddError(u) << overload.Failure();
             return;
         }
 
@@ -765,7 +751,7 @@
                 err << "unary instruction result type (" << result->Type()->FriendlyName()
                     << ") does not match overload result type ("
                     << overload->return_type->FriendlyName() << ")";
-                AddError(u, InstError(u, err.str()));
+                AddError(u) << err.str();
             }
         }
     }
@@ -775,8 +761,7 @@
     CheckOperandNotNull(if_, if_->Condition(), If::kConditionOperandOffset);
 
     if (if_->Condition() && !if_->Condition()->Type()->Is<core::type::Bool>()) {
-        AddError(if_, If::kConditionOperandOffset,
-                 InstError(if_, "condition must be a `bool` type"));
+        AddError(if_, If::kConditionOperandOffset) << "condition must be a `bool` type";
     }
 
     control_stack_.Push(if_);
@@ -824,38 +809,36 @@
         [&](const ir::Return* ret) { CheckReturn(ret); },  //
         [&](const ir::TerminateInvocation*) {},            //
         [&](const ir::Unreachable*) {},                    //
-        [&](Default) { AddError(b, InstError(b, "missing validation")); });
+        [&](Default) { AddError(b) << "missing validation"; });
 }
 
 void Validator::CheckExit(const Exit* e) {
     if (e->ControlInstruction() == nullptr) {
-        AddError(e, InstError(e, "has no parent control instruction"));
+        AddError(e) << "has no parent control instruction";
         return;
     }
 
     if (control_stack_.IsEmpty()) {
-        AddError(e, InstError(e, "found outside all control instructions"));
+        AddError(e) << "found outside all control instructions";
         return;
     }
 
     auto results = e->ControlInstruction()->Results();
     auto args = e->Args();
     if (results.Length() != args.Length()) {
-        AddError(e, InstError(e, std::string("args count (") + std::to_string(args.Length()) +
-                                     ") does not match control instruction result count (" +
-                                     std::to_string(results.Length()) + ")"));
-        AddNote(e->ControlInstruction(), "control instruction");
+        AddError(e) << ("args count (") << args.Length()
+                    << ") does not match control instruction result count (" << results.Length()
+                    << ")";
+        AddNote(e->ControlInstruction()) << "control instruction";
         return;
     }
 
     for (size_t i = 0; i < results.Length(); ++i) {
         if (results[i] && args[i] && results[i]->Type() != args[i]->Type()) {
-            AddError(
-                e, i,
-                InstError(e, std::string("argument type (") + results[i]->Type()->FriendlyName() +
-                                 ") does not match control instruction type (" +
-                                 args[i]->Type()->FriendlyName() + ")"));
-            AddNote(e->ControlInstruction(), "control instruction");
+            AddError(e, i) << "argument type (" << results[i]->Type()->FriendlyName()
+                           << ") does not match control instruction type ("
+                           << args[i]->Type()->FriendlyName() << ")";
+            AddNote(e->ControlInstruction()) << "control instruction";
         }
     }
 
@@ -864,31 +847,31 @@
         [&](const ir::ExitIf* i) { CheckExitIf(i); },          //
         [&](const ir::ExitLoop* l) { CheckExitLoop(l); },      //
         [&](const ir::ExitSwitch* s) { CheckExitSwitch(s); },  //
-        [&](Default) { AddError(e, InstError(e, "missing validation")); });
+        [&](Default) { AddError(e) << "missing validation"; });
 }
 
 void Validator::CheckExitIf(const ExitIf* e) {
     if (control_stack_.Back() != e->If()) {
-        AddError(e, InstError(e, "if target jumps over other control instructions"));
-        AddNote(control_stack_.Back(), "first control instruction jumped");
+        AddError(e) << "if target jumps over other control instructions";
+        AddNote(control_stack_.Back()) << "first control instruction jumped";
     }
 }
 
 void Validator::CheckReturn(const Return* ret) {
     auto* func = ret->Func();
     if (func == nullptr) {
-        AddError(ret, InstError(ret, "undefined function"));
+        AddError(ret) << "undefined function";
         return;
     }
     if (func->ReturnType()->Is<core::type::Void>()) {
         if (ret->Value()) {
-            AddError(ret, InstError(ret, "unexpected return value"));
+            AddError(ret) << "unexpected return value";
         }
     } else {
         if (!ret->Value()) {
-            AddError(ret, InstError(ret, "expected return value"));
+            AddError(ret) << "expected return value";
         } else if (ret->Value()->Type() != func->ReturnType()) {
-            AddError(ret, InstError(ret, "return value type does not match function return type"));
+            AddError(ret) << "return value type does not match function return type";
         }
     }
 }
@@ -902,15 +885,14 @@
         }
         // A exit switch can step over if instructions, but no others.
         if (!ctrl->Is<ir::If>()) {
-            AddError(exit, InstError(exit, std::string(control->FriendlyName()) +
-                                               " target jumps over other control instructions"));
-            AddNote(ctrl, "first control instruction jumped");
+            AddError(exit) << control->FriendlyName()
+                           << " target jumps over other control instructions";
+            AddNote(ctrl) << "first control instruction jumped";
             return;
         }
     }
     if (!found) {
-        AddError(exit, InstError(exit, std::string(control->FriendlyName()) +
-                                           " not found in parent control instructions"));
+        AddError(exit) << control->FriendlyName() << " not found in parent control instructions";
     }
 }
 
@@ -927,14 +909,14 @@
         // Found parent loop
         if (inst->Block()->Parent() == control) {
             if (inst->Block() == control->Continuing()) {
-                AddError(l, InstError(l, "loop exit jumps out of continuing block"));
+                AddError(l) << "loop exit jumps out of continuing block";
                 if (control->Continuing() != l->Block()) {
-                    AddNote(control->Continuing(), "in continuing block");
+                    AddNote(control->Continuing()) << "in continuing block";
                 }
             } else if (inst->Block() == control->Initializer()) {
-                AddError(l, InstError(l, "loop exit not permitted in loop initializer"));
+                AddError(l) << "loop exit not permitted in loop initializer";
                 if (control->Initializer() != l->Block()) {
-                    AddNote(control->Initializer(), "in initializer block");
+                    AddNote(control->Initializer()) << "in initializer block";
                 }
             }
             break;
@@ -949,11 +931,11 @@
     if (auto* from = l->From()) {
         auto* mv = from->Type()->As<core::type::MemoryView>();
         if (!mv) {
-            AddError(l, Load::kFromOperandOffset, "load source operand is not a memory view");
+            AddError(l, Load::kFromOperandOffset) << "load source operand is not a memory view";
             return;
         }
         if (l->Result(0)->Type() != mv->StoreType()) {
-            AddError(l, Load::kFromOperandOffset, "result type does not match source store type");
+            AddError(l, Load::kFromOperandOffset) << "result type does not match source store type";
         }
     }
 }
@@ -965,11 +947,12 @@
         if (auto* to = s->To()) {
             auto* mv = to->Type()->As<core::type::MemoryView>();
             if (!mv) {
-                AddError(s, Store::kFromOperandOffset, "store target operand is not a memory view");
+                AddError(s, Store::kFromOperandOffset)
+                    << "store target operand is not a memory view";
                 return;
             }
             if (from->Type() != mv->StoreType()) {
-                AddError(s, Store::kFromOperandOffset, "value type does not match store type");
+                AddError(s, Store::kFromOperandOffset) << "value type does not match store type";
             }
         }
     }
@@ -983,7 +966,7 @@
     if (auto* res = l->Result(0)) {
         if (auto* el_ty = GetVectorPtrElementType(l, LoadVectorElement::kFromOperandOffset)) {
             if (res->Type() != el_ty) {
-                AddResultError(l, 0, "result type does not match vector pointer element type");
+                AddResultError(l, 0) << "result type does not match vector pointer element type";
             }
         }
     }
@@ -997,8 +980,8 @@
     if (auto* value = s->Value()) {
         if (auto* el_ty = GetVectorPtrElementType(s, StoreVectorElement::kToOperandOffset)) {
             if (value->Type() != el_ty) {
-                AddError(s, StoreVectorElement::kValueOperandOffset,
-                         "value type does not match vector pointer element type");
+                AddError(s, StoreVectorElement::kValueOperandOffset)
+                    << "value type does not match vector pointer element type";
             }
         }
     }
@@ -1023,7 +1006,7 @@
         }
     }
 
-    AddError(inst, idx, "operand must be a pointer to vector, got " + type->FriendlyName());
+    AddError(inst, idx) << "operand must be a pointer to vector, got " << type->FriendlyName();
     return nullptr;
 }
 
diff --git a/src/tint/lang/core/ir/validator_test.cc b/src/tint/lang/core/ir/validator_test.cc
index cd5faeb..256cf8f 100644
--- a/src/tint/lang/core/ir/validator_test.cc
+++ b/src/tint/lang/core/ir/validator_test.cc
@@ -60,7 +60,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:2:3 error: root block: invalid instruction: tint::core::ir::Loop
+              R"(:2:3 error: loop: root block: invalid instruction: tint::core::ir::Loop
   loop [b: %b2] {  # loop_1
   ^^^^^^^^^^^^^
 
@@ -922,7 +922,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:3:5 error: block: terminator which isn't the final instruction
+              R"(:3:5 error: return: block: terminator which isn't the final instruction
     ret
     ^^^
 
@@ -1447,7 +1447,7 @@
 
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
-    EXPECT_EQ(res.Failure().reason.Str(), R"(error: orphaned instruction: load
+    EXPECT_EQ(res.Failure().reason.Str(), R"(error: load: orphaned instruction: load
 note: # Disassembly
 %my_func = func():void -> %b1 {
   %b1 = block {
@@ -3157,7 +3157,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:19 error: load source operand is not a memory view
+              R"(:4:19 error: load: load source operand is not a memory view
     %3:f32 = load %l
                   ^^
 
@@ -3188,7 +3188,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:19 error: result type does not match source store type
+              R"(:4:19 error: load: result type does not match source store type
     %3:f32 = load %2
                   ^^
 
@@ -3277,7 +3277,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:15 error: store target operand is not a memory view
+              R"(:4:15 error: store: store target operand is not a memory view
     store %l, 42u
               ^^^
 
@@ -3308,7 +3308,7 @@
     auto res = ir::Validate(mod);
     ASSERT_NE(res, Success);
     EXPECT_EQ(res.Failure().reason.Str(),
-              R"(:4:15 error: value type does not match store type
+              R"(:4:15 error: store: value type does not match store type
     store %2, 42u
               ^^^
 
@@ -3468,7 +3468,7 @@
   %b1 = block {
   ^^^^^^^^^^^
 
-:4:37 error: value type does not match vector pointer element type
+:4:37 error: store_vector_element: value type does not match vector pointer element type
     store_vector_element %2, undef, 2i
                                     ^^
 
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index c40bcbb..6ecc839 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -393,8 +393,8 @@
     auto* dst_type = TypeOf(call);
 
     if (!dst_type->is_integer_scalar_or_vector() && !dst_type->is_float_scalar_or_vector()) {
-        diagnostics_.AddError(diag::System::Writer,
-                              "Unable to do bitcast to type " + dst_type->FriendlyName());
+        diagnostics_.AddError(diag::System::Writer, Source{})
+            << "Unable to do bitcast to type " << dst_type->FriendlyName();
         return;
     }
 
@@ -1517,9 +1517,7 @@
             out << "imageStore";
             break;
         default:
-            diagnostics_.AddError(diag::System::Writer,
-                                  "Internal compiler error: Unhandled texture builtin '" +
-                                      std::string(builtin->str()) + "'");
+            TINT_ICE() << "Unhandled texture builtin '" << std::string(builtin->str()) << "'";
             return;
     }
 
@@ -1742,8 +1740,8 @@
         case wgsl::BuiltinFn::kUnpack4X8Unorm:
             return "unpackUnorm4x8";
         default:
-            diagnostics_.AddError(diag::System::Writer,
-                                  "Unknown builtin method: " + std::string(builtin->str()));
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "Unknown builtin method: " << builtin;
     }
 
     return "";
@@ -1921,9 +1919,9 @@
         [&](const ast::Let* let) { EmitProgramConstVariable(let); },
         [&](const ast::Override*) {
             // Override is removed with SubstituteOverride
-            diagnostics_.AddError(diag::System::Writer,
-                                  "override-expressions should have been removed with the "
-                                  "SubstituteOverride transform");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "override-expressions should have been removed with the "
+                   "SubstituteOverride transform";
         },
         [&](const ast::Const*) {
             // Constants are embedded at their use
@@ -2187,10 +2185,9 @@
             out << "local_size_" << (i == 0 ? "x" : i == 1 ? "y" : "z") << " = ";
 
             if (!wgsize[i].has_value()) {
-                diagnostics_.AddError(
-                    diag::System::Writer,
-                    "override-expressions should have been removed with the SubstituteOverride "
-                    "transform");
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << "override-expressions should have been removed with the SubstituteOverride "
+                       "transform";
                 return;
             }
             out << std::to_string(wgsize[i].value());
@@ -2291,8 +2288,8 @@
 
             auto count = a->ConstantCount();
             if (!count) {
-                diagnostics_.AddError(diag::System::Writer,
-                                      core::type::Array::kErrExpectedConstantCount);
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << core::type::Array::kErrExpectedConstantCount;
                 return;
             }
 
@@ -2343,7 +2340,8 @@
                     return;
                 }
             }
-            diagnostics_.AddError(diag::System::Writer, "unknown integer literal suffix type");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "unknown integer literal suffix type";
         },  //
         TINT_ICE_ON_NO_MATCH);
 }
@@ -2395,8 +2393,8 @@
 
         auto count = arr->ConstantCount();
         if (!count) {
-            diagnostics_.AddError(diag::System::Writer,
-                                  core::type::Array::kErrExpectedConstantCount);
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << core::type::Array::kErrExpectedConstantCount;
             return;
         }
 
@@ -2407,8 +2405,8 @@
             EmitZeroValue(out, arr->ElemType());
         }
     } else {
-        diagnostics_.AddError(diag::System::Writer,
-                              "Invalid type for zero emission: " + type->FriendlyName());
+        diagnostics_.AddError(diag::System::Writer, Source{})
+            << "Invalid type for zero emission: " << type->FriendlyName();
     }
 }
 
@@ -2683,8 +2681,8 @@
             } else {
                 auto count = arr->ConstantCount();
                 if (!count) {
-                    diagnostics_.AddError(diag::System::Writer,
-                                          core::type::Array::kErrExpectedConstantCount);
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << core::type::Array::kErrExpectedConstantCount;
                     return;
                 }
                 sizes.push_back(count.value());
@@ -2839,7 +2837,7 @@
     } else if (type->Is<core::type::Void>()) {
         out << "void";
     } else {
-        diagnostics_.AddError(diag::System::Writer, "unknown type in EmitType");
+        diagnostics_.AddError(diag::System::Writer, Source{}) << "unknown type in EmitType";
     }
 }
 
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
index 475f00a..4c8c40c 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer_test.cc
@@ -35,7 +35,7 @@
 using GlslASTPrinterTest = TestHelper;
 
 TEST_F(GlslASTPrinterTest, InvalidProgram) {
-    Diagnostics().AddError(diag::System::Writer, "make the program invalid");
+    Diagnostics().AddError(diag::System::Writer, Source{}) << "make the program invalid";
     ASSERT_FALSE(IsValid());
     auto program = resolver::Resolve(*this);
     ASSERT_FALSE(program.IsValid());
diff --git a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
index 25cc723..9b09f57 100644
--- a/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/combine_samplers.cc
@@ -441,8 +441,8 @@
     auto* binding_info = inputs.Get<BindingInfo>();
     if (!binding_info) {
         ProgramBuilder b;
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
index 7d3a4ab..89cebe1 100644
--- a/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
+++ b/src/tint/lang/glsl/writer/ast_raise/texture_builtins_from_uniform.cc
@@ -74,10 +74,9 @@
     ApplyResult Run() {
         auto* cfg = inputs.Get<Config>();
         if (cfg == nullptr) {
-            b.Diagnostics().AddError(
-                diag::System::Transform,
-                "missing transform data for " +
-                    std::string(tint::TypeInfo::Of<TextureBuiltinsFromUniform>().name));
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "missing transform data for "
+                << tint::TypeInfo::Of<TextureBuiltinsFromUniform>().name;
             return resolver::Resolve(b);
         }
         ubo_bindingpoint_ordering = cfg->ubo_bindingpoint_ordering;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
index ab2551a..658beeb 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer.cc
@@ -702,8 +702,8 @@
     auto* dst_el_type = dst_type->DeepestElement();
 
     if (!dst_el_type->is_integer_scalar() && !dst_el_type->is_float_scalar()) {
-        diagnostics_.AddError(diag::System::Writer,
-                              "Unable to do bitcast to type " + dst_el_type->FriendlyName());
+        diagnostics_.AddError(diag::System::Writer, Source{})
+            << "Unable to do bitcast to type " << dst_el_type->FriendlyName();
         return false;
     }
 
@@ -2450,8 +2450,7 @@
                     break;
                 }
                 default:
-                    diagnostics_.AddError(diag::System::Writer,
-                                          "Internal error: unhandled data packing builtin");
+                    TINT_ICE() << " unhandled data packing builtin";
                     return false;
             }
 
@@ -2517,8 +2516,7 @@
                     Line(b) << "return f16tof32(uint2(i & 0xffff, i >> 16));";
                     break;
                 default:
-                    diagnostics_.AddError(diag::System::Writer,
-                                          "Internal error: unhandled data packing builtin");
+                    TINT_ICE() << "unhandled data packing builtin";
                     return false;
             }
 
@@ -2579,28 +2577,27 @@
             return false;
     }
 
-    return CallBuiltinHelper(
-        out, expr, builtin, [&](TextBuffer* b, const std::vector<std::string>& params) {
-            std::string functionName;
-            switch (builtin->Fn()) {
-                case wgsl::BuiltinFn::kDot4I8Packed:
-                    Line(b) << "int accumulator = 0;";
-                    functionName = "dot4add_i8packed";
-                    break;
-                case wgsl::BuiltinFn::kDot4U8Packed:
-                    Line(b) << "uint accumulator = 0u;";
-                    functionName = "dot4add_u8packed";
-                    break;
-                default:
-                    diagnostics_.AddError(diag::System::Writer,
-                                          "Internal error: unhandled DP4a builtin");
-                    return false;
-            }
-            Line(b) << "return " << functionName << "(" << params[0] << ", " << params[1]
-                    << ", accumulator);";
+    return CallBuiltinHelper(out, expr, builtin,
+                             [&](TextBuffer* b, const std::vector<std::string>& params) {
+                                 std::string functionName;
+                                 switch (builtin->Fn()) {
+                                     case wgsl::BuiltinFn::kDot4I8Packed:
+                                         Line(b) << "int accumulator = 0;";
+                                         functionName = "dot4add_i8packed";
+                                         break;
+                                     case wgsl::BuiltinFn::kDot4U8Packed:
+                                         Line(b) << "uint accumulator = 0u;";
+                                         functionName = "dot4add_u8packed";
+                                         break;
+                                     default:
+                                         TINT_ICE() << "Internal error: unhandled DP4a builtin";
+                                         return false;
+                                 }
+                                 Line(b) << "return " << functionName << "(" << params[0] << ", "
+                                         << params[1] << ", accumulator);";
 
-            return true;
-        });
+                                 return true;
+                             });
 }
 
 bool ASTPrinter::EmitBarrierCall(StringStream& out, const sem::BuiltinFn* builtin) {
@@ -2932,9 +2929,7 @@
             out << "[";
             break;
         default:
-            diagnostics_.AddError(diag::System::Writer,
-                                  "Internal compiler error: Unhandled texture builtin '" +
-                                      std::string(builtin->str()) + "'");
+            TINT_ICE() << "Unhandled texture builtin '" << builtin << "'";
             return false;
     }
 
@@ -3120,8 +3115,8 @@
         case wgsl::BuiltinFn::kSubgroupBroadcast:
             return "WaveReadLaneAt";
         default:
-            diagnostics_.AddError(diag::System::Writer,
-                                  "Unknown builtin method: " + std::string(builtin->str()));
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "Unknown builtin method: " << builtin->str();
     }
 
     return "";
@@ -3392,9 +3387,8 @@
                 case core::AddressSpace::kWorkgroup:
                     return EmitWorkgroupVariable(sem);
                 case core::AddressSpace::kPushConstant:
-                    diagnostics_.AddError(
-                        diag::System::Writer,
-                        "unhandled address space " + tint::ToString(sem->AddressSpace()));
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << "unhandled address space " << sem->AddressSpace();
                     return false;
                 default: {
                     TINT_ICE() << "unhandled address space " << sem->AddressSpace();
@@ -3404,9 +3398,9 @@
         },
         [&](const ast::Override*) {
             // Override is removed with SubstituteOverride
-            diagnostics_.AddError(diag::System::Writer,
-                                  "override-expressions should have been removed with the "
-                                  "SubstituteOverride transform");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "override-expressions should have been removed with the SubstituteOverride "
+                   "transform";
             return false;
         },
         [&](const ast::Const*) {
@@ -3629,10 +3623,9 @@
                     out << ", ";
                 }
                 if (!wgsize[i].has_value()) {
-                    diagnostics_.AddError(
-                        diag::System::Writer,
-                        "override-expressions should have been removed with the SubstituteOverride "
-                        "transform");
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << "override-expressions should have been removed with the "
+                           "SubstituteOverride transform";
                     return false;
                 }
                 out << std::to_string(wgsize[i].value());
@@ -3784,8 +3777,8 @@
 
             auto count = a->ConstantCount();
             if (!count) {
-                diagnostics_.AddError(diag::System::Writer,
-                                      core::type::Array::kErrExpectedConstantCount);
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << core::type::Array::kErrExpectedConstantCount;
                 return false;
             }
 
@@ -3880,7 +3873,8 @@
                     out << "u";
                     return true;
             }
-            diagnostics_.AddError(diag::System::Writer, "unknown integer literal suffix type");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "unknown integer literal suffix type";
             return false;
         },  //
         TINT_ICE_ON_NO_MATCH);
@@ -4347,8 +4341,8 @@
                 }
                 const auto count = arr->ConstantCount();
                 if (!count) {
-                    diagnostics_.AddError(diag::System::Writer,
-                                          core::type::Array::kErrExpectedConstantCount);
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << core::type::Array::kErrExpectedConstantCount;
                     return false;
                 }
 
@@ -4587,7 +4581,7 @@
             if (auto builtin = attributes.builtin) {
                 auto name = builtin_to_attribute(builtin.value());
                 if (name.empty()) {
-                    diagnostics_.AddError(diag::System::Writer, "unsupported builtin");
+                    diagnostics_.AddError(diag::System::Writer, Source{}) << "unsupported builtin";
                     return false;
                 }
                 post += " : " + name;
@@ -4595,7 +4589,8 @@
             if (auto interpolation = attributes.interpolation) {
                 auto mod = interpolation_to_modifiers(interpolation->type, interpolation->sampling);
                 if (mod.empty()) {
-                    diagnostics_.AddError(diag::System::Writer, "unsupported interpolation");
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << "unsupported interpolation";
                     return false;
                 }
                 pre += mod;
diff --git a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
index 88db758..8348b54 100644
--- a/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/hlsl/writer/ast_printer/ast_printer_test.cc
@@ -35,7 +35,7 @@
 using HlslASTPrinterTest = TestHelper;
 
 TEST_F(HlslASTPrinterTest, InvalidProgram) {
-    Diagnostics().AddError(diag::System::Writer, "make the program invalid");
+    Diagnostics().AddError(diag::System::Writer, Source{}) << "make the program invalid";
     ASSERT_FALSE(IsValid());
     auto program = resolver::Resolve(*this);
     ASSERT_FALSE(program.IsValid());
diff --git a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
index 610fc4b..f266d83 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/num_workgroups_from_uniform.cc
@@ -90,8 +90,8 @@
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc b/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
index b3fa496..04e7b14 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/pixel_local.cc
@@ -453,9 +453,8 @@
     uint32_t ROVRegisterIndex(uint32_t field_index) {
         auto idx = cfg.pls_member_to_rov_reg.Get(field_index);
         if (TINT_UNLIKELY(!idx)) {
-            b.Diagnostics().AddError(diag::System::Transform,
-                                     "PixelLocal::Config::attachments missing entry for field " +
-                                         std::to_string(field_index));
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "PixelLocal::Config::attachments missing entry for field " << field_index;
             return 0;
         }
         return *idx;
@@ -466,9 +465,8 @@
     core::TexelFormat ROVTexelFormat(uint32_t field_index) {
         auto format = cfg.pls_member_to_rov_format.Get(field_index);
         if (TINT_UNLIKELY(!format)) {
-            b.Diagnostics().AddError(diag::System::Transform,
-                                     "PixelLocal::Config::attachments missing entry for field " +
-                                         std::to_string(field_index));
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "PixelLocal::Config::attachments missing entry for field " << field_index;
             return core::TexelFormat::kUndefined;
         }
         return *format;
@@ -485,8 +483,8 @@
     auto* cfg = inputs.Get<Config>();
     if (!cfg) {
         ProgramBuilder b;
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
index d6c1b25..7028093 100644
--- a/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
+++ b/src/tint/lang/hlsl/writer/ast_raise/truncate_interstage_variables.cc
@@ -70,10 +70,9 @@
 
     const auto* data = config.Get<Config>();
     if (data == nullptr) {
-        b.Diagnostics().AddError(
-            diag::System::Transform,
-            "missing transform data for " +
-                std::string(tint::TypeInfo::Of<TruncateInterstageVariables>().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for "
+            << tint::TypeInfo::Of<TruncateInterstageVariables>().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/hlsl/writer/common/option_helpers.cc b/src/tint/lang/hlsl/writer/common/option_helpers.cc
index aace10b..416569d 100644
--- a/src/tint/lang/hlsl/writer/common/option_helpers.cc
+++ b/src/tint/lang/hlsl/writer/common/option_helpers.cc
@@ -54,10 +54,8 @@
                                                          const binding::BindingInfo& dst) -> bool {
         if (auto binding = seen_wgsl_bindings.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate WGSL binding point: " << src;
-
-                diagnostics.AddError(diag::System::Writer, str.str());
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate WGSL binding point: " << src;
                 return true;
             }
         }
@@ -69,9 +67,8 @@
                                     const tint::BindingPoint& dst) -> bool {
         if (auto binding = map.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate MSL binding point: [binding: " << src.binding << "]";
-                diagnostics.AddError(diag::System::Writer, str.str());
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate MSL binding point: [binding: " << src.binding << "]";
                 return true;
             }
         }
@@ -97,27 +94,27 @@
 
     // Storage and uniform are both [[buffer()]]
     if (!valid(seen_hlsl_buffer_bindings, options.bindings.uniform)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing uniform", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing uniform";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_hlsl_buffer_bindings, options.bindings.storage)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage";
         return Failure{std::move(diagnostics)};
     }
 
     // Sampler is [[sampler()]]
     if (!valid(seen_hlsl_sampler_bindings, options.bindings.sampler)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing sampler", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing sampler";
         return Failure{std::move(diagnostics)};
     }
 
     // Texture and storage texture are [[texture()]]
     if (!valid(seen_hlsl_texture_bindings, options.bindings.texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing texture";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_hlsl_texture_bindings, options.bindings.storage_texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage_texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage_texture";
         return Failure{std::move(diagnostics)};
     }
 
@@ -129,22 +126,26 @@
 
         // Validate with the actual source regardless of what the remapper will do
         if (wgsl_seen(src_binding, plane0)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
 
         // Plane0 & Plane1 are [[texture()]]
         if (hlsl_seen(seen_hlsl_texture_bindings, plane0, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         if (hlsl_seen(seen_hlsl_texture_bindings, plane1, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         // Metadata is [[buffer()]]
         if (hlsl_seen(seen_hlsl_buffer_bindings, metadata, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
     }
diff --git a/src/tint/lang/msl/intrinsic/data.cc b/src/tint/lang/msl/intrinsic/data.cc
index 261ee51..c4f7487 100644
--- a/src/tint/lang/msl/intrinsic/data.cc
+++ b/src/tint/lang/msl/intrinsic/data.cc
@@ -82,8 +82,8 @@
     }
     return BuildU32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "u32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "u32";
   }
 };
 
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
index 5a3a574..c2442f5 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer.cc
@@ -305,9 +305,9 @@
             },
             [&](const ast::Override*) {
                 // Override is removed with SubstituteOverride
-                diagnostics_.AddError(diag::System::Writer,
-                                      "override-expressions should have been removed with the "
-                                      "SubstituteOverride transform.");
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << "override-expressions should have been removed with the "
+                       "SubstituteOverride transform.";
                 return false;
             },
             [&](const ast::Function* func) {
@@ -364,7 +364,8 @@
             return false;
         }
     } else {
-        diagnostics_.AddError(diag::System::Writer, "unknown alias type: " + ty->FriendlyName());
+        diagnostics_.AddError(diag::System::Writer, Source{})
+            << "unknown alias type: " << ty->FriendlyName();
         return false;
     }
 
@@ -1063,7 +1064,8 @@
             std::vector<const char*> dims;
             switch (texture_type->dim()) {
                 case core::type::TextureDimension::kNone:
-                    diagnostics_.AddError(diag::System::Writer, "texture dimension is kNone");
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << "texture dimension is kNone";
                     return false;
                 case core::type::TextureDimension::k1d:
                     dims = {"width"};
@@ -1262,9 +1264,8 @@
                 out << "gradientcube(";
                 break;
             default: {
-                StringStream err;
-                err << "MSL does not support gradients for " << dim << " textures";
-                diagnostics_.AddError(diag::System::Writer, err.str());
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << "MSL does not support gradients for " << dim << " textures";
                 return false;
             }
         }
@@ -1620,15 +1621,13 @@
             out += "unpack_unorm2x16_to_float";
             break;
         case wgsl::BuiltinFn::kArrayLength:
-            diagnostics_.AddError(
-                diag::System::Writer,
-                "Unable to translate builtin: " + std::string(builtin->str()) +
-                    "\nDid you forget to pass array_length_from_uniform generator "
-                    "options?");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "Unable to translate builtin: " << builtin->Fn()
+                << "\nDid you forget to pass array_length_from_uniform generator options?";
             return "";
         default:
-            diagnostics_.AddError(diag::System::Writer,
-                                  "Unknown import method: " + std::string(builtin->str()));
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "Unknown import method: " << builtin->Fn();
             return "";
     }
     return out;
@@ -1803,8 +1802,8 @@
 
             auto count = a->ConstantCount();
             if (!count) {
-                diagnostics_.AddError(diag::System::Writer,
-                                      core::type::Array::kErrExpectedConstantCount);
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << core::type::Array::kErrExpectedConstantCount;
                 return false;
             }
 
@@ -1874,7 +1873,8 @@
                     return true;
                 }
             }
-            diagnostics_.AddError(diag::System::Writer, "unknown integer literal suffix type");
+            diagnostics_.AddError(diag::System::Writer, Source{})
+                << "unknown integer literal suffix type";
             return false;
         },  //
         TINT_ICE_ON_NO_MATCH);
@@ -2069,7 +2069,8 @@
 
                         auto name = BuiltinToAttribute(builtin);
                         if (name.empty()) {
-                            diagnostics_.AddError(diag::System::Writer, "unknown builtin");
+                            diagnostics_.AddError(diag::System::Writer, Source{})
+                                << "unknown builtin";
                             return false;
                         }
                         out << " [[" << name << "]]";
@@ -2526,8 +2527,8 @@
             } else {
                 auto count = arr->ConstantCount();
                 if (!count) {
-                    diagnostics_.AddError(diag::System::Writer,
-                                          core::type::Array::kErrExpectedConstantCount);
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << core::type::Array::kErrExpectedConstantCount;
                     return false;
                 }
 
@@ -2622,7 +2623,8 @@
                     out << "cube_array";
                     break;
                 default:
-                    diagnostics_.AddError(diag::System::Writer, "Invalid texture dimensions");
+                    diagnostics_.AddError(diag::System::Writer, Source{})
+                        << "Invalid texture dimensions";
                     return false;
             }
             if (tex->IsAnyOf<core::type::MultisampledTexture,
@@ -2655,8 +2657,8 @@
                     } else if (storage->access() == core::Access::kWrite) {
                         out << ", access::write";
                     } else {
-                        diagnostics_.AddError(diag::System::Writer,
-                                              "Invalid access control for storage texture");
+                        diagnostics_.AddError(diag::System::Writer, Source{})
+                            << "Invalid access control for storage texture";
                         return false;
                     }
                     return true;
@@ -2797,7 +2799,7 @@
         if (auto builtin = attributes.builtin) {
             auto name = BuiltinToAttribute(builtin.value());
             if (name.empty()) {
-                diagnostics_.AddError(diag::System::Writer, "unknown builtin");
+                diagnostics_.AddError(diag::System::Writer, Source{}) << "unknown builtin";
                 return false;
             }
             out << " [[" << name << "]]";
@@ -2839,7 +2841,8 @@
         if (auto interpolation = attributes.interpolation) {
             auto name = InterpolationToAttribute(interpolation->type, interpolation->sampling);
             if (name.empty()) {
-                diagnostics_.AddError(diag::System::Writer, "unknown interpolation attribute");
+                diagnostics_.AddError(diag::System::Writer, Source{})
+                    << "unknown interpolation attribute";
                 return false;
             }
             out << " [[" << name << "]]";
diff --git a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
index ab858e3..a78f3ee 100644
--- a/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/msl/writer/ast_printer/ast_printer_test.cc
@@ -39,7 +39,7 @@
 using MslASTPrinterTest = TestHelper;
 
 TEST_F(MslASTPrinterTest, InvalidProgram) {
-    Diagnostics().AddError(diag::System::Writer, "make the program invalid");
+    Diagnostics().AddError(diag::System::Writer, Source{}) << "make the program invalid";
     ASSERT_FALSE(IsValid());
     auto program = resolver::Resolve(*this);
     ASSERT_FALSE(program.IsValid());
diff --git a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
index 140b209..0084d30 100644
--- a/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
+++ b/src/tint/lang/msl/writer/ast_raise/module_scope_var_to_entry_point_param.cc
@@ -244,9 +244,8 @@
             case core::AddressSpace::kWorkgroup:
                 break;
             case core::AddressSpace::kPushConstant: {
-                ctx.dst->Diagnostics().AddError(
-                    diag::System::Transform,
-                    "unhandled module-scope address space (" + tint::ToString(sc) + ")");
+                ctx.dst->Diagnostics().AddError(diag::System::Transform, Source{})
+                    << "unhandled module-scope address space (" << sc << ")";
                 break;
             }
             default: {
diff --git a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
index 2f2a216..c6cc55a 100644
--- a/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
+++ b/src/tint/lang/msl/writer/ast_raise/pixel_local.cc
@@ -257,9 +257,8 @@
     uint32_t AttachmentIndex(uint32_t field_index) {
         auto idx = cfg.attachments.Get(field_index);
         if (TINT_UNLIKELY(!idx)) {
-            b.Diagnostics().AddError(diag::System::Transform,
-                                     "PixelLocal::Config::attachments missing entry for field " +
-                                         std::to_string(field_index));
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "PixelLocal::Config::attachments missing entry for field " << field_index;
             return 0;
         }
         return *idx;
@@ -276,8 +275,8 @@
     auto* cfg = inputs.Get<Config>();
     if (!cfg) {
         ProgramBuilder b;
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/msl/writer/common/option_helpers.cc b/src/tint/lang/msl/writer/common/option_helpers.cc
index d72d984..cb6a162 100644
--- a/src/tint/lang/msl/writer/common/option_helpers.cc
+++ b/src/tint/lang/msl/writer/common/option_helpers.cc
@@ -54,10 +54,8 @@
                                                          const binding::BindingInfo& dst) -> bool {
         if (auto binding = seen_wgsl_bindings.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate WGSL binding point: " << src;
-
-                diagnostics.AddError(diag::System::Writer, str.str());
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate WGSL binding point: " << src;
                 return true;
             }
         }
@@ -69,9 +67,8 @@
                                    const tint::BindingPoint& dst) -> bool {
         if (auto binding = map.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate MSL binding point: [binding: " << src.binding << "]";
-                diagnostics.AddError(diag::System::Writer, str.str());
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate MSL binding point: [binding: " << src.binding << "]";
                 return true;
             }
         }
@@ -97,27 +94,27 @@
 
     // Storage and uniform are both [[buffer()]]
     if (!valid(seen_msl_buffer_bindings, options.bindings.uniform)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing uniform", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing uniform";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_msl_buffer_bindings, options.bindings.storage)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage";
         return Failure{std::move(diagnostics)};
     }
 
     // Sampler is [[sampler()]]
     if (!valid(seen_msl_sampler_bindings, options.bindings.sampler)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing sampler", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing sampler";
         return Failure{std::move(diagnostics)};
     }
 
     // Texture and storage texture are [[texture()]]
     if (!valid(seen_msl_texture_bindings, options.bindings.texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing texture";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(seen_msl_texture_bindings, options.bindings.storage_texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage_texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage_texture";
         return Failure{std::move(diagnostics)};
     }
 
@@ -129,22 +126,26 @@
 
         // Validate with the actual source regardless of what the remapper will do
         if (wgsl_seen(src_binding, plane0)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
 
         // Plane0 & Plane1 are [[texture()]]
         if (msl_seen(seen_msl_texture_bindings, plane0, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         if (msl_seen(seen_msl_texture_bindings, plane1, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         // Metadata is [[buffer()]]
         if (msl_seen(seen_msl_buffer_bindings, metadata, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
     }
diff --git a/src/tint/lang/spirv/intrinsic/data.cc b/src/tint/lang/spirv/intrinsic/data.cc
index 110a151..691263a 100644
--- a/src/tint/lang/spirv/intrinsic/data.cc
+++ b/src/tint/lang/spirv/intrinsic/data.cc
@@ -83,8 +83,8 @@
     }
     return BuildBool(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bool";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "bool";
   }
 };
 
@@ -97,8 +97,8 @@
     }
     return BuildF32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f32";
   }
 };
 
@@ -111,8 +111,8 @@
     }
     return BuildF16(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f16";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f16";
   }
 };
 
@@ -125,8 +125,8 @@
     }
     return BuildI32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "i32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "i32";
   }
 };
 
@@ -139,8 +139,8 @@
     }
     return BuildU32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "u32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "u32";
   }
 };
 
@@ -158,9 +158,9 @@
     }
     return BuildVec2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -178,9 +178,9 @@
     }
     return BuildVec3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -198,9 +198,9 @@
     }
     return BuildVec4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -218,9 +218,9 @@
     }
     return BuildMat2X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -238,9 +238,9 @@
     }
     return BuildMat2X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -258,9 +258,9 @@
     }
     return BuildMat2X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -278,9 +278,9 @@
     }
     return BuildMat3X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -298,9 +298,9 @@
     }
     return BuildMat3X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -318,9 +318,9 @@
     }
     return BuildMat3X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -338,9 +338,9 @@
     }
     return BuildMat4X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -358,9 +358,9 @@
     }
     return BuildMat4X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -378,9 +378,9 @@
     }
     return BuildMat4X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -403,12 +403,10 @@
     }
     return BuildVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "vec" << N << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "vec" << style::Type << N << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -436,13 +434,11 @@
     }
     return BuildMat(state, ty, N, M, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string M = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "mat" << N << "x" << M << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText M;
+  state->PrintNum(M);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "mat" << style::Type << N << style::Type << "x" << style::Type << M << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -460,9 +456,9 @@
     }
     return BuildAtomic(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "atomic<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "atomic" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -475,8 +471,8 @@
     }
     return BuildSampler(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler";
   }
 };
 
@@ -489,8 +485,8 @@
     }
     return BuildSamplerComparison(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler_comparison";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler_comparison";
   }
 };
 
@@ -508,9 +504,9 @@
     }
     return BuildTexture1D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_1d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_1d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -528,9 +524,9 @@
     }
     return BuildTexture2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -548,9 +544,9 @@
     }
     return BuildTexture2DArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -568,9 +564,9 @@
     }
     return BuildTexture3D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_3d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_3d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -588,9 +584,9 @@
     }
     return BuildTextureCube(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -608,9 +604,9 @@
     }
     return BuildTextureCubeArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -628,9 +624,9 @@
     }
     return BuildTextureMultisampled2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_multisampled_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_multisampled_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -643,8 +639,8 @@
     }
     return BuildTextureDepth2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d";
   }
 };
 
@@ -657,8 +653,8 @@
     }
     return BuildTextureDepth2DArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d_array";
   }
 };
 
@@ -671,8 +667,8 @@
     }
     return BuildTextureDepthCube(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube";
   }
 };
 
@@ -685,8 +681,8 @@
     }
     return BuildTextureDepthCubeArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube_array";
   }
 };
 
@@ -699,8 +695,8 @@
     }
     return BuildTextureDepthMultisampled2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_multisampled_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_multisampled_2d";
   }
 };
 
@@ -723,10 +719,10 @@
     }
     return BuildTextureStorage1D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_1d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_1d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -749,10 +745,10 @@
     }
     return BuildTextureStorage2D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -775,10 +771,10 @@
     }
     return BuildTextureStorage2DArray(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d_array<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d_array" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -801,10 +797,10 @@
     }
     return BuildTextureStorage3D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_3d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_3d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -832,11 +828,11 @@
     }
     return BuildPtr(state, ty, S, T, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string S = state->NumName();
-  const std::string T = state->TypeName();
-  const std::string A = state->NumName();
-    return "ptr<" + S + ", " + T + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText S;
+  state->PrintNum(S);StyledText T;
+  state->PrintType(T);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "ptr" << style::Code << "<" << style::Type << S << style::Code << ", " << style::Type << T << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -849,8 +845,8 @@
     }
     return BuildStructWithRuntimeArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "struct_with_runtime_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "struct_with_runtime_array";
   }
 };
 
@@ -868,9 +864,9 @@
     }
     return BuildSampledImage(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "sampled_image<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "sampled_image" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -886,13 +882,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match iu32'
@@ -906,13 +899,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fiu32'
@@ -929,13 +919,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar'
@@ -958,13 +945,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match samplers'
@@ -978,13 +962,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kSamplerMatcher.string(nullptr) << " or " << kSamplerComparisonMatcher.string(nullptr);
-    return ss.str();
-  }
+ kSamplerMatcher.print(nullptr, out); out << TextStyle{} << " or "; kSamplerComparisonMatcher.print(nullptr, out);}
 };
 
 /// EnumMatcher for 'match read_write'
@@ -995,8 +976,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read_write";
   }
 };
 
@@ -1008,8 +989,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "storage";
   }
 };
 
@@ -1024,8 +1005,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "workgroup or storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "workgroup"<< TextStyle{} << " or " << style::Enum << "storage";
   }
 };
 
@@ -1045,8 +1026,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bgra8unorm, rgba8unorm, rgba8snorm, rgba16float, r32float, rg32float or rgba32float";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "bgra8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8snorm"<< TextStyle{} << ", " << style::Enum << "rgba16float"<< TextStyle{} << ", " << style::Enum << "r32float"<< TextStyle{} << ", " << style::Enum << "rg32float"<< TextStyle{} << " or " << style::Enum << "rgba32float";
   }
 };
 
@@ -1064,8 +1045,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8sint, rgba16sint, r32sint, rg32sint or rgba32sint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8sint"<< TextStyle{} << ", " << style::Enum << "rgba16sint"<< TextStyle{} << ", " << style::Enum << "r32sint"<< TextStyle{} << ", " << style::Enum << "rg32sint"<< TextStyle{} << " or " << style::Enum << "rgba32sint";
   }
 };
 
@@ -1083,8 +1064,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8uint, rgba16uint, r32uint, rg32uint or rgba32uint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8uint"<< TextStyle{} << ", " << style::Enum << "rgba16uint"<< TextStyle{} << ", " << style::Enum << "r32uint"<< TextStyle{} << ", " << style::Enum << "rg32uint"<< TextStyle{} << " or " << style::Enum << "rgba32uint";
   }
 };
 
@@ -1099,8 +1080,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
@@ -1115,8 +1096,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "write or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "write"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
diff --git a/src/tint/lang/spirv/reader/ast_lower/atomics.cc b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
index 06e9d43..181ab19 100644
--- a/src/tint/lang/spirv/reader/ast_lower/atomics.cc
+++ b/src/tint/lang/spirv/reader/ast_lower/atomics.cc
@@ -224,10 +224,9 @@
                 }
                 auto count = arr->ConstantCount();
                 if (!count) {
-                    ctx.dst->Diagnostics().AddError(
-                        diag::System::Transform,
-                        "the Atomics transform does not currently support array counts that "
-                        "use override values");
+                    ctx.dst->Diagnostics().AddError(diag::System::Transform, Source{})
+                        << "the Atomics transform does not currently support array counts that use "
+                           "override values";
                     count = 1;
                 }
                 return b.ty.array(AtomicTypeFor(arr->ElemType()), u32(count.value()));
diff --git a/src/tint/lang/spirv/reader/ast_parser/barrier_test.cc b/src/tint/lang/spirv/reader/ast_parser/barrier_test.cc
index d106222..7fa6c89 100644
--- a/src/tint/lang/spirv/reader/ast_parser/barrier_test.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/barrier_test.cc
@@ -51,7 +51,7 @@
     auto p = std::make_unique<ASTParser>(test::Assemble(preamble + spirv));
     if (!p->BuildAndParseInternalModule()) {
         ProgramBuilder builder;
-        builder.Diagnostics().AddError(diag::System::Reader, p->error());
+        builder.Diagnostics().AddError(diag::System::Reader, Source{}) << p->error();
         return Program(std::move(builder));
     }
     return p->Program();
diff --git a/src/tint/lang/spirv/reader/ast_parser/parse.cc b/src/tint/lang/spirv/reader/ast_parser/parse.cc
index 988fc08..c3dea0e 100644
--- a/src/tint/lang/spirv/reader/ast_parser/parse.cc
+++ b/src/tint/lang/spirv/reader/ast_parser/parse.cc
@@ -83,7 +83,7 @@
     ProgramBuilder& builder = parser.builder();
     if (!parsed) {
         // TODO(bclayton): Migrate ASTParser to using diagnostics.
-        builder.Diagnostics().AddError(diag::System::Reader, parser.error());
+        builder.Diagnostics().AddError(diag::System::Reader, Source{}) << parser.error();
         return Program(std::move(builder));
     }
 
diff --git a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
index ad5f484..e76c3c2 100644
--- a/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
+++ b/src/tint/lang/spirv/writer/ast_printer/ast_printer_test.cc
@@ -34,7 +34,7 @@
 using SpirvASTPrinterTest = TestHelper;
 
 TEST_F(SpirvASTPrinterTest, InvalidProgram) {
-    Diagnostics().AddError(diag::System::Writer, "make the program invalid");
+    Diagnostics().AddError(diag::System::Writer, Source{}) << "make the program invalid";
     ASSERT_FALSE(IsValid());
     auto program = resolver::Resolve(*this);
     ASSERT_FALSE(program.IsValid());
diff --git a/src/tint/lang/spirv/writer/common/option_helper.cc b/src/tint/lang/spirv/writer/common/option_helper.cc
index 37f2377..76a21ed 100644
--- a/src/tint/lang/spirv/writer/common/option_helper.cc
+++ b/src/tint/lang/spirv/writer/common/option_helper.cc
@@ -48,10 +48,8 @@
                                                          const binding::BindingInfo& dst) -> bool {
         if (auto binding = seen_wgsl_bindings.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate WGSL binding point: " << src;
-
-                diagnostics.AddError(diag::System::Writer, str.str());
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate WGSL binding point: " << src;
                 return true;
             }
         }
@@ -63,10 +61,9 @@
                                                            const tint::BindingPoint& dst) -> bool {
         if (auto binding = seen_spirv_bindings.Get(src)) {
             if (*binding != dst) {
-                std::stringstream str;
-                str << "found duplicate SPIR-V binding point: [group: " << src.group
+                diagnostics.AddError(diag::System::Writer, Source{})
+                    << "found duplicate SPIR-V binding point: [group: " << src.group
                     << ", binding: " << src.binding << "]";
-                diagnostics.AddError(diag::System::Writer, str.str());
                 return true;
             }
         }
@@ -91,23 +88,23 @@
     };
 
     if (!valid(options.bindings.uniform)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing uniform", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing uniform";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.storage)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing texture";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.storage_texture)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing storage_texture", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing storage_texture";
         return Failure{std::move(diagnostics)};
     }
     if (!valid(options.bindings.sampler)) {
-        diagnostics.AddNote(diag::System::Writer, "when processing sampler", {});
+        diagnostics.AddNote(diag::System::Writer, Source{}) << "when processing sampler";
         return Failure{std::move(diagnostics)};
     }
 
@@ -119,20 +116,24 @@
 
         // Validate with the actual source regardless of what the remapper will do
         if (wgsl_seen(src_binding, plane0)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
 
         if (spirv_seen(plane0, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         if (spirv_seen(plane1, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
         if (spirv_seen(metadata, src_binding)) {
-            diagnostics.AddNote(diag::System::Writer, "when processing external_texture", {});
+            diagnostics.AddNote(diag::System::Writer, Source{})
+                << "when processing external_texture";
             return Failure{std::move(diagnostics)};
         }
     }
diff --git a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
index fa6bc9e..bdb43cf 100644
--- a/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
+++ b/src/tint/lang/wgsl/ast/transform/array_length_from_uniform.cc
@@ -82,10 +82,9 @@
     ApplyResult Run() {
         auto* cfg = inputs.Get<Config>();
         if (cfg == nullptr) {
-            b.Diagnostics().AddError(
-                diag::System::Transform,
-                "missing transform data for " +
-                    std::string(tint::TypeInfo::Of<ArrayLengthFromUniform>().name));
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "missing transform data for "
+                << tint::TypeInfo::Of<ArrayLengthFromUniform>().name;
             return resolver::Resolve(b);
         }
 
diff --git a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
index c3c8cbe..31ef518 100644
--- a/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
+++ b/src/tint/lang/wgsl/ast/transform/binding_remapper.cc
@@ -66,8 +66,8 @@
 
     auto* remappings = inputs.Get<Remappings>();
     if (!remappings) {
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
@@ -112,18 +112,15 @@
             if (ac_it != remappings->access_controls.end()) {
                 core::Access access = ac_it->second;
                 if (access == core::Access::kUndefined) {
-                    b.Diagnostics().AddError(diag::System::Transform,
-                                             "invalid access mode (" +
-                                                 std::to_string(static_cast<uint32_t>(access)) +
-                                                 ")");
+                    b.Diagnostics().AddError(diag::System::Transform, Source{})
+                        << "invalid access mode (" << static_cast<uint32_t>(access) << ")";
                     return resolver::Resolve(b);
                 }
                 auto* sem = src.Sem().Get(var);
                 if (sem->AddressSpace() != core::AddressSpace::kStorage) {
-                    b.Diagnostics().AddError(
-                        diag::System::Transform,
-                        "cannot apply access control to variable with address space " +
-                            std::string(tint::ToString(sem->AddressSpace())));
+                    b.Diagnostics().AddError(diag::System::Transform, Source{})
+                        << "cannot apply access control to variable with address space "
+                        << sem->AddressSpace();
                     return resolver::Resolve(b);
                 }
                 auto* ty = sem->Type()->UnwrapRef();
diff --git a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
index db9a682..3e2029b 100644
--- a/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/lang/wgsl/ast/transform/canonicalize_entry_point_io.cc
@@ -979,8 +979,8 @@
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
index 815a3ed..6cbaec6 100644
--- a/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
+++ b/src/tint/lang/wgsl/ast/transform/multiplanar_external_texture.cc
@@ -135,10 +135,9 @@
 
             BindingsMap::const_iterator it = new_binding_points->bindings_map.find(bp);
             if (it == new_binding_points->bindings_map.end()) {
-                b.Diagnostics().AddError(
-                    diag::System::Transform,
-                    "missing new binding points for texture_external at binding {" +
-                        std::to_string(bp.group) + "," + std::to_string(bp.binding) + "}");
+                b.Diagnostics().AddError(diag::System::Transform, Source{})
+                    << "missing new binding points for texture_external at binding {" << bp.group
+                    << "," << bp.binding << "}";
                 continue;
             }
 
@@ -552,8 +551,8 @@
     ProgramBuilder b;
     program::CloneContext ctx{&b, &src, /* auto_clone_symbols */ true};
     if (!new_binding_points) {
-        b.Diagnostics().AddError(diag::System::Transform, "missing new binding point data for " +
-                                                              std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing new binding point data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/push_constant_helper.cc b/src/tint/lang/wgsl/ast/transform/push_constant_helper.cc
index af6fe57..ba10cf4 100644
--- a/src/tint/lang/wgsl/ast/transform/push_constant_helper.cc
+++ b/src/tint/lang/wgsl/ast/transform/push_constant_helper.cc
@@ -64,7 +64,8 @@
 void PushConstantHelper::InsertMember(const char* name, ast::Type type, uint32_t offset) {
     auto& member = member_map[offset];
     if (TINT_UNLIKELY(member != nullptr)) {
-        ctx.dst->Diagnostics().AddError(diag::System::Transform, "struct member offset collision");
+        ctx.dst->Diagnostics().AddError(diag::System::Transform, Source{})
+            << "struct member offset collision";
     }
     member = ctx.dst->Member(name, type, Vector{ctx.dst->MemberOffset(core::AInt(offset))});
 }
diff --git a/src/tint/lang/wgsl/ast/transform/robustness.cc b/src/tint/lang/wgsl/ast/transform/robustness.cc
index c3f9c57..42273c5 100644
--- a/src/tint/lang/wgsl/ast/transform/robustness.cc
+++ b/src/tint/lang/wgsl/ast/transform/robustness.cc
@@ -272,8 +272,8 @@
                 }
                 // Note: Don't be tempted to use the array override variable as an expression here,
                 // the name might be shadowed!
-                b.Diagnostics().AddError(diag::System::Transform,
-                                         core::type::Array::kErrExpectedConstantCount);
+                b.Diagnostics().AddError(diag::System::Transform, Source{})
+                    << core::type::Array::kErrExpectedConstantCount;
                 return nullptr;
             },  //
             TINT_ICE_ON_NO_MATCH);
diff --git a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
index 78843cc..a0952eb 100644
--- a/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
+++ b/src/tint/lang/wgsl/ast/transform/single_entry_point.cc
@@ -55,8 +55,8 @@
 
     auto* cfg = inputs.Get<Config>();
     if (cfg == nullptr) {
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "missing transform data for " + std::string(TypeInfo().name));
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "missing transform data for " << TypeInfo().name;
         return resolver::Resolve(b);
     }
 
@@ -72,8 +72,8 @@
         }
     }
     if (entry_point == nullptr) {
-        b.Diagnostics().AddError(diag::System::Transform,
-                                 "entry point '" + cfg->entry_point_name + "' not found");
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "entry point '" << cfg->entry_point_name << "' not found";
         return resolver::Resolve(b);
     }
 
diff --git a/src/tint/lang/wgsl/ast/transform/substitute_override.cc b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
index 74d8b7b..4711b36 100644
--- a/src/tint/lang/wgsl/ast/transform/substitute_override.cc
+++ b/src/tint/lang/wgsl/ast/transform/substitute_override.cc
@@ -71,7 +71,8 @@
 
     const auto* data = config.Get<Config>();
     if (!data) {
-        b.Diagnostics().AddError(diag::System::Transform, "Missing override substitution data");
+        b.Diagnostics().AddError(diag::System::Transform, Source{})
+            << "Missing override substitution data";
         return resolver::Resolve(b);
     }
 
@@ -90,9 +91,8 @@
         auto iter = data->map.find(sem->Attributes().override_id.value());
         if (iter == data->map.end()) {
             if (!w->initializer) {
-                b.Diagnostics().AddError(
-                    diag::System::Transform,
-                    "Initializer not provided for override, and override not overridden.");
+                b.Diagnostics().AddError(diag::System::Transform, Source{})
+                    << "Initializer not provided for override, and override not overridden.";
                 return nullptr;
             }
             return b.Const(source, sym, ty, ctx.Clone(w->initializer));
@@ -108,8 +108,8 @@
             [&](const core::type::F16*) { return b.Expr(f16(value)); });
 
         if (!ctor) {
-            b.Diagnostics().AddError(diag::System::Transform,
-                                     "Failed to create override-expression");
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "Failed to create override-expression";
             return nullptr;
         }
 
diff --git a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
index 31c2c29..8eae5a6 100644
--- a/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
+++ b/src/tint/lang/wgsl/ast/transform/vertex_pulling.cc
@@ -72,11 +72,11 @@
     kFloat,  // unsigned normalized, signed normalized, and float
 };
 
-/// Writes the VertexFormat to the stream.
+/// Writes the VertexFormat to the diagnostic.
 /// @param out the stream to write to
 /// @param format the VertexFormat to write
 /// @returns out so calls can be chained
-StringStream& operator<<(StringStream& out, VertexFormat format) {
+diag::Diagnostic& operator<<(diag::Diagnostic& out, VertexFormat format) {
     switch (format) {
         case VertexFormat::kUint8x2:
             return out << "uint8x2";
@@ -260,16 +260,16 @@
         for (auto* fn : src.AST().Functions()) {
             if (fn->PipelineStage() == PipelineStage::kVertex) {
                 if (func != nullptr) {
-                    b.Diagnostics().AddError(
-                        diag::System::Transform,
-                        "VertexPulling found more than one vertex entry point");
+                    b.Diagnostics().AddError(diag::System::Transform, Source{})
+                        << "VertexPulling found more than one vertex entry point";
                     return resolver::Resolve(b);
                 }
                 func = fn;
             }
         }
         if (func == nullptr) {
-            b.Diagnostics().AddError(diag::System::Transform, "Vertex stage entry point not found");
+            b.Diagnostics().AddError(diag::System::Transform, Source{})
+                << "Vertex stage entry point not found";
             return resolver::Resolve(b);
         }
 
@@ -357,12 +357,10 @@
             const VertexBufferLayoutDescriptor& buffer_layout = cfg.vertex_state[buffer_idx];
 
             if ((buffer_layout.array_stride & 3) != 0) {
-                b.Diagnostics().AddError(
-                    diag::System::Transform,
-                    "WebGPU requires that vertex stride must be a multiple of 4 bytes, "
-                    "but VertexPulling array stride for buffer " +
-                        std::to_string(buffer_idx) + " was " +
-                        std::to_string(buffer_layout.array_stride) + " bytes");
+                b.Diagnostics().AddError(diag::System::Transform, Source{})
+                    << "WebGPU requires that vertex stride must be a multiple of 4 bytes, "
+                       "but VertexPulling array stride for buffer "
+                    << buffer_idx << " was " << buffer_layout.array_stride << " bytes";
                 return nullptr;
             }
 
@@ -397,12 +395,10 @@
 
                 // Base types must match between the vertex stream and the WGSL variable
                 if (!IsTypeCompatible(var_dt, fmt_dt)) {
-                    StringStream err;
-                    err << "VertexAttributeDescriptor for location "
-                        << std::to_string(attribute_desc.shader_location) << " has format "
-                        << attribute_desc.format << " but shader expects "
-                        << var.type->FriendlyName();
-                    b.Diagnostics().AddError(diag::System::Transform, err.str());
+                    b.Diagnostics().AddError(diag::System::Transform, Source{})
+                        << "VertexAttributeDescriptor for location "
+                        << attribute_desc.shader_location << " has format " << attribute_desc.format
+                        << " but shader expects " << var.type->FriendlyName();
                     return nullptr;
                 }
 
diff --git a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
index e394520..d0e7f74 100644
--- a/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
+++ b/src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.cc
@@ -381,8 +381,8 @@
                 //      `(idx % modulo) / division`
                 auto count = arr->ConstantCount();
                 if (!count) {
-                    ctx.dst->Diagnostics().AddError(diag::System::Transform,
-                                                    core::type::Array::kErrExpectedConstantCount);
+                    ctx.dst->Diagnostics().AddError(diag::System::Transform, Source{})
+                        << core::type::Array::kErrExpectedConstantCount;
                     return Expression{};  // error
                 }
                 auto modulo = num_values * count.value();
diff --git a/src/tint/lang/wgsl/extension.cc b/src/tint/lang/wgsl/extension.cc
index 3069ccf..1240173 100644
--- a/src/tint/lang/wgsl/extension.cc
+++ b/src/tint/lang/wgsl/extension.cc
@@ -60,6 +60,9 @@
     if (str == "chromium_internal_dual_source_blending") {
         return Extension::kChromiumInternalDualSourceBlending;
     }
+    if (str == "chromium_internal_graphite") {
+        return Extension::kChromiumInternalGraphite;
+    }
     if (str == "chromium_internal_relaxed_uniform_layout") {
         return Extension::kChromiumInternalRelaxedUniformLayout;
     }
@@ -85,6 +88,8 @@
             return "chromium_experimental_subgroups";
         case Extension::kChromiumInternalDualSourceBlending:
             return "chromium_internal_dual_source_blending";
+        case Extension::kChromiumInternalGraphite:
+            return "chromium_internal_graphite";
         case Extension::kChromiumInternalRelaxedUniformLayout:
             return "chromium_internal_relaxed_uniform_layout";
         case Extension::kF16:
diff --git a/src/tint/lang/wgsl/extension.h b/src/tint/lang/wgsl/extension.h
index 7de7e82..3eccedf 100644
--- a/src/tint/lang/wgsl/extension.h
+++ b/src/tint/lang/wgsl/extension.h
@@ -52,6 +52,7 @@
     kChromiumExperimentalPushConstant,
     kChromiumExperimentalSubgroups,
     kChromiumInternalDualSourceBlending,
+    kChromiumInternalGraphite,
     kChromiumInternalRelaxedUniformLayout,
     kF16,
 };
@@ -74,10 +75,15 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr std::string_view kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",     "chromium_experimental_framebuffer_fetch",
-    "chromium_experimental_pixel_local",        "chromium_experimental_push_constant",
-    "chromium_experimental_subgroups",          "chromium_internal_dual_source_blending",
-    "chromium_internal_relaxed_uniform_layout", "f16",
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_framebuffer_fetch",
+    "chromium_experimental_pixel_local",
+    "chromium_experimental_push_constant",
+    "chromium_experimental_subgroups",
+    "chromium_internal_dual_source_blending",
+    "chromium_internal_graphite",
+    "chromium_internal_relaxed_uniform_layout",
+    "f16",
 };
 
 /// All extensions
@@ -88,6 +94,7 @@
     Extension::kChromiumExperimentalPushConstant,
     Extension::kChromiumExperimentalSubgroups,
     Extension::kChromiumInternalDualSourceBlending,
+    Extension::kChromiumInternalGraphite,
     Extension::kChromiumInternalRelaxedUniformLayout,
     Extension::kF16,
 };
diff --git a/src/tint/lang/wgsl/extension_bench.cc b/src/tint/lang/wgsl/extension_bench.cc
index 70c0d73..c2128d3 100644
--- a/src/tint/lang/wgsl/extension_bench.cc
+++ b/src/tint/lang/wgsl/extension_bench.cc
@@ -87,20 +87,27 @@
         "chromium_internakk_ualsourc_blendHng",
         "chromium_inRRrnal_dujl_sourceblgnding",
         "chromiuminternal_duab_source_blendin",
-        "chromium_internal_relaxed_uniform_lajout",
-        "chromium_internal_relxed_uniform_layout",
-        "chroium_inqernal_rlaxed_uniform_layout",
+        "chromiumjinternal_graphite",
+        "chromium_inernal_graphite",
+        "cromiu_internaq_graphite",
+        "chromium_internal_graphite",
+        "chromium_intenalNNgraphite",
+        "chromiuminternal_gvaphite",
+        "chromium_internal_grphitQQ",
+        "chromirm_intenal_rfflaxed_unifrm_layout",
+        "chromium_internal_jelaxed_uniform_layout",
+        "chromium_interna_relNNxed_uwwiform_lay82t",
         "chromium_internal_relaxed_uniform_layout",
-        "chromium_internNNl_relaxed_uniform_layou",
-        "chromium_internal_relaxvvd_unifom_laout",
-        "chromium_internalrelaxed_uniQQorm_layout",
-        "ff",
-        "fj6",
-        "wNN2",
+        "chromium_internal_relaxed_uniform_layut",
+        "chromium_internal_relaxed_rrniform_layout",
+        "chromium_internal_relaxedGuniform_layout",
+        "FF16",
+        "",
+        "rr1",
         "f16",
-        "f6",
-        "rr16",
-        "fG6",
+        "1",
+        "DJ1",
+        "",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/lang/wgsl/extension_test.cc b/src/tint/lang/wgsl/extension_test.cc
index 8bcbd0c..11a75b0 100644
--- a/src/tint/lang/wgsl/extension_test.cc
+++ b/src/tint/lang/wgsl/extension_test.cc
@@ -63,6 +63,7 @@
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
     {"chromium_experimental_subgroups", Extension::kChromiumExperimentalSubgroups},
     {"chromium_internal_dual_source_blending", Extension::kChromiumInternalDualSourceBlending},
+    {"chromium_internal_graphite", Extension::kChromiumInternalGraphite},
     {"chromium_internal_relaxed_uniform_layout", Extension::kChromiumInternalRelaxedUniformLayout},
     {"f16", Extension::kF16},
 };
@@ -86,12 +87,15 @@
     {"chromium_internal_dual_soErce_blending", Extension::kUndefined},
     {"chromiuPP_internal_dual_sourceblenTTing", Extension::kUndefined},
     {"chromim_internadd_dual_sxxurce_blending", Extension::kUndefined},
-    {"chromium_interna44_relaxed_uniform_layout", Extension::kUndefined},
-    {"chromium_internal_relaxed_uniformSSlayouVV", Extension::kUndefined},
-    {"chromiumRnteRnal_re22axed_uniform_layout", Extension::kUndefined},
-    {"96", Extension::kUndefined},
-    {"f1", Extension::kUndefined},
-    {"VOR6", Extension::kUndefined},
+    {"chromi44m_internal_graphite", Extension::kUndefined},
+    {"chromSSuVV_internal_graphite", Extension::kUndefined},
+    {"cRromium_nternR22_graphite", Extension::kUndefined},
+    {"chromium_int9rnal_relaxed_Fnifor_layout", Extension::kUndefined},
+    {"chrmium_internal_relaxed_uniform_layout", Extension::kUndefined},
+    {"VRhHomium_internal_relaxd_uniform_OOayout", Extension::kUndefined},
+    {"y1", Extension::kUndefined},
+    {"l77rrn6", Extension::kUndefined},
+    {"4016", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/lang/wgsl/helpers/check_supported_extensions.cc b/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
index bb35e15..72f65c6 100644
--- a/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
+++ b/src/tint/lang/wgsl/helpers/check_supported_extensions.cc
@@ -48,10 +48,9 @@
     for (auto* enable : module.Enables()) {
         for (auto* ext : enable->extensions) {
             if (!set.Contains(ext->name)) {
-                diags.AddError(diag::System::Writer,
-                               std::string(writer_name) + " backend does not support extension '" +
-                                   tint::ToString(ext->name) + "'",
-                               ext->source);
+                diags.AddError(diag::System::Writer, ext->source)
+                    << writer_name << " backend does not support extension " << style::Code
+                    << ext->name;
                 return false;
             }
         }
diff --git a/src/tint/lang/wgsl/inspector/inspector.cc b/src/tint/lang/wgsl/inspector/inspector.cc
index 8efa350..f6f7980 100644
--- a/src/tint/lang/wgsl/inspector/inspector.cc
+++ b/src/tint/lang/wgsl/inspector/inspector.cc
@@ -571,12 +571,13 @@
 const ast::Function* Inspector::FindEntryPointByName(const std::string& name) {
     auto* func = program_.AST().Functions().Find(program_.Symbols().Get(name));
     if (!func) {
-        diagnostics_.AddError(diag::System::Inspector, name + " was not found!");
+        diagnostics_.AddError(diag::System::Inspector, Source{}) << name << " was not found!";
         return nullptr;
     }
 
     if (!func->IsEntryPoint()) {
-        diagnostics_.AddError(diag::System::Inspector, name + " is not an entry point!");
+        diagnostics_.AddError(diag::System::Inspector, Source{})
+            << name << " is not an entry point!";
         return nullptr;
     }
 
diff --git a/src/tint/lang/wgsl/intrinsic/data.cc b/src/tint/lang/wgsl/intrinsic/data.cc
index 7c54003..96b5ad0 100644
--- a/src/tint/lang/wgsl/intrinsic/data.cc
+++ b/src/tint/lang/wgsl/intrinsic/data.cc
@@ -105,8 +105,8 @@
     }
     return BuildBool(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bool";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "bool";
   }
 };
 
@@ -119,10 +119,8 @@
     }
     return BuildIa(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    ss << "abstract-int";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out  << style::Type << "abstract-int";
   }
 };
 
@@ -135,10 +133,8 @@
     }
     return BuildFa(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    ss << "abstract-float";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out  << style::Type << "abstract-float";
   }
 };
 
@@ -151,8 +147,8 @@
     }
     return BuildI32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "i32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "i32";
   }
 };
 
@@ -165,8 +161,8 @@
     }
     return BuildU32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "u32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "u32";
   }
 };
 
@@ -179,8 +175,8 @@
     }
     return BuildF32(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f32";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f32";
   }
 };
 
@@ -193,8 +189,8 @@
     }
     return BuildF16(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "f16";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "f16";
   }
 };
 
@@ -212,9 +208,9 @@
     }
     return BuildVec2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -232,9 +228,9 @@
     }
     return BuildVec3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -252,9 +248,9 @@
     }
     return BuildVec4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "vec4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "vec4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -272,9 +268,9 @@
     }
     return BuildMat2X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -292,9 +288,9 @@
     }
     return BuildMat2X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -312,9 +308,9 @@
     }
     return BuildMat2X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat2x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat2x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -332,9 +328,9 @@
     }
     return BuildMat3X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -352,9 +348,9 @@
     }
     return BuildMat3X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -372,9 +368,9 @@
     }
     return BuildMat3X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat3x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat3x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -392,9 +388,9 @@
     }
     return BuildMat4X2(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x2<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x2" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -412,9 +408,9 @@
     }
     return BuildMat4X3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -432,9 +428,9 @@
     }
     return BuildMat4X4(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "mat4x4<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "mat4x4" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -457,12 +453,10 @@
     }
     return BuildVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "vec" << N << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "vec" << style::Type << N << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -490,13 +484,11 @@
     }
     return BuildMat(state, ty, N, M, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string M = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "mat" << N << "x" << M << "<" << T << ">";
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText M;
+  state->PrintNum(M);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "mat" << style::Type << N << style::Type << "x" << style::Type << M << style::Type << "<" << style::Type << T << style::Type << ">";
   }
 };
 
@@ -524,11 +516,11 @@
     }
     return BuildPtr(state, ty, S, T, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string S = state->NumName();
-  const std::string T = state->TypeName();
-  const std::string A = state->NumName();
-    return "ptr<" + S + ", " + T + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText S;
+  state->PrintNum(S);StyledText T;
+  state->PrintType(T);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "ptr" << style::Code << "<" << style::Type << S << style::Code << ", " << style::Type << T << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -546,9 +538,9 @@
     }
     return BuildAtomic(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "atomic<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "atomic" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -566,9 +558,9 @@
     }
     return BuildArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -581,8 +573,8 @@
     }
     return BuildSampler(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler";
   }
 };
 
@@ -595,8 +587,8 @@
     }
     return BuildSamplerComparison(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "sampler_comparison";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "sampler_comparison";
   }
 };
 
@@ -614,9 +606,9 @@
     }
     return BuildTexture1D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_1d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_1d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -634,9 +626,9 @@
     }
     return BuildTexture2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -654,9 +646,9 @@
     }
     return BuildTexture2DArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_2d_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_2d_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -674,9 +666,9 @@
     }
     return BuildTexture3D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_3d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_3d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -694,9 +686,9 @@
     }
     return BuildTextureCube(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -714,9 +706,9 @@
     }
     return BuildTextureCubeArray(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_cube_array<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_cube_array" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -734,9 +726,9 @@
     }
     return BuildTextureMultisampled2D(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "texture_multisampled_2d<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "texture_multisampled_2d" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -749,8 +741,8 @@
     }
     return BuildTextureDepth2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d";
   }
 };
 
@@ -763,8 +755,8 @@
     }
     return BuildTextureDepth2DArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_2d_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_2d_array";
   }
 };
 
@@ -777,8 +769,8 @@
     }
     return BuildTextureDepthCube(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube";
   }
 };
 
@@ -791,8 +783,8 @@
     }
     return BuildTextureDepthCubeArray(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_cube_array";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_cube_array";
   }
 };
 
@@ -805,8 +797,8 @@
     }
     return BuildTextureDepthMultisampled2D(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_depth_multisampled_2d";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_depth_multisampled_2d";
   }
 };
 
@@ -829,10 +821,10 @@
     }
     return BuildTextureStorage1D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_1d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_1d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -855,10 +847,10 @@
     }
     return BuildTextureStorage2D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -881,10 +873,10 @@
     }
     return BuildTextureStorage2DArray(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_2d_array<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_2d_array" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -907,10 +899,10 @@
     }
     return BuildTextureStorage3D(state, ty, F, A);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string F = state->NumName();
-  const std::string A = state->NumName();
-    return "texture_storage_3d<" + F + ", " + A + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText F;
+  state->PrintNum(F);StyledText A;
+  state->PrintNum(A);
+    out << style::Type << "texture_storage_3d" << style::Code << "<" << style::Type << F << style::Code << ", " << style::Type << A << style::Code << ">";
   }
 };
 
@@ -923,8 +915,8 @@
     }
     return BuildTextureExternal(state, ty);
   },
-/* string */ [](MatchState*) -> std::string {
-    return "texture_external";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
+    out << style::Type << "texture_external";
   }
 };
 
@@ -942,9 +934,9 @@
     }
     return BuildPackedVec3(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "packedVec3<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "packedVec3" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -962,11 +954,9 @@
     }
     return BuildModfResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__modf_result_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__modf_result_" << style::Type << T;
   }
 };
 
@@ -989,12 +979,10 @@
     }
     return BuildModfResultVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__modf_result_vec" << N << "_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__modf_result_vec" << style::Type << N << style::Type << "_" << style::Type << T;
   }
 };
 
@@ -1012,11 +1000,9 @@
     }
     return BuildFrexpResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__frexp_result_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__frexp_result_" << style::Type << T;
   }
 };
 
@@ -1039,12 +1025,10 @@
     }
     return BuildFrexpResultVec(state, ty, N, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string N = state->NumName();
-  const std::string T = state->TypeName();
-    StringStream ss;
-    ss << "__frexp_result_vec" << N << "_" << T;
-    return ss.str();
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText N;
+  state->PrintNum(N);StyledText T;
+  state->PrintType(T);
+    out  << style::Type << "__frexp_result_vec" << style::Type << N << style::Type << "_" << style::Type << T;
   }
 };
 
@@ -1062,9 +1046,9 @@
     }
     return BuildAtomicCompareExchangeResult(state, ty, T);
   },
-/* string */ [](MatchState* state) -> std::string {
-  const std::string T = state->TypeName();
-    return "__atomic_compare_exchange_result<" + T + ">";
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {StyledText T;
+  state->PrintType(T);
+    out << style::Type << "__atomic_compare_exchange_result" << style::Code << "<" << style::Type << T << style::Code << ">";
   }
 };
 
@@ -1095,13 +1079,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match concrete_scalar'
@@ -1124,13 +1105,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_f32'
@@ -1156,13 +1134,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_f16'
@@ -1188,13 +1163,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_i32'
@@ -1220,13 +1192,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_u32'
@@ -1252,13 +1221,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kBoolMatcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kBoolMatcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match scalar_no_bool'
@@ -1284,13 +1250,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kF16Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kF16Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fia_fiu32_f16'
@@ -1316,13 +1279,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kFaMatcher.string(nullptr) << ", " << kIaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fia_fi32_f16'
@@ -1345,13 +1305,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kFaMatcher.string(nullptr) << ", " << kIaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fia_fiu32'
@@ -1374,13 +1331,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kFaMatcher.string(nullptr) << ", " << kIaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fa_f32'
@@ -1394,13 +1348,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kFaMatcher.string(nullptr) << " or " << kF32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kFaMatcher.print(nullptr, out); out << TextStyle{} << " or "; kF32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fa_f32_f16'
@@ -1417,13 +1368,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kFaMatcher.string(nullptr) << ", " << kF32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kFaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match ia_iu32'
@@ -1440,13 +1388,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match ia_i32'
@@ -1460,13 +1405,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kIaMatcher.string(nullptr) << " or " << kI32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kIaMatcher.print(nullptr, out); out << TextStyle{} << " or "; kI32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fiu32_f16'
@@ -1486,13 +1428,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << ", " << kU32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kU32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fiu32'
@@ -1509,13 +1448,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fi32_f16'
@@ -1532,13 +1468,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << ", " << kI32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << ", "; kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match fi32'
@@ -1552,13 +1485,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << " or " << kI32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kI32Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match f32_f16'
@@ -1572,13 +1502,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kF32Matcher.string(nullptr) << " or " << kF16Matcher.string(nullptr);
-    return ss.str();
-  }
+ kF32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kF16Matcher.print(nullptr, out);}
 };
 
 /// TypeMatcher for 'match iu32'
@@ -1592,13 +1519,10 @@
     }
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss << kI32Matcher.string(nullptr) << " or " << kU32Matcher.string(nullptr);
-    return ss.str();
-  }
+ kI32Matcher.print(nullptr, out); out << TextStyle{} << " or "; kU32Matcher.print(nullptr, out);}
 };
 
 /// EnumMatcher for 'match f32_texel_format'
@@ -1617,8 +1541,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "bgra8unorm, rgba8unorm, rgba8snorm, rgba16float, r32float, rg32float or rgba32float";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "bgra8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8unorm"<< TextStyle{} << ", " << style::Enum << "rgba8snorm"<< TextStyle{} << ", " << style::Enum << "rgba16float"<< TextStyle{} << ", " << style::Enum << "r32float"<< TextStyle{} << ", " << style::Enum << "rg32float"<< TextStyle{} << " or " << style::Enum << "rgba32float";
   }
 };
 
@@ -1636,8 +1560,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8sint, rgba16sint, r32sint, rg32sint or rgba32sint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8sint"<< TextStyle{} << ", " << style::Enum << "rgba16sint"<< TextStyle{} << ", " << style::Enum << "r32sint"<< TextStyle{} << ", " << style::Enum << "rg32sint"<< TextStyle{} << " or " << style::Enum << "rgba32sint";
   }
 };
 
@@ -1655,8 +1579,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "rgba8uint, rgba16uint, r32uint, rg32uint or rgba32uint";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "rgba8uint"<< TextStyle{} << ", " << style::Enum << "rgba16uint"<< TextStyle{} << ", " << style::Enum << "r32uint"<< TextStyle{} << ", " << style::Enum << "rg32uint"<< TextStyle{} << " or " << style::Enum << "rgba32uint";
   }
 };
 
@@ -1668,8 +1592,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "write";
   }
 };
 
@@ -1681,8 +1605,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read_write";
   }
 };
 
@@ -1697,8 +1621,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "read or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "read"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
@@ -1713,8 +1637,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "write or read_write";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "write"<< TextStyle{} << " or " << style::Enum << "read_write";
   }
 };
 
@@ -1730,8 +1654,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "function, private or workgroup";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "function"<< TextStyle{} << ", " << style::Enum << "private"<< TextStyle{} << " or " << style::Enum << "workgroup";
   }
 };
 
@@ -1746,8 +1670,8 @@
         return Number::invalid;
     }
   },
-/* string */ [](MatchState*) -> std::string {
-    return "workgroup or storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "workgroup"<< TextStyle{} << " or " << style::Enum << "storage";
   }
 };
 
@@ -1759,8 +1683,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "storage";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "storage";
   }
 };
 
@@ -1772,8 +1696,8 @@
     }
     return Number::invalid;
   },
-/* string */ [](MatchState*) -> std::string {
-    return "workgroup";
+/* print */ [](MatchState*, StyledText& out) {
+  out<< style::Enum << "workgroup";
   }
 };
 
diff --git a/src/tint/lang/wgsl/intrinsic/table_test.cc b/src/tint/lang/wgsl/intrinsic/table_test.cc
index 2c98839..cbbeeda 100644
--- a/src/tint/lang/wgsl/intrinsic/table_test.cc
+++ b/src/tint/lang/wgsl/intrinsic/table_test.cc
@@ -79,7 +79,7 @@
     auto result =
         table.Lookup(wgsl::BuiltinFn::kCos, Empty, Vector{i32}, core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchU32) {
@@ -99,7 +99,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kUnpack2X16Float, Empty, Vector{f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchI32) {
@@ -126,7 +126,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kTextureLoad, Empty, Vector{tex, f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchIU32AsI32) {
@@ -154,7 +154,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kCountOneBits, Empty, Vector{f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchFIU32AsI32) {
@@ -198,7 +198,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kClamp, Empty, Vector{bool_, bool_, bool_},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchBool) {
@@ -219,7 +219,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kSelect, Empty, Vector{f32, f32, f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchPointer) {
@@ -241,7 +241,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kAtomicLoad, Empty, Vector{atomic_i32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchArray) {
@@ -264,7 +264,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kArrayLength, Empty, Vector{f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchSampler) {
@@ -293,7 +293,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kTextureSample, Empty, Vector{tex, f32, vec2f},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchSampledTexture) {
@@ -417,7 +417,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kTextureLoad, Empty, Vector{f32, vec2i},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchTemplateType) {
@@ -437,7 +437,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kClamp, Empty, Vector{f32, u32, f32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchOpenSizeVector) {
@@ -460,7 +460,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kClamp, Empty, Vector{vec2f, u32, vec2f},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchOpenSizeMatrix) {
@@ -482,7 +482,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kDeterminant, Empty, Vector{mat3x2f},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, MatchDifferentArgsElementType_Builtin_ConstantEval) {
@@ -544,37 +544,37 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kTextureDimensions, Empty, Vector{bool_, bool_},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_EQ(result.Failure(),
-              R"(no matching call to textureDimensions(bool, bool)
+    ASSERT_EQ(result.Failure().Plain(),
+              R"(no matching call to 'textureDimensions(bool, bool)'
 
 27 candidate functions:
-  textureDimensions(texture: texture_1d<T>, level: L) -> u32  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_1d<T>) -> u32  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_3d<T>) -> vec3<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_depth_2d) -> vec2<u32>
-  textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>
-  textureDimensions(texture: texture_storage_1d<F, A>) -> u32
-  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>
-  textureDimensions(texture: texture_external) -> vec2<u32>
+  'textureDimensions(texture: texture_1d<T>, level: L) -> u32'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_1d<T>) -> u32'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_1d<F, A>) -> u32'
+  'textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>'
+  'textureDimensions(texture: texture_external) -> vec2<u32>'
 )");
 }
 
@@ -584,37 +584,37 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kTextureDimensions, Empty, Vector{tex, bool_},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_EQ(result.Failure(),
-              R"(no matching call to textureDimensions(texture_depth_2d, bool)
+    ASSERT_EQ(result.Failure().Plain(),
+              R"(no matching call to 'textureDimensions(texture_depth_2d, bool)'
 
 27 candidate functions:
-  textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_1d<T>, level: L) -> u32  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>  where: T is f32, i32 or u32, L is i32 or u32
-  textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>  where: L is i32 or u32
-  textureDimensions(texture: texture_depth_2d) -> vec2<u32>
-  textureDimensions(texture: texture_1d<T>) -> u32  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_3d<T>) -> vec3<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>  where: T is f32, i32 or u32
-  textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube) -> vec2<u32>
-  textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>
-  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>
-  textureDimensions(texture: texture_storage_1d<F, A>) -> u32
-  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>
-  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>
-  textureDimensions(texture: texture_external) -> vec2<u32>
+  'textureDimensions(texture: texture_depth_2d, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_1d<T>, level: L) -> u32'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>, level: L) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>, level: L) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32', 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_cube_array, level: L) -> vec2<u32>'  where: 'L' is 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_1d<T>) -> u32'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_2d_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_3d<T>) -> vec3<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_cube_array<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<u32>'  where: 'T' is 'f32', 'i32' or 'u32'
+  'textureDimensions(texture: texture_depth_2d_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_cube_array) -> vec2<u32>'
+  'textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_1d<F, A>) -> u32'
+  'textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<u32>'
+  'textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<u32>'
+  'textureDimensions(texture: texture_external) -> vec2<u32>'
 )");
 }
 
@@ -630,11 +630,11 @@
     auto* bool_ = create<core::type::Bool>();
     auto result = table.Lookup(core::UnaryOp::kNegation, bool_, core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator - (bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator - (bool)'
 
 2 candidate operators:
-  operator - (T) -> T  where: T is abstract-float, abstract-int, f32, i32 or f16
-  operator - (vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32 or f16
+  'operator - (T) -> T'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32' or 'f16'
+  'operator - (vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32' or 'f16'
 )");
 }
 
@@ -672,18 +672,18 @@
         table.Lookup(core::BinaryOp::kMultiply, f32, bool_, core::EvaluationStage::kConstant,
                      /* is_compound */ false);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator * (f32, bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator * (f32, bool)'
 
 9 candidate operators:
-  operator * (T, T) -> T  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator * (vecN<T>, T) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator * (T, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator * (T, matNxM<T>) -> matNxM<T>  where: T is abstract-float, f32 or f16
-  operator * (matNxM<T>, T) -> matNxM<T>  where: T is abstract-float, f32 or f16
-  operator * (vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator * (matCxR<T>, vecC<T>) -> vecR<T>  where: T is abstract-float, f32 or f16
-  operator * (vecR<T>, matCxR<T>) -> vecC<T>  where: T is abstract-float, f32 or f16
-  operator * (matKxR<T>, matCxK<T>) -> matCxR<T>  where: T is abstract-float, f32 or f16
+  'operator * (T, T) -> T'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator * (vecN<T>, T) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator * (T, vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator * (T, matNxM<T>) -> matNxM<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator * (matNxM<T>, T) -> matNxM<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator * (vecN<T>, vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator * (matCxR<T>, vecC<T>) -> vecR<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator * (vecR<T>, matCxR<T>) -> vecC<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator * (matKxR<T>, matCxK<T>) -> matCxR<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -706,18 +706,18 @@
         table.Lookup(core::BinaryOp::kMultiply, f32, bool_, core::EvaluationStage::kConstant,
                      /* is_compound */ true);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(), R"(no matching overload for operator *= (f32, bool)
+    EXPECT_EQ(result.Failure().Plain(), R"(no matching overload for 'operator *= (f32, bool)'
 
 9 candidate operators:
-  operator *= (T, T) -> T  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator *= (vecN<T>, T) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator *= (T, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator *= (T, matNxM<T>) -> matNxM<T>  where: T is abstract-float, f32 or f16
-  operator *= (matNxM<T>, T) -> matNxM<T>  where: T is abstract-float, f32 or f16
-  operator *= (vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  operator *= (matCxR<T>, vecC<T>) -> vecR<T>  where: T is abstract-float, f32 or f16
-  operator *= (vecR<T>, matCxR<T>) -> vecC<T>  where: T is abstract-float, f32 or f16
-  operator *= (matKxR<T>, matCxK<T>) -> matCxR<T>  where: T is abstract-float, f32 or f16
+  'operator *= (T, T) -> T'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (vecN<T>, T) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (T, vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (T, matNxM<T>) -> matNxM<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator *= (matNxM<T>, T) -> matNxM<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator *= (vecN<T>, vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'operator *= (matCxR<T>, vecC<T>) -> vecR<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator *= (vecR<T>, matCxR<T>) -> vecC<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'operator *= (matKxR<T>, matCxK<T>) -> matCxR<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -757,29 +757,29 @@
     auto result = table.Lookup(CtorConv::kVec3, Empty, Vector{i32, f32, i32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(),
-              R"(no matching constructor for vec3(i32, f32, i32)
+    EXPECT_EQ(result.Failure().Plain(),
+              R"(no matching constructor for 'vec3(i32, f32, i32)'
 
 12 candidate constructors:
-  vec3(x: T, y: T, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(xy: vec2<T>, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(xy: vec2<T>, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(x: T, yz: vec2<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(x: T, yz: vec2<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(vec3<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(vec3<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3() -> vec3<abstract-int>
-  vec3<T>() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(x: T, y: T, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
+  'vec3(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3() -> vec3<abstract-int>'
+  'vec3<T>() -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f32', 'U' is 'abstract-int', 'abstract-float', 'i32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f16', 'U' is 'abstract-int', 'abstract-float', 'f32', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'i32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'u32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'bool', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -789,29 +789,29 @@
     auto result = table.Lookup(CtorConv::kVec3, Vector{i32}, Vector{i32, f32, i32},
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(),
-              R"(no matching constructor for vec3<i32>(i32, f32, i32)
+    EXPECT_EQ(result.Failure().Plain(),
+              R"(no matching constructor for 'vec3<i32>(i32, f32, i32)'
 
 12 candidate constructors:
-  vec3<T>(x: T, y: T, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(xy: vec2<T>, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(xy: vec2<T>, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(x: T, yz: vec2<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(x: T, yz: vec2<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(vec3<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(vec3<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3() -> vec3<abstract-int>
-  vec3<T>() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(x: T, y: T, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'vec3<T>(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3() -> vec3<abstract-int>'
+  'vec3<T>() -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f32', 'U' is 'abstract-int', 'abstract-float', 'i32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f16', 'U' is 'abstract-int', 'abstract-float', 'f32', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'i32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'u32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'bool', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -898,29 +898,29 @@
     auto result =
         table.Lookup(CtorConv::kVec3, Vector{f32}, Vector{arr}, core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    EXPECT_EQ(result.Failure(),
-              R"(no matching constructor for vec3<f32>(array<u32>)
+    EXPECT_EQ(result.Failure().Plain(),
+              R"(no matching constructor for 'vec3<f32>(array<u32>)'
 
 12 candidate constructors:
-  vec3<T>(vec3<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3() -> vec3<abstract-int>
-  vec3<T>() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3<T>(x: T, yz: vec2<T>) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(x: T, yz: vec2<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(xy: vec2<T>, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(xy: vec2<T>, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3<T>(x: T, y: T, z: T) -> vec3<T>  where: T is f32, f16, i32, u32 or bool
-  vec3(x: T, y: T, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3(T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  vec3(vec3<T>) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'vec3<T>(vec3<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3() -> vec3<abstract-int>'
+  'vec3<T>() -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(x: T, yz: vec2<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(xy: vec2<T>, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3<T>(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(x: T, y: T, z: T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(T) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'vec3(vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 
 5 candidate conversions:
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
-  vec3<T>(vec3<U>) -> vec3<T>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f32', 'U' is 'abstract-int', 'abstract-float', 'i32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'f16', 'U' is 'abstract-int', 'abstract-float', 'f32', 'i32', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'i32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'u32', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'bool'
+  'vec3<T>(vec3<U>) -> vec3<T>'  where: 'T' is 'bool', 'U' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32' or 'u32'
 )");
 }
 
@@ -964,7 +964,7 @@
     auto result = table.Lookup(wgsl::BuiltinFn::kAbs, Empty, std::move(arg_tys),
                                core::EvaluationStage::kConstant);
     ASSERT_NE(result, Success);
-    ASSERT_THAT(result.Failure(), HasSubstr("no matching call"));
+    ASSERT_THAT(result.Failure().Plain(), HasSubstr("no matching call"));
 }
 
 TEST_F(WgslIntrinsicTableTest, OverloadResolution) {
diff --git a/src/tint/lang/wgsl/program/program.cc b/src/tint/lang/wgsl/program/program.cc
index bcd8e93..c649781 100644
--- a/src/tint/lang/wgsl/program/program.cc
+++ b/src/tint/lang/wgsl/program/program.cc
@@ -81,7 +81,7 @@
         // If the builder claims to be invalid, then we really should have an error
         // message generated. If we find a situation where the program is not valid
         // and there are no errors reported, add one here.
-        diagnostics_.AddError(diag::System::Program, "invalid program generated");
+        diagnostics_.AddError(diag::System::Program, Source{}) << "invalid program generated";
     }
 }
 
diff --git a/src/tint/lang/wgsl/program/program_test.cc b/src/tint/lang/wgsl/program/program_test.cc
index 1a59adb..2572ca5 100644
--- a/src/tint/lang/wgsl/program/program_test.cc
+++ b/src/tint/lang/wgsl/program/program_test.cc
@@ -93,19 +93,19 @@
 }
 
 TEST_F(ProgramTest, DiagnosticsMove) {
-    Diagnostics().AddError(diag::System::Program, "an error message");
+    Diagnostics().AddError(diag::System::Program, Source{}) << "an error message";
 
     Program program_a(std::move(*this));
     EXPECT_FALSE(program_a.IsValid());
     EXPECT_EQ(program_a.Diagnostics().Count(), 1u);
     EXPECT_EQ(program_a.Diagnostics().NumErrors(), 1u);
-    EXPECT_EQ(program_a.Diagnostics().begin()->message, "an error message");
+    EXPECT_EQ(program_a.Diagnostics().begin()->message.Plain(), "an error message");
 
     Program program_b(std::move(program_a));
     EXPECT_FALSE(program_b.IsValid());
     EXPECT_EQ(program_b.Diagnostics().Count(), 1u);
     EXPECT_EQ(program_b.Diagnostics().NumErrors(), 1u);
-    EXPECT_EQ(program_b.Diagnostics().begin()->message, "an error message");
+    EXPECT_EQ(program_b.Diagnostics().begin()->message.Plain(), "an error message");
 }
 
 TEST_F(ProgramTest, ReuseMovedFromVariable) {
diff --git a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
index 6f689b3..adb1d1c 100644
--- a/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/enable_directive_test.cc
@@ -205,7 +205,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_relaxed_uniform_layout', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_framebuffer_fetch', 'chromium_experimental_pixel_local', 'chromium_experimental_push_constant', 'chromium_experimental_subgroups', 'chromium_internal_dual_source_blending', 'chromium_internal_graphite', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/lang/wgsl/reader/parser/error_msg_test.cc b/src/tint/lang/wgsl/reader/parser/error_msg_test.cc
index dc46bc5..2e79a9b 100644
--- a/src/tint/lang/wgsl/reader/parser/error_msg_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/error_msg_test.cc
@@ -38,16 +38,16 @@
 
 class ParserImplErrorTest : public WGSLParserTest {};
 
-#define EXPECT(SOURCE, EXPECTED)                                                   \
-    do {                                                                           \
-        std::string source = SOURCE;                                               \
-        std::string expected = EXPECTED;                                           \
-        auto p = parser(source);                                                   \
-        p->set_max_errors(5);                                                      \
-        EXPECT_EQ(false, p->Parse());                                              \
-        auto diagnostics = p->builder().Diagnostics();                             \
-        EXPECT_EQ(true, diagnostics.ContainsErrors());                             \
-        EXPECT_EQ(expected, diag::Formatter(formatter_style).Format(diagnostics)); \
+#define EXPECT(SOURCE, EXPECTED)                                                           \
+    do {                                                                                   \
+        std::string source = SOURCE;                                                       \
+        std::string expected = EXPECTED;                                                   \
+        auto p = parser(source);                                                           \
+        p->set_max_errors(5);                                                              \
+        EXPECT_EQ(false, p->Parse());                                                      \
+        auto diagnostics = p->builder().Diagnostics();                                     \
+        EXPECT_EQ(true, diagnostics.ContainsErrors());                                     \
+        EXPECT_EQ(expected, diag::Formatter(formatter_style).Format(diagnostics).Plain()); \
     } while (false)
 
 TEST_F(ParserImplErrorTest, AdditiveInvalidExpr) {
diff --git a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
index 31a6dbc..54818ea 100644
--- a/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
+++ b/src/tint/lang/wgsl/reader/parser/error_resync_test.cc
@@ -36,15 +36,15 @@
 
 class ParserImplErrorResyncTest : public WGSLParserTest {};
 
-#define EXPECT(SOURCE, EXPECTED)                                                   \
-    do {                                                                           \
-        std::string source = SOURCE;                                               \
-        std::string expected = EXPECTED;                                           \
-        auto p = parser(source);                                                   \
-        EXPECT_EQ(false, p->Parse());                                              \
-        auto diagnostics = p->builder().Diagnostics();                             \
-        EXPECT_EQ(true, diagnostics.ContainsErrors());                             \
-        EXPECT_EQ(expected, diag::Formatter(formatter_style).Format(diagnostics)); \
+#define EXPECT(SOURCE, EXPECTED)                                                           \
+    do {                                                                                   \
+        std::string source = SOURCE;                                                       \
+        std::string expected = EXPECTED;                                                   \
+        auto p = parser(source);                                                           \
+        EXPECT_EQ(false, p->Parse());                                                      \
+        auto diagnostics = p->builder().Diagnostics();                                     \
+        EXPECT_EQ(true, diagnostics.ContainsErrors());                                     \
+        EXPECT_EQ(expected, diag::Formatter(formatter_style).Format(diagnostics).Plain()); \
     } while (false)
 
 TEST_F(ParserImplErrorResyncTest, BadFunctionDecls) {
diff --git a/src/tint/lang/wgsl/reader/parser/parser.cc b/src/tint/lang/wgsl/reader/parser/parser.cc
index 0716617..315d701 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.cc
+++ b/src/tint/lang/wgsl/reader/parser/parser.cc
@@ -242,22 +242,28 @@
 
 Parser::Failure::Errored Parser::AddError(const Source& source, std::string_view err) {
     if (silence_diags_ == 0) {
-        builder_.Diagnostics().AddError(diag::System::Reader, err, source);
+        builder_.Diagnostics().AddError(diag::System::Reader, source) << err;
+    }
+    return Failure::kErrored;
+}
+
+Parser::Failure::Errored Parser::AddError(const Source& source, StyledText&& err) {
+    if (silence_diags_ == 0) {
+        builder_.Diagnostics().AddError(diag::System::Reader, source) << std::move(err);
     }
     return Failure::kErrored;
 }
 
 void Parser::AddNote(const Source& source, std::string_view err) {
     if (silence_diags_ == 0) {
-        builder_.Diagnostics().AddNote(diag::System::Reader, err, source);
+        builder_.Diagnostics().AddNote(diag::System::Reader, source) << err;
     }
 }
 
 void Parser::deprecated(const Source& source, std::string_view msg) {
     if (silence_diags_ == 0) {
-        builder_.Diagnostics().AddWarning(diag::System::Reader,
-                                          "use of deprecated language feature: " + std::string(msg),
-                                          source);
+        builder_.Diagnostics().AddWarning(diag::System::Reader, source)
+            << "use of deprecated language feature: " << msg;
     }
 }
 
@@ -927,7 +933,7 @@
     }
 
     /// Create a sensible error message
-    StringStream err;
+    StyledText err;
     err << "expected " << name;
 
     if (!use.empty()) {
@@ -951,7 +957,7 @@
     }
 
     synchronized_ = false;
-    return AddError(t.source(), err.str());
+    return AddError(t.source(), std::move(err));
 }
 
 Expect<ast::Type> Parser::expect_type(std::string_view use) {
diff --git a/src/tint/lang/wgsl/reader/parser/parser.h b/src/tint/lang/wgsl/reader/parser/parser.h
index 9124e89..993c544 100644
--- a/src/tint/lang/wgsl/reader/parser/parser.h
+++ b/src/tint/lang/wgsl/reader/parser/parser.h
@@ -41,6 +41,7 @@
 #include "src/tint/lang/wgsl/reader/parser/token.h"
 #include "src/tint/lang/wgsl/resolver/resolve.h"
 #include "src/tint/utils/diagnostic/formatter.h"
+#include "src/tint/utils/text/styled_text.h"
 
 namespace tint::ast {
 class BreakStatement;
@@ -328,7 +329,7 @@
     /// @returns the parser error string
     std::string error() const {
         diag::Formatter formatter{{false, false, false, false}};
-        return formatter.Format(builder_.Diagnostics());
+        return formatter.Format(builder_.Diagnostics()).Plain();
     }
 
     /// @returns the Program. The program builder in the parser will be reset
@@ -373,6 +374,12 @@
     /// @return `Failure::Errored::kError` so that you can combine an AddError()
     /// call and return on the same line.
     Failure::Errored AddError(const Source& source, std::string_view msg);
+    /// Appends an error at `source` with the message `msg`
+    /// @param source the source to associate the error with
+    /// @param msg the error message
+    /// @return `Failure::Errored::kError` so that you can combine an AddError()
+    /// call and return on the same line.
+    Failure::Errored AddError(const Source& source, StyledText&& msg);
     /// Appends a note at `source` with the message `msg`
     /// @param source the source to associate the error with
     /// @param msg the note message
diff --git a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
index 751f21c..f54aba6 100644
--- a/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
+++ b/src/tint/lang/wgsl/reader/program_to_ir/program_to_ir.cc
@@ -200,8 +200,8 @@
         ~ControlStackScope() { impl_->control_stack_.Pop(); }
     };
 
-    void AddError(const Source& s, const std::string& err) {
-        diagnostics_.AddError(tint::diag::System::IR, err, s);
+    diag::Diagnostic& AddError(const Source& source) {
+        return diagnostics_.AddError(tint::diag::System::IR, source);
     }
 
     bool NeedTerminator() { return current_block_ && !current_block_->Terminator(); }
@@ -1003,8 +1003,8 @@
                     if (mat->ConstantValue()) {
                         auto* cv = mat->ConstantValue()->Clone(impl.clone_ctx_);
                         if (!cv) {
-                            impl.AddError(expr->source, "failed to get constant value for call " +
-                                                            std::string(expr->TypeInfo().name));
+                            impl.AddError(expr->source) << "failed to get constant value for call "
+                                                        << expr->TypeInfo().name;
                             return;
                         }
                         Bind(expr, impl.builder_.Constant(cv));
@@ -1017,15 +1017,15 @@
                 for (const auto* arg : expr->args) {
                     auto value = GetValue(arg);
                     if (!value) {
-                        impl.AddError(arg->source, "failed to convert arguments");
+                        impl.AddError(arg->source) << "failed to convert arguments";
                         return;
                     }
                     args.Push(value);
                 }
                 auto* sem = impl.program_.Sem().Get<sem::Call>(expr);
                 if (!sem) {
-                    impl.AddError(expr->source, "failed to get semantic information for call " +
-                                                    std::string(expr->TypeInfo().name));
+                    impl.AddError(expr->source)
+                        << "failed to get semantic information for call " << expr->TypeInfo().name;
                     return;
                 }
                 auto* ty = sem->Target()->ReturnType()->Clone(impl.clone_ctx_.type_ctx);
@@ -1063,8 +1063,8 @@
             void EmitIdentifier(const ast::IdentifierExpression* i) {
                 auto* v = impl.scopes_.Get(i->identifier->symbol);
                 if (TINT_UNLIKELY(!v)) {
-                    impl.AddError(i->source,
-                                  "unable to find identifier " + i->identifier->symbol.Name());
+                    impl.AddError(i->source)
+                        << "unable to find identifier " << i->identifier->symbol.Name();
                     return;
                 }
                 Bind(i, v);
@@ -1073,14 +1073,14 @@
             void EmitLiteral(const ast::LiteralExpression* lit) {
                 auto* sem = impl.program_.Sem().Get(lit);
                 if (!sem) {
-                    impl.AddError(lit->source, "failed to get semantic information for node " +
-                                                   std::string(lit->TypeInfo().name));
+                    impl.AddError(lit->source)
+                        << "failed to get semantic information for node " << lit->TypeInfo().name;
                     return;
                 }
                 auto* cv = sem->ConstantValue()->Clone(impl.clone_ctx_);
                 if (!cv) {
-                    impl.AddError(lit->source, "failed to get constant value for node " +
-                                                   std::string(lit->TypeInfo().name));
+                    impl.AddError(lit->source)
+                        << "failed to get constant value for node " << lit->TypeInfo().name;
                     return;
                 }
                 auto* val = impl.builder_.Constant(cv);
@@ -1270,9 +1270,8 @@
                 scopes_.Set(l->name->symbol, let->Result(0));
             },
             [&](const ast::Override*) {
-                AddError(var->source,
-                         "found an `Override` variable. The SubstituteOverrides "
-                         "transform must be run before converting to IR");
+                AddError(var->source) << "found an `Override` variable. The SubstituteOverrides "
+                                         "transform must be run before converting to IR";
             },
             [&](const ast::Const*) {
                 // Skip. This should be handled by const-eval already, so the const will be a
@@ -1344,7 +1343,7 @@
     auto r = b.Build();
     if (r != Success) {
         diag::List err = std::move(r.Failure().reason);
-        err.AddNote(diag::System::IR, "AST:\n" + Program::printer(program), Source{});
+        err.AddNote(diag::System::IR, Source{}) << "AST:\n" + Program::printer(program);
         return Failure{err};
     }
 
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index 40c4f79..f2e20be 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -41,8 +41,8 @@
     if (TINT_UNLIKELY(file->content.data.size() >
                       static_cast<size_t>(std::numeric_limits<uint32_t>::max()))) {
         ProgramBuilder b;
-        b.Diagnostics().AddError(tint::diag::System::Reader,
-                                 "WGSL source must be 0xffffffff bytes or fewer");
+        b.Diagnostics().AddError(tint::diag::System::Reader, Source{})
+            << "WGSL source must be 0xffffffff bytes or fewer";
         return Program(std::move(b));
     }
     Parser parser(file);
diff --git a/src/tint/lang/wgsl/resolver/address_space_layout_validation_test.cc b/src/tint/lang/wgsl/resolver/address_space_layout_validation_test.cc
index a1bfe87..4fe832a 100644
--- a/src/tint/lang/wgsl/resolver/address_space_layout_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/address_space_layout_validation_test.cc
@@ -59,7 +59,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: the offset of a struct member of type 'f32' in address space 'storage' must be a multiple of 4 bytes, but 'b' is currently at offset 5. Consider setting @align(4) on this member
+        R"(34:56 error: the offset of a struct member of type 'f32' in address space 'storage' must be a multiple of 4 bytes, but 'b' is currently at offset 5. Consider setting '@align(4)' on this member
 12:34 note: see layout of struct:
 /*           align(4) size(12) */ struct S {
 /* offset(0) align(4) size( 5) */   a : f32;
@@ -120,7 +120,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: the offset of a struct member of type 'Inner' in address space 'uniform' must be a multiple of 16 bytes, but 'inner' is currently at offset 4. Consider setting @align(16) on this member
+        R"(56:78 error: the offset of a struct member of type 'Inner' in address space 'uniform' must be a multiple of 16 bytes, but 'inner' is currently at offset 4. Consider setting '@align(16)' on this member
 34:56 note: see layout of struct:
 /*           align(4) size(8) */ struct Outer {
 /* offset(0) align(4) size(4) */   scalar : f32;
@@ -189,7 +189,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: the offset of a struct member of type '@stride(16) array<f32, 10>' in address space 'uniform' must be a multiple of 16 bytes, but 'inner' is currently at offset 4. Consider setting @align(16) on this member
+        R"(56:78 error: the offset of a struct member of type '@stride(16) array<f32, 10>' in address space 'uniform' must be a multiple of 16 bytes, but 'inner' is currently at offset 4. Consider setting '@align(16)' on this member
 12:34 note: see layout of struct:
 /*             align(4) size(164) */ struct Outer {
 /* offset(  0) align(4) size(  4) */   scalar : f32;
@@ -254,7 +254,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(78:90 error: uniform storage requires that the number of bytes between the start of the previous member of type struct and the current member be a multiple of 16 bytes, but there are currently 8 bytes between 'inner' and 'scalar'. Consider setting @align(16) on this member
+        R"(78:90 error: 'uniform' storage requires that the number of bytes between the start of the previous member of type struct and the current member be a multiple of 16 bytes, but there are currently 8 bytes between 'inner' and 'scalar'. Consider setting '@align(16)' on this member
 34:56 note: see layout of struct:
 /*            align(4) size(12) */ struct Outer {
 /* offset( 0) align(1) size( 5) */   inner : Inner;
@@ -306,7 +306,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(78:90 error: uniform storage requires that the number of bytes between the start of the previous member of type struct and the current member be a multiple of 16 bytes, but there are currently 20 bytes between 'inner' and 'scalar'. Consider setting @align(16) on this member
+        R"(78:90 error: 'uniform' storage requires that the number of bytes between the start of the previous member of type struct and the current member be a multiple of 16 bytes, but there are currently 20 bytes between 'inner' and 'scalar'. Consider setting '@align(16)' on this member
 34:56 note: see layout of struct:
 /*            align(4) size(24) */ struct Outer {
 /* offset( 0) align(4) size(20) */   inner : Inner;
@@ -424,7 +424,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
+        R"(34:56 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
 12:34 note: see layout of struct:
 /*            align(4) size(44) */ struct Outer {
 /* offset( 0) align(4) size(40) */   inner : array<f32, 10>;
@@ -458,7 +458,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'vec2<f32>' has a stride of 8 bytes. Consider using a vec4 instead.
+        R"(34:56 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'vec2<f32>' has a stride of 8 bytes. Consider using a vec4 instead.
 12:34 note: see layout of struct:
 /*            align(8) size(88) */ struct Outer {
 /* offset( 0) align(8) size(80) */   inner : array<vec2<f32>, 10>;
@@ -501,7 +501,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'ArrayElem' has a stride of 8 bytes. Consider using the @size attribute on the last struct member.
+        R"(34:56 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'ArrayElem' has a stride of 8 bytes. Consider using the '@size' attribute on the last struct member.
 12:34 note: see layout of struct:
 /*            align(4) size(84) */ struct Outer {
 /* offset( 0) align(4) size(80) */   inner : array<ArrayElem, 10>;
@@ -519,7 +519,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(78:90 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.)");
+        R"(78:90 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.)");
 }
 
 TEST_F(ResolverAddressSpaceLayoutValidationTest, UniformBuffer_InvalidArrayStride_NestedArray) {
@@ -541,7 +541,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
+        R"(34:56 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'f32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
 12:34 note: see layout of struct:
 /*            align(4) size(64) */ struct Outer {
 /* offset( 0) align(4) size(64) */   inner : array<array<f32, 4>, 4>;
@@ -591,7 +591,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(34:56 error: the offset of a struct member of type 'f32' in address space 'push_constant' must be a multiple of 4 bytes, but 'b' is currently at offset 5. Consider setting @align(4) on this member
+        R"(34:56 error: the offset of a struct member of type 'f32' in address space 'push_constant' must be a multiple of 4 bytes, but 'b' is currently at offset 5. Consider setting '@align(4)' on this member
 12:34 note: see layout of struct:
 /*           align(4) size(12) */ struct S {
 /* offset(0) align(4) size( 5) */   a : f32;
diff --git a/src/tint/lang/wgsl/resolver/address_space_validation_test.cc b/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
index c415620..aacf005 100644
--- a/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/address_space_validation_test.cc
@@ -56,7 +56,7 @@
     Alias("g", ty(Source{{12, 34}}, "ptr", ty.f32()));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: 'ptr' requires at least 2 template arguments");
+    EXPECT_EQ(r()->error(), R"(12:34 error: 'ptr' requires at least 2 template arguments)");
 }
 
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_FunctionAddressSpace_Fail) {
@@ -171,7 +171,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -183,7 +183,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 note: while instantiating ptr<storage, bool, read>)");
 }
 
@@ -198,7 +198,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -212,7 +212,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 note: while instantiating ptr<storage, bool, read>)");
 }
 
@@ -225,7 +225,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'ptr<private, f32, read_write>' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -557,7 +557,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: uniform storage requires that array elements are aligned to 16 bytes, but array element of type 'i32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
+        R"(12:34 error: 'uniform' storage requires that array elements are aligned to 16 bytes, but array element of type 'i32' has a stride of 4 bytes. Consider using a vector or struct as the element type instead.
 note: see layout of struct:
 /*           align(4) size(4) */ struct S {
 /* offset(0) align(4) size(4) */   m : array<i32>;
@@ -574,7 +574,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -586,7 +586,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
 56:78 note: while instantiating ptr<uniform, bool, read>)");
 }
 
@@ -601,7 +601,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -615,7 +615,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'uniform' as it is non-host-shareable
 56:78 note: while instantiating ptr<uniform, bool, read>)");
 }
 
@@ -628,7 +628,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'uniform' as it is non-host-shareable
+        R"(12:34 error: type 'ptr<private, f32, read_write>' cannot be used in address space 'uniform' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -852,7 +852,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'push_constant' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'push_constant' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -865,7 +865,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'push_constant' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'push_constant' as it is non-host-shareable
 note: while instantiating ptr<push_constant, bool, read_write>)");
 }
 
@@ -879,7 +879,7 @@
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "error: using f16 types in 'push_constant' address space is not implemented yet");
+              "error: using 'f16' in 'push_constant' address space is not implemented yet");
 }
 
 TEST_F(ResolverAddressSpaceValidationTest, PointerAlias_PushConstantF16) {
@@ -892,7 +892,7 @@
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "error: using f16 types in 'push_constant' address space is not implemented yet");
+              "error: using 'f16' in 'push_constant' address space is not implemented yet");
 }
 
 TEST_F(ResolverAddressSpaceValidationTest, GlobalVariable_PushConstantPointer) {
@@ -905,7 +905,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'ptr<private, f32, read_write>' cannot be used in address space 'push_constant' as it is non-host-shareable
+        R"(12:34 error: type 'ptr<private, f32, read_write>' cannot be used in address space 'push_constant' as it is non-host-shareable
 56:78 note: while instantiating 'var' g)");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/assignment_validation_test.cc b/src/tint/lang/wgsl/resolver/assignment_validation_test.cc
index 826fbd0..61eb5a5 100644
--- a/src/tint/lang/wgsl/resolver/assignment_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/assignment_validation_test.cc
@@ -54,8 +54,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: cannot store into a read-only type 'ref<storage, "
-              "i32, read>'");
+              R"(56:78 error: cannot store into a read-only type 'ref<storage, i32, read>')");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignIncompatibleTypes) {
@@ -234,9 +233,9 @@
     WrapInFunction(Assign(Expr(Source{{12, 34}}, "a"), 2_i));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to override 'a'
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to 'override a'
 12:34 note: 'override' variables are immutable
-56:78 note: override 'a' declared here)");
+56:78 note: 'override a' declared here)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignToLet_Fail) {
@@ -248,9 +247,9 @@
                    Assign(Expr(Source{{12, 34}}, "a"), 2_i));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to let 'a'
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to 'let a'
 12:34 note: 'let' variables are immutable
-56:78 note: let 'a' declared here)");
+56:78 note: 'let a' declared here)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignToConst_Fail) {
@@ -262,9 +261,9 @@
                    Assign(Expr(Source{{12, 34}}, "a"), 2_i));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to const 'a'
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to 'const a'
 12:34 note: 'const' variables are immutable
-56:78 note: const 'a' declared here)");
+56:78 note: 'const a' declared here)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignToParam_Fail) {
@@ -294,7 +293,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: cannot assign to value of type 'i32'
 56:78 note: 'let' variables are immutable
-98:76 note: let 'a' declared here)");
+98:76 note: 'let a' declared here)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignNonConstructible_Handle) {
@@ -366,10 +365,9 @@
     WrapInFunction(Assign(Phony(), Expr(Source{{12, 34}}, "s")));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: cannot assign 'S' to '_'. "
-              "'_' can only be assigned a constructible, pointer, texture or "
-              "sampler type");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: cannot assign 'S' to '_'. '_' can only be assigned a constructible, pointer, texture or sampler type)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignToPhony_DynamicArray_Fail) {
@@ -388,10 +386,9 @@
     WrapInFunction(Assign(Phony(), MemberAccessor(Source{{12, 34}}, "s", "arr")));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: cannot assign 'array<i32>' to '_'. "
-              "'_' can only be assigned a constructible, pointer, texture or sampler "
-              "type");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: cannot assign 'array<i32>' to '_'. '_' can only be assigned a constructible, pointer, texture or sampler type)");
 }
 
 TEST_F(ResolverAssignmentValidationTest, AssignToPhony_Pass) {
diff --git a/src/tint/lang/wgsl/resolver/atomics_validation_test.cc b/src/tint/lang/wgsl/resolver/atomics_validation_test.cc
index 890e076..bd8cf06 100644
--- a/src/tint/lang/wgsl/resolver/atomics_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/atomics_validation_test.cc
@@ -74,7 +74,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: atomic variables must have <storage> or <workgroup> address space");
+              "12:34 error: 'atomic' variables must have 'storage' or 'workgroup' address space");
 }
 
 TEST_F(ResolverAtomicValidationTest, InvalidAddressSpace_Array) {
@@ -82,7 +82,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: atomic variables must have <storage> or <workgroup> address space");
+              "12:34 error: 'atomic' variables must have 'storage' or 'workgroup' address space");
 }
 
 TEST_F(ResolverAtomicValidationTest, InvalidAddressSpace_Struct) {
@@ -91,8 +91,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: atomic variables must have <storage> or <workgroup> address space\n"
-              "note: atomic sub-type of 's' is declared here");
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
+note: atomic sub-type of 's' is declared here)");
 }
 
 TEST_F(ResolverAtomicValidationTest, InvalidAddressSpace_StructOfStruct) {
@@ -106,8 +106,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: atomic variables must have <storage> or <workgroup> address space\n"
-              "note: atomic sub-type of 'Outer' is declared here");
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
+note: atomic sub-type of 'Outer' is declared here)");
 }
 
 TEST_F(ResolverAtomicValidationTest, InvalidAddressSpace_StructOfStructOfArray) {
@@ -121,7 +121,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(56:78 error: atomic variables must have <storage> or <workgroup> address space
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
 12:34 note: atomic sub-type of 'Outer' is declared here)");
 }
 
@@ -134,8 +134,9 @@
     GlobalVar(Source{{56, 78}}, "v", ty.Of(atomic_array), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "56:78 error: atomic variables must have <storage> or <workgroup> address space");
+    EXPECT_EQ(
+        r()->error(),
+        R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space)");
 }
 
 TEST_F(ResolverAtomicValidationTest, InvalidAddressSpace_ArrayOfStruct) {
@@ -149,7 +150,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(56:78 error: atomic variables must have <storage> or <workgroup> address space
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
 12:34 note: atomic sub-type of 'array<S, 5>' is declared here)");
 }
 
@@ -166,7 +167,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(56:78 error: atomic variables must have <storage> or <workgroup> address space
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
 12:34 note: atomic sub-type of 'array<S, 5>' is declared here)");
 }
 
@@ -205,7 +206,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(56:78 error: atomic variables must have <storage> or <workgroup> address space
+              R"(56:78 error: 'atomic' variables must have 'storage' or 'workgroup' address space
 12:34 note: atomic sub-type of 'S0' is declared here)");
 }
 
@@ -217,7 +218,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: atomic variables in <storage> address space must have read_write access mode
+        R"(56:78 error: atomic variables in 'storage' address space must have 'read_write' access mode
 12:34 note: atomic sub-type of 's' is declared here)");
 }
 
@@ -229,7 +230,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: atomic variables in <storage> address space must have read_write access mode
+        R"(56:78 error: atomic variables in 'storage' address space must have 'read_write' access mode
 12:34 note: atomic sub-type of 's' is declared here)");
 }
 
@@ -246,7 +247,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: atomic variables in <storage> address space must have read_write access mode
+        R"(56:78 error: atomic variables in 'storage' address space must have 'read_write' access mode
 12:34 note: atomic sub-type of 'Outer' is declared here)");
 }
 
@@ -263,7 +264,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: atomic variables in <storage> address space must have read_write access mode
+        R"(56:78 error: atomic variables in 'storage' address space must have 'read_write' access mode
 12:34 note: atomic sub-type of 'Outer' is declared here)");
 }
 
@@ -304,7 +305,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: atomic variables in <storage> address space must have read_write access mode
+        R"(12:34 error: atomic variables in 'storage' address space must have 'read_write' access mode
 56:78 note: atomic sub-type of 'S0' is declared here)");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
index 33cccae..a36e21f 100644
--- a/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/attribute_validation_test.cc
@@ -136,23 +136,23 @@
 static std::vector<TestParams> OnlyDiagnosticValidFor(std::string thing) {
     return {TestParams{
                 {AttributeKind::kAlign},
-                "1:2 error: @align is not valid for " + thing,
+                "1:2 error: '@align' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kBinding},
-                "1:2 error: @binding is not valid for " + thing,
+                "1:2 error: '@binding' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kBlendSrc},
-                "1:2 error: @blend_src is not valid for " + thing,
+                "1:2 error: '@blend_src' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kBuiltinPosition},
-                "1:2 error: @builtin is not valid for " + thing,
+                "1:2 error: '@builtin' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kColor},
-                "1:2 error: @color is not valid for " + thing,
+                "1:2 error: '@color' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kDiagnostic},
@@ -160,51 +160,51 @@
             },
             TestParams{
                 {AttributeKind::kGroup},
-                "1:2 error: @group is not valid for " + thing,
+                "1:2 error: '@group' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kId},
-                "1:2 error: @id is not valid for " + thing,
+                "1:2 error: '@id' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kInterpolate},
-                "1:2 error: @interpolate is not valid for " + thing,
+                "1:2 error: '@interpolate' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kInvariant},
-                "1:2 error: @invariant is not valid for " + thing,
+                "1:2 error: '@invariant' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kLocation},
-                "1:2 error: @location is not valid for " + thing,
+                "1:2 error: '@location' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kMustUse},
-                "1:2 error: @must_use is not valid for " + thing,
+                "1:2 error: '@must_use' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kOffset},
-                "1:2 error: @offset is not valid for " + thing,
+                "1:2 error: '@offset' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kSize},
-                "1:2 error: @size is not valid for " + thing,
+                "1:2 error: '@size' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kStageCompute},
-                "1:2 error: @stage is not valid for " + thing,
+                "1:2 error: '@stage' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kStride},
-                "1:2 error: @stride is not valid for " + thing,
+                "1:2 error: '@stride' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kWorkgroupSize},
-                "1:2 error: @workgroup_size is not valid for " + thing,
+                "1:2 error: '@workgroup_size' is not valid for " + thing,
             },
             TestParams{
                 {AttributeKind::kBinding, AttributeKind::kGroup},
-                "1:2 error: @binding is not valid for " + thing,
+                "1:2 error: '@binding' is not valid for " + thing,
             }};
 }
 
@@ -316,23 +316,23 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for functions)",
+            R"(1:2 error: '@align' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for functions)",
+            R"(1:2 error: '@binding' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for functions)",
+            R"(1:2 error: '@blend_src' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for functions)",
+            R"(1:2 error: '@builtin' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for functions)",
+            R"(1:2 error: '@color' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
@@ -340,39 +340,39 @@
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for functions)",
+            R"(1:2 error: '@group' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for functions)",
+            R"(1:2 error: '@id' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for functions)",
+            R"(1:2 error: '@interpolate' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for functions)",
+            R"(1:2 error: '@invariant' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for functions)",
+            R"(1:2 error: '@location' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use can only be applied to functions that return a value)",
+            R"(1:2 error: '@must_use' can only be applied to functions that return a value)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for functions)",
+            R"(1:2 error: '@offset' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for functions)",
+            R"(1:2 error: '@size' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(9:9 error: a compute shader must include 'workgroup_size' in its attributes)",
+            R"(9:9 error: a compute shader must include '@workgroup_size' in its attributes)",
         },
         TestParams{
             {AttributeKind::kStageCompute, AttributeKind::kWorkgroupSize},
@@ -380,11 +380,11 @@
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for functions)",
+            R"(1:2 error: '@stride' is not valid for functions)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is only valid for compute stages)",
+            R"(1:2 error: '@workgroup_size' is only valid for compute stages)",
         }));
 
 using NonVoidFunctionAttributeTest = TestWithParams;
@@ -400,23 +400,23 @@
                          testing::Values(
                              TestParams{
                                  {AttributeKind::kAlign},
-                                 R"(1:2 error: @align is not valid for functions)",
+                                 R"(1:2 error: '@align' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kBinding},
-                                 R"(1:2 error: @binding is not valid for functions)",
+                                 R"(1:2 error: '@binding' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kBlendSrc},
-                                 R"(1:2 error: @blend_src is not valid for functions)",
+                                 R"(1:2 error: '@blend_src' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kBuiltinPosition},
-                                 R"(1:2 error: @builtin is not valid for functions)",
+                                 R"(1:2 error: '@builtin' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kColor},
-                                 R"(1:2 error: @color is not valid for functions)",
+                                 R"(1:2 error: '@color' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kDiagnostic},
@@ -424,23 +424,23 @@
                              },
                              TestParams{
                                  {AttributeKind::kGroup},
-                                 R"(1:2 error: @group is not valid for functions)",
+                                 R"(1:2 error: '@group' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kId},
-                                 R"(1:2 error: @id is not valid for functions)",
+                                 R"(1:2 error: '@id' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kInterpolate},
-                                 R"(1:2 error: @interpolate is not valid for functions)",
+                                 R"(1:2 error: '@interpolate' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kInvariant},
-                                 R"(1:2 error: @invariant is not valid for functions)",
+                                 R"(1:2 error: '@invariant' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kLocation},
-                                 R"(1:2 error: @location is not valid for functions)",
+                                 R"(1:2 error: '@location' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kMustUse},
@@ -448,11 +448,11 @@
                              },
                              TestParams{
                                  {AttributeKind::kOffset},
-                                 R"(1:2 error: @offset is not valid for functions)",
+                                 R"(1:2 error: '@offset' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kSize},
-                                 R"(1:2 error: @size is not valid for functions)",
+                                 R"(1:2 error: '@size' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kStageCompute},
@@ -464,11 +464,11 @@
                              },
                              TestParams{
                                  {AttributeKind::kStride},
-                                 R"(1:2 error: @stride is not valid for functions)",
+                                 R"(1:2 error: '@stride' is not valid for functions)",
                              },
                              TestParams{
                                  {AttributeKind::kWorkgroupSize},
-                                 R"(1:2 error: @workgroup_size is only valid for compute stages)",
+                                 R"(1:2 error: '@workgroup_size' is only valid for compute stages)",
                              }));
 }  // namespace FunctionTests
 
@@ -491,71 +491,71 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for function parameters)",
+            R"(1:2 error: '@align' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for function parameters)",
+            R"(1:2 error: '@binding' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for function parameters)",
+            R"(1:2 error: '@blend_src' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for non-entry point function parameters)",
+            R"(1:2 error: '@builtin' is not valid for non-entry point function parameters)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for function parameters)",
+            R"(1:2 error: '@color' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for function parameters)",
+            R"(1:2 error: '@diagnostic' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for function parameters)",
+            R"(1:2 error: '@group' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for function parameters)",
+            R"(1:2 error: '@id' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for non-entry point function parameters)",
+            R"(1:2 error: '@interpolate' is not valid for non-entry point function parameters)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for non-entry point function parameters)",
+            R"(1:2 error: '@invariant' is not valid for non-entry point function parameters)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for non-entry point function parameters)",
+            R"(1:2 error: '@location' is not valid for non-entry point function parameters)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for function parameters)",
+            R"(1:2 error: '@must_use' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for function parameters)",
+            R"(1:2 error: '@offset' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for function parameters)",
+            R"(1:2 error: '@size' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for function parameters)",
+            R"(1:2 error: '@stage' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for function parameters)",
+            R"(1:2 error: '@stride' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+            R"(1:2 error: '@workgroup_size' is not valid for function parameters)",
         }));
 
 using FunctionReturnTypeAttributeTest = TestWithParams;
@@ -576,71 +576,71 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for non-entry point function return types)",
+            R"(1:2 error: '@align' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for non-entry point function return types)",
+            R"(1:2 error: '@binding' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for non-entry point function return types)",
+            R"(1:2 error: '@blend_src' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for non-entry point function return types)",
+            R"(1:2 error: '@builtin' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for non-entry point function return types)",
+            R"(1:2 error: '@color' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for non-entry point function return types)",
+            R"(1:2 error: '@diagnostic' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for non-entry point function return types)",
+            R"(1:2 error: '@group' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for non-entry point function return types)",
+            R"(1:2 error: '@id' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for non-entry point function return types)",
+            R"(1:2 error: '@interpolate' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for non-entry point function return types)",
+            R"(1:2 error: '@invariant' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for non-entry point function return types)",
+            R"(1:2 error: '@location' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for non-entry point function return types)",
+            R"(1:2 error: '@must_use' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for non-entry point function return types)",
+            R"(1:2 error: '@offset' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for non-entry point function return types)",
+            R"(1:2 error: '@size' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for non-entry point function return types)",
+            R"(1:2 error: '@stage' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for non-entry point function return types)",
+            R"(1:2 error: '@stride' is not valid for non-entry point function return types)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for non-entry point function return types)",
+            R"(1:2 error: '@workgroup_size' is not valid for non-entry point function return types)",
         }));
 }  // namespace FunctionInputAndOutputTests
 
@@ -666,71 +666,71 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for function parameters)",
+            R"(1:2 error: '@align' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for function parameters)",
+            R"(1:2 error: '@binding' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for function parameters)",
+            R"(1:2 error: '@blend_src' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin(position) cannot be used for compute shader input)",
+            R"(1:2 error: '@builtin(position)' cannot be used for compute shader input)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color can only be used for fragment shader input)",
+            R"(1:2 error: '@color' can only be used for fragment shader input)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for function parameters)",
+            R"(1:2 error: '@diagnostic' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for function parameters)",
+            R"(1:2 error: '@group' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for function parameters)",
+            R"(1:2 error: '@id' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate cannot be used by compute shaders)",
+            R"(1:2 error: '@interpolate' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant cannot be used by compute shaders)",
+            R"(1:2 error: '@invariant' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location cannot be used by compute shaders)",
+            R"(1:2 error: '@location' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for function parameters)",
+            R"(1:2 error: '@must_use' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for function parameters)",
+            R"(1:2 error: '@offset' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for function parameters)",
+            R"(1:2 error: '@size' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for function parameters)",
+            R"(1:2 error: '@stage' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for function parameters)",
+            R"(1:2 error: '@stride' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+            R"(1:2 error: '@workgroup_size' is not valid for function parameters)",
         }));
 
 using FragmentShaderParameterAttributeTest = TestWithParams;
@@ -750,15 +750,15 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for function parameters)",
+            R"(1:2 error: '@align' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for function parameters)",
+            R"(1:2 error: '@binding' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for function parameters)",
+            R"(1:2 error: '@blend_src' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
@@ -771,19 +771,19 @@
         TestParams{
             {AttributeKind::kColor, AttributeKind::kLocation},
             R"(3:4 error: multiple entry point IO attributes
-1:2 note: previously consumed @color)",
+1:2 note: previously consumed '@color')",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for function parameters)",
+            R"(1:2 error: '@diagnostic' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for function parameters)",
+            R"(1:2 error: '@group' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for function parameters)",
+            R"(1:2 error: '@id' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
@@ -791,7 +791,7 @@
         },
         TestParams{
             {AttributeKind::kInterpolate, AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @interpolate can only be used with @location)",
+            R"(1:2 error: '@interpolate' can only be used with '@location')",
         },
         TestParams{
             {AttributeKind::kInterpolate, AttributeKind::kLocation},
@@ -811,27 +811,27 @@
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for function parameters)",
+            R"(1:2 error: '@must_use' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for function parameters)",
+            R"(1:2 error: '@offset' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for function parameters)",
+            R"(1:2 error: '@size' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for function parameters)",
+            R"(1:2 error: '@stage' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for function parameters)",
+            R"(1:2 error: '@stride' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+            R"(1:2 error: '@workgroup_size' is not valid for function parameters)",
         }));
 
 using VertexShaderParameterAttributeTest = TestWithParams;
@@ -858,35 +858,35 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for function parameters)",
+            R"(1:2 error: '@align' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for function parameters)",
+            R"(1:2 error: '@binding' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for function parameters)",
+            R"(1:2 error: '@blend_src' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin(position) cannot be used for vertex shader input)",
+            R"(1:2 error: '@builtin(position)' cannot be used for vertex shader input)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color can only be used for fragment shader input)",
+            R"(1:2 error: '@color' can only be used for fragment shader input)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for function parameters)",
+            R"(1:2 error: '@diagnostic' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for function parameters)",
+            R"(1:2 error: '@group' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for function parameters)",
+            R"(1:2 error: '@id' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
@@ -898,7 +898,7 @@
         },
         TestParams{
             {AttributeKind::kInterpolate, AttributeKind::kBuiltinPosition},
-            R"(3:4 error: @builtin(position) cannot be used for vertex shader input)",
+            R"(3:4 error: '@builtin(position)' cannot be used for vertex shader input)",
         },
         TestParams{
             {AttributeKind::kInvariant},
@@ -906,11 +906,11 @@
         },
         TestParams{
             {AttributeKind::kInvariant, AttributeKind::kLocation},
-            R"(1:2 error: @invariant must be applied to a position builtin)",
+            R"(1:2 error: '@invariant' must be applied to a '@builtin(position)')",
         },
         TestParams{
             {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
-            R"(3:4 error: @builtin(position) cannot be used for vertex shader input)",
+            R"(3:4 error: '@builtin(position)' cannot be used for vertex shader input)",
         },
         TestParams{
             {AttributeKind::kLocation},
@@ -918,27 +918,27 @@
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for function parameters)",
+            R"(1:2 error: '@must_use' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for function parameters)",
+            R"(1:2 error: '@offset' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for function parameters)",
+            R"(1:2 error: '@size' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for function parameters)",
+            R"(1:2 error: '@stage' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for function parameters)",
+            R"(1:2 error: '@stride' is not valid for function parameters)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for function parameters)",
+            R"(1:2 error: '@workgroup_size' is not valid for function parameters)",
         }));
 
 using ComputeShaderReturnTypeAttributeTest = TestWithParams;
@@ -963,71 +963,71 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for entry point return types)",
+            R"(1:2 error: '@align' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for entry point return types)",
+            R"(1:2 error: '@binding' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src can only be used for fragment shader output)",
+            R"(1:2 error: '@blend_src' can only be used for fragment shader output)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin(position) cannot be used for compute shader output)",
+            R"(1:2 error: '@builtin(position)' cannot be used for compute shader output)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for entry point return types)",
+            R"(1:2 error: '@color' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+            R"(1:2 error: '@diagnostic' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for entry point return types)",
+            R"(1:2 error: '@group' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for entry point return types)",
+            R"(1:2 error: '@id' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate cannot be used by compute shaders)",
+            R"(1:2 error: '@interpolate' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant cannot be used by compute shaders)",
+            R"(1:2 error: '@invariant' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location cannot be used by compute shaders)",
+            R"(1:2 error: '@location' cannot be used by compute shaders)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for entry point return types)",
+            R"(1:2 error: '@must_use' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for entry point return types)",
+            R"(1:2 error: '@offset' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for entry point return types)",
+            R"(1:2 error: '@size' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for entry point return types)",
+            R"(1:2 error: '@stage' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for entry point return types)",
+            R"(1:2 error: '@stride' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+            R"(1:2 error: '@workgroup_size' is not valid for entry point return types)",
         }));
 
 using FragmentShaderReturnTypeAttributeTest = TestWithParams;
@@ -1049,11 +1049,11 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for entry point return types)",
+            R"(1:2 error: '@align' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for entry point return types)",
+            R"(1:2 error: '@binding' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
@@ -1065,23 +1065,23 @@
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin(position) cannot be used for fragment shader output)",
+            R"(1:2 error: '@builtin(position)' cannot be used for fragment shader output)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for entry point return types)",
+            R"(1:2 error: '@color' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+            R"(1:2 error: '@diagnostic' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for entry point return types)",
+            R"(1:2 error: '@group' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for entry point return types)",
+            R"(1:2 error: '@id' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
@@ -1097,7 +1097,7 @@
         },
         TestParams{
             {AttributeKind::kInvariant, AttributeKind::kLocation},
-            R"(1:2 error: @invariant must be applied to a position builtin)",
+            R"(1:2 error: '@invariant' must be applied to a '@builtin(position)')",
         },
         TestParams{
             {AttributeKind::kLocation},
@@ -1105,31 +1105,31 @@
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for entry point return types)",
+            R"(1:2 error: '@must_use' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for entry point return types)",
+            R"(1:2 error: '@offset' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for entry point return types)",
+            R"(1:2 error: '@size' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for entry point return types)",
+            R"(1:2 error: '@stage' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for entry point return types)",
+            R"(1:2 error: '@stride' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+            R"(1:2 error: '@workgroup_size' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
-            R"(1:2 error: @binding is not valid for entry point return types)",
+            R"(1:2 error: '@binding' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBlendSrc, AttributeKind::kLocation},
@@ -1161,15 +1161,15 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for entry point return types)",
+            R"(1:2 error: '@align' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for entry point return types)",
+            R"(1:2 error: '@binding' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src can only be used for fragment shader output)",
+            R"(1:2 error: '@blend_src' can only be used for fragment shader output)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
@@ -1177,23 +1177,23 @@
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for entry point return types)",
+            R"(1:2 error: '@color' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for entry point return types)",
+            R"(1:2 error: '@diagnostic' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for entry point return types)",
+            R"(1:2 error: '@group' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for entry point return types)",
+            R"(1:2 error: '@id' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate can only be used with @location)",
+            R"(1:2 error: '@interpolate' can only be used with '@location')",
         },
         TestParams{
             {AttributeKind::kInvariant},
@@ -1202,35 +1202,35 @@
         TestParams{
             {AttributeKind::kLocation},
             R"(9:9 error: multiple entry point IO attributes
-1:2 note: previously consumed @location)",
+1:2 note: previously consumed '@location')",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for entry point return types)",
+            R"(1:2 error: '@must_use' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for entry point return types)",
+            R"(1:2 error: '@offset' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for entry point return types)",
+            R"(1:2 error: '@size' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for entry point return types)",
+            R"(1:2 error: '@stage' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for entry point return types)",
+            R"(1:2 error: '@stride' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for entry point return types)",
+            R"(1:2 error: '@workgroup_size' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
-            R"(1:2 error: @binding is not valid for entry point return types)",
+            R"(1:2 error: '@binding' is not valid for entry point return types)",
         },
         TestParams{
             {AttributeKind::kLocation, AttributeKind::kLocation},
@@ -1286,75 +1286,75 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for struct declarations)",
+            R"(1:2 error: '@align' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for struct declarations)",
+            R"(1:2 error: '@binding' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for struct declarations)",
+            R"(1:2 error: '@blend_src' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for struct declarations)",
+            R"(1:2 error: '@builtin' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for struct declarations)",
+            R"(1:2 error: '@diagnostic' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kColor},
-            R"(1:2 error: @color is not valid for struct declarations)",
+            R"(1:2 error: '@color' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for struct declarations)",
+            R"(1:2 error: '@group' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for struct declarations)",
+            R"(1:2 error: '@id' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for struct declarations)",
+            R"(1:2 error: '@interpolate' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for struct declarations)",
+            R"(1:2 error: '@invariant' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for struct declarations)",
+            R"(1:2 error: '@location' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for struct declarations)",
+            R"(1:2 error: '@must_use' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for struct declarations)",
+            R"(1:2 error: '@offset' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for struct declarations)",
+            R"(1:2 error: '@size' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for struct declarations)",
+            R"(1:2 error: '@stage' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for struct declarations)",
+            R"(1:2 error: '@stride' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for struct declarations)",
+            R"(1:2 error: '@workgroup_size' is not valid for 'struct' declarations)",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
-            R"(1:2 error: @binding is not valid for struct declarations)",
+            R"(1:2 error: '@binding' is not valid for 'struct' declarations)",
         }));
 
 using StructMemberAttributeTest = TestWithParams;
@@ -1364,94 +1364,95 @@
 
     CHECK();
 }
-INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
-                         StructMemberAttributeTest,
-                         testing::Values(
-                             TestParams{
-                                 {AttributeKind::kAlign},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kBinding},
-                                 R"(1:2 error: @binding is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kBlendSrc},
-                                 R"(1:2 error: @blend_src can only be used with @location(0))",
-                             },
-                             TestParams{
-                                 {AttributeKind::kBuiltinPosition},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kColor},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kDiagnostic},
-                                 R"(1:2 error: @diagnostic is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kGroup},
-                                 R"(1:2 error: @group is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kId},
-                                 R"(1:2 error: @id is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kInterpolate},
-                                 R"(1:2 error: @interpolate can only be used with @location)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kInterpolate, AttributeKind::kLocation},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kInvariant},
-                                 R"(1:2 error: @invariant must be applied to a position builtin)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kLocation},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kMustUse},
-                                 R"(1:2 error: @must_use is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kOffset},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kSize},
-                                 Pass,
-                             },
-                             TestParams{
-                                 {AttributeKind::kStageCompute},
-                                 R"(1:2 error: @stage is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kStride},
-                                 R"(1:2 error: @stride is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kWorkgroupSize},
-                                 R"(1:2 error: @workgroup_size is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kBinding, AttributeKind::kGroup},
-                                 R"(1:2 error: @binding is not valid for struct members)",
-                             },
-                             TestParams{
-                                 {AttributeKind::kAlign, AttributeKind::kAlign},
-                                 R"(3:4 error: duplicate align attribute
+INSTANTIATE_TEST_SUITE_P(
+    ResolverAttributeValidationTest,
+    StructMemberAttributeTest,
+    testing::Values(
+        TestParams{
+            {AttributeKind::kAlign},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kBinding},
+            R"(1:2 error: '@binding' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kBlendSrc},
+            R"(1:2 error: '@blend_src' can only be used with '@location(0)')",
+        },
+        TestParams{
+            {AttributeKind::kBuiltinPosition},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kColor},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kDiagnostic},
+            R"(1:2 error: '@diagnostic' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kGroup},
+            R"(1:2 error: '@group' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kId},
+            R"(1:2 error: '@id' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate},
+            R"(1:2 error: '@interpolate' can only be used with '@location')",
+        },
+        TestParams{
+            {AttributeKind::kInterpolate, AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kInvariant},
+            R"(1:2 error: '@invariant' must be applied to a position builtin)",
+        },
+        TestParams{
+            {AttributeKind::kInvariant, AttributeKind::kBuiltinPosition},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kLocation},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kMustUse},
+            R"(1:2 error: '@must_use' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kOffset},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kSize},
+            Pass,
+        },
+        TestParams{
+            {AttributeKind::kStageCompute},
+            R"(1:2 error: '@stage' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kStride},
+            R"(1:2 error: '@stride' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kWorkgroupSize},
+            R"(1:2 error: '@workgroup_size' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kBinding, AttributeKind::kGroup},
+            R"(1:2 error: '@binding' is not valid for 'struct' members)",
+        },
+        TestParams{
+            {AttributeKind::kAlign, AttributeKind::kAlign},
+            R"(3:4 error: duplicate align attribute
 1:2 note: first attribute declared here)",
-                             }));
+        }));
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Const) {
     GlobalConst("val", ty.i32(), Expr(1_i));
@@ -1467,7 +1468,7 @@
               Vector{Member("a", ty.f32(), Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @align value must be a positive, power-of-two integer)");
+              R"(12:34 error: '@align' value must be a positive, power-of-two integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstPowerOfTwo) {
@@ -1477,7 +1478,7 @@
               Vector{Member("a", ty.f32(), Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @align value must be a positive, power-of-two integer)");
+              R"(12:34 error: '@align' value must be a positive, power-of-two integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstF32) {
@@ -1486,7 +1487,7 @@
     Structure("mystruct",
               Vector{Member("a", ty.f32(), Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @align must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@align' value must be an 'i32' or 'u32')");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_ConstU32) {
@@ -1511,7 +1512,7 @@
     Structure("mystruct",
               Vector{Member("a", ty.f32(), Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @align must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@align' value must be an 'i32' or 'u32')");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Var) {
@@ -1522,8 +1523,8 @@
               Vector{Member(Source{{12, 5}}, "a", ty.f32(),
                             Vector{MemberAlign(Expr(Source{{12, 35}}, "val"))})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:35 error: var 'val' cannot be referenced at module-scope
-1:2 note: var 'val' declared here)");
+    EXPECT_EQ(r()->error(), R"(12:35 error: 'var val' cannot be referenced at module-scope
+1:2 note: 'var val' declared here)");
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Override) {
@@ -1550,7 +1551,7 @@
     Structure("mystruct",
               Vector{Member("a", ty.f32(), Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be a positive integer)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@size' value must be a positive integer)");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_ConstF32) {
@@ -1559,7 +1560,7 @@
     Structure("mystruct",
               Vector{Member("a", ty.f32(), Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@size' value must be an 'i32' or 'u32')");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_ConstU32) {
@@ -1584,7 +1585,7 @@
     Structure("mystruct",
               Vector{Member("a", ty.f32(), Vector{MemberSize(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@size' value must be an 'i32' or 'u32')");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_Var) {
@@ -1595,8 +1596,8 @@
               Vector{Member(Source{{12, 5}}, "a", ty.f32(),
                             Vector{MemberSize(Expr(Source{{12, 35}}, "val"))})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:35 error: var 'val' cannot be referenced at module-scope
-1:2 note: var 'val' declared here)");
+    EXPECT_EQ(r()->error(), R"(12:35 error: 'var val' cannot be referenced at module-scope
+1:2 note: 'var val' declared here)");
 }
 
 TEST_F(StructMemberAttributeTest, Size_Attribute_Override) {
@@ -1620,7 +1621,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: @size can only be applied to members where the member's type size can be fully determined at shader creation time)");
+        R"(12:34 error: '@size' can only be applied to members where the member's type size can be fully determined at shader creation time)");
 }
 
 }  // namespace StructAndStructMemberTests
@@ -1641,59 +1642,59 @@
                          testing::Values(
                              TestParams{
                                  {AttributeKind::kAlign},
-                                 R"(1:2 error: @align is not valid for array types)",
+                                 R"(1:2 error: '@align' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kBinding},
-                                 R"(1:2 error: @binding is not valid for array types)",
+                                 R"(1:2 error: '@binding' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kBlendSrc},
-                                 R"(1:2 error: @blend_src is not valid for array types)",
+                                 R"(1:2 error: '@blend_src' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kBuiltinPosition},
-                                 R"(1:2 error: @builtin is not valid for array types)",
+                                 R"(1:2 error: '@builtin' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kDiagnostic},
-                                 R"(1:2 error: @diagnostic is not valid for array types)",
+                                 R"(1:2 error: '@diagnostic' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kGroup},
-                                 R"(1:2 error: @group is not valid for array types)",
+                                 R"(1:2 error: '@group' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kId},
-                                 R"(1:2 error: @id is not valid for array types)",
+                                 R"(1:2 error: '@id' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kInterpolate},
-                                 R"(1:2 error: @interpolate is not valid for array types)",
+                                 R"(1:2 error: '@interpolate' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kInvariant},
-                                 R"(1:2 error: @invariant is not valid for array types)",
+                                 R"(1:2 error: '@invariant' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kLocation},
-                                 R"(1:2 error: @location is not valid for array types)",
+                                 R"(1:2 error: '@location' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kMustUse},
-                                 R"(1:2 error: @must_use is not valid for array types)",
+                                 R"(1:2 error: '@must_use' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kOffset},
-                                 R"(1:2 error: @offset is not valid for array types)",
+                                 R"(1:2 error: '@offset' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kSize},
-                                 R"(1:2 error: @size is not valid for array types)",
+                                 R"(1:2 error: '@size' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kStageCompute},
-                                 R"(1:2 error: @stage is not valid for array types)",
+                                 R"(1:2 error: '@stage' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kStride},
@@ -1701,11 +1702,11 @@
                              },
                              TestParams{
                                  {AttributeKind::kWorkgroupSize},
-                                 R"(1:2 error: @workgroup_size is not valid for array types)",
+                                 R"(1:2 error: '@workgroup_size' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kBinding, AttributeKind::kGroup},
-                                 R"(1:2 error: @binding is not valid for array types)",
+                                 R"(1:2 error: '@binding' is not valid for 'array' types)",
                              },
                              TestParams{
                                  {AttributeKind::kStride, AttributeKind::kStride},
@@ -1732,67 +1733,67 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for module-scope 'var')",
+            R"(1:2 error: '@align' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(9:9 error: resource variables require @group and @binding attributes)",
+            R"(9:9 error: resource variables require '@group' and '@binding' attributes)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for module-scope 'var')",
+            R"(1:2 error: '@blend_src' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for module-scope 'var')",
+            R"(1:2 error: '@builtin' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for module-scope 'var')",
+            R"(1:2 error: '@diagnostic' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(9:9 error: resource variables require @group and @binding attributes)",
+            R"(9:9 error: resource variables require '@group' and '@binding' attributes)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for module-scope 'var')",
+            R"(1:2 error: '@id' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for module-scope 'var')",
+            R"(1:2 error: '@interpolate' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for module-scope 'var')",
+            R"(1:2 error: '@invariant' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for module-scope 'var')",
+            R"(1:2 error: '@location' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for module-scope 'var')",
+            R"(1:2 error: '@must_use' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for module-scope 'var')",
+            R"(1:2 error: '@offset' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for module-scope 'var')",
+            R"(1:2 error: '@size' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for module-scope 'var')",
+            R"(1:2 error: '@stage' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for module-scope 'var')",
+            R"(1:2 error: '@stride' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for module-scope 'var')",
+            R"(1:2 error: '@workgroup_size' is not valid for module-scope 'var')",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
@@ -1810,7 +1811,7 @@
     WrapInFunction(v);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @binding is not valid for function-scope 'var'");
+    EXPECT_EQ(r()->error(), "12:34 error: '@binding' is not valid for function-scope 'var'");
 }
 
 TEST_F(VariableAttributeTest, LocalLet) {
@@ -1819,7 +1820,7 @@
     WrapInFunction(v);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @binding is not valid for 'let' declaration");
+    EXPECT_EQ(r()->error(), "12:34 error: '@binding' is not valid for 'let' declaration");
 }
 
 using ConstantAttributeTest = TestWithParams;
@@ -1836,71 +1837,71 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for 'const' declaration)",
+            R"(1:2 error: '@align' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for 'const' declaration)",
+            R"(1:2 error: '@binding' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for 'const' declaration)",
+            R"(1:2 error: '@blend_src' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for 'const' declaration)",
+            R"(1:2 error: '@builtin' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for 'const' declaration)",
+            R"(1:2 error: '@diagnostic' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for 'const' declaration)",
+            R"(1:2 error: '@group' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kId},
-            R"(1:2 error: @id is not valid for 'const' declaration)",
+            R"(1:2 error: '@id' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for 'const' declaration)",
+            R"(1:2 error: '@interpolate' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for 'const' declaration)",
+            R"(1:2 error: '@invariant' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for 'const' declaration)",
+            R"(1:2 error: '@location' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for 'const' declaration)",
+            R"(1:2 error: '@must_use' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for 'const' declaration)",
+            R"(1:2 error: '@offset' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for 'const' declaration)",
+            R"(1:2 error: '@size' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for 'const' declaration)",
+            R"(1:2 error: '@stage' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for 'const' declaration)",
+            R"(1:2 error: '@stride' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for 'const' declaration)",
+            R"(1:2 error: '@workgroup_size' is not valid for 'const' declaration)",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
-            R"(1:2 error: @binding is not valid for 'const' declaration)",
+            R"(1:2 error: '@binding' is not valid for 'const' declaration)",
         }));
 
 using OverrideAttributeTest = TestWithParams;
@@ -1917,27 +1918,27 @@
     testing::Values(
         TestParams{
             {AttributeKind::kAlign},
-            R"(1:2 error: @align is not valid for 'override' declaration)",
+            R"(1:2 error: '@align' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kBinding},
-            R"(1:2 error: @binding is not valid for 'override' declaration)",
+            R"(1:2 error: '@binding' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kBlendSrc},
-            R"(1:2 error: @blend_src is not valid for 'override' declaration)",
+            R"(1:2 error: '@blend_src' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kBuiltinPosition},
-            R"(1:2 error: @builtin is not valid for 'override' declaration)",
+            R"(1:2 error: '@builtin' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kDiagnostic},
-            R"(1:2 error: @diagnostic is not valid for 'override' declaration)",
+            R"(1:2 error: '@diagnostic' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kGroup},
-            R"(1:2 error: @group is not valid for 'override' declaration)",
+            R"(1:2 error: '@group' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kId},
@@ -1945,43 +1946,43 @@
         },
         TestParams{
             {AttributeKind::kInterpolate},
-            R"(1:2 error: @interpolate is not valid for 'override' declaration)",
+            R"(1:2 error: '@interpolate' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kInvariant},
-            R"(1:2 error: @invariant is not valid for 'override' declaration)",
+            R"(1:2 error: '@invariant' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kLocation},
-            R"(1:2 error: @location is not valid for 'override' declaration)",
+            R"(1:2 error: '@location' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kMustUse},
-            R"(1:2 error: @must_use is not valid for 'override' declaration)",
+            R"(1:2 error: '@must_use' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kOffset},
-            R"(1:2 error: @offset is not valid for 'override' declaration)",
+            R"(1:2 error: '@offset' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kSize},
-            R"(1:2 error: @size is not valid for 'override' declaration)",
+            R"(1:2 error: '@size' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kStageCompute},
-            R"(1:2 error: @stage is not valid for 'override' declaration)",
+            R"(1:2 error: '@stage' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kStride},
-            R"(1:2 error: @stride is not valid for 'override' declaration)",
+            R"(1:2 error: '@stride' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kWorkgroupSize},
-            R"(1:2 error: @workgroup_size is not valid for 'override' declaration)",
+            R"(1:2 error: '@workgroup_size' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kBinding, AttributeKind::kGroup},
-            R"(1:2 error: @binding is not valid for 'override' declaration)",
+            R"(1:2 error: '@binding' is not valid for 'override' declaration)",
         },
         TestParams{
             {AttributeKind::kId, AttributeKind::kId},
@@ -2011,7 +2012,7 @@
 }
 INSTANTIATE_TEST_SUITE_P(ResolverAttributeValidationTest,
                          SwitchBodyAttributeTest,
-                         testing::ValuesIn(OnlyDiagnosticValidFor("switch body")));
+                         testing::ValuesIn(OnlyDiagnosticValidFor("'switch' body")));
 
 using IfStatementAttributeTest = TestWithParams;
 TEST_P(IfStatementAttributeTest, IsValid) {
@@ -2278,7 +2279,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, StorageBufferMissingBinding) {
@@ -2289,7 +2290,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, TextureMissingBinding) {
@@ -2297,7 +2298,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, SamplerMissingBinding) {
@@ -2305,7 +2306,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, BindingPairMissingBinding) {
@@ -2313,7 +2314,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, BindingPairMissingGroup) {
@@ -2321,7 +2322,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: resource variables require @group and @binding attributes)");
+              R"(12:34 error: resource variables require '@group' and '@binding' attributes)");
 }
 
 TEST_F(ResourceAttributeTest, BindingPointUsedTwiceByEntryPoint) {
@@ -2346,7 +2347,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(56:78 error: entry point 'F' references multiple variables that use the same resource binding @group(2), @binding(1)
+        R"(56:78 error: entry point 'F' references multiple variables that use the same resource binding '@group(2)', '@binding(1)'
 12:34 note: first resource binding usage declared here)");
 }
 
@@ -2383,8 +2384,9 @@
               Group(2_a));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              R"(12:34 error: non-resource variables must not have @group or @binding attributes)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: non-resource variables must not have '@group' or '@binding' attributes)");
 }
 
 }  // namespace
@@ -2401,7 +2403,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @workgroup_size is only valid for compute stages)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' is only valid for compute stages)");
 }
 
 TEST_F(WorkgroupAttribute, NotAComputeShader) {
@@ -2412,7 +2414,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @workgroup_size is only valid for compute stages)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' is only valid for compute stages)");
 }
 
 TEST_F(WorkgroupAttribute, DuplicateAttribute) {
@@ -2560,7 +2562,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: integral user-defined fragment inputs must have a flat interpolation attribute)");
+        R"(12:34 error: integral user-defined fragment inputs must have a '@interpolate(flat)' attribute)");
 }
 
 TEST_F(InterpolateTest, VertexOutput_Integer_MissingFlatInterpolation) {
@@ -2580,7 +2582,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: integral user-defined vertex outputs must have a flat interpolation attribute
+        R"(12:34 error: integral user-defined vertex outputs must have a '@interpolate(flat)' attribute
 note: while analyzing entry point 'main')");
 }
 
@@ -2628,7 +2630,7 @@
               Binding(Source{{12, 34}}, -2_i), Group(1_i));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @binding value must be non-negative)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@binding' value must be non-negative)");
 }
 
 TEST_F(GroupAndBindingTest, Binding_F32) {
@@ -2636,7 +2638,7 @@
               Binding(Source{{12, 34}}, 2.0_f), Group(1_u));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @binding must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@binding' must be an 'i32' or 'u32' value)");
 }
 
 TEST_F(GroupAndBindingTest, Binding_AFloat) {
@@ -2644,7 +2646,7 @@
               Binding(Source{{12, 34}}, 2.0_a), Group(1_u));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @binding must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@binding' must be an 'i32' or 'u32' value)");
 }
 
 TEST_F(GroupAndBindingTest, Group_NonConstant) {
@@ -2662,7 +2664,7 @@
               Group(Source{{12, 34}}, -1_i));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @group value must be non-negative)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@group' value must be non-negative)");
 }
 
 TEST_F(GroupAndBindingTest, Group_F32) {
@@ -2670,7 +2672,7 @@
               Group(Source{{12, 34}}, 1.0_f));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @group must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@group' must be an 'i32' or 'u32' value)");
 }
 
 TEST_F(GroupAndBindingTest, Group_AFloat) {
@@ -2678,7 +2680,7 @@
               Group(Source{{12, 34}}, 1.0_a));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @group must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@group' must be an 'i32' or 'u32' value)");
 }
 
 using IdTest = ResolverTest;
@@ -2709,19 +2711,19 @@
 TEST_F(IdTest, Negative) {
     Override("val", ty.f32(), Vector{Id(Source{{12, 34}}, -1_i)});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @id value must be non-negative)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@id' value must be non-negative)");
 }
 
 TEST_F(IdTest, F32) {
     Override("val", ty.f32(), Vector{Id(Source{{12, 34}}, 1_f)});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @id must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@id' must be an 'i32' or 'u32' value)");
 }
 
 TEST_F(IdTest, AFloat) {
     Override("val", ty.f32(), Vector{Id(Source{{12, 34}}, 1.0_a)});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @id must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@id' must be an 'i32' or 'u32' value)");
 }
 
 enum class LocationAttributeType {
@@ -2795,19 +2797,19 @@
 TEST_P(LocationTest, Negative) {
     Build(Expr(-1_a));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location value must be non-negative)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' value must be non-negative)");
 }
 
 TEST_P(LocationTest, F32) {
     Build(Expr(1_f));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' must be an 'i32' or 'u32' value)");
 }
 
 TEST_P(LocationTest, AFloat) {
     Build(Expr(1.0_a));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location must be an i32 or u32 value)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' must be an 'i32' or 'u32' value)");
 }
 
 INSTANTIATE_TEST_SUITE_P(LocationTest,
diff --git a/src/tint/lang/wgsl/resolver/bitcast_validation_test.cc b/src/tint/lang/wgsl/resolver/bitcast_validation_test.cc
index e96a255..bcd8bab 100644
--- a/src/tint/lang/wgsl/resolver/bitcast_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/bitcast_validation_test.cc
@@ -168,7 +168,7 @@
 
     WrapInFunction(Bitcast(Source{{12, 34}}, dst.ast(*this), src.expr(*this, 0)));
 
-    std::string expected = "12:34 error: no matching call to bitcast<${TO}>(${FROM})";
+    std::string expected = "12:34 error: no matching call to 'bitcast<${TO}>(${FROM})'";
     expected = ReplaceAll(expected, "${FROM}", src.sem(*this)->FriendlyName());
     expected = ReplaceAll(expected, "${TO}", dst.sem(*this)->FriendlyName());
 
diff --git a/src/tint/lang/wgsl/resolver/builtin_test.cc b/src/tint/lang/wgsl/resolver/builtin_test.cc
index 37dc276..7dfdf9f 100644
--- a/src/tint/lang/wgsl/resolver/builtin_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_test.cc
@@ -163,12 +163,12 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select()
+              R"(error: no matching call to 'select()'
 
 3 candidate functions:
-  select(T, T, bool) -> T  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'select(T, T, bool) -> T'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, bool) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -179,12 +179,12 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(i32, i32, i32)
+              R"(error: no matching call to 'select(i32, i32, i32)'
 
 3 candidate functions:
-  select(T, T, bool) -> T  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'select(T, T, bool) -> T'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, bool) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -197,12 +197,12 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(mat2x2<f32>, mat2x2<f32>, bool)
+              R"(error: no matching call to 'select(mat2x2<f32>, mat2x2<f32>, bool)'
 
 3 candidate functions:
-  select(T, T, bool) -> T  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'select(T, T, bool) -> T'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, bool) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -213,12 +213,12 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(f32, vec2<f32>, bool)
+              R"(error: no matching call to 'select(f32, vec2<f32>, bool)'
 
 3 candidate functions:
-  select(T, T, bool) -> T  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'select(T, T, bool) -> T'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, bool) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -229,12 +229,12 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(vec2<f32>, vec3<f32>, bool)
+              R"(error: no matching call to 'select(vec2<f32>, vec3<f32>, bool)'
 
 3 candidate functions:
-  select(T, T, bool) -> T  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  'select(T, T, bool) -> T'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, bool) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
+  'select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'i32', 'u32' or 'bool'
 )");
 }
 
@@ -268,10 +268,10 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to arrayLength(ptr<private, array<i32, 4>, read_write>)
+              R"(error: no matching call to 'arrayLength(ptr<private, array<i32, 4>, read_write>)'
 
 1 candidate function:
-  arrayLength(ptr<storage, array<T>, A>) -> u32
+  'arrayLength(ptr<storage, array<T>, A>) -> u32'
 )");
 }
 
@@ -306,7 +306,7 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_THAT(r()->error(),
-                HasSubstr("error: no matching call to " + std::string(param.name) + "()"));
+                HasSubstr("error: no matching call to '" + std::string(param.name) + "()'"));
 }
 
 TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, OneParam_Scalar_f32) {
@@ -331,7 +331,7 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) + "(f32)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) + "(f32)'"));
     }
 }
 
@@ -357,8 +357,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<f32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<f32>)'"));
     }
 }
 
@@ -378,8 +378,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f32, f32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f32, f32)'"));
     }
 }
 
@@ -402,8 +402,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<f32>, vec3<f32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<f32>, vec3<f32>)'"));
     }
 }
 
@@ -423,8 +423,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f32, f32, f32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f32, f32, f32)'"));
     }
 }
 
@@ -449,8 +449,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<f32>, vec3<f32>, vec3<f32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<f32>, vec3<f32>, vec3<f32>)'"));
     }
 }
 
@@ -470,8 +470,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f32, f32, f32, f32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f32, f32, f32, f32)'"));
     }
 }
 
@@ -496,8 +496,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<f32>, vec3<f32>, vec3<f32>, vec3<f32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<f32>, vec3<f32>, vec3<f32>, vec3<f32>)'"));
     }
 }
 
@@ -525,7 +525,7 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) + "(f16)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) + "(f16)'"));
     }
 }
 
@@ -553,8 +553,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<f16>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<f16>)'"));
     }
 }
 
@@ -576,8 +576,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f16, f16)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f16, f16)'"));
     }
 }
 
@@ -602,8 +602,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<f16>, vec3<f16>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<f16>, vec3<f16>)'"));
     }
 }
 
@@ -625,8 +625,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f16, f16, f16)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f16, f16, f16)'"));
     }
 }
 
@@ -653,8 +653,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<f16>, vec3<f16>, vec3<f16>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<f16>, vec3<f16>, vec3<f16>)'"));
     }
 }
 
@@ -676,8 +676,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(f16, f16, f16, f16)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(f16, f16, f16, f16)'"));
     }
 }
 
@@ -704,8 +704,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<f16>, vec3<f16>, vec3<f16>, vec3<f16>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<f16>, vec3<f16>, vec3<f16>, vec3<f16>)'"));
     }
 }
 
@@ -797,10 +797,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to cross()
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'cross()'
 
 1 candidate function:
-  cross(vec3<T>, vec3<T>) -> vec3<T>  where: T is abstract-float, f32 or f16
+  'cross(vec3<T>, vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -810,10 +810,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to cross(f32, f32)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'cross(f32, f32)'
 
 1 candidate function:
-  cross(vec3<T>, vec3<T>) -> vec3<T>  where: T is abstract-float, f32 or f16
+  'cross(vec3<T>, vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -824,10 +824,10 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec3<i32>, vec3<i32>)
+              R"(error: no matching call to 'cross(vec3<i32>, vec3<i32>)'
 
 1 candidate function:
-  cross(vec3<T>, vec3<T>) -> vec3<T>  where: T is abstract-float, f32 or f16
+  'cross(vec3<T>, vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -840,10 +840,10 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec4<f32>, vec4<f32>)
+              R"(error: no matching call to 'cross(vec4<f32>, vec4<f32>)'
 
 1 candidate function:
-  cross(vec3<T>, vec3<T>) -> vec3<T>  where: T is abstract-float, f32 or f16
+  'cross(vec3<T>, vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -856,10 +856,10 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec3<f32>, vec3<f32>, vec3<f32>)
+              R"(error: no matching call to 'cross(vec3<f32>, vec3<f32>, vec3<f32>)'
 
 1 candidate function:
-  cross(vec3<T>, vec3<T>) -> vec3<T>  where: T is abstract-float, f32 or f16
+  'cross(vec3<T>, vec3<T>) -> vec3<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -915,11 +915,12 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to distance(vec3<f32>, vec3<f32>, vec3<f32>)
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to 'distance(vec3<f32>, vec3<f32>, vec3<f32>)'
 
 2 candidate functions:
-  distance(T, T) -> T  where: T is abstract-float, f32 or f16
-  distance(vecN<T>, vecN<T>) -> T  where: T is abstract-float, f32 or f16
+  'distance(T, T) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'distance(vecN<T>, vecN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -929,11 +930,11 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to distance(vec3<f32>)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'distance(vec3<f32>)'
 
 2 candidate functions:
-  distance(T, T) -> T  where: T is abstract-float, f32 or f16
-  distance(vecN<T>, vecN<T>) -> T  where: T is abstract-float, f32 or f16
+  'distance(T, T) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'distance(vecN<T>, vecN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -943,11 +944,11 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to distance()
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'distance()'
 
 2 candidate functions:
-  distance(T, T) -> T  where: T is abstract-float, f32 or f16
-  distance(vecN<T>, vecN<T>) -> T  where: T is abstract-float, f32 or f16
+  'distance(T, T) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'distance(vecN<T>, vecN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1089,11 +1090,11 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to frexp(i32, ptr<workgroup, i32, read_write>)
+              R"(error: no matching call to 'frexp(i32, ptr<workgroup, i32, read_write>)'
 
 2 candidate functions:
-  frexp(T) -> __frexp_result_T  where: T is abstract-float, f32 or f16
-  frexp(vecN<T>) -> __frexp_result_vecN_T  where: T is abstract-float, f32 or f16
+  'frexp(T) -> __frexp_result_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'frexp(vecN<T>) -> __frexp_result_vecN_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1148,11 +1149,11 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to length()
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'length()'
 
 2 candidate functions:
-  length(T) -> T  where: T is abstract-float, f32 or f16
-  length(vecN<T>) -> T  where: T is abstract-float, f32 or f16
+  'length(T) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'length(vecN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1162,11 +1163,11 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to length(f32, f32)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'length(f32, f32)'
 
 2 candidate functions:
-  length(T) -> T  where: T is abstract-float, f32 or f16
-  length(vecN<T>) -> T  where: T is abstract-float, f32 or f16
+  'length(T) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'length(vecN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1338,11 +1339,11 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(i32, ptr<workgroup, f32, read_write>)
+              R"(error: no matching call to 'modf(i32, ptr<workgroup, f32, read_write>)'
 
 2 candidate functions:
-  modf(T) -> __modf_result_T  where: T is abstract-float, f32 or f16
-  modf(vecN<T>) -> __modf_result_vecN_T  where: T is abstract-float, f32 or f16
+  'modf(T) -> __modf_result_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'modf(vecN<T>) -> __modf_result_vecN_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1354,11 +1355,11 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(f32, ptr<workgroup, i32, read_write>)
+              R"(error: no matching call to 'modf(f32, ptr<workgroup, i32, read_write>)'
 
 2 candidate functions:
-  modf(T) -> __modf_result_T  where: T is abstract-float, f32 or f16
-  modf(vecN<T>) -> __modf_result_vecN_T  where: T is abstract-float, f32 or f16
+  'modf(T) -> __modf_result_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'modf(vecN<T>) -> __modf_result_vecN_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1368,11 +1369,11 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to modf(f32, f32)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'modf(f32, f32)'
 
 2 candidate functions:
-  modf(T) -> __modf_result_T  where: T is abstract-float, f32 or f16
-  modf(vecN<T>) -> __modf_result_vecN_T  where: T is abstract-float, f32 or f16
+  'modf(T) -> __modf_result_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'modf(vecN<T>) -> __modf_result_vecN_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1384,11 +1385,11 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(vec2<f32>, ptr<workgroup, vec4<f32>, read_write>)
+              R"(error: no matching call to 'modf(vec2<f32>, ptr<workgroup, vec4<f32>, read_write>)'
 
 2 candidate functions:
-  modf(T) -> __modf_result_T  where: T is abstract-float, f32 or f16
-  modf(vecN<T>) -> __modf_result_vecN_T  where: T is abstract-float, f32 or f16
+  'modf(T) -> __modf_result_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
+  'modf(vecN<T>) -> __modf_result_vecN_T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1425,10 +1426,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to normalize()
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'normalize()'
 
 1 candidate function:
-  normalize(vecN<T>) -> vecN<T>  where: T is abstract-float, f32 or f16
+  'normalize(vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1463,7 +1464,7 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_THAT(r()->error(),
-                HasSubstr("error: no matching call to " + std::string(param.name) + "()"));
+                HasSubstr("error: no matching call to '" + std::string(param.name) + "()'"));
 }
 
 TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Scalar_i32) {
@@ -1483,7 +1484,7 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) + "(i32)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) + "(i32)'"));
     }
 }
 
@@ -1506,8 +1507,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<i32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<i32>)'"));
     }
 }
 
@@ -1528,7 +1529,7 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) + "(u32)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) + "(u32)'"));
     }
 }
 
@@ -1551,8 +1552,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<u32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<u32>)'"));
     }
 }
 
@@ -1572,8 +1573,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(i32, i32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(i32, i32)'"));
     }
 }
 
@@ -1596,8 +1597,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<i32>, vec3<i32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<i32>, vec3<i32>)'"));
     }
 }
 
@@ -1617,8 +1618,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(u32, u32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(u32, u32)'"));
     }
 }
 
@@ -1641,8 +1642,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(vec3<u32>, vec3<u32>)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(vec3<u32>, vec3<u32>)'"));
     }
 }
 
@@ -1662,8 +1663,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(i32, i32, i32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(i32, i32, i32)'"));
     }
 }
 
@@ -1688,8 +1689,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<i32>, vec3<i32>, vec3<i32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<i32>, vec3<i32>, vec3<i32>)'"));
     }
 }
 
@@ -1709,8 +1710,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(u32, u32, u32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(u32, u32, u32)'"));
     }
 }
 
@@ -1735,8 +1736,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<u32>, vec3<u32>, vec3<u32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<u32>, vec3<u32>, vec3<u32>)'"));
     }
 }
 
@@ -1756,8 +1757,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(i32, i32, i32, i32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(i32, i32, i32, i32)'"));
     }
 }
 
@@ -1782,8 +1783,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<i32>, vec3<i32>, vec3<i32>, vec3<i32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<i32>, vec3<i32>, vec3<i32>, vec3<i32>)'"));
     }
 }
 
@@ -1803,8 +1804,8 @@
         // Invalid parameter count.
         EXPECT_FALSE(r()->Resolve());
 
-        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
-                                            std::string(param.name) + "(u32, u32, u32, u32)"));
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" +
+                                            std::string(param.name) + "(u32, u32, u32, u32)'"));
     }
 }
 
@@ -1829,8 +1830,8 @@
         EXPECT_FALSE(r()->Resolve());
 
         EXPECT_THAT(r()->error(),
-                    HasSubstr("error: no matching call to " + std::string(param.name) +
-                              "(vec3<u32>, vec3<u32>, vec3<u32>, vec3<u32>)"));
+                    HasSubstr("error: no matching call to '" + std::string(param.name) +
+                              "(vec3<u32>, vec3<u32>, vec3<u32>, vec3<u32>)'"));
     }
 }
 
@@ -1942,10 +1943,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(mat2x3<f32>)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'determinant(mat2x3<f32>)'
 
 1 candidate function:
-  determinant(matNxN<T>) -> T  where: T is abstract-float, f32 or f16
+  'determinant(matNxN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -1957,10 +1958,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(f32)
+    EXPECT_EQ(r()->error(), R"(error: no matching call to 'determinant(f32)'
 
 1 candidate function:
-  determinant(matNxN<T>) -> T  where: T is abstract-float, f32 or f16
+  'determinant(matNxN<T>) -> T'  where: 'T' is 'abstract-float', 'f32' or 'f16'
 )");
 }
 
@@ -2026,10 +2027,10 @@
     EXPECT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to dot(f32, f32)
+              R"(error: no matching call to 'dot(f32, f32)'
 
 1 candidate function:
-  dot(vecN<T>, vecN<T>) -> T  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
+  'dot(vecN<T>, vecN<T>) -> T'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
 )");
 }
 
@@ -2079,10 +2080,10 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), "error: no matching call to " + name +
-                                "()\n\n"
-                                "2 candidate functions:\n  " +
-                                name + "(f32) -> f32\n  " + name + "(vecN<f32>) -> vecN<f32>\n");
+    EXPECT_EQ(r()->error(), "error: no matching call to '" + name +
+                                "()'\n\n"
+                                "2 candidate functions:\n  '" +
+                                name + "(f32) -> f32'\n  '" + name + "(vecN<f32>) -> vecN<f32>'\n");
 }
 
 INSTANTIATE_TEST_SUITE_P(ResolverTest,
@@ -2612,7 +2613,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" + std::string(param.name)));
 }
 
 TEST_P(ResolverBuiltinTest_DataPacking, Error_NoParams) {
@@ -2623,7 +2624,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" + std::string(param.name)));
 }
 
 TEST_P(ResolverBuiltinTest_DataPacking, Error_TooManyParams) {
@@ -2638,7 +2639,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" + std::string(param.name)));
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -2709,7 +2710,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to '" + std::string(param.name)));
 }
 
 INSTANTIATE_TEST_SUITE_P(
diff --git a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
index 9dcab3d..0c063d8 100644
--- a/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_validation_test.cc
@@ -164,14 +164,14 @@
     WrapInFunction(Call(Source{{56, 78}}, "mix", 1_f, 2_f, 3_f));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: no matching constructor for i32(f32, f32, f32)
+    EXPECT_EQ(r()->error(), R"(56:78 error: no matching constructor for 'i32(f32, f32, f32)'
 
 2 candidate constructors:
-  i32(i32) -> i32
-  i32() -> i32
+  'i32(i32) -> i32'
+  'i32() -> i32'
 
 1 candidate conversion:
-  i32(T) -> i32  where: T is abstract-int, abstract-float, f32, f16, u32 or bool
+  'i32(T) -> i32'  where: 'T' is 'abstract-int', 'abstract-float', 'f32', 'f16', 'u32' or 'bool'
 )");
 }
 
@@ -572,7 +572,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'dot4I8Packed' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -601,7 +601,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'dot4U8Packed' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -628,7 +628,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'pack4xI8' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -655,7 +655,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'pack4xU8' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -682,7 +682,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'pack4xI8Clamp' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -709,7 +709,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'pack4xU8Clamp' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -736,7 +736,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'unpack4xI8' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -763,7 +763,7 @@
     EXPECT_FALSE(resolver.Resolve());
     EXPECT_EQ(resolver.error(),
               "12:34 error: built-in function 'unpack4xU8' requires the "
-              "packed_4x8_integer_dot_product language feature, which is not allowed in the "
+              "'packed_4x8_integer_dot_product' language feature, which is not allowed in the "
               "current environment");
 }
 
@@ -778,10 +778,10 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(error: no matching call to workgroupUniformLoad(ptr<storage, i32, read_write>)
+              R"(error: no matching call to 'workgroupUniformLoad(ptr<storage, i32, read_write>)'
 
 1 candidate function:
-  workgroupUniformLoad(ptr<workgroup, T, read_write>) -> T
+  'workgroupUniformLoad(ptr<workgroup, T, read_write>) -> T'
 )");
 }
 
@@ -990,10 +990,9 @@
 
     Resolver resolver{this, wgsl::AllowedFeatures{}};
     EXPECT_FALSE(resolver.Resolve());
-    EXPECT_EQ(resolver.error(),
-              "12:34 error: built-in function 'textureBarrier' requires the "
-              "readonly_and_readwrite_storage_textures language feature, which is not allowed in "
-              "the current environment");
+    EXPECT_EQ(
+        resolver.error(),
+        R"(12:34 error: built-in function 'textureBarrier' requires the 'readonly_and_readwrite_storage_textures' language feature, which is not allowed in the current environment)");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/resolver/builtins_validation_test.cc b/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
index 616161d..c108ab8 100644
--- a/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtins_validation_test.cc
@@ -146,8 +146,8 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         StringStream err;
-        err << "12:34 error: @builtin(" << params.builtin << ")";
-        err << " cannot be used for " << params.stage << " shader input";
+        err << "12:34 error: '@builtin(" << params.builtin << ")' cannot be used for "
+            << params.stage << " shader input";
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(r()->error(), err.str());
     }
@@ -180,7 +180,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: @builtin(frag_depth) cannot be used for fragment shader input");
+              "12:34 error: '@builtin(frag_depth)' cannot be used for fragment shader input");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragDepthIsInputStruct_Fail) {
@@ -214,7 +214,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @builtin(frag_depth) cannot be used for fragment shader input
+              R"(12:34 error: '@builtin(frag_depth)' cannot be used for fragment shader input
 note: while analyzing entry point 'fragShader')");
 }
 
@@ -271,7 +271,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(position) must be 'vec4<f32>'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(position)' must be 'vec4<f32>'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, PositionNotF32_ReturnType_Fail) {
@@ -287,7 +287,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(position) must be 'vec4<f32>'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(position)' must be 'vec4<f32>'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, PositionIsVec4h_Fail) {
@@ -304,7 +304,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(position) must be 'vec4<f32>'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(position)' must be 'vec4<f32>'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragDepthNotF32_Struct_Fail) {
@@ -333,7 +333,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(frag_depth) must be 'f32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(frag_depth)' must be 'f32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, SampleMaskNotU32_Struct_Fail) {
@@ -362,7 +362,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(sample_mask) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(sample_mask)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, SampleMaskNotU32_ReturnType_Fail) {
@@ -377,7 +377,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(sample_mask) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(sample_mask)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, SampleMaskIsNotU32_Fail) {
@@ -403,7 +403,7 @@
              Location(0_a),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(sample_mask) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(sample_mask)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, SampleIndexIsNotU32_Struct_Fail) {
@@ -432,7 +432,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(sample_index) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(sample_index)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, SampleIndexIsNotU32_Fail) {
@@ -458,7 +458,7 @@
              Location(0_a),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(sample_index) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(sample_index)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, PositionIsNotF32_Fail) {
@@ -484,7 +484,7 @@
              Location(0_a),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(position) must be 'vec4<f32>'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(position)' must be 'vec4<f32>'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragDepthIsNotF32_Fail) {
@@ -503,7 +503,7 @@
              Builtin(Source{{12, 34}}, core::BuiltinValue::kFragDepth),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(frag_depth) must be 'f32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(frag_depth)' must be 'f32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, VertexIndexIsNotU32_Fail) {
@@ -528,7 +528,7 @@
              Builtin(core::BuiltinValue::kPosition),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(vertex_index) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(vertex_index)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, InstanceIndexIsNotU32) {
@@ -553,7 +553,7 @@
              Builtin(core::BuiltinValue::kPosition),
          });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(instance_index) must be 'u32'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(instance_index)' must be 'u32'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragmentBuiltin_Pass) {
@@ -673,8 +673,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: store type of @builtin(workgroup_id) must be "
-              "'vec3<u32>'");
+              R"(12:34 error: store type of '@builtin(workgroup_id)' must be 'vec3<u32>')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, ComputeBuiltin_NumWorkgroupsNotVec3U32) {
@@ -688,8 +687,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: store type of @builtin(num_workgroups) must be "
-              "'vec3<u32>'");
+              R"(12:34 error: store type of '@builtin(num_workgroups)' must be 'vec3<u32>')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, ComputeBuiltin_GlobalInvocationNotVec3U32) {
@@ -703,8 +701,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: store type of @builtin(global_invocation_id) must be "
-              "'vec3<u32>'");
+              R"(12:34 error: store type of '@builtin(global_invocation_id)' must be 'vec3<u32>')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, ComputeBuiltin_LocalInvocationIndexNotU32) {
@@ -718,8 +715,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: store type of @builtin(local_invocation_index) must be "
-              "'u32'");
+              R"(12:34 error: store type of '@builtin(local_invocation_index)' must be 'u32')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, ComputeBuiltin_LocalInvocationNotVec3U32) {
@@ -733,8 +729,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: store type of @builtin(local_invocation_id) must be "
-              "'vec3<u32>'");
+              R"(12:34 error: store type of '@builtin(local_invocation_id)' must be 'vec3<u32>')");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FragmentBuiltinStruct_Pass) {
@@ -800,7 +795,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(front_facing) must be 'bool'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(front_facing)' must be 'bool'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, FrontFacingMemberIsNotBool_Fail) {
@@ -829,7 +824,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: store type of @builtin(front_facing) must be 'bool'");
+    EXPECT_EQ(r()->error(), "12:34 error: store type of '@builtin(front_facing)' must be 'bool'");
 }
 
 // TODO(crbug.com/tint/1846): This isn't a validation test, but this sits next to other @builtin
diff --git a/src/tint/lang/wgsl/resolver/call_validation_test.cc b/src/tint/lang/wgsl/resolver/call_validation_test.cc
index 25121ae..001fb4e 100644
--- a/src/tint/lang/wgsl/resolver/call_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/call_validation_test.cc
@@ -143,7 +143,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: cannot take the address of let 'z'");
+    EXPECT_EQ(r()->error(), "12:34 error: cannot take the address of 'let z'");
 }
 
 TEST_F(ResolverCallValidationTest,
@@ -481,7 +481,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: ignoring return value of function 'fn_must_use' annotated with @must_use
+        R"(12:34 error: ignoring return value of function 'fn_must_use' annotated with '@must_use'
 56:78 note: function 'fn_must_use' declared here)");
 }
 
@@ -519,11 +519,11 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: no matching call to min<i32>(abstract-int, abstract-int)
+              R"(12:34 error: no matching call to 'min<i32>(abstract-int, abstract-int)'
 
 2 candidate functions:
-  min(T, T) -> T  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
-  min(vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, f32, i32, u32 or f16
+  'min(T, T) -> T'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
+  'min(vecN<T>, vecN<T>) -> vecN<T>'  where: 'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16'
 )");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/compound_assignment_validation_test.cc b/src/tint/lang/wgsl/resolver/compound_assignment_validation_test.cc
index 578cd22..132ef50 100644
--- a/src/tint/lang/wgsl/resolver/compound_assignment_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/compound_assignment_validation_test.cc
@@ -86,7 +86,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching overload for operator += (i32, f32)"));
+                HasSubstr("12:34 error: no matching overload for 'operator += (i32, f32)'"));
 }
 
 TEST_F(ResolverCompoundAssignmentValidationTest, IncompatibleOp) {
@@ -103,7 +103,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching overload for operator |= (f32, f32)"));
+                HasSubstr("12:34 error: no matching overload for 'operator |= (f32, f32)'"));
 }
 
 TEST_F(ResolverCompoundAssignmentValidationTest, VectorScalar_Pass) {
@@ -198,7 +198,7 @@
 
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching overload for operator *= (vec4<f32>, mat4x2<f32>)"));
+        HasSubstr("12:34 error: no matching overload for 'operator *= (vec4<f32>, mat4x2<f32>)'"));
 }
 
 TEST_F(ResolverCompoundAssignmentValidationTest, VectorMatrix_ResultMismatch) {
@@ -242,7 +242,7 @@
     WrapInFunction(CompoundAssign(Source{{56, 78}}, Phony(), 1_i, core::BinaryOp::kAdd));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("56:78 error: no matching overload for operator += (void, i32)"));
+                HasSubstr("56:78 error: no matching overload for 'operator += (void, i32)'"));
 }
 
 TEST_F(ResolverCompoundAssignmentValidationTest, ReadOnlyBuffer) {
@@ -266,9 +266,9 @@
     WrapInFunction(a, CompoundAssign(Expr(Source{{56, 78}}, "a"), 1_i, core::BinaryOp::kAdd));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: cannot assign to let 'a'
+    EXPECT_EQ(r()->error(), R"(56:78 error: cannot assign to 'let a'
 56:78 note: 'let' variables are immutable
-12:34 note: let 'a' declared here)");
+12:34 note: 'let a' declared here)");
 }
 
 TEST_F(ResolverCompoundAssignmentValidationTest, LhsLiteral) {
@@ -288,7 +288,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("error: no matching overload for operator += (atomic<i32>, atomic<i32>)"));
+        HasSubstr("error: no matching overload for 'operator += (atomic<i32>, atomic<i32>)'"));
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/resolver/control_block_validation_test.cc b/src/tint/lang/wgsl/resolver/control_block_validation_test.cc
index a5eb482..9f38183 100644
--- a/src/tint/lang/wgsl/resolver/control_block_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/control_block_validation_test.cc
@@ -360,8 +360,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: duplicate switch case '2'\n"
-              "12:34 note: previous case declared here");
+              R"(56:78 error: duplicate switch case '2'
+12:34 note: previous case declared here)");
 }
 
 TEST_F(ResolverControlBlockValidationTest, NonUniqueCaseSelectorValueSint_Fail) {
@@ -387,8 +387,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: duplicate switch case '-10'\n"
-              "12:34 note: previous case declared here");
+              R"(56:78 error: duplicate switch case '-10'
+12:34 note: previous case declared here)");
 }
 
 TEST_F(ResolverControlBlockValidationTest, SwitchCase_Pass) {
@@ -511,8 +511,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "56:78 error: duplicate switch case '10'\n"
-              "12:34 note: previous case declared here");
+              R"(56:78 error: duplicate switch case '10'
+12:34 note: previous case declared here)");
 }
 
 TEST_F(ResolverControlBlockValidationTest, NonUniqueCaseSelectorSameCase_BothExpression_Fail) {
@@ -532,8 +532,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: duplicate switch case '10'\n"
-              "56:78 note: previous case declared here");
+              R"(12:34 error: duplicate switch case '10'
+56:78 note: previous case declared here)");
 }
 
 TEST_F(ResolverControlBlockValidationTest, NonUniqueCaseSelectorSame_Case_Expression_Fail) {
@@ -553,8 +553,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: duplicate switch case '10'\n"
-              "56:78 note: previous case declared here");
+              R"(12:34 error: duplicate switch case '10'
+56:78 note: previous case declared here)");
 }
 
 TEST_F(ResolverControlBlockValidationTest, Switch_OverrideCondition_Fail) {
diff --git a/src/tint/lang/wgsl/resolver/dependency_graph.cc b/src/tint/lang/wgsl/resolver/dependency_graph.cc
index 27f11ae..7e2db61 100644
--- a/src/tint/lang/wgsl/resolver/dependency_graph.cc
+++ b/src/tint/lang/wgsl/resolver/dependency_graph.cc
@@ -131,14 +131,14 @@
 /// A map of global name to Global
 using GlobalMap = Hashmap<Symbol, Global*, 16>;
 
-/// Raises an error diagnostic with the given message and source.
-void AddError(diag::List& diagnostics, const std::string& msg, const Source& source) {
-    diagnostics.AddError(diag::System::Resolver, msg, source);
+/// @returns a new error diagnostic with the given source.
+diag::Diagnostic& AddError(diag::List& diagnostics, const Source& source) {
+    return diagnostics.AddError(diag::System::Resolver, source);
 }
 
-/// Raises a note diagnostic with the given message and source.
-void AddNote(diag::List& diagnostics, const std::string& msg, const Source& source) {
-    diagnostics.AddNote(diag::System::Resolver, msg, source);
+/// @returns a new note diagnostic with the given source.
+diag::Diagnostic& AddNote(diag::List& diagnostics, const Source& source) {
+    return diagnostics.AddNote(diag::System::Resolver, source);
 }
 
 /// DependencyScanner is used to traverse a module to build the list of
@@ -335,8 +335,8 @@
         auto* old = scope_stack_.Set(symbol, node);
         if (old != nullptr && node != old) {
             auto name = symbol.Name();
-            AddError(diagnostics_, "redeclaration of '" + name + "'", node->source);
-            AddNote(diagnostics_, "'" + name + "' previously declared here", old->source);
+            AddError(diagnostics_, node->source) << "redeclaration of '" << name << "'";
+            AddNote(diagnostics_, old->source) << "'" << name << "' previously declared here";
         }
     }
 
@@ -355,7 +355,7 @@
                 return ast::TraverseAction::Descend;
             });
             if (!ok) {
-                AddError(diagnostics_, "TraverseExpressions failed", next->source);
+                AddError(diagnostics_, next->source) << "TraverseExpressions failed";
                 return;
             }
         }
@@ -768,8 +768,8 @@
     /// found in `stack`.
     /// @param stack is the global dependency stack that contains a loop.
     void CyclicDependencyFound(const Global* root, VectorRef<const Global*> stack) {
-        StringStream msg;
-        msg << "cyclic dependency found: ";
+        auto& err = AddError(diagnostics_, root->node->source);
+        err << "cyclic dependency found: ";
         constexpr size_t kLoopNotStarted = ~0u;
         size_t loop_start = kLoopNotStarted;
         for (size_t i = 0; i < stack.Length(); i++) {
@@ -778,19 +778,18 @@
                 loop_start = i;
             }
             if (loop_start != kLoopNotStarted) {
-                msg << "'" << NameOf(e->node) << "' -> ";
+                err << "'" << NameOf(e->node) << "' -> ";
             }
         }
-        msg << "'" << NameOf(root->node) << "'";
-        AddError(diagnostics_, msg.str(), root->node->source);
+        err << "'" << NameOf(root->node) << "'";
+
         for (size_t i = loop_start; i < stack.Length(); i++) {
             auto* from = stack[i];
             auto* to = (i + 1 < stack.Length()) ? stack[i + 1] : stack[loop_start];
             auto info = DepInfoFor(from, to);
-            AddNote(diagnostics_,
-                    KindOf(from->node) + " '" + NameOf(from->node) + "' references " +
-                        KindOf(to->node) + " '" + NameOf(to->node) + "' here",
-                    info.source);
+            AddNote(diagnostics_, info.source)
+                << KindOf(from->node) + " '" << NameOf(from->node) << "' references "
+                << KindOf(to->node) << " '" << NameOf(to->node) << "' here";
         }
     }
 
diff --git a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
index ab8677e..55c6704 100644
--- a/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/dual_source_blending_extension_test.cc
@@ -50,7 +50,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: use of @blend_src requires enabling extension 'chromium_internal_dual_source_blending')");
+        R"(12:34 error: use of '@blend_src' requires enabling extension 'chromium_internal_dual_source_blending')");
 }
 
 class DualSourceBlendingExtensionTests : public ResolverTest {
@@ -68,7 +68,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_srci32' or 'u32' value");
 }
 
 // Using a floating point number as an index value should fail.
@@ -78,7 +78,7 @@
                                    Vector{Location(0_a), BlendSrc(Source{{12, 34}}, 1.0_a)}),
                         });
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @location must be an i32 or u32 value");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_srci32' or 'u32' value");
 }
 
 // Using a number less than zero as an index value should fail.
@@ -89,7 +89,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src value must be zero or one");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' value must be zero or one");
 }
 
 // Using a number greater than one as an index value should fail.
@@ -100,7 +100,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src value must be zero or one");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' value must be zero or one");
 }
 
 // Using an index value at the same location multiple times should fail.
@@ -112,7 +112,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @location(0) @blend_src(0) appears multiple times");
+    EXPECT_EQ(r()->error(), "12:34 error: '@location(0) @blend_src(0)' appears multiple times");
 }
 
 // Using the index attribute without a location attribute should fail.
@@ -123,7 +123,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' can only be used with '@location(0)'");
 }
 
 // Using the index attribute without a location attribute should fail.
@@ -139,7 +139,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' can only be used with '@location(0)'");
 }
 
 // Using an index attribute on a struct member should pass.
@@ -172,7 +172,7 @@
                         });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' can only be used with '@location(0)'");
 }
 
 // Using the index attribute with a non-zero location should fail.
@@ -188,7 +188,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: @blend_src can only be used with @location(0)");
+    EXPECT_EQ(r()->error(), "12:34 error: '@blend_src' can only be used with '@location(0)'");
 }
 
 TEST_F(DualSourceBlendingExtensionTests, NoNonZeroCollisionsBetweenInAndOut) {
@@ -253,8 +253,8 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
-56:78 note: use of @blend_src here
+        R"(12:34 error: use of '@blend_src' requires all the output '@location' attributes of the entry point to be paired with a '@blend_src' attribute
+56:78 note: use of '@blend_src' here
 note: while analyzing entry point 'F')");
 }
 
@@ -286,8 +286,8 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(3:4 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
-1:2 note: use of @blend_src here
+        R"(3:4 error: use of '@blend_src' requires all the output '@location' attributes of the entry point to be paired with a '@blend_src' attribute
+1:2 note: use of '@blend_src' here
 note: while analyzing entry point 'F')");
 }
 
@@ -311,8 +311,8 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(1:2 error: use of @blend_src requires all the output @location attributes of the entry point to be paired with a @blend_src attribute
-note: use of @blend_src here
+        R"(1:2 error: use of '@blend_src' requires all the output '@location' attributes of the entry point to be paired with a '@blend_src' attribute
+note: use of '@blend_src' here
 5:6 note: while analyzing entry point 'F')");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
index 444f9e5..0a43290 100644
--- a/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/entry_point_validation_test.cc
@@ -119,7 +119,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
-13:43 note: previously consumed @location)");
+13:43 note: previously consumed '@location')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_Valid) {
@@ -171,7 +171,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
-13:43 note: previously consumed @location
+13:43 note: previously consumed '@location'
 12:34 note: while analyzing entry point 'main')");
 }
 
@@ -227,7 +227,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @builtin(frag_depth) appears multiple times as pipeline output
+              R"(12:34 error: '@builtin(frag_depth)' appears multiple times as pipeline output
 12:34 note: while analyzing entry point 'main')");
 }
 
@@ -286,7 +286,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
-13:43 note: previously consumed @location)");
+13:43 note: previously consumed '@location')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_Valid) {
@@ -338,7 +338,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
-13:43 note: previously consumed @location
+13:43 note: previously consumed '@location'
 12:34 note: while analyzing entry point 'main')");
 }
 
@@ -393,7 +393,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: @builtin(sample_index) appears multiple times as pipeline input");
+              "12:34 error: '@builtin(sample_index)' appears multiple times as pipeline input");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_DuplicateBuiltins) {
@@ -427,7 +427,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @builtin(sample_index) appears multiple times as pipeline input
+              R"(12:34 error: '@builtin(sample_index)' appears multiple times as pipeline input
 12:34 note: while analyzing entry point 'main')");
 }
 
@@ -772,8 +772,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: cannot apply @location to declaration of type 'bool'
-34:56 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(12:34 error: cannot apply '@location' to declaration of type 'bool'
+34:56 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadType_Output_Array) {
@@ -793,8 +793,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: cannot apply @location to declaration of type 'array<f32, 2>'
-34:56 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(12:34 error: cannot apply '@location' to declaration of type 'array<f32, 2>'
+34:56 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadType_Input_Struct) {
@@ -821,8 +821,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: cannot apply @location to declaration of type 'Input'
-13:43 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(12:34 error: cannot apply '@location' to declaration of type 'Input'
+13:43 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadType_Input_Struct_NestedStruct) {
@@ -879,8 +879,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(13:43 error: cannot apply @location to declaration of type 'array<f32>'
-note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(13:43 error: cannot apply '@location' to declaration of type 'array<f32>'
+note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadMemberType_Input) {
@@ -906,8 +906,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(34:56 error: cannot apply @location to declaration of type 'array<i32>'
-12:34 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(34:56 error: cannot apply '@location' to declaration of type 'array<i32>'
+12:34 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadMemberType_Output) {
@@ -931,8 +931,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(34:56 error: cannot apply @location to declaration of type 'atomic<i32>'
-12:34 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(34:56 error: cannot apply '@location' to declaration of type 'atomic<i32>'
+12:34 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, BadMemberType_Unused) {
@@ -946,8 +946,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(34:56 error: cannot apply @location to declaration of type 'mat3x2<f32>'
-12:34 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(34:56 error: cannot apply '@location' to declaration of type 'mat3x2<f32>'
+12:34 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, ReturnType_Struct_Valid) {
@@ -998,8 +998,8 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot apply @location to declaration of type 'Output'
-13:43 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot apply '@location' to declaration of type 'Output'
+13:43 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, ReturnType_Struct_NestedStruct) {
@@ -1054,8 +1054,8 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(13:43 error: cannot apply @location to declaration of type 'array<f32>'
-12:34 note: @location must only be applied to declarations of numeric scalar or numeric vector type)");
+              R"(13:43 error: cannot apply '@location' to declaration of type 'array<f32>'
+12:34 note: '@location' must only be applied to declarations of numeric scalar or numeric vector type)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocation_Input) {
@@ -1072,7 +1072,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location cannot be used by compute shaders)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' cannot be used by compute shaders)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocation_Output) {
@@ -1087,7 +1087,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location cannot be used by compute shaders)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' cannot be used by compute shaders)");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocationStructMember_Output) {
@@ -1106,9 +1106,8 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: @location cannot be used by compute shaders\n"
-              "56:78 note: while analyzing entry point 'main'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' cannot be used by compute shaders
+56:78 note: while analyzing entry point 'main')");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocationStructMember_Input) {
@@ -1125,9 +1124,8 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: @location cannot be used by compute shaders\n"
-              "56:78 note: while analyzing entry point 'main'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location' cannot be used by compute shaders
+56:78 note: while analyzing entry point 'main')");
 }
 
 TEST_F(LocationAttributeTests, Duplicate_input) {
@@ -1153,7 +1151,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @location(1) appears multiple times)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@location(1)' appears multiple times)");
 }
 
 TEST_F(LocationAttributeTests, Duplicate_struct) {
@@ -1186,7 +1184,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(34:56 error: @location(1) appears multiple times
+              R"(34:56 error: '@location(1)' appears multiple times
 12:34 note: while analyzing entry point 'main')");
 }
 
diff --git a/src/tint/lang/wgsl/resolver/expression_kind_test.cc b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
index 94c91c8..1334b8b 100644
--- a/src/tint/lang/wgsl/resolver/expression_kind_test.cc
+++ b/src/tint/lang/wgsl/resolver/expression_kind_test.cc
@@ -622,34 +622,34 @@
         {Def::kParameter, Use::kUnaryOp, kPass},
 
         {Def::kStruct, Use::kAccess, R"(5:6 error: cannot use type 'STRUCT' as access
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kAddressSpace,
          R"(5:6 error: cannot use type 'STRUCT' as address space
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kBinaryOp, R"(5:6 error: cannot use type 'STRUCT' as value
-1:2 note: struct 'STRUCT' declared here
+1:2 note: 'struct STRUCT' declared here
 7:8 note: are you missing '()'?)"},
         {Def::kStruct, Use::kBuiltinValue,
          R"(5:6 error: cannot use type 'STRUCT' as builtin value
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kFunctionReturnType, kPass},
         {Def::kStruct, Use::kInterpolationSampling,
          R"(5:6 error: cannot use type 'STRUCT' as interpolation sampling
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kInterpolationType,
          R"(5:6 error: cannot use type 'STRUCT' as interpolation type
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kMemberType, kPass},
         {Def::kStruct, Use::kTexelFormat, R"(5:6 error: cannot use type 'STRUCT' as texel format
-1:2 note: struct 'STRUCT' declared here)"},
+1:2 note: 'struct STRUCT' declared here)"},
         {Def::kStruct, Use::kValueExpression,
          R"(5:6 error: cannot use type 'STRUCT' as value
-1:2 note: struct 'STRUCT' declared here
+1:2 note: 'struct STRUCT' declared here
 7:8 note: are you missing '()'?)"},
         {Def::kStruct, Use::kVariableType, kPass},
         {Def::kStruct, Use::kUnaryOp,
          R"(5:6 error: cannot use type 'STRUCT' as value
-1:2 note: struct 'STRUCT' declared here
+1:2 note: 'struct STRUCT' declared here
 7:8 note: are you missing '()'?)"},
 
         {Def::kTexelFormat, Use::kAccess,
@@ -704,40 +704,40 @@
          R"(5:6 error: cannot use type 'i32' as value
 7:8 note: are you missing '()'?)"},
 
-        {Def::kVariable, Use::kAccess, R"(5:6 error: cannot use const 'VARIABLE' as access
-1:2 note: const 'VARIABLE' declared here)"},
+        {Def::kVariable, Use::kAccess, R"(5:6 error: cannot use 'const VARIABLE' as access
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kAddressSpace,
-         R"(5:6 error: cannot use const 'VARIABLE' as address space
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as address space
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kBinaryOp, kPass},
         {Def::kVariable, Use::kBuiltinValue,
-         R"(5:6 error: cannot use const 'VARIABLE' as builtin value
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as builtin value
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kCallStmt,
-         R"(5:6 error: cannot use const 'VARIABLE' as call target
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as call target
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kCallExpr,
-         R"(5:6 error: cannot use const 'VARIABLE' as call target
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as call target
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kFunctionReturnType,
-         R"(5:6 error: cannot use const 'VARIABLE' as type
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as type
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kInterpolationSampling,
-         R"(5:6 error: cannot use const 'VARIABLE' as interpolation sampling
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as interpolation sampling
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kInterpolationType,
-         R"(5:6 error: cannot use const 'VARIABLE' as interpolation type
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as interpolation type
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kMemberType,
-         R"(5:6 error: cannot use const 'VARIABLE' as type
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as type
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kTexelFormat,
-         R"(5:6 error: cannot use const 'VARIABLE' as texel format
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as texel format
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kValueExpression, kPass},
         {Def::kVariable, Use::kVariableType,
-         R"(5:6 error: cannot use const 'VARIABLE' as type
-1:2 note: const 'VARIABLE' declared here)"},
+         R"(5:6 error: cannot use 'const VARIABLE' as type
+1:2 note: 'const VARIABLE' declared here)"},
         {Def::kVariable, Use::kUnaryOp, kPass},
     }));
 
diff --git a/src/tint/lang/wgsl/resolver/f16_extension_test.cc b/src/tint/lang/wgsl/resolver/f16_extension_test.cc
index c7f7fbd..632f790 100644
--- a/src/tint/lang/wgsl/resolver/f16_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/f16_extension_test.cc
@@ -53,7 +53,7 @@
     GlobalVar("v", ty.f16(Source{{12, 34}}), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 TEST_F(ResolverF16ExtensionTest, Vec2TypeUsedWithExtension) {
@@ -71,7 +71,7 @@
     GlobalVar("v", ty.vec2(ty.f16(Source{{12, 34}})), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 TEST_F(ResolverF16ExtensionTest, Vec2TypeInitUsedWithExtension) {
@@ -89,7 +89,7 @@
     GlobalVar("v", Call(ty.vec2(ty.f16(Source{{12, 34}}))), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 TEST_F(ResolverF16ExtensionTest, Vec2TypeConvUsedWithExtension) {
@@ -108,7 +108,7 @@
               core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 TEST_F(ResolverF16ExtensionTest, F16LiteralUsedWithExtension) {
@@ -126,7 +126,7 @@
     GlobalVar("v", Expr(Source{{12, 34}}, 16_h), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 using ResolverF16ExtensionBuiltinTypeAliasTest = ResolverTestWithParam<const char*>;
@@ -146,7 +146,7 @@
     GlobalVar("v", ty(Source{{12, 34}}, GetParam()), core::AddressSpace::kPrivate);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: f16 type used without 'f16' extension enabled");
+    EXPECT_EQ(r()->error(), "12:34 error: 'f16' type used without 'f16' extension enabled");
 }
 
 INSTANTIATE_TEST_SUITE_P(ResolverF16ExtensionBuiltinTypeAliasTest,
diff --git a/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc b/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
index a9c6576..b18be5c 100644
--- a/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/framebuffer_fetch_extension_test.cc
@@ -66,7 +66,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch')");
+        R"(12:34 error: use of '@color' requires enabling extension 'chromium_experimental_framebuffer_fetch')");
 }
 
 TEST_F(FramebufferFetchExtensionTest, ColorMemberUsedWithExtension) {
@@ -94,7 +94,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch')");
+        R"(12:34 error: use of '@color' requires enabling extension 'chromium_experimental_framebuffer_fetch')");
 }
 
 TEST_F(FramebufferFetchExtensionTest, DuplicateColorParams) {
@@ -112,7 +112,7 @@
          ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(1:2 error: @color(1) appears multiple times)");
+    EXPECT_EQ(r()->error(), R"(1:2 error: '@color(1)' appears multiple times)");
 }
 
 TEST_F(FramebufferFetchExtensionTest, DuplicateColorStruct) {
@@ -136,7 +136,7 @@
          Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(1:2 error: @color(1) appears multiple times)");
+    EXPECT_EQ(r()->error(), R"(1:2 error: '@color(1)' appears multiple times)");
 }
 
 TEST_F(FramebufferFetchExtensionTest, DuplicateColorParamAndStruct) {
@@ -163,7 +163,7 @@
          ty.void_(), Empty, Vector{Stage(ast::PipelineStage::kFragment)});
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(1:2 error: @color(2) appears multiple times
+    EXPECT_EQ(r()->error(), R"(1:2 error: '@color(2)' appears multiple times
 note: while analyzing entry point 'f')");
 }
 
@@ -206,8 +206,8 @@
     } else {
         EXPECT_FALSE(r()->Resolve());
         auto expected =
-            ReplaceAll(R"(12:34 error: cannot apply @color to declaration of type '$TYPE'
-56:78 note: @color must only be applied to declarations of numeric scalar or numeric vector type)",
+            ReplaceAll(R"(12:34 error: cannot apply '@color' to declaration of type '$TYPE'
+56:78 note: '@color' must only be applied to declarations of numeric scalar or numeric vector type)",
                        "$TYPE", GetParam().name);
         EXPECT_EQ(r()->error(), expected);
     }
@@ -230,8 +230,8 @@
     } else {
         EXPECT_FALSE(r()->Resolve());
         auto expected =
-            ReplaceAll(R"(12:34 error: cannot apply @color to declaration of type '$TYPE'
-56:78 note: @color must only be applied to declarations of numeric scalar or numeric vector type)",
+            ReplaceAll(R"(12:34 error: cannot apply '@color' to declaration of type '$TYPE'
+56:78 note: '@color' must only be applied to declarations of numeric scalar or numeric vector type)",
                        "$TYPE", GetParam().name);
         EXPECT_EQ(r()->error(), expected);
     }
diff --git a/src/tint/lang/wgsl/resolver/function_validation_test.cc b/src/tint/lang/wgsl/resolver/function_validation_test.cc
index 2a01da4..3de0b09 100644
--- a/src/tint/lang/wgsl/resolver/function_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/function_validation_test.cc
@@ -150,7 +150,7 @@
 
     ASSERT_TRUE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), "12:34 warning: code is unreachable");
+    EXPECT_EQ(r()->error(), R"(12:34 warning: code is unreachable)");
     EXPECT_TRUE(Sem().Get(decl_a)->IsReachable());
     EXPECT_TRUE(Sem().Get(ret)->IsReachable());
     EXPECT_FALSE(Sem().Get(assign_a)->IsReachable());
@@ -170,7 +170,7 @@
     Func("func", tint::Empty, ty.void_(), Vector{decl_a, Block(Block(Block(ret))), assign_a});
 
     ASSERT_TRUE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 warning: code is unreachable");
+    EXPECT_EQ(r()->error(), R"(12:34 warning: code is unreachable)");
     EXPECT_TRUE(Sem().Get(decl_a)->IsReachable());
     EXPECT_TRUE(Sem().Get(ret)->IsReachable());
     EXPECT_FALSE(Sem().Get(assign_a)->IsReachable());
@@ -207,7 +207,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: discard statement cannot be used in vertex pipeline stage");
+              R"(12:34 error: discard statement cannot be used in vertex pipeline stage)");
 }
 
 TEST_F(ResolverFunctionValidationTest, DiscardCalledIndirectlyFromComputeEntryPoint) {
@@ -260,7 +260,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing return at end of function");
+    EXPECT_EQ(r()->error(), R"(12:34 error: missing return at end of function)");
 }
 
 TEST_F(ResolverFunctionValidationTest, VoidFunctionEndWithoutReturnStatementEmptyBody_Pass) {
@@ -277,7 +277,7 @@
     Func(Source{{12, 34}}, "func", tint::Empty, ty.i32(), tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: missing return at end of function");
+    EXPECT_EQ(r()->error(), R"(12:34 error: missing return at end of function)");
 }
 
 TEST_F(ResolverFunctionValidationTest, FunctionTypeMustMatchReturnStatementType_Pass) {
@@ -343,7 +343,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function 'v' does not return a value");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function 'v' does not return a value)");
 }
 
 TEST_F(ResolverFunctionValidationTest, FunctionTypeMustMatchReturnStatementTypeMissing_fail) {
@@ -623,8 +623,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size arguments must be of the same type, either i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: '@workgroup_size' arguments must be of the same type, either 'i32' or 'u32'");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_MismatchType_I32) {
@@ -638,8 +639,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size arguments must be of the same type, either i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: '@workgroup_size' arguments must be of the same type, either 'i32' or 'u32'");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_TypeMismatch) {
@@ -654,8 +656,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size arguments must be of the same type, either i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: '@workgroup_size' arguments must be of the same type, either 'i32' or 'u32'");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_TypeMismatch2) {
@@ -672,8 +675,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size arguments must be of the same type, either i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: '@workgroup_size' arguments must be of the same type, either 'i32' or 'u32'");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Mismatch_ConstU32) {
@@ -690,8 +694,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size arguments must be of the same type, either i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: '@workgroup_size' arguments must be of the same type, either 'i32' or 'u32'");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Literal_BadType) {
@@ -707,8 +712,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        "12:34 error: workgroup_size argument must be a constant or override-expression of type "
-        "abstract-integer, i32 or u32");
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Literal_Negative) {
@@ -722,7 +726,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' argument must be at least 1)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Literal_Zero) {
@@ -736,7 +740,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' argument must be at least 1)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_BadType) {
@@ -753,8 +757,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        "12:34 error: workgroup_size argument must be a constant or override-expression of type "
-        "abstract-integer, i32 or u32");
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_Negative) {
@@ -769,7 +772,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' argument must be at least 1)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_Zero) {
@@ -784,7 +787,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' argument must be at least 1)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_Const_NestedZeroValueInitializer) {
@@ -799,7 +802,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@workgroup_size' argument must be at least 1)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_0x100_0x100) {
@@ -812,7 +815,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+    EXPECT_EQ(r()->error(), R"(12:34 error: total workgroup grid size cannot exceed 0xffffffff)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_0x10000) {
@@ -825,7 +828,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+    EXPECT_EQ(r()->error(), R"(12:34 error: total workgroup grid size cannot exceed 0xffffffff)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_C_0x10000) {
@@ -840,7 +843,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+    EXPECT_EQ(r()->error(), R"(12:34 error: total workgroup grid size cannot exceed 0xffffffff)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_C) {
@@ -855,7 +858,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+    EXPECT_EQ(r()->error(), R"(12:34 error: total workgroup grid size cannot exceed 0xffffffff)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_O_0x10000) {
@@ -870,7 +873,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+    EXPECT_EQ(r()->error(), R"(12:34 error: total workgroup grid size cannot exceed 0xffffffff)");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_NonConst) {
@@ -885,9 +888,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override-expression of "
-              "type abstract-integer, i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_InvalidExpr_x) {
@@ -901,9 +904,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override-expression of "
-              "type abstract-integer, i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_InvalidExpr_y) {
@@ -917,9 +920,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override-expression of "
-              "type abstract-integer, i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_InvalidExpr_z) {
@@ -933,9 +936,9 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override-expression of "
-              "type abstract-integer, i32 or u32");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: '@workgroup_size' argument must be a constant or override-expression of type 'abstract-integer', 'i32' or 'u32')");
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_NonPlain) {
@@ -943,7 +946,7 @@
     Func("f", tint::Empty, ret_type, tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function return type must be a constructible type");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function return type must be a constructible type)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_AtomicInt) {
@@ -951,7 +954,7 @@
     Func("f", tint::Empty, ret_type, tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function return type must be a constructible type");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function return type must be a constructible type)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_ArrayOfAtomic) {
@@ -959,7 +962,7 @@
     Func("f", tint::Empty, ret_type, tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function return type must be a constructible type");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function return type must be a constructible type)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_StructOfAtomic) {
@@ -970,7 +973,7 @@
     Func("f", tint::Empty, ret_type, tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function return type must be a constructible type");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function return type must be a constructible type)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ReturnIsConstructible_RuntimeArray) {
@@ -978,7 +981,7 @@
     Func("f", tint::Empty, ret_type, tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function return type must be a constructible type");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function return type must be a constructible type)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ParameterStoreType_NonAtomicFree) {
@@ -990,7 +993,7 @@
     Func("f", Vector{bar}, ty.void_(), tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: type of function parameter must be constructible");
+    EXPECT_EQ(r()->error(), R"(12:34 error: type of function parameter must be constructible)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ParameterStoreType_AtomicFree) {
@@ -1022,7 +1025,7 @@
     Func(Source{{12, 34}}, "f", params, ty.void_(), tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: function declares 256 parameters, maximum is 255");
+    EXPECT_EQ(r()->error(), R"(12:34 error: function declares 256 parameters, maximum is 255)");
 }
 
 TEST_F(ResolverFunctionValidationTest, ParameterVectorNoType) {
@@ -1032,7 +1035,7 @@
          tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'vec3'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: expected '<' for 'vec3')");
 }
 
 TEST_F(ResolverFunctionValidationTest, ParameterMatrixNoType) {
@@ -1042,7 +1045,7 @@
          tint::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: expected '<' for 'mat3x3'");
+    EXPECT_EQ(r()->error(), R"(12:34 error: expected '<' for 'mat3x3')");
 }
 
 enum class Expectation {
diff --git a/src/tint/lang/wgsl/resolver/host_shareable_validation_test.cc b/src/tint/lang/wgsl/resolver/host_shareable_validation_test.cc
index 50e7448..e4018af 100644
--- a/src/tint/lang/wgsl/resolver/host_shareable_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/host_shareable_validation_test.cc
@@ -50,7 +50,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while analyzing structure member S.x
 90:12 note: while instantiating 'var' g)");
 }
@@ -66,7 +66,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'vec3<bool>' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'vec3<bool>' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while analyzing structure member S.x
 90:12 note: while instantiating 'var' g)");
 }
@@ -82,7 +82,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(12:34 error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 56:78 note: while analyzing structure member S.x
 90:12 note: while instantiating 'var' g)");
 }
@@ -101,7 +101,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
+        R"(error: type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
 1:2 note: while analyzing structure member I1.x
 3:4 note: while analyzing structure member I2.y
 5:6 note: while analyzing structure member I3.z
diff --git a/src/tint/lang/wgsl/resolver/increment_decrement_validation_test.cc b/src/tint/lang/wgsl/resolver/increment_decrement_validation_test.cc
index 2f4ea33..3f2d7ea 100644
--- a/src/tint/lang/wgsl/resolver/increment_decrement_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/increment_decrement_validation_test.cc
@@ -166,7 +166,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(56:78 error: cannot modify 'let'
-12:34 note: 'a' is declared here:)");
+12:34 note: 'let a' declared here)");
 }
 
 TEST_F(ResolverIncrementDecrementValidationTest, Parameter) {
@@ -182,7 +182,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(56:78 error: cannot modify function parameter
-12:34 note: 'a' is declared here:)");
+12:34 note: parameter 'a' declared here)");
 }
 
 TEST_F(ResolverIncrementDecrementValidationTest, ReturnValue) {
diff --git a/src/tint/lang/wgsl/resolver/materialize_test.cc b/src/tint/lang/wgsl/resolver/materialize_test.cc
index 19eff8c..d156bda 100644
--- a/src/tint/lang/wgsl/resolver/materialize_test.cc
+++ b/src/tint/lang/wgsl/resolver/materialize_test.cc
@@ -440,16 +440,16 @@
             std::string expect;
             switch (method) {
                 case Method::kBuiltinArg:
-                    expect = "error: no matching call to min(" + data.target_type_name + ", " +
-                             data.abstract_type_name + ")";
+                    expect = "error: no matching call to 'min(" + data.target_type_name + ", " +
+                             data.abstract_type_name + ")'";
                     break;
                 case Method::kBinaryOp:
-                    expect = "error: no matching overload for operator + (" +
-                             data.target_type_name + ", " + data.abstract_type_name + ")";
+                    expect = "error: no matching overload for 'operator + (" +
+                             data.target_type_name + ", " + data.abstract_type_name + ")'";
                     break;
                 case Method::kCompoundAssign:
-                    expect = "error: no matching overload for operator += (" +
-                             data.target_type_name + ", " + data.abstract_type_name + ")";
+                    expect = "error: no matching overload for 'operator += (" +
+                             data.target_type_name + ", " + data.abstract_type_name + ")'";
                     break;
                 default:
                     expect = "error: cannot convert value of type '" + data.abstract_type_name +
diff --git a/src/tint/lang/wgsl/resolver/override_test.cc b/src/tint/lang/wgsl/resolver/override_test.cc
index 3077e79..df09b2e 100644
--- a/src/tint/lang/wgsl/resolver/override_test.cc
+++ b/src/tint/lang/wgsl/resolver/override_test.cc
@@ -106,8 +106,8 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(56:78 error: @id values must be unique
-12:34 note: a override with an ID of 7 was previously declared here:)");
+    EXPECT_EQ(r()->error(), R"(56:78 error: '@id' values must be unique
+12:34 note: a override with an ID of 7 was previously declared here)");
 }
 
 TEST_F(ResolverOverrideTest, IdTooLarge) {
@@ -115,7 +115,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), "12:34 error: @id value must be between 0 and 65535");
+    EXPECT_EQ(r()->error(), "12:34 error: '@id' value must be between 0 and 65535");
 }
 
 TEST_F(ResolverOverrideTest, TransitiveReferences_DirectUse) {
diff --git a/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
index eceb5ae..74ea8e1 100644
--- a/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/pixel_local_extension_test.cc
@@ -354,8 +354,8 @@
         EXPECT_FALSE(r()->Resolve());
         EXPECT_EQ(
             r()->error(),
-            R"(12:34 error: struct members used in the 'pixel_local' address space can only be of the type 'i32', 'u32' or 'f32'
-56:78 note: struct 'S' used in the 'pixel_local' address space here)");
+            R"(12:34 error: 'struct' members used in the 'pixel_local' address space can only be of the type 'i32', 'u32' or 'f32'
+56:78 note: 'struct S' used in the 'pixel_local' address space here)");
     }
 }
 
diff --git a/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc b/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
index 1895890..e7b05da 100644
--- a/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/ptr_ref_validation_test.cc
@@ -61,7 +61,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of let 'l')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of 'let l')");
 }
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfConst) {
@@ -74,7 +74,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of const 'c')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of 'const c')");
 }
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfOverride) {
@@ -87,7 +87,7 @@
 
     EXPECT_FALSE(r()->Resolve());
 
-    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of override 'o')");
+    EXPECT_EQ(r()->error(), R"(12:34 error: cannot take the address of 'override o')");
 }
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfParameter) {
@@ -112,7 +112,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: cannot take the address of var 't' in handle address space)");
+              R"(12:34 error: cannot take the address of 'var t' in handle address space)");
 }
 
 TEST_F(ResolverPtrRefValidationTest, AddressOfFunction) {
@@ -255,7 +255,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: cannot take the address of var 't' in handle address space)");
+              R"(12:34 error: cannot take the address of 'var t' in handle address space)");
 }
 
 TEST_F(ResolverPtrRefValidationTest, DerefOfLiteral) {
@@ -306,7 +306,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: cannot initialize let of type "
+              "12:34 error: cannot initialize 'let' of type "
               "'ptr<storage, i32, read>' with value of type "
               "'ptr<storage, i32, read_write>'");
 }
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 2bf95e9..20ec2a4 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -108,6 +108,8 @@
 #include "src/tint/utils/math/math.h"
 #include "src/tint/utils/text/string.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/text_style.h"
 
 using namespace tint::core::fluent_types;  // NOLINT
 
@@ -269,7 +271,8 @@
             attribute,  //
             [&](const ast::InternalAttribute* attr) -> bool { return InternalAttribute(attr); },
             [&](Default) {
-                ErrorInvalidAttribute(attribute, "'let' declaration");
+                ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "let"
+                                                              << style::Plain << " declaration");
                 return false;
             });
         if (!ok) {
@@ -278,7 +281,8 @@
     }
 
     if (TINT_UNLIKELY(!v->initializer)) {
-        AddError("'let' declaration must have an initializer", v->source);
+        AddError(v->source) << style::Keyword << "let" << style::Plain
+                            << " declaration must have an initializer";
         return nullptr;
     }
 
@@ -299,7 +303,8 @@
 
     if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined,
                                       const_cast<core::type::Type*>(sem->Type()), v->source)) {
-        AddNote("while instantiating 'let' " + v->name->symbol.Name(), v->source);
+        AddNote(v->source) << "while instantiating " << style::Keyword << "let " << style::Variable
+                           << v->name->symbol.Name();
         return nullptr;
     }
 
@@ -346,7 +351,7 @@
             ty = init->Type();
         }
     } else if (!ty) {
-        AddError("override declaration requires a type or initializer", v->source);
+        AddError(v->source) << "override declaration requires a type or initializer";
         return nullptr;
     }
     sem->SetType(ty);
@@ -357,7 +362,8 @@
 
     if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined,
                                       const_cast<core::type::Type*>(ty), v->source)) {
-        AddNote("while instantiating 'override' " + v->name->symbol.Name(), v->source);
+        AddNote(v->source) << "while instantiating " << style::Keyword << "override "
+                           << style::Variable << v->name->symbol.Name();
         return nullptr;
     }
 
@@ -374,21 +380,25 @@
                     return false;
                 }
                 if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-                    AddError("@id must be an i32 or u32 value", attr->source);
+                    AddError(attr->source)
+                        << style::Attribute << "@id" << style::Plain << " must be an "
+                        << style::Type << "i32" << style::Plain << " or " << style::Type << "u32"
+                        << style::Plain << " value";
                     return false;
                 }
 
                 auto const_value = materialized->ConstantValue();
                 auto value = const_value->ValueAs<AInt>();
                 if (value < 0) {
-                    AddError("@id value must be non-negative", attr->source);
+                    AddError(attr->source) << style::Attribute << "@id" << style::Plain
+                                           << " value must be non-negative";
                     return false;
                 }
                 if (value > std::numeric_limits<decltype(OverrideId::value)>::max()) {
-                    AddError(
-                        "@id value must be between 0 and " +
-                            std::to_string(std::numeric_limits<decltype(OverrideId::value)>::max()),
-                        attr->source);
+                    AddError(attr->source)
+                        << style::Attribute << "@id" << style::Plain
+                        << " value must be between 0 and "
+                        << std::numeric_limits<decltype(OverrideId::value)>::max();
                     return false;
                 }
 
@@ -400,7 +410,8 @@
                 return true;
             },
             [&](Default) {
-                ErrorInvalidAttribute(attribute, "'override' declaration");
+                ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "override"
+                                                              << style::Plain << " declaration");
                 return false;
             });
         if (!ok) {
@@ -426,7 +437,9 @@
         Mark(attribute);
         bool ok = Switch(attribute,  //
                          [&](Default) {
-                             ErrorInvalidAttribute(attribute, "'const' declaration");
+                             ErrorInvalidAttribute(attribute,
+                                                   StyledText{} << style::Keyword << "const"
+                                                                << style::Plain << " declaration");
                              return false;
                          });
         if (!ok) {
@@ -435,7 +448,7 @@
     }
 
     if (TINT_UNLIKELY(!c->initializer)) {
-        AddError("'const' declaration must have an initializer", c->source);
+        AddError(c->source) << "'const' declaration must have an initializer";
         return nullptr;
     }
 
@@ -480,7 +493,7 @@
 
     if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined,
                                       const_cast<core::type::Type*>(ty), c->source)) {
-        AddNote("while instantiating 'const' " + c->name->symbol.Name(), c->source);
+        AddNote(c->source) << "while instantiating 'const' " << c->name->symbol.Name();
         return nullptr;
     }
 
@@ -542,7 +555,7 @@
     }
 
     if (!storage_ty) {
-        AddError("var declaration requires a type or initializer", var->source);
+        AddError(var->source) << "var declaration requires a type or initializer";
         return nullptr;
     }
 
@@ -568,7 +581,8 @@
     if (!is_global && sem->AddressSpace() != core::AddressSpace::kFunction &&
         validator_.IsValidationEnabled(var->attributes,
                                        ast::DisabledValidation::kIgnoreAddressSpace)) {
-        AddError("function-scope 'var' declaration must use 'function' address space", var->source);
+        AddError(var->source)
+            << "function-scope 'var' declaration must use 'function' address space";
         return nullptr;
     }
 
@@ -592,7 +606,7 @@
     if (!ApplyAddressSpaceUsageToType(sem->AddressSpace(),
                                       const_cast<core::type::Type*>(sem->Type()),
                                       var->type ? var->type->source : var->source)) {
-        AddNote("while instantiating 'var' " + var->name->symbol.Name(), var->source);
+        AddNote(var->source) << "while instantiating 'var' " << var->name->symbol.Name();
         return nullptr;
     }
 
@@ -684,7 +698,8 @@
                 case kErrored:
                     return nullptr;
                 case kInvalid:
-                    ErrorInvalidAttribute(attribute, "module-scope 'var'");
+                    ErrorInvalidAttribute(
+                        attribute, StyledText{} << "module-scope " << style::Keyword << "var");
                     return nullptr;
             }
         }
@@ -700,7 +715,8 @@
                 attribute,
                 [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
                 [&](Default) {
-                    ErrorInvalidAttribute(attribute, "function-scope 'var'");
+                    ErrorInvalidAttribute(
+                        attribute, StyledText{} << "function-scope " << style::Keyword << "var");
                     return false;
                 });
             if (!ok) {
@@ -721,7 +737,7 @@
     b.Sem().Add(param, sem);
 
     auto add_note = [&] {
-        AddNote("while instantiating parameter " + param->name->symbol.Name(), param->source);
+        AddNote(param->source) << "while instantiating parameter " << param->name->symbol.Name();
     };
 
     if (func->IsEntryPoint()) {
@@ -759,7 +775,7 @@
                 [&](const ast::GroupAttribute* attr) {
                     if (validator_.IsValidationEnabled(
                             param->attributes, ast::DisabledValidation::kEntryPointParameter)) {
-                        ErrorInvalidAttribute(attribute, "function parameters");
+                        ErrorInvalidAttribute(attribute, StyledText{} << "function parameters");
                         return false;
                     }
                     auto value = GroupAttribute(attr);
@@ -772,7 +788,7 @@
                 [&](const ast::BindingAttribute* attr) -> bool {
                     if (validator_.IsValidationEnabled(
                             param->attributes, ast::DisabledValidation::kEntryPointParameter)) {
-                        ErrorInvalidAttribute(attribute, "function parameters");
+                        ErrorInvalidAttribute(attribute, StyledText{} << "function parameters");
                         return false;
                     }
                     auto value = BindingAttribute(attr);
@@ -783,7 +799,7 @@
                     return true;
                 },
                 [&](Default) {
-                    ErrorInvalidAttribute(attribute, "function parameters");
+                    ErrorInvalidAttribute(attribute, StyledText{} << "function parameters");
                     return false;
                 });
             if (!ok) {
@@ -802,9 +818,10 @@
                 [&](Default) {
                     if (attribute->IsAnyOf<ast::LocationAttribute, ast::BuiltinAttribute,
                                            ast::InvariantAttribute, ast::InterpolateAttribute>()) {
-                        ErrorInvalidAttribute(attribute, "non-entry point function parameters");
+                        ErrorInvalidAttribute(
+                            attribute, StyledText{} << "non-entry point function parameters");
                     } else {
-                        ErrorInvalidAttribute(attribute, "function parameters");
+                        ErrorInvalidAttribute(attribute, StyledText{} << "function parameters");
                     }
                     return false;
                 });
@@ -895,9 +912,8 @@
                 increment_next_id();
             }
             if (ids_exhausted) {
-                AddError(
-                    "number of 'override' variables exceeded limit of " + std::to_string(kLimit),
-                    decl->source);
+                AddError(decl->source)
+                    << "number of 'override' variables exceeded limit of " << kLimit;
                 return false;
             }
             id = next_id;
@@ -952,12 +968,12 @@
     }
     auto* cond = expr->ConstantValue();
     if (auto* ty = cond->Type(); !ty->Is<core::type::Bool>()) {
-        AddError("const assertion condition must be a bool, got '" + ty->FriendlyName() + "'",
-                 assertion->condition->source);
+        AddError(assertion->condition->source)
+            << "const assertion condition must be a bool, got '" << ty->FriendlyName() << "'";
         return nullptr;
     }
     if (!cond->ValueAs<bool>()) {
-        AddError("const assertion failed", assertion->source);
+        AddError(assertion->source) << "const assertion failed";
         return nullptr;
     }
     auto* sem = b.create<sem::Statement>(assertion, current_compound_statement_, current_function_);
@@ -997,7 +1013,7 @@
             },
             [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
             [&](Default) {
-                ErrorInvalidAttribute(attribute, "functions");
+                ErrorInvalidAttribute(attribute, StyledText{} << "functions");
                 return false;
             });
         if (!ok) {
@@ -1017,8 +1033,8 @@
         {  // Check the parameter name is unique for the function
             if (auto added = parameter_names.Add(param->name->symbol, param->source); !added) {
                 auto name = param->name->symbol.Name();
-                AddError("redefinition of parameter '" + name + "'", param->source);
-                AddNote("previous definition is here", added.value);
+                AddError(param->source) << "redefinition of parameter '" << name << "'";
+                AddNote(added.value) << "previous definition is here";
                 return nullptr;
             }
         }
@@ -1119,19 +1135,20 @@
                 case kErrored:
                     return nullptr;
                 case kInvalid:
-                    ErrorInvalidAttribute(attribute, "entry point return types");
+                    ErrorInvalidAttribute(attribute, StyledText{} << "entry point return types");
                     return nullptr;
             }
         }
     } else {
         for (auto* attribute : decl->return_type_attributes) {
             Mark(attribute);
-            bool ok = Switch(attribute,  //
-                             [&](Default) {
-                                 ErrorInvalidAttribute(attribute,
-                                                       "non-entry point function return types");
-                                 return false;
-                             });
+            bool ok =
+                Switch(attribute,  //
+                       [&](Default) {
+                           ErrorInvalidAttribute(
+                               attribute, StyledText{} << "non-entry point function return types");
+                           return false;
+                       });
             if (!ok) {
                 return nullptr;
             }
@@ -1140,8 +1157,8 @@
 
     if (auto* str = return_type->As<core::type::Struct>()) {
         if (!ApplyAddressSpaceUsageToType(core::AddressSpace::kUndefined, str, decl->source)) {
-            AddNote("while instantiating return type for " + decl->name->symbol.Name(),
-                    decl->source);
+            AddNote(decl->source) << "while instantiating return type for "
+                                  << decl->name->symbol.Name();
             return nullptr;
         }
 
@@ -1169,9 +1186,8 @@
     if (decl->body) {
         Mark(decl->body);
         if (TINT_UNLIKELY(current_compound_statement_)) {
-            StringStream err;
-            err << "Resolver::Function() called with a current compound statement";
-            AddICE(err.str(), decl->body->source);
+            AddICE("Resolver::Function() called with a current compound statement",
+                   decl->body->source);
             return nullptr;
         }
         auto* body = StatementScope(decl->body, b.create<sem::FunctionBlockStatement>(func),
@@ -1265,11 +1281,12 @@
 
         // Error cases
         [&](const ast::CaseStatement*) {
-            AddError("case statement can only be used inside a switch statement", stmt->source);
+            AddError(stmt->source) << "case statement can only be used inside a switch statement";
             return nullptr;
         },
         [&](Default) {
-            AddError("unknown statement type: " + std::string(stmt->TypeInfo().name), stmt->source);
+            AddError(stmt->source)
+                << "unknown statement type: " << std::string(stmt->TypeInfo().name);
             return nullptr;
         });
 }
@@ -1294,12 +1311,12 @@
                     return false;
                 }
                 if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-                    AddError("case selector must be an i32 or u32 value", sel->source);
+                    AddError(sel->source) << "case selector must be an i32 or u32 value";
                     return false;
                 }
                 const_value = materialized->ConstantValue();
                 if (!const_value) {
-                    AddError("case selector must be a constant expression", sel->source);
+                    AddError(sel->source) << "case selector must be a constant expression";
                     return false;
                 }
             }
@@ -1491,9 +1508,8 @@
     if (!ast::TraverseExpressions<ast::TraverseOrder::RightToLeft>(
             root, [&](const ast::Expression* expr, size_t depth) {
                 if (depth > kMaxExpressionDepth) {
-                    AddError(
-                        "reached max expression depth of " + std::to_string(kMaxExpressionDepth),
-                        expr->source);
+                    AddError(expr->source)
+                        << "reached max expression depth of " << kMaxExpressionDepth;
                     failed = true;
                     return ast::TraverseAction::Stop;
                 }
@@ -1509,7 +1525,7 @@
                 sorted.Push(expr);
                 return ast::TraverseAction::Descend;
             })) {
-        AddError("TraverseExpressions failed", root->source);
+        AddError(root->source) << "TraverseExpressions failed";
         return nullptr;
     }
 
@@ -1569,7 +1585,7 @@
                             return ast::TraverseAction::Descend;
                         });
                     if (!r) {
-                        AddError("TraverseExpressions failed", root->source);
+                        AddError(root->source) << "TraverseExpressions failed";
                         return nullptr;
                     }
                 }
@@ -1630,10 +1646,9 @@
     if (TINT_UNLIKELY(
             address_space_expr->Value() == core::AddressSpace::kPixelLocal &&
             !enabled_extensions_.Contains(wgsl::Extension::kChromiumExperimentalPixelLocal))) {
-        StringStream err;
-        err << "'pixel_local' address space requires the '"
-            << wgsl::Extension::kChromiumExperimentalPixelLocal << "' extension enabled";
-        AddError(err.str(), expr->source);
+        AddError(expr->source) << "'pixel_local' address space requires the '"
+                               << wgsl::Extension::kChromiumExperimentalPixelLocal
+                               << "' extension enabled";
         return nullptr;
     }
     return address_space_expr;
@@ -1702,18 +1717,18 @@
         std::string access;                   // the access performed for the "other" expression
     };
     auto make_error = [&](const sem::ValueExpression* arg, Alias&& var) {
-        AddError("invalid aliased pointer argument", arg->Declaration()->source);
+        AddError(arg->Declaration()->source) << "invalid aliased pointer argument";
         switch (var.type) {
             case Alias::Argument:
-                AddNote("aliases with another argument passed here",
-                        var.expr->Declaration()->source);
+                AddNote(var.expr->Declaration()->source)
+                    << "aliases with another argument passed here";
                 break;
             case Alias::ModuleScope: {
                 auto* func = var.expr->Stmt()->Function();
                 auto func_name = func->Declaration()->name->symbol.Name();
-                AddNote(
-                    "aliases with module-scope variable " + var.access + " in '" + func_name + "'",
-                    var.expr->Declaration()->source);
+                AddNote(var.expr->Declaration()->source)
+                    << "aliases with module-scope variable " << var.access << " in '" << func_name
+                    << "'";
                 break;
             }
         }
@@ -1987,10 +2002,9 @@
     if (memory_view) {
         if (memory_view->Is<core::type::Pointer>() &&
             !allowed_features_.features.count(wgsl::LanguageFeature::kPointerCompositeAccess)) {
-            AddError(
-                "pointer composite access requires the pointer_composite_access language feature, "
-                "which is not allowed in the current environment",
-                expr->source);
+            AddError(expr->source)
+                << "pointer composite access requires the pointer_composite_access language "
+                   "feature, which is not allowed in the current environment";
             return nullptr;
         }
         storage_ty = memory_view->StoreType();
@@ -2004,7 +2018,7 @@
             return b.create<core::type::Vector>(mat->type(), mat->rows());
         },
         [&](Default) {
-            AddError("cannot index type '" + sem_.TypeNameOf(storage_ty) + "'", expr->source);
+            AddError(expr->source) << "cannot index type '" << sem_.TypeNameOf(storage_ty) << "'";
             return nullptr;
         });
     if (ty == nullptr) {
@@ -2013,8 +2027,8 @@
 
     auto* idx_ty = idx->Type()->UnwrapRef();
     if (!idx_ty->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("index must be of type 'i32' or 'u32', found: '" + sem_.TypeNameOf(idx_ty) + "'",
-                 idx->Declaration()->source);
+        AddError(idx->Declaration()->source)
+            << "index must be of type 'i32' or 'u32', found: '" << sem_.TypeNameOf(idx_ty) << "'";
         return nullptr;
     }
 
@@ -2084,7 +2098,7 @@
         auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type()->UnwrapRef(); });
         auto match = intrinsic_table_.Lookup(ty, template_args, arg_tys, args_stage);
         if (match != Success) {
-            AddError(match.Failure(), expr->source);
+            AddError(expr->source) << match.Failure();
             return nullptr;
         }
 
@@ -2243,7 +2257,7 @@
                 return arr_or_str_init(str, call_target);
             },
             [&](Default) {
-                AddError("type is not constructible", expr->source);
+                AddError(expr->source) << "type is not constructible";
                 return nullptr;
             });
     };
@@ -2283,14 +2297,14 @@
                     tint::Transform(args, [](auto* arg) { return arg->Type()->UnwrapRef(); });
                 auto el_ty = core::type::Type::Common(arg_tys);
                 if (TINT_UNLIKELY(!el_ty)) {
-                    AddError("cannot infer common array element type from constructor arguments",
-                             expr->source);
+                    AddError(expr->source)
+                        << "cannot infer common array element type from constructor arguments";
                     Hashset<const core::type::Type*, 8> types;
                     for (size_t i = 0; i < args.Length(); i++) {
                         if (types.Add(args[i]->Type())) {
-                            AddNote("argument " + std::to_string(i) + " is of type '" +
-                                        sem_.TypeNameOf(args[i]->Type()) + "'",
-                                    args[i]->Declaration()->source);
+                            AddNote(args[i]->Declaration()->source)
+                                << "argument " << i << " is of type '"
+                                << sem_.TypeNameOf(args[i]->Type()) << "'";
                         }
                     }
                     return nullptr;
@@ -2367,7 +2381,7 @@
     auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type()->UnwrapRef(); });
     auto overload = intrinsic_table_.Lookup(fn, tmpl_args, arg_tys, arg_stage);
     if (overload != Success) {
-        AddError(overload.Failure(), expr->source);
+        AddError(expr->source) << overload.Failure();
         return nullptr;
     }
 
@@ -2406,7 +2420,7 @@
     }
 
     if (target->IsDeprecated()) {
-        AddWarning("use of deprecated builtin", expr->source);
+        AddWarning(expr->source) << "use of deprecated builtin";
     }
 
     // If the builtin is @const, and all arguments have constant values, evaluate the builtin
@@ -2903,7 +2917,7 @@
     }
 
     if (!ApplyAddressSpaceUsageToType(address_space, store_ty, tmpl_ident->arguments[1]->source)) {
-        AddNote("while instantiating " + out->FriendlyName(), ident->source);
+        AddNote(ident->source) << "while instantiating " << out->FriendlyName();
         return nullptr;
     }
     return out;
@@ -2990,8 +3004,9 @@
     auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>();
     if (!tmpl_ident) {
         if (TINT_UNLIKELY(min_args != 0)) {
-            AddError("expected '<' for '" + ident->symbol.Name() + "'",
-                     Source{ident->source.range.end});
+            AddError(Source{ident->source.range.end})
+                << "expected " << style::Code << "<" << style::Plain << " for " << style::Code
+                << ident->symbol.Name();
         }
         return nullptr;
     }
@@ -3006,22 +3021,19 @@
     }
     if (min_args == max_args) {
         if (TINT_UNLIKELY(ident->arguments.Length() != min_args)) {
-            AddError("'" + ident->symbol.Name() + "' requires " + std::to_string(min_args) +
-                         " template arguments",
-                     ident->source);
+            AddError(ident->source) << style::Code << ident->symbol.Name() << style::Plain
+                                    << " requires " << min_args << " template arguments";
             return false;
         }
     } else {
         if (TINT_UNLIKELY(ident->arguments.Length() < min_args)) {
-            AddError("'" + ident->symbol.Name() + "' requires at least " +
-                         std::to_string(min_args) + " template arguments",
-                     ident->source);
+            AddError(ident->source) << style::Code << ident->symbol.Name() << style::Plain
+                                    << " requires at least " << min_args << " template arguments";
             return false;
         }
         if (TINT_UNLIKELY(ident->arguments.Length() > max_args)) {
-            AddError("'" + ident->symbol.Name() + "' requires at most " + std::to_string(max_args) +
-                         " template arguments",
-                     ident->source);
+            AddError(ident->source) << style::Code << ident->symbol.Name() << style::Plain
+                                    << " requires at most " << max_args << " template arguments";
             return false;
         }
     }
@@ -3047,9 +3059,7 @@
     const auto& signature = builtin->Signature();
     int texture_index = signature.IndexOf(core::ParameterUsage::kTexture);
     if (TINT_UNLIKELY(texture_index == -1)) {
-        StringStream err;
-        err << "texture builtin without texture parameter";
-        AddICE(err.str(), {});
+        AddICE("texture builtin without texture parameter", {});
         return;
     }
     if (auto* user =
@@ -3287,14 +3297,14 @@
                             auto symbol = ident->symbol;
                             if (auto decl = loop_block->Decls().Get(symbol)) {
                                 if (decl->order >= loop_block->NumDeclsAtFirstContinue()) {
-                                    AddError("continue statement bypasses declaration of '" +
-                                                 symbol.Name() + "'",
-                                             loop_block->FirstContinue()->source);
-                                    AddNote("identifier '" + symbol.Name() + "' declared here",
-                                            decl->variable->Declaration()->source);
-                                    AddNote("identifier '" + symbol.Name() +
-                                                "' referenced in continuing block here",
-                                            expr->source);
+                                    AddError(loop_block->FirstContinue()->source)
+                                        << "continue statement bypasses declaration of '"
+                                        << symbol.Name() << "'";
+                                    AddNote(decl->variable->Declaration()->source)
+                                        << "identifier '" << symbol.Name() << "' declared here";
+                                    AddNote(expr->source)
+                                        << "identifier '" << symbol.Name()
+                                        << "' referenced in continuing block here";
                                     return nullptr;
                                 }
                             }
@@ -3308,9 +3318,10 @@
                     }
                     if (!current_function_ && variable->Declaration()->Is<ast::Var>()) {
                         // Use of a module-scope 'var' outside of a function.
-                        std::string desc = "var '" + ident->symbol.Name() + "' ";
-                        AddError(desc + "cannot be referenced at module-scope", expr->source);
-                        AddNote(desc + "declared here", variable->Declaration()->source);
+                        AddError(expr->source)
+                            << style::Keyword << "var " << style::Variable << ident->symbol.Name()
+                            << style::Plain << " cannot be referenced at module-scope";
+                        sem_.NoteDeclarationSource(variable->Declaration());
                         return nullptr;
                     }
                 }
@@ -3420,10 +3431,9 @@
     if (memory_view) {
         if (memory_view->Is<core::type::Pointer>() &&
             !allowed_features_.features.count(wgsl::LanguageFeature::kPointerCompositeAccess)) {
-            AddError(
-                "pointer composite access requires the pointer_composite_access language feature, "
-                "which is not allowed in the current environment",
-                expr->source);
+            AddError(expr->source)
+                << "pointer composite access requires the pointer_composite_access language "
+                   "feature, which is not allowed in the current environment";
             return nullptr;
         }
         storage_ty = memory_view->StoreType();
@@ -3452,7 +3462,7 @@
             }
 
             if (member == nullptr) {
-                AddError("struct member " + symbol.Name() + " not found", expr->source);
+                AddError(expr->source) << "struct member " << symbol.Name() << " not found";
                 return nullptr;
             }
 
@@ -3497,20 +3507,20 @@
                         swizzle.Push(3u);
                         break;
                     default:
-                        AddError(
-                            "invalid vector swizzle character",
-                            expr->member->source.Begin() + static_cast<uint32_t>(swizzle.Length()));
+                        AddError(expr->member->source.Begin() +
+                                 static_cast<uint32_t>(swizzle.Length()))
+                            << "invalid vector swizzle character";
                         return nullptr;
                 }
 
                 if (swizzle.Back() >= vec->Width()) {
-                    AddError("invalid vector swizzle member", expr->member->source);
+                    AddError(expr->member->source) << "invalid vector swizzle member";
                     return nullptr;
                 }
             }
 
             if (size < 1 || size > 4) {
-                AddError("invalid vector swizzle size", expr->member->source);
+                AddError(expr->member->source) << "invalid vector swizzle size";
                 return nullptr;
             }
 
@@ -3519,8 +3529,8 @@
             auto is_xyzw = [](char c) { return c == 'x' || c == 'y' || c == 'z' || c == 'w'; };
             if (!std::all_of(s.begin(), s.end(), is_rgba) &&
                 !std::all_of(s.begin(), s.end(), is_xyzw)) {
-                AddError("invalid mixing of vector swizzle characters rgba with xyzw",
-                         expr->member->source);
+                AddError(expr->member->source)
+                    << "invalid mixing of vector swizzle characters rgba with xyzw";
                 return nullptr;
             }
 
@@ -3554,8 +3564,8 @@
         },
 
         [&](Default) {
-            AddError("cannot index into expression of type '" + sem_.TypeNameOf(storage_ty) + "'",
-                     expr->object->source);
+            AddError(expr->object->source)
+                << "cannot index into expression of type '" << sem_.TypeNameOf(storage_ty) << "'";
             return nullptr;
         });
 }
@@ -3581,7 +3591,7 @@
     auto overload = intrinsic_table_.Lookup(expr->op, lhs->Type()->UnwrapRef(),
                                             rhs->Type()->UnwrapRef(), stage, false);
     if (overload != Success) {
-        AddError(overload.Failure(), expr->source);
+        AddError(expr->source) << overload.Failure();
         return nullptr;
     }
 
@@ -3663,9 +3673,9 @@
         case core::UnaryOp::kAddressOf:
             if (auto* ref = expr_ty->As<core::type::Reference>()) {
                 if (ref->StoreType()->UnwrapRef()->is_handle()) {
-                    AddError("cannot take the address of " + sem_.Describe(expr) +
-                                 " in handle address space",
-                             unary->expr->source);
+                    AddError(unary->expr->source)
+                        << "cannot take the address of " << sem_.Describe(expr)
+                        << " in handle address space";
                     return nullptr;
                 }
 
@@ -3674,7 +3684,8 @@
                 if ((array && sem_.TypeOf(array->object)->UnwrapRef()->Is<core::type::Vector>()) ||
                     (member &&
                      sem_.TypeOf(member->object)->UnwrapRef()->Is<core::type::Vector>())) {
-                    AddError("cannot take the address of a vector component", unary->expr->source);
+                    AddError(unary->expr->source)
+                        << "cannot take the address of a vector component";
                     return nullptr;
                 }
 
@@ -3683,7 +3694,8 @@
 
                 root_ident = expr->RootIdentifier();
             } else {
-                AddError("cannot take the address of " + sem_.Describe(expr), unary->expr->source);
+                AddError(unary->expr->source)
+                    << "cannot take the address of " << sem_.Describe(expr);
                 return nullptr;
             }
             break;
@@ -3694,8 +3706,8 @@
                                                      ptr->Access());
                 root_ident = expr->RootIdentifier();
             } else {
-                AddError("cannot dereference expression of type '" + sem_.TypeNameOf(expr_ty) + "'",
-                         unary->expr->source);
+                AddError(unary->expr->source) << "cannot dereference expression of type "
+                                              << style::Type << sem_.TypeNameOf(expr_ty);
                 return nullptr;
             }
             break;
@@ -3704,7 +3716,7 @@
             stage = expr->Stage();
             auto overload = intrinsic_table_.Lookup(unary->op, expr_ty->UnwrapRef(), stage);
             if (overload != Success) {
-                AddError(overload.Failure(), unary->source);
+                AddError(unary->source) << overload.Failure();
                 return nullptr;
             }
             ty = overload->return_type;
@@ -3755,14 +3767,17 @@
     }
 
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("@location must be an i32 or u32 value", attr->source);
+        AddError(attr->source) << style::Attribute << "@location" << style::Plain << " must be an "
+                               << style::Type << "i32" << style::Plain << " or " << style::Type
+                               << "u32" << style::Plain << " value";
         return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
-        AddError("@location value must be non-negative", attr->source);
+        AddError(attr->source) << style::Attribute << "@location" << style::Plain
+                               << " value must be non-negative";
         return Failure{};
     }
 
@@ -3779,14 +3794,17 @@
     }
 
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("@color must be an i32 or u32 value", attr->source);
+        AddError(attr->source) << style::Attribute << "@color" << style::Plain << " must be an "
+                               << style::Type << "i32" << style::Plain << " or " << style::Type
+                               << "u32" << style::Plain << " value";
         return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
-        AddError("@color value must be non-negative", attr->source);
+        AddError(attr->source) << style::Attribute << "@color" << style::Plain
+                               << " value must be non-negative";
         return Failure{};
     }
 
@@ -3802,14 +3820,17 @@
     }
 
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("@location must be an i32 or u32 value", attr->source);
+        AddError(attr->source) << style::Attribute << "@blend_src" << style::Plain << style::Type
+                               << "i32" << style::Plain << " or " << style::Type << "u32"
+                               << style::Plain << " value";
         return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value != 0 && value != 1) {
-        AddError("@blend_src value must be zero or one", attr->source);
+        AddError(attr->source) << style::Attribute << "@blend_src" << style::Plain
+                               << " value must be zero or one";
         return Failure{};
     }
 
@@ -3825,14 +3846,17 @@
         return Failure{};
     }
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("@binding must be an i32 or u32 value", attr->source);
+        AddError(attr->source) << style::Attribute << "@binding" << style::Plain << " must be an "
+                               << style::Type << "i32" << style::Plain << " or " << style::Type
+                               << "u32" << style::Plain << " value";
         return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
-        AddError("@binding value must be non-negative", attr->source);
+        AddError(attr->source) << style::Attribute << "@binding" << style::Plain
+                               << " value must be non-negative";
         return Failure{};
     }
     return static_cast<uint32_t>(value);
@@ -3847,14 +3871,17 @@
         return Failure{};
     }
     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-        AddError("@group must be an i32 or u32 value", attr->source);
+        AddError(attr->source) << style::Attribute << "@group" << style::Plain << " must be an "
+                               << style::Type << "i32" << style::Plain << " or " << style::Type
+                               << "u32" << style::Plain << " value";
         return Failure{};
     }
 
     auto const_value = materialized->ConstantValue();
     auto value = const_value->ValueAs<AInt>();
     if (value < 0) {
-        AddError("@group value must be non-negative", attr->source);
+        AddError(attr->source) << style::Attribute << "@group" << style::Plain
+                               << " value must be non-negative";
         return Failure{};
     }
     return static_cast<uint32_t>(value);
@@ -3871,9 +3898,13 @@
     Vector<const sem::ValueExpression*, 3> args;
     Vector<const core::type::Type*, 3> arg_tys;
 
-    constexpr const char* kErrBadExpr =
-        "workgroup_size argument must be a constant or override-expression of type "
-        "abstract-integer, i32 or u32";
+    auto err_bad_expr = [&](const ast::Expression* value) {
+        AddError(value->source) << style::Attribute << "@workgroup_size" << style::Plain
+                                << " argument must be a constant or override-expression of type "
+                                << style::Type << "abstract-integer" << style::Plain << ", "
+                                << style::Type << "i32" << style::Plain << " or " << style::Type
+                                << "u32";
+    };
 
     for (size_t i = 0; i < 3; i++) {
         // Each argument to this attribute can either be a literal, an identifier for a
@@ -3888,13 +3919,13 @@
         }
         auto* ty = expr->Type();
         if (!ty->IsAnyOf<core::type::I32, core::type::U32, core::type::AbstractInt>()) {
-            AddError(kErrBadExpr, value->source);
+            err_bad_expr(value);
             return Failure{};
         }
 
         if (expr->Stage() != core::EvaluationStage::kConstant &&
             expr->Stage() != core::EvaluationStage::kOverride) {
-            AddError(kErrBadExpr, value->source);
+            err_bad_expr(value);
             return Failure{};
         }
 
@@ -3904,8 +3935,9 @@
 
     auto* common_ty = core::type::Type::Common(arg_tys);
     if (!common_ty) {
-        AddError("workgroup_size arguments must be of the same type, either i32 or u32",
-                 attr->source);
+        AddError(attr->source) << style::Attribute << "@workgroup_size" << style::Plain
+                               << " arguments must be of the same type, either " << style::Type
+                               << "i32" << style::Plain << " or " << style::Type << "u32";
         return Failure{};
     }
 
@@ -3921,7 +3953,8 @@
         }
         if (auto* value = materialized->ConstantValue()) {
             if (value->ValueAs<AInt>() < 1) {
-                AddError("workgroup_size argument must be at least 1", values[i]->source);
+                AddError(values[i]->source) << style::Attribute << "@workgroup_size" << style::Plain
+                                            << " argument must be at least 1";
                 return Failure{};
             }
             ws[i] = value->ValueAs<u32>();
@@ -3934,7 +3967,7 @@
     for (size_t i = 1; i < 3; i++) {
         total_size *= static_cast<uint64_t>(ws[i].value_or(1));
         if (total_size > 0xffffffff) {
-            AddError("total workgroup grid size cannot exceed 0xffffffff", values[i]->source);
+            AddError(values[i]->source) << "total workgroup grid size cannot exceed 0xffffffff";
             return Failure{};
         }
     }
@@ -4013,12 +4046,13 @@
             if (rule != wgsl::ChromiumDiagnosticRule::kUndefined) {
                 validator_.DiagnosticFilters().Set(rule, control.severity);
             } else {
-                StringStream ss;
-                ss << "unrecognized diagnostic rule 'chromium." << name << "'\n";
+                auto& warning = AddWarning(control.rule_name->source)
+                                << "unrecognized diagnostic rule " << style::Code << "chromium."
+                                << name << style::Plain << "\n";
                 tint::SuggestAlternativeOptions opts;
                 opts.prefix = "chromium.";
-                tint::SuggestAlternatives(name, wgsl::kChromiumDiagnosticRuleStrings, ss, opts);
-                AddWarning(ss.str(), control.rule_name->source);
+                tint::SuggestAlternatives(name, wgsl::kChromiumDiagnosticRuleStrings,
+                                          warning.message, opts);
             }
         }
         return true;
@@ -4028,10 +4062,10 @@
     if (rule != wgsl::CoreDiagnosticRule::kUndefined) {
         validator_.DiagnosticFilters().Set(rule, control.severity);
     } else {
-        StringStream ss;
-        ss << "unrecognized diagnostic rule '" << name << "'\n";
-        tint::SuggestAlternatives(name, wgsl::kCoreDiagnosticRuleStrings, ss);
-        AddWarning(ss.str(), control.rule_name->source);
+        auto& warning = AddWarning(control.rule_name->source)
+                        << "unrecognized diagnostic rule " << style::Code << name << style::Plain
+                        << "\n";
+        tint::SuggestAlternatives(name, wgsl::kCoreDiagnosticRuleStrings, warning.message);
     }
     return true;
 }
@@ -4041,9 +4075,8 @@
         Mark(ext);
         enabled_extensions_.Add(ext->name);
         if (!allowed_features_.extensions.count(ext->name)) {
-            StringStream ss;
-            ss << "extension '" << ext->name << "' is not allowed in the current environment";
-            AddError(ss.str(), ext->source);
+            AddError(ext->source) << "extension " << style::Code << ext->name << style::Plain
+                                  << " is not allowed in the current environment";
             return false;
         }
     }
@@ -4053,10 +4086,8 @@
 bool Resolver::Requires(const ast::Requires* req) {
     for (auto feature : req->features) {
         if (!allowed_features_.features.count(feature)) {
-            StringStream ss;
-            ss << "language feature '" << wgsl::ToString(feature)
-               << "' is not allowed in the current environment";
-            AddError(ss.str(), req->source);
+            AddError(req->source) << "language feature " << style::Code << wgsl::ToString(feature)
+                                  << style::Plain << " is not allowed in the current environment";
             return false;
         }
     }
@@ -4111,17 +4142,16 @@
         case core::EvaluationStage::kConstant: {
             auto* count_val = count_sem->ConstantValue();
             if (auto* ty = count_val->Type(); !ty->is_integer_scalar()) {
-                AddError(
-                    "array count must evaluate to a constant integer expression, but is type '" +
-                        ty->FriendlyName() + "'",
-                    count_expr->source);
+                AddError(count_expr->source)
+                    << "array count must evaluate to a constant integer expression, but is type "
+                    << style::Type << ty->FriendlyName();
                 return nullptr;
             }
 
             int64_t count = count_val->ValueAs<AInt>();
             if (count < 1) {
-                AddError("array count (" + std::to_string(count) + ") must be greater than 0",
-                         count_expr->source);
+                AddError(count_expr->source)
+                    << "array count (" << count << ") must be greater than 0";
                 return nullptr;
             }
 
@@ -4129,9 +4159,9 @@
         }
 
         default: {
-            AddError(
-                "array count must evaluate to a constant integer expression or override variable",
-                count_expr->source);
+            AddError(count_expr->source)
+                << "array count must evaluate to a constant integer expression "
+                   "or override variable";
             return nullptr;
         }
     }
@@ -4162,7 +4192,8 @@
                 return true;
             },
             [&](Default) {
-                ErrorInvalidAttribute(attribute, "array types");
+                ErrorInvalidAttribute(
+                    attribute, StyledText{} << style::Type << "array" << style::Plain << " types");
                 return false;
             });
         if (!ok) {
@@ -4188,10 +4219,8 @@
     if (auto const_count = el_count->As<core::type::ConstantArrayCount>()) {
         size = const_count->value * stride;
         if (size > std::numeric_limits<uint32_t>::max()) {
-            StringStream msg;
-            msg << "array byte size (0x" << std::hex << size
-                << ") must not exceed 0xffffffff bytes";
-            AddError(msg.str(), count_source);
+            AddError(count_source) << "array byte size (0x" << std::hex << size
+                                   << ") must not exceed 0xffffffff bytes";
             return nullptr;
         }
     } else if (el_count->Is<core::type::RuntimeArrayCount>()) {
@@ -4205,9 +4234,8 @@
     //  https://gpuweb.github.io/gpuweb/wgsl/#limits
     const size_t nest_depth = 1 + NestDepth(el_ty);
     if (nest_depth > kMaxNestDepthOfCompositeType) {
-        AddError("array has nesting depth of " + std::to_string(nest_depth) + ", maximum is " +
-                     std::to_string(kMaxNestDepthOfCompositeType),
-                 array_source);
+        AddError(array_source) << "array has nesting depth of " << nest_depth << ", maximum is "
+                               << kMaxNestDepthOfCompositeType;
         return nullptr;
     }
     nest_depth_.Add(out, nest_depth);
@@ -4241,9 +4269,9 @@
         // https://gpuweb.github.io/gpuweb/wgsl/#limits
         const size_t kMaxNumStructMembers = 16383;
         if (str->members.Length() > kMaxNumStructMembers) {
-            AddError("struct '" + struct_name() + "' has " + std::to_string(str->members.Length()) +
-                         " members, maximum is " + std::to_string(kMaxNumStructMembers),
-                     str->source);
+            AddError(str->source) << style::Keyword << "struct " << style::Type << struct_name()
+                                  << style::Plain << " has " << str->members.Length()
+                                  << " members, maximum is " << kMaxNumStructMembers;
             return nullptr;
         }
     }
@@ -4257,7 +4285,8 @@
         bool ok = Switch(
             attribute, [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
             [&](Default) {
-                ErrorInvalidAttribute(attribute, "struct declarations");
+                ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "struct"
+                                                              << style::Plain << " declarations");
                 return false;
             });
         if (!ok) {
@@ -4283,8 +4312,9 @@
         Mark(member);
         Mark(member->name);
         if (auto added = member_map.Add(member->name->symbol, member); !added) {
-            AddError("redefinition of '" + member->name->symbol.Name() + "'", member->source);
-            AddNote("previous definition is here", added.value->source);
+            AddError(member->source)
+                << "redefinition of " << style::Code << member->name->symbol.Name();
+            AddNote(added.value->source) << "previous definition is here";
             return nullptr;
         }
 
@@ -4298,8 +4328,8 @@
 
         // validator_.Validate member type
         if (!validator_.IsPlain(type)) {
-            AddError(sem_.TypeNameOf(type) + " cannot be used as the type of a structure member",
-                     member->source);
+            AddError(member->source)
+                << sem_.TypeNameOf(type) << " cannot be used as the type of a structure member";
             return nullptr;
         }
 
@@ -4333,13 +4363,13 @@
                     }
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@offset must be constant expression", attr->expr->source);
+                        AddError(attr->expr->source) << "@offset must be constant expression";
                         return false;
                     }
                     offset = const_value->ValueAs<uint64_t>();
 
                     if (offset < struct_size) {
-                        AddError("offsets must be in ascending order", attr->source);
+                        AddError(attr->source) << "offsets must be in ascending order";
                         return false;
                     }
                     has_offset_attr = true;
@@ -4354,20 +4384,23 @@
                         return false;
                     }
                     if (!materialized->Type()->IsAnyOf<core::type::I32, core::type::U32>()) {
-                        AddError("@align must be an i32 or u32 value", attr->source);
+                        AddError(attr->source) << style::Attribute << "@align" << style::Plain
+                                               << " value must be an " << style::Type << "i32"
+                                               << style::Plain << " or " << style::Type << "u32";
                         return false;
                     }
 
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@align must be constant expression", attr->source);
+                        AddError(attr->source) << style::Attribute << "@align" << style::Plain
+                                               << " value must be constant expression";
                         return false;
                     }
                     auto value = const_value->ValueAs<AInt>();
 
                     if (value <= 0 || !tint::IsPowerOfTwo(value)) {
-                        AddError("@align value must be a positive, power-of-two integer",
-                                 attr->source);
+                        AddError(attr->source) << style::Attribute << "@align" << style::Plain
+                                               << " value must be a positive, power-of-two integer";
                         return false;
                     }
                     align = u32(value);
@@ -4383,27 +4416,31 @@
                         return false;
                     }
                     if (!materialized->Type()->IsAnyOf<core::type::U32, core::type::I32>()) {
-                        AddError("@size must be an i32 or u32 value", attr->source);
+                        AddError(attr->source) << style::Attribute << "@size" << style::Plain
+                                               << " value must be an " << style::Type << "i32"
+                                               << style::Plain << " or " << style::Type << "u32";
                         return false;
                     }
 
                     auto const_value = materialized->ConstantValue();
                     if (!const_value) {
-                        AddError("@size must be constant expression", attr->expr->source);
+                        AddError(attr->expr->source) << style::Attribute << "@size" << style::Plain
+                                                     << " value must be constant expression";
                         return false;
                     }
                     {
                         auto value = const_value->ValueAs<AInt>();
                         if (value <= 0) {
-                            AddError("@size must be a positive integer", attr->source);
+                            AddError(attr->source) << style::Attribute << "@size" << style::Plain
+                                                   << " value must be a positive integer";
                             return false;
                         }
                     }
                     auto value = const_value->ValueAs<uint64_t>();
                     if (value < size) {
-                        AddError("@size must be at least as big as the type's size (" +
-                                     std::to_string(size) + ")",
-                                 attr->source);
+                        AddError(attr->source)
+                            << style::Attribute << "@size" << style::Plain
+                            << " must be at least as big as the type's size (" << size << ")";
                         return false;
                     }
                     size = u32(value);
@@ -4460,14 +4497,17 @@
                 [&](const ast::StrideAttribute* attr) {
                     if (validator_.IsValidationEnabled(
                             member->attributes, ast::DisabledValidation::kIgnoreStrideAttribute)) {
-                        ErrorInvalidAttribute(attribute, "struct members");
+                        ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "struct"
+                                                                      << style::Plain
+                                                                      << " members");
                         return false;
                     }
                     return StrideAttribute(attr);
                 },
                 [&](const ast::InternalAttribute* attr) { return InternalAttribute(attr); },
                 [&](Default) {
-                    ErrorInvalidAttribute(attribute, "struct members");
+                    ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "struct"
+                                                                  << style::Plain << " members");
                     return false;
                 });
             if (!ok) {
@@ -4476,16 +4516,17 @@
         }
 
         if (has_offset_attr && (has_align_attr || has_size_attr)) {
-            AddError("@offset cannot be used with @align or @size", member->source);
+            AddError(member->source) << style::Attribute << "@offset" << style::Plain
+                                     << " cannot be used with " << style::Attribute << "@align"
+                                     << style::Plain << " or " << style::Attribute << "@size";
             return nullptr;
         }
 
         offset = tint::RoundUp(align, offset);
         if (offset > std::numeric_limits<uint32_t>::max()) {
-            StringStream msg;
-            msg << "struct member offset (0x" << std::hex << offset << ") must not exceed 0x"
+            AddError(member->source)
+                << "struct member offset (0x" << std::hex << offset << ") must not exceed 0x"
                 << std::hex << std::numeric_limits<uint32_t>::max() << " bytes";
-            AddError(msg.str(), member->source);
             return nullptr;
         }
 
@@ -4504,9 +4545,8 @@
     struct_size = tint::RoundUp(struct_align, struct_size);
 
     if (struct_size > std::numeric_limits<uint32_t>::max()) {
-        StringStream msg;
-        msg << "struct size (0x" << std::hex << struct_size << ") must not exceed 0xffffffff bytes";
-        AddError(msg.str(), str->source);
+        AddError(str->source) << "struct size (0x" << std::hex << struct_size
+                              << ") must not exceed 0xffffffff bytes";
         return nullptr;
     }
     if (TINT_UNLIKELY(struct_align > std::numeric_limits<uint32_t>::max())) {
@@ -4543,10 +4583,9 @@
     //  https://gpuweb.github.io/gpuweb/wgsl/#limits
     const size_t nest_depth = 1 + members_nest_depth;
     if (nest_depth > kMaxNestDepthOfCompositeType) {
-        AddError("struct '" + struct_name() + "' has nesting depth of " +
-                     std::to_string(nest_depth) + ", maximum is " +
-                     std::to_string(kMaxNestDepthOfCompositeType),
-                 str->source);
+        AddError(str->source) << style::Keyword << "struct " << style::Type << struct_name()
+                              << style::Plain << " has nesting depth of " << nest_depth
+                              << ", maximum is " << kMaxNestDepthOfCompositeType;
         return nullptr;
     }
     nest_depth_.Add(out, nest_depth);
@@ -4634,7 +4673,8 @@
                 attribute,
                 [&](const ast::DiagnosticAttribute* attr) { return DiagnosticAttribute(attr); },
                 [&](Default) {
-                    ErrorInvalidAttribute(attribute, "switch body");
+                    ErrorInvalidAttribute(attribute, StyledText{} << style::Keyword << "switch"
+                                                                  << style::Plain << " body");
                     return false;
                 });
             if (!ok) {
@@ -4789,7 +4829,7 @@
         auto overload = intrinsic_table_.Lookup(stmt->op, lhs->Type()->UnwrapRef(),
                                                 rhs->Type()->UnwrapRef(), stage, true);
         if (overload != Success) {
-            AddError(overload.Failure(), stmt->source);
+            AddError(stmt->source) << overload.Failure();
             return false;
         }
 
@@ -4861,10 +4901,9 @@
             if (decl && !ApplyAddressSpaceUsageToType(address_space,
                                                       const_cast<core::type::Type*>(member->Type()),
                                                       decl->type->source)) {
-                StringStream err;
-                err << "while analyzing structure member " << sem_.TypeNameOf(str) << "."
+                AddNote(member->Declaration()->source)
+                    << "while analyzing structure member " << sem_.TypeNameOf(str) << "."
                     << member->Name().Name();
-                AddNote(err.str(), member->Declaration()->source);
                 return false;
             }
         }
@@ -4874,16 +4913,15 @@
     if (auto* arr = ty->As<sem::Array>()) {
         if (address_space != core::AddressSpace::kStorage) {
             if (arr->Count()->Is<core::type::RuntimeArrayCount>()) {
-                AddError("runtime-sized arrays can only be used in the <storage> address space",
-                         usage);
+                AddError(usage)
+                    << "runtime-sized arrays can only be used in the <storage> address space";
                 return false;
             }
 
             auto count = arr->ConstantCount();
             if (count.has_value() && count.value() >= kMaxArrayElementCount) {
-                AddError("array count (" + std::to_string(count.value()) + ") must be less than " +
-                             std::to_string(kMaxArrayElementCount),
-                         usage);
+                AddError(usage) << "array count (" << count.value() << ") must be less than "
+                                << kMaxArrayElementCount;
                 return false;
             }
         }
@@ -4892,10 +4930,9 @@
     }
 
     if (core::IsHostShareable(address_space) && !validator_.IsHostShareable(ty)) {
-        StringStream err;
-        err << "Type '" << sem_.TypeNameOf(ty) << "' cannot be used in address space '"
-            << address_space << "' as it is non-host-shareable";
-        AddError(err.str(), usage);
+        AddError(usage) << "type " << style::Type << sem_.TypeNameOf(ty) << style::Plain
+                        << " cannot be used in address space " << style::Enum << address_space
+                        << style::Plain << " as it is non-host-shareable";
         return false;
     }
 
@@ -4917,7 +4954,7 @@
                 attribute,  //
                 [&](const ast::DiagnosticAttribute* attr) { return DiagnosticAttribute(attr); },
                 [&](Default) {
-                    ErrorInvalidAttribute(attribute, use);
+                    ErrorInvalidAttribute(attribute, StyledText{} << use);
                     return false;
                 });
             if (!ok) {
@@ -4964,9 +5001,8 @@
     TINT_SCOPED_ASSIGNMENT(current_scoping_depth_, current_scoping_depth_ + 1);
 
     if (current_scoping_depth_ > kMaxStatementDepth) {
-        AddError("statement nesting depth / chaining length exceeds limit of " +
-                     std::to_string(kMaxStatementDepth),
-                 ast->source);
+        AddError(ast->source) << "statement nesting depth / chaining length exceeds limit of "
+                              << kMaxStatementDepth;
         return nullptr;
     }
 
@@ -5004,9 +5040,8 @@
 
 bool Resolver::CheckNotTemplated(const char* use, const ast::Identifier* ident) {
     if (TINT_UNLIKELY(ident->Is<ast::TemplatedIdentifier>())) {
-        AddError(
-            std::string(use) + " '" + ident->symbol.Name() + "' does not take template arguments",
-            ident->source);
+        AddError(ident->source) << use << " " << style::Code << ident->symbol.Name() << style::Plain
+                                << " does not take template arguments";
         if (auto resolved = dependencies_.resolved_identifiers.Get(ident)) {
             if (auto* ast_node = resolved->Node()) {
                 sem_.NoteDeclarationSource(ast_node);
@@ -5017,11 +5052,12 @@
     return true;
 }
 
-void Resolver::ErrorInvalidAttribute(const ast::Attribute* attr, std::string_view use) {
-    AddError("@" + attr->Name() + " is not valid for " + std::string(use), attr->source);
+void Resolver::ErrorInvalidAttribute(const ast::Attribute* attr, StyledText use) {
+    AddError(attr->source) << style::Attribute << "@" << attr->Name() << style::Plain
+                           << " is not valid for " << use;
 }
 
-void Resolver::AddICE(const std::string& msg, const Source& source) const {
+void Resolver::AddICE(std::string_view msg, const Source& source) const {
     if (source.file) {
         TINT_ICE() << source << ": " << msg;
     } else {
@@ -5031,20 +5067,19 @@
     err.severity = diag::Severity::InternalCompilerError;
     err.system = diag::System::Resolver;
     err.source = source;
-    err.message = msg;
-    diagnostics_.Add(std::move(err));
+    diagnostics_.Add(std::move(err)) << msg;
 }
 
-void Resolver::AddError(const std::string& msg, const Source& source) const {
-    diagnostics_.AddError(diag::System::Resolver, msg, source);
+diag::Diagnostic& Resolver::AddError(const Source& source) const {
+    return diagnostics_.AddError(diag::System::Resolver, source);
 }
 
-void Resolver::AddWarning(const std::string& msg, const Source& source) const {
-    diagnostics_.AddWarning(diag::System::Resolver, msg, source);
+diag::Diagnostic& Resolver::AddWarning(const Source& source) const {
+    return diagnostics_.AddWarning(diag::System::Resolver, source);
 }
 
-void Resolver::AddNote(const std::string& msg, const Source& source) const {
-    diagnostics_.AddNote(diag::System::Resolver, msg, source);
+diag::Diagnostic& Resolver::AddNote(const Source& source) const {
+    return diagnostics_.AddNote(diag::System::Resolver, source);
 }
 
 }  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 5cf2e60..5dcab43 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -51,6 +51,7 @@
 #include "src/tint/lang/wgsl/sem/struct.h"
 #include "src/tint/utils/containers/bitset.h"
 #include "src/tint/utils/containers/unique_vector.h"
+#include "src/tint/utils/text/styled_text.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -635,19 +636,19 @@
     /// Raises an error that the attribute is not valid for the given use.
     /// @param attr the invalue attribute
     /// @param use the thing that the attribute was applied to
-    void ErrorInvalidAttribute(const ast::Attribute* attr, std::string_view use);
+    void ErrorInvalidAttribute(const ast::Attribute* attr, StyledText use);
 
     /// Adds the given internal compiler error message to the diagnostics
-    void AddICE(const std::string& msg, const Source& source) const;
+    void AddICE(std::string_view msg, const Source& source) const;
 
-    /// Adds the given error message to the diagnostics
-    void AddError(const std::string& msg, const Source& source) const;
+    /// @returns a new error message added to the program's diagnostics
+    diag::Diagnostic& AddError(const Source& source) const;
 
-    /// Adds the given warning message to the diagnostics
-    void AddWarning(const std::string& msg, const Source& source) const;
+    /// @returns a new warning message added to the program's diagnostics
+    diag::Diagnostic& AddWarning(const Source& source) const;
 
-    /// Adds the given note message to the diagnostics
-    void AddNote(const std::string& msg, const Source& source) const;
+    /// @returns a new note message added to the program's diagnostics
+    diag::Diagnostic& AddNote(const Source& source) const;
 
     /// @returns the core::type::Type for the builtin type @p builtin_ty with the identifier @p
     /// ident
diff --git a/src/tint/lang/wgsl/resolver/resolver_test.cc b/src/tint/lang/wgsl/resolver/resolver_test.cc
index c2392dd..3d21f7a 100644
--- a/src/tint/lang/wgsl/resolver/resolver_test.cc
+++ b/src/tint/lang/wgsl/resolver/resolver_test.cc
@@ -1751,7 +1751,7 @@
     WrapInFunction(expr);
 
     ASSERT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching overload for operator "));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching overload for 'operator "));
 }
 INSTANTIATE_TEST_SUITE_P(ResolverTest,
                          Expr_Binary_Test_Invalid,
@@ -1795,7 +1795,7 @@
         ASSERT_TRUE(TypeOf(expr) == result_type);
     } else {
         ASSERT_FALSE(r()->Resolve());
-        EXPECT_THAT(r()->error(), HasSubstr("no matching overload for operator *"));
+        EXPECT_THAT(r()->error(), HasSubstr("no matching overload for 'operator *"));
     }
 }
 auto all_dimension_values = testing::Values(2u, 3u, 4u);
@@ -1833,7 +1833,7 @@
         ASSERT_TRUE(TypeOf(expr) == result_type);
     } else {
         ASSERT_FALSE(r()->Resolve());
-        EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching overload for operator * "));
+        EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching overload for 'operator * "));
     }
 }
 INSTANTIATE_TEST_SUITE_P(ResolverTest,
@@ -2117,7 +2117,7 @@
     WrapInFunction(der);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for operator ! (vec4<f32>)"));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for 'operator ! (vec4<f32>)"));
 }
 
 TEST_F(ResolverTest, UnaryOp_Complement) {
@@ -2127,7 +2127,7 @@
     WrapInFunction(der);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for operator ~ (vec4<f32>)"));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for 'operator ~ (vec4<f32>)"));
 }
 
 TEST_F(ResolverTest, UnaryOp_Negation) {
@@ -2137,7 +2137,7 @@
     WrapInFunction(der);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for operator - (u32)"));
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching overload for 'operator - (u32)"));
 }
 
 TEST_F(ResolverTest, TextureSampler_TextureSample) {
@@ -2419,7 +2419,7 @@
          });
 
     ASSERT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "error: cannot take the address of var 's' in handle address space");
+    EXPECT_EQ(r()->error(), "error: cannot take the address of 'var s' in handle address space");
 }
 
 TEST_F(ResolverTest, ModuleDependencyOrderedDeclarations) {
@@ -2533,7 +2533,7 @@
     }
     Structure(Source{{12, 34}}, "S", std::move(members));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: struct 'S' has 16384 members, maximum is 16383");
+    EXPECT_EQ(r()->error(), "12:34 error: 'struct S' has 16384 members, maximum is 16383");
 }
 
 TEST_F(ResolverTest, MaxNumStructMembers_WithIgnoreStructMemberLimit_Valid) {
@@ -2574,7 +2574,7 @@
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: struct 'S254' has nesting depth of 256, maximum is 255");
+    EXPECT_EQ(r()->error(), "12:34 error: 'struct S254' has nesting depth of 256, maximum is 255");
 }
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithVector_Valid) {
@@ -2596,7 +2596,7 @@
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: struct 'S253' has nesting depth of 256, maximum is 255");
+    EXPECT_EQ(r()->error(), "12:34 error: 'struct S253' has nesting depth of 256, maximum is 255");
 }
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithMatrix_Valid) {
@@ -2618,7 +2618,7 @@
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: struct 'S252' has nesting depth of 256, maximum is 255");
+    EXPECT_EQ(r()->error(), "12:34 error: 'struct S252' has nesting depth of 256, maximum is 255");
 }
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Arrays_Valid) {
@@ -2714,7 +2714,7 @@
         s = Structure(source, "S" + std::to_string(i), Vector{Member("m", ty.Of(s))});
     }
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: struct 'S251' has nesting depth of 256, maximum is 255");
+    EXPECT_EQ(r()->error(), "12:34 error: 'struct S251' has nesting depth of 256, maximum is 255");
 }
 
 TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfStruct_Valid) {
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.cc b/src/tint/lang/wgsl/resolver/sem_helper.cc
index e364a91..5164284 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.cc
+++ b/src/tint/lang/wgsl/resolver/sem_helper.cc
@@ -35,6 +35,8 @@
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/lang/wgsl/sem/value_expression.h"
 #include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/text_style.h"
 
 namespace tint::resolver {
 
@@ -68,69 +70,71 @@
 
     auto* type = ty_expr->Type();
     if (auto* incomplete = type->As<IncompleteType>(); TINT_UNLIKELY(incomplete)) {
-        AddError("expected '<' for '" + std::string(ToString(incomplete->builtin)) + "'",
-                 expr->Declaration()->source.End());
+        AddError(expr->Declaration()->source.End())
+            << "expected " << style::Code << "<" << style::Plain << " for " << style::Type
+            << incomplete->builtin;
         return nullptr;
     }
 
     return ty_expr;
 }
 
-std::string SemHelper::Describe(const sem::Expression* expr) const {
-    return Switch(
+StyledText SemHelper::Describe(const sem::Expression* expr) const {
+    StyledText text;
+
+    Switch(
         expr,  //
         [&](const sem::VariableUser* var_expr) {
             auto* variable = var_expr->Variable()->Declaration();
             auto name = variable->name->symbol.Name();
-            auto* kind = Switch(
-                variable,                                            //
-                [&](const ast::Var*) { return "var"; },              //
-                [&](const ast::Let*) { return "let"; },              //
-                [&](const ast::Const*) { return "const"; },          //
-                [&](const ast::Parameter*) { return "parameter"; },  //
-                [&](const ast::Override*) { return "override"; },    //
-                [&](Default) { return "variable"; });
-            return std::string(kind) + " '" + name + "'";
+            Switch(
+                variable,                                                             //
+                [&](const ast::Var*) { text << style::Keyword << "var"; },            //
+                [&](const ast::Let*) { text << style::Keyword << "let"; },            //
+                [&](const ast::Const*) { text << style::Keyword << "const"; },        //
+                [&](const ast::Parameter*) { text << "parameter"; },                  //
+                [&](const ast::Override*) { text << style::Keyword << "override"; },  //
+                [&](Default) { text << "variable"; });
+            text << " " << style::Variable << name;
         },
         [&](const sem::ValueExpression* val_expr) {
-            auto type = val_expr->Type()->FriendlyName();
-            return "value of type '" + type + "'";
+            text << "value of type " << style::Type << val_expr->Type()->FriendlyName();
         },
         [&](const sem::TypeExpression* ty_expr) {
-            auto name = ty_expr->Type()->FriendlyName();
-            return "type '" + name + "'";
+            text << "type " << style::Type << ty_expr->Type()->FriendlyName();
         },
         [&](const sem::FunctionExpression* fn_expr) {
             auto* fn = fn_expr->Function()->Declaration();
-            auto name = fn->name->symbol.Name();
-            return "function '" + name + "'";
+            text << "function " << style::Function << fn->name->symbol.Name();
         },
         [&](const sem::BuiltinEnumExpression<wgsl::BuiltinFn>* fn) {
-            return "builtin function '" + tint::ToString(fn->Value()) + "'";
+            text << "builtin function " << style::Function << fn->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::Access>* access) {
-            return "access '" + tint::ToString(access->Value()) + "'";
+            text << "access " << style::Enum << access->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::AddressSpace>* addr) {
-            return "address space '" + tint::ToString(addr->Value()) + "'";
+            text << "address space " << style::Enum << addr->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::BuiltinValue>* builtin) {
-            return "builtin value '" + tint::ToString(builtin->Value()) + "'";
+            text << "builtin value " << style::Enum << builtin->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::InterpolationSampling>* fmt) {
-            return "interpolation sampling '" + tint::ToString(fmt->Value()) + "'";
+            text << "interpolation sampling " << style::Enum << fmt->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::InterpolationType>* fmt) {
-            return "interpolation type '" + tint::ToString(fmt->Value()) + "'";
+            text << "interpolation type " << style::Enum << fmt->Value();
         },
         [&](const sem::BuiltinEnumExpression<core::TexelFormat>* fmt) {
-            return "texel format '" + tint::ToString(fmt->Value()) + "'";
+            text << "texel format " << style::Enum << fmt->Value();
         },
         [&](const UnresolvedIdentifier* ui) {
             auto name = ui->Identifier()->identifier->symbol.Name();
-            return "unresolved identifier '" + name + "'";
+            text << "unresolved identifier " << style::Code << name;
         },  //
         TINT_ICE_ON_NO_MATCH);
+
+    return text << style::Plain;
 }
 
 void SemHelper::ErrorUnexpectedExprKind(
@@ -140,7 +144,7 @@
     if (auto* ui = expr->As<UnresolvedIdentifier>()) {
         auto* ident = ui->Identifier();
         auto name = ident->identifier->symbol.Name();
-        AddError("unresolved " + std::string(wanted) + " '" + name + "'", ident->source);
+        AddError(ident->source) << "unresolved " << wanted << " " << style::Code << name;
         if (!suggestions.IsEmpty()) {
             // Filter out suggestions that have a leading underscore.
             Vector<std::string_view, 8> filtered;
@@ -149,15 +153,13 @@
                     filtered.Push(str);
                 }
             }
-            StringStream msg;
-            tint::SuggestAlternatives(name, filtered.Slice(), msg);
-            AddNote(msg.str(), ident->source);
+            auto& note = AddNote(ident->source);
+            tint::SuggestAlternatives(name, filtered.Slice(), note.message);
         }
         return;
     }
 
-    AddError("cannot use " + Describe(expr) + " as " + std::string(wanted),
-             expr->Declaration()->source);
+    AddError(expr->Declaration()->source) << "cannot use " << Describe(expr) << " as " << wanted;
     NoteDeclarationSource(expr->Declaration());
 }
 
@@ -166,7 +168,8 @@
     if (auto* ident = expr->Declaration()->As<ast::IdentifierExpression>()) {
         if (expr->IsAnyOf<sem::FunctionExpression, sem::TypeExpression,
                           sem::BuiltinEnumExpression<wgsl::BuiltinFn>>()) {
-            AddNote("are you missing '()'?", ident->source.End());
+            AddNote(ident->source.End())
+                << "are you missing " << style::Code << "()" << style::Plain << "?";
         }
     }
 }
@@ -188,40 +191,48 @@
     Switch(
         node,
         [&](const ast::Struct* n) {
-            AddNote("struct '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "struct " << style::Type
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Alias* n) {
-            AddNote("alias '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "alias " << style::Type
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Var* n) {
-            AddNote("var '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "var " << style::Variable
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Let* n) {
-            AddNote("let '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "let " << style::Variable
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Override* n) {
-            AddNote("override '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "override " << style::Variable
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Const* n) {
-            AddNote("const '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << style::Keyword << "const " << style::Variable
+                               << n->name->symbol.Name() << style::Plain << " declared here";
         },
         [&](const ast::Parameter* n) {
-            AddNote("parameter '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << "parameter " << style::Variable << n->name->symbol.Name()
+                               << style::Plain << " declared here";
         },
         [&](const ast::Function* n) {
-            AddNote("function '" + n->name->symbol.Name() + "' declared here", n->source);
+            AddNote(n->source) << "function " << style::Function << n->name->symbol.Name()
+                               << style::Plain << " declared here";
         });
 }
 
-void SemHelper::AddError(const std::string& msg, const Source& source) const {
-    builder_->Diagnostics().AddError(diag::System::Resolver, msg, source);
+diag::Diagnostic& SemHelper::AddError(const Source& source) const {
+    return builder_->Diagnostics().AddError(diag::System::Resolver, source);
 }
 
-void SemHelper::AddWarning(const std::string& msg, const Source& source) const {
-    builder_->Diagnostics().AddWarning(diag::System::Resolver, msg, source);
+diag::Diagnostic& SemHelper::AddWarning(const Source& source) const {
+    return builder_->Diagnostics().AddWarning(diag::System::Resolver, source);
 }
 
-void SemHelper::AddNote(const std::string& msg, const Source& source) const {
-    builder_->Diagnostics().AddNote(diag::System::Resolver, msg, source);
+diag::Diagnostic& SemHelper::AddNote(const Source& source) const {
+    return builder_->Diagnostics().AddNote(diag::System::Resolver, source);
 }
 }  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/sem_helper.h b/src/tint/lang/wgsl/resolver/sem_helper.h
index ad63ac2..fd7b700 100644
--- a/src/tint/lang/wgsl/resolver/sem_helper.h
+++ b/src/tint/lang/wgsl/resolver/sem_helper.h
@@ -40,6 +40,7 @@
 #include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/utils/containers/map.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/text/styled_text.h"
 
 namespace tint::resolver {
 
@@ -287,17 +288,17 @@
 
     /// @param expr the expression to describe
     /// @return a string that describes @p expr. Useful for diagnostics.
-    std::string Describe(const sem::Expression* expr) const;
+    StyledText Describe(const sem::Expression* expr) const;
 
   private:
-    /// Adds the given error message to the diagnostics
-    void AddError(const std::string& msg, const Source& source) const;
+    /// @returns a new error diagnostics
+    diag::Diagnostic& AddError(const Source& source) const;
 
-    /// Adds the given warning message to the diagnostics
-    void AddWarning(const std::string& msg, const Source& source) const;
+    /// @returns a new warning diagnostics
+    diag::Diagnostic& AddWarning(const Source& source) const;
 
-    /// Adds the given note message to the diagnostics
-    void AddNote(const std::string& msg, const Source& source) const;
+    /// @returns a new note diagnostics
+    diag::Diagnostic& AddNote(const Source& source) const;
 
     ProgramBuilder* builder_;
 };
diff --git a/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc b/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
index e8e784f..7fdb6c1 100644
--- a/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
+++ b/src/tint/lang/wgsl/resolver/subgroups_extension_test.cc
@@ -48,7 +48,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(error: use of @builtin(subgroup_size) attribute requires enabling extension 'chromium_experimental_subgroups')");
+        R"(error: use of '@builtin(subgroup_size)' attribute requires enabling extension 'chromium_experimental_subgroups')");
 }
 
 // Using a subgroup_invocation_id builtin attribute without chromium_experimental_subgroups enabled
@@ -62,7 +62,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        R"(error: use of @builtin(subgroup_invocation_id) attribute requires enabling extension 'chromium_experimental_subgroups')");
+        R"(error: use of '@builtin(subgroup_invocation_id)' attribute requires enabling extension 'chromium_experimental_subgroups')");
 }
 
 // Using an i32 for a subgroup_size builtin input should fail.
@@ -74,7 +74,7 @@
               });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "error: store type of @builtin(subgroup_size) must be 'u32'");
+    EXPECT_EQ(r()->error(), "error: store type of '@builtin(subgroup_size)' must be 'u32'");
 }
 
 // Using an i32 for a subgroup_invocation_id builtin input should fail.
@@ -86,7 +86,8 @@
               });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "error: store type of @builtin(subgroup_invocation_id) must be 'u32'");
+    EXPECT_EQ(r()->error(),
+              "error: store type of '@builtin(subgroup_invocation_id)' must be 'u32'");
 }
 
 // Using builtin(subgroup_size) for anything other than a compute shader input should fail.
@@ -98,7 +99,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "error: @builtin(subgroup_size) is only valid as a compute shader input");
+              "error: '@builtin(subgroup_size)' is only valid as a compute shader input");
 }
 
 // Using builtin(subgroup_invocation_id) for anything other than a compute shader input should fail.
@@ -110,7 +111,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "error: @builtin(subgroup_invocation_id) is only valid as a compute shader input");
+              "error: '@builtin(subgroup_invocation_id)' is only valid as a compute shader input");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/resolver/type_validation_test.cc b/src/tint/lang/wgsl/resolver/type_validation_test.cc
index 0264afc..8d191a8 100644
--- a/src/tint/lang/wgsl/resolver/type_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/type_validation_test.cc
@@ -384,7 +384,7 @@
     GlobalVar("a", ty.array(Source{{12, 34}}, ty.f32(), "size"), core::AddressSpace::kPrivate);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -396,7 +396,7 @@
               core::AddressSpace::kWorkgroup);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -409,7 +409,7 @@
     Structure("S", Vector{Member("a", ty.array(Source{{12, 34}}, ty.f32(), "size"))});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -425,7 +425,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -441,7 +441,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -459,7 +459,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -477,7 +477,7 @@
          });
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: array with an 'override' element count can only be used as the store "
+              "12:34 error: 'array' with an 'override' element count can only be used as the store "
               "type of a 'var<workgroup>'");
 }
 
@@ -493,9 +493,9 @@
     GlobalVar("b", ty.array(ty.f32(), Add("size", 1_i)), core::AddressSpace::kWorkgroup);
     WrapInFunction(Assign(Source{{12, 34}}, "a", "b"));
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: cannot assign 'array<f32, [unnamed override-expression]>' to "
-              "'array<f32, [unnamed override-expression]>'");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: cannot assign 'array<f32, [unnamed override-expression]>' to 'array<f32, [unnamed override-expression]>')");
 }
 
 TEST_F(ResolverTypeValidationTest, ArraySize_NamedOverride_Param) {
@@ -536,8 +536,8 @@
               core::AddressSpace::kPrivate);
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: var 'size' cannot be referenced at module-scope
-note: var 'size' declared here)");
+              R"(12:34 error: 'var size' cannot be referenced at module-scope
+note: 'var size' declared here)");
 }
 
 TEST_F(ResolverTypeValidationTest, ArraySize_FunctionConst) {
@@ -1511,7 +1511,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: type 'A' does not take template arguments
-56:78 note: alias 'A' declared here)");
+56:78 note: 'alias A' declared here)");
 }
 
 INSTANTIATE_TEST_SUITE_P(BuiltinTypes,
@@ -1564,7 +1564,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: type 'S' does not take template arguments
-56:78 note: struct 'S' declared here)");
+56:78 note: 'struct S' declared here)");
 }
 
 TEST_F(ResolverUntemplatedTypeUsedWithTemplateArgs, Struct_Ctor) {
@@ -1576,7 +1576,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: type 'S' does not take template arguments
-note: struct 'S' declared here)");
+note: 'struct S' declared here)");
 }
 
 TEST_F(ResolverUntemplatedTypeUsedWithTemplateArgs, AliasedArray_Type) {
@@ -1589,7 +1589,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: type 'A' does not take template arguments
-note: alias 'A' declared here)");
+note: 'alias A' declared here)");
 }
 
 TEST_F(ResolverUntemplatedTypeUsedWithTemplateArgs, AliasedArray_Ctor) {
@@ -1601,7 +1601,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: type 'A' does not take template arguments
-note: alias 'A' declared here)");
+note: 'alias A' declared here)");
 }
 
 }  // namespace TypeDoesNotTakeTemplateArgs
diff --git a/src/tint/lang/wgsl/resolver/uniformity.cc b/src/tint/lang/wgsl/resolver/uniformity.cc
index c86127a..818e5e1 100644
--- a/src/tint/lang/wgsl/resolver/uniformity.cc
+++ b/src/tint/lang/wgsl/resolver/uniformity.cc
@@ -1829,9 +1829,8 @@
         auto* control_flow = TraceBackAlongPathUntil(
             non_uniform_source, [](Node* node) { return node->affects_control_flow; });
         if (control_flow) {
-            diagnostics_.AddNote(diag::System::Resolver,
-                                 "control flow depends on possibly non-uniform value",
-                                 control_flow->ast->source);
+            diagnostics_.AddNote(diag::System::Resolver, control_flow->ast->source)
+                << "control flow depends on possibly non-uniform value";
             // TODO(jrprice): There are cases where the function with uniformity requirements is not
             // actually inside this control flow construct, for example:
             // - A conditional interrupt (e.g. break), with a barrier elsewhere in the loop
@@ -1876,58 +1875,52 @@
             non_uniform_source->ast,
             [&](const ast::IdentifierExpression* ident) {
                 auto* var = sem_.GetVal(ident)->UnwrapLoad()->As<sem::VariableUser>()->Variable();
-                StringStream ss;
                 if (auto* param = var->As<sem::Parameter>()) {
                     auto* func = param->Owner()->As<sem::Function>();
-                    ss << param_type(param) << "'" << NameFor(ident) << "' of '" << NameFor(func)
-                       << "' may be non-uniform";
+                    diagnostics_.AddNote(diag::System::Resolver, ident->source)
+                        << param_type(param) << "'" << NameFor(ident) << "' of '" << NameFor(func)
+                        << "' may be non-uniform";
                 } else {
-                    ss << "reading from " << var_type(var) << "'" << NameFor(ident)
-                       << "' may result in a non-uniform value";
+                    diagnostics_.AddNote(diag::System::Resolver, ident->source)
+                        << "reading from " << var_type(var) << "'" << NameFor(ident)
+                        << "' may result in a non-uniform value";
                 }
-                diagnostics_.AddNote(diag::System::Resolver, ss.str(), ident->source);
             },
             [&](const ast::Variable* v) {
                 auto* var = sem_.Get(v);
-                StringStream ss;
-                ss << "reading from " << var_type(var) << "'" << NameFor(v)
-                   << "' may result in a non-uniform value";
-                diagnostics_.AddNote(diag::System::Resolver, ss.str(), v->source);
+                diagnostics_.AddNote(diag::System::Resolver, v->source)
+                    << "reading from " << var_type(var) << "'" << NameFor(v)
+                    << "' may result in a non-uniform value";
             },
             [&](const ast::CallExpression* c) {
                 auto target_name = NameFor(c->target);
                 switch (non_uniform_source->type) {
                     case Node::kFunctionCallReturnValue: {
-                        diagnostics_.AddNote(
-                            diag::System::Resolver,
-                            "return value of '" + target_name + "' may be non-uniform", c->source);
+                        diagnostics_.AddNote(diag::System::Resolver, c->source)
+                            << "return value of '" + target_name + "' may be non-uniform";
                         break;
                     }
                     case Node::kFunctionCallArgumentContents: {
                         auto* arg = c->args[non_uniform_source->arg_index];
                         auto* var = sem_.GetVal(arg)->RootIdentifier();
-                        StringStream ss;
-                        ss << "reading from " << var_type(var) << "'" << NameFor(var)
-                           << "' may result in a non-uniform value";
-                        diagnostics_.AddNote(diag::System::Resolver, ss.str(),
-                                             var->Declaration()->source);
+                        diagnostics_.AddNote(diag::System::Resolver, var->Declaration()->source)
+                            << "reading from " << var_type(var) << "'" << NameFor(var)
+                            << "' may result in a non-uniform value";
                         break;
                     }
                     case Node::kFunctionCallArgumentValue: {
                         auto* arg = c->args[non_uniform_source->arg_index];
                         // TODO(jrprice): Which output? (return value vs another pointer argument).
-                        diagnostics_.AddNote(diag::System::Resolver,
-                                             "passing non-uniform pointer to '" + target_name +
-                                                 "' may produce a non-uniform output",
-                                             arg->source);
+                        diagnostics_.AddNote(diag::System::Resolver, arg->source)
+                            << "passing non-uniform pointer to '" << target_name
+                            << "' may produce a non-uniform output";
                         break;
                     }
                     case Node::kFunctionCallPointerArgumentResult: {
-                        diagnostics_.AddNote(
-                            diag::System::Resolver,
-                            "contents of pointer may become non-uniform after calling '" +
-                                target_name + "'",
-                            c->args[non_uniform_source->arg_index]->source);
+                        diagnostics_.AddNote(diag::System::Resolver,
+                                             c->args[non_uniform_source->arg_index]->source)
+                            << "contents of pointer may become non-uniform after calling '"
+                            << target_name << "'";
                         break;
                     }
                     default: {
@@ -1937,8 +1930,8 @@
                 }
             },
             [&](const ast::Expression* e) {
-                diagnostics_.AddNote(diag::System::Resolver,
-                                     "result of expression may be non-uniform", e->source);
+                diagnostics_.AddNote(diag::System::Resolver, e->source)
+                    << "result of expression may be non-uniform";
             },  //
             TINT_ICE_ON_NO_MATCH);
     }
diff --git a/src/tint/lang/wgsl/resolver/validation_test.cc b/src/tint/lang/wgsl/resolver/validation_test.cc
index a3c74fd..809ade9 100644
--- a/src/tint/lang/wgsl/resolver/validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/validation_test.cc
@@ -1194,7 +1194,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @align value must be a positive, power-of-two integer)");
+              R"(12:34 error: '@align' value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, NonPOTStructMemberAlignAttribute) {
@@ -1204,7 +1204,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @align value must be a positive, power-of-two integer)");
+              R"(12:34 error: '@align' value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberAlignAttribute) {
@@ -1214,7 +1214,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 error: @align value must be a positive, power-of-two integer)");
+              R"(12:34 error: '@align' value must be a positive, power-of-two integer)");
 }
 
 TEST_F(ResolverValidationTest, ZeroStructMemberSizeAttribute) {
@@ -1223,7 +1223,8 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @size must be at least as big as the type's size (4))");
+    EXPECT_EQ(r()->error(),
+              R"(12:34 error: '@size' must be at least as big as the type's size (4))");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndSizeAttribute) {
@@ -1233,7 +1234,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@offset' cannot be used with '@align' or '@size')");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAttribute) {
@@ -1243,7 +1244,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@offset' cannot be used with '@align' or '@size')");
 }
 
 TEST_F(ResolverValidationTest, OffsetAndAlignAndSizeAttribute) {
@@ -1253,7 +1254,7 @@
                    });
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: @offset cannot be used with @align or @size)");
+    EXPECT_EQ(r()->error(), R"(12:34 error: '@offset' cannot be used with '@align' or '@size')");
 }
 
 TEST_F(ResolverTest, Expr_Initializer_Cast_Pointer) {
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 18f23cc..9546c27 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -88,6 +88,8 @@
 #include "src/tint/utils/math/math.h"
 #include "src/tint/utils/text/string.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/text_style.h"
 
 using namespace tint::core::fluent_types;  // NOLINT
 
@@ -134,11 +136,6 @@
     }
 }
 
-// Helper to stringify a pipeline IO attribute.
-std::string AttrToStr(const ast::Attribute* attr) {
-    return "@" + attr->Name();
-}
-
 template <typename CALLBACK>
 void TraverseCallChain(const sem::Function* from, const sem::Function* to, CALLBACK&& callback) {
     for (auto* f : from->TransitivelyCalledFunctions()) {
@@ -180,34 +177,29 @@
 
 Validator::~Validator() = default;
 
-void Validator::AddError(const std::string& msg, const Source& source) const {
-    diagnostics_.AddError(diag::System::Resolver, msg, source);
+diag::Diagnostic& Validator::AddError(const Source& source) const {
+    return diagnostics_.AddError(diag::System::Resolver, source);
 }
 
-void Validator::AddWarning(const std::string& msg, const Source& source) const {
-    diagnostics_.AddWarning(diag::System::Resolver, msg, source);
+diag::Diagnostic& Validator::AddWarning(const Source& source) const {
+    return diagnostics_.AddWarning(diag::System::Resolver, source);
 }
 
-void Validator::AddNote(const std::string& msg, const Source& source) const {
-    diagnostics_.AddNote(diag::System::Resolver, msg, source);
+diag::Diagnostic& Validator::AddNote(const Source& source) const {
+    return diagnostics_.AddNote(diag::System::Resolver, source);
 }
 
-bool Validator::AddDiagnostic(wgsl::DiagnosticRule rule,
-                              const std::string& msg,
-                              const Source& source) const {
+diag::Diagnostic* Validator::MaybeAddDiagnostic(wgsl::DiagnosticRule rule,
+                                                const Source& source) const {
     auto severity = diagnostic_filters_.Get(rule);
     if (severity != wgsl::DiagnosticSeverity::kOff) {
         diag::Diagnostic d{};
         d.severity = ToSeverity(severity);
         d.system = diag::System::Resolver;
         d.source = source;
-        d.message = msg;
-        diagnostics_.Add(std::move(d));
-        if (severity == wgsl::DiagnosticSeverity::kError) {
-            return false;
-        }
+        return &diagnostics_.Add(std::move(d));
     }
-    return true;
+    return nullptr;
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#plain-types-section
@@ -316,11 +308,11 @@
 
     for (auto pair : incompatible) {
         if (enabled_extensions_.Contains(pair.first) && enabled_extensions_.Contains(pair.second)) {
-            std::string a{ToString(pair.first)};
-            std::string b{ToString(pair.second)};
-            AddError("extension '" + a + "' cannot be used with extension '" + b + "'",
-                     source_of(pair.first));
-            AddNote("'" + b + "' enabled here", source_of(pair.second));
+            AddError(source_of(pair.first))
+                << "extension " << style::Code << pair.first << style::Plain
+                << " cannot be used with extension " << style::Code << pair.second;
+            AddNote(source_of(pair.second))
+                << style::Code << pair.second << style::Plain << " enabled here";
             return false;
         }
     }
@@ -332,7 +324,7 @@
     // https://gpuweb.github.io/gpuweb/wgsl/#atomic-types
     // T must be either u32 or i32.
     if (!s->Type()->IsAnyOf<core::type::U32, core::type::I32>()) {
-        AddError("atomic only supports i32 or u32 types", a->arguments[0]->source);
+        AddError(a->arguments[0]->source) << "atomic only supports i32 or u32 types";
         return false;
     }
     return true;
@@ -340,7 +332,7 @@
 
 bool Validator::Pointer(const ast::TemplatedIdentifier* a, const core::type::Pointer* s) const {
     if (s->AddressSpace() == core::AddressSpace::kUndefined) {
-        AddError("ptr missing address space", a->source);
+        AddError(a->source) << "ptr missing address space";
         return false;
     }
 
@@ -350,15 +342,15 @@
         // * For the storage address space, the access mode is optional, and defaults to read.
         // * For other address spaces, the access mode must not be written.
         if (s->AddressSpace() != core::AddressSpace::kStorage) {
-            AddError("only pointers in <storage> address space may specify an access mode",
-                     a->source);
+            AddError(a->source)
+                << "only pointers in <storage> address space may specify an access mode";
             return false;
         }
     }
 
     if (auto* store_ty = s->StoreType(); !IsStorable(store_ty)) {
-        AddError(sem_.TypeNameOf(store_ty) + " cannot be used as the store type of a pointer",
-                 a->arguments[1]->source);
+        AddError(a->arguments[1]->source)
+            << sem_.TypeNameOf(store_ty) + " cannot be used as the store type of a pointer";
         return false;
     }
 
@@ -371,42 +363,39 @@
         case core::Access::kRead:
             if (!allowed_features_.features.count(
                     wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures)) {
-                AddError(
+                AddError(source) <<
+
                     "read-only storage textures require the "
                     "readonly_and_readwrite_storage_textures language feature, which is not "
-                    "allowed in the current environment",
-                    source);
+                    "allowed in the current environment";
                 return false;
             }
             break;
         case core::Access::kReadWrite:
             if (!allowed_features_.features.count(
                     wgsl::LanguageFeature::kReadonlyAndReadwriteStorageTextures)) {
-                AddError(
-                    "read-write storage textures require the "
-                    "readonly_and_readwrite_storage_textures language feature, which is not "
-                    "allowed in the current environment",
-                    source);
+                AddError(source)
+                    << "read-write storage textures require the "
+                       "readonly_and_readwrite_storage_textures language feature, which is not "
+                       "allowed in the current environment";
                 return false;
             }
             break;
         case core::Access::kWrite:
             break;
         case core::Access::kUndefined:
-            AddError("storage texture missing access control", source);
+            AddError(source) << "storage texture missing access control";
             return false;
     }
 
     if (!IsValidStorageTextureDimension(t->dim())) {
-        AddError("cube dimensions for storage textures are not supported", source);
+        AddError(source) << "cube dimensions for storage textures are not supported";
         return false;
     }
 
     if (!IsValidStorageTextureTexelFormat(t->texel_format())) {
-        AddError(
-            "image format must be one of the texel formats specified for storage "
-            "textues in https://gpuweb.github.io/gpuweb/wgsl/#texel-formats",
-            source);
+        AddError(source) << "image format must be one of the texel formats specified for storage "
+                            "textues in https://gpuweb.github.io/gpuweb/wgsl/#texel-formats";
         return false;
     }
     return true;
@@ -414,7 +403,7 @@
 
 bool Validator::SampledTexture(const core::type::SampledTexture* t, const Source& source) const {
     if (!t->type()->UnwrapRef()->IsAnyOf<core::type::F32, core::type::I32, core::type::U32>()) {
-        AddError("texture_2d<type>: type must be f32, i32 or u32", source);
+        AddError(source) << "texture_2d<type>: type must be f32, i32 or u32";
         return false;
     }
 
@@ -424,12 +413,12 @@
 bool Validator::MultisampledTexture(const core::type::MultisampledTexture* t,
                                     const Source& source) const {
     if (t->dim() != core::type::TextureDimension::k2d) {
-        AddError("only 2d multisampled textures are supported", source);
+        AddError(source) << "only 2d multisampled textures are supported";
         return false;
     }
 
     if (!t->type()->UnwrapRef()->IsAnyOf<core::type::F32, core::type::I32, core::type::U32>()) {
-        AddError("texture_multisampled_2d<type>: type must be f32, i32 or u32", source);
+        AddError(source) << "texture_multisampled_2d<type>: type must be f32, i32 or u32";
         return false;
     }
 
@@ -440,9 +429,8 @@
                             const core::type::Type* from,
                             const Source& source) const {
     if (core::type::Type::ConversionRank(from, to) == core::type::Type::kNoConversion) {
-        AddError("cannot convert value of type '" + sem_.TypeNameOf(from) + "' to type '" +
-                     sem_.TypeNameOf(to) + "'",
-                 source);
+        AddError(source) << "cannot convert value of type " << style::Type << sem_.TypeNameOf(from)
+                         << style::Plain << " to type " << style::Type << sem_.TypeNameOf(to);
         return false;
     }
     return true;
@@ -456,10 +444,10 @@
 
     // Value type has to match storage type
     if (storage_ty != value_type) {
-        StringStream s;
-        s << "cannot initialize " << v->Kind() << " of type '" << sem_.TypeNameOf(storage_ty)
-          << "' with value of type '" << sem_.TypeNameOf(initializer_ty) << "'";
-        AddError(s.str(), v->source);
+        AddError(v->source) << "cannot initialize " << style::Keyword << v->Kind() << style::Plain
+                            << " of type " << style::Type << sem_.TypeNameOf(storage_ty)
+                            << style::Plain << " with value of type " << style::Type
+                            << sem_.TypeNameOf(initializer_ty);
         return false;
     }
 
@@ -501,16 +489,18 @@
     }
 
     auto note_usage = [&] {
-        AddNote("'" + store_ty->FriendlyName() + "' used in address space '" +
-                    tint::ToString(address_space) + "' here",
-                source);
+        AddNote(source) << style::Type << store_ty->FriendlyName() << style::Plain
+                        << " used in address space " << style::Enum << address_space << style::Plain
+                        << " here";
     };
 
     // Among three host-shareable address spaces, f16 is supported in "uniform" and
     // "storage" address space, but not "push_constant" address space yet.
     if (Is<core::type::F16>(store_ty->DeepestElement()) &&
         address_space == core::AddressSpace::kPushConstant) {
-        AddError("using f16 types in 'push_constant' address space is not implemented yet", source);
+        AddError(source) << "using " << style::Type << "f16" << style::Plain << " in "
+                         << style::Enum << "push_constant" << style::Plain
+                         << " address space is not implemented yet";
         return false;
     }
 
@@ -521,7 +511,7 @@
 
             // Recurse into the member type.
             if (!AddressSpaceLayout(m->Type(), address_space, m->Declaration()->type->source)) {
-                AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
+                AddNote(str->Declaration()->source) << "see layout of struct:\n" << str->Layout();
                 note_usage();
                 return false;
             }
@@ -530,20 +520,21 @@
             if (m->Offset() % required_align != 0 &&
                 !enabled_extensions_.Contains(
                     wgsl::Extension::kChromiumInternalRelaxedUniformLayout)) {
-                AddError("the offset of a struct member of type '" +
-                             m->Type()->UnwrapRef()->FriendlyName() + "' in address space '" +
-                             tint::ToString(address_space) + "' must be a multiple of " +
-                             std::to_string(required_align) + " bytes, but '" + member_name_of(m) +
-                             "' is currently at offset " + std::to_string(m->Offset()) +
-                             ". Consider setting @align(" + std::to_string(required_align) +
-                             ") on this member",
-                         m->Declaration()->source);
+                AddError(m->Declaration()->source)
+                    << "the offset of a struct member of type " << style::Type
+                    << m->Type()->UnwrapRef()->FriendlyName() << style::Plain
+                    << " in address space " << style::Enum << address_space << style::Plain
+                    << " must be a multiple of " << required_align << " bytes, but "
+                    << style::Variable << member_name_of(m) << style::Plain
+                    << " is currently at offset " << m->Offset() << ". Consider setting "
+                    << style::Attribute << "@align" << style::Code << "(" << required_align << ")"
+                    << style::Plain << " on this member";
 
-                AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
+                AddNote(str->Declaration()->source) << "see layout of struct:\n" << str->Layout();
 
                 if (auto* member_str = m->Type()->As<sem::Struct>()) {
-                    AddNote("and layout of struct member:\n" + member_str->Layout(),
-                            member_str->Declaration()->source);
+                    AddNote(member_str->Declaration()->source) << "and layout of struct member:\n"
+                                                               << member_str->Layout();
                 }
 
                 note_usage();
@@ -558,20 +549,24 @@
                 if (prev_to_curr_offset % 16 != 0 &&
                     !enabled_extensions_.Contains(
                         wgsl::Extension::kChromiumInternalRelaxedUniformLayout)) {
-                    AddError(
-                        "uniform storage requires that the number of bytes between the start of "
-                        "the previous member of type struct and the current member be a multiple "
-                        "of 16 bytes, but there are currently " +
-                            std::to_string(prev_to_curr_offset) + " bytes between '" +
-                            member_name_of(prev_member) + "' and '" + member_name_of(m) +
-                            "'. Consider setting @align(16) on this member",
-                        m->Declaration()->source);
+                    AddError(m->Declaration()->source)
+                        << style::Enum << "uniform" << style::Plain
+                        << " storage requires that the number of bytes between the start of the "
+                           "previous member of type struct and the current member be a "
+                           "multiple of 16 bytes, but there are currently "
+                        << prev_to_curr_offset << " bytes between " << style::Variable
+                        << member_name_of(prev_member) << style::Plain << " and " << style::Variable
+                        << member_name_of(m) << style::Plain << ". Consider setting "
+                        << style::Attribute << "@align" << style::Code << "(16)" << style::Plain
+                        << " on this member";
 
-                    AddNote("see layout of struct:\n" + str->Layout(), str->Declaration()->source);
+                    AddNote(str->Declaration()->source) << "see layout of struct:\n"
+                                                        << str->Layout();
 
                     auto* prev_member_str = prev_member->Type()->As<sem::Struct>();
-                    AddNote("and layout of previous member struct:\n" + prev_member_str->Layout(),
-                            prev_member_str->Declaration()->source);
+                    AddNote(prev_member_str->Declaration()->source)
+                        << "and layout of previous member struct:\n"
+                        << prev_member_str->Layout();
                     note_usage();
                     return false;
                 }
@@ -596,25 +591,24 @@
             if (arr->Stride() % 16 != 0) {
                 // Since WGSL has no stride attribute, try to provide a useful hint for how the
                 // shader author can resolve the issue.
-                std::string hint;
+                StyledText hint;
                 if (arr->ElemType()->Is<core::type::Scalar>()) {
-                    hint = "Consider using a vector or struct as the element type instead.";
+                    hint << "Consider using a vector or struct as the element type instead.";
                 } else if (auto* vec = arr->ElemType()->As<core::type::Vector>();
                            vec && vec->type()->Size() == 4) {
-                    hint = "Consider using a vec4 instead.";
+                    hint << "Consider using a vec4 instead.";
                 } else if (arr->ElemType()->Is<sem::Struct>()) {
-                    hint = "Consider using the @size attribute on the last struct member.";
+                    hint << "Consider using the " << style::Attribute << "@size" << style::Plain
+                         << " attribute on the last struct member.";
                 } else {
-                    hint =
-                        "Consider wrapping the element type in a struct and using the @size "
-                        "attribute.";
+                    hint << "Consider wrapping the element type in a struct and using the "
+                         << style::Attribute << "@size" << style::Plain << " attribute.";
                 }
-                AddError(
-                    "uniform storage requires that array elements are aligned to 16 bytes, but "
-                    "array element of type '" +
-                        arr->ElemType()->FriendlyName() + "' has a stride of " +
-                        std::to_string(arr->Stride()) + " bytes. " + hint,
-                    source);
+                AddError(source) << style::Enum << "uniform" << style::Plain
+                                 << " storage requires that array elements are aligned to "
+                                    "16 bytes, but array element of type "
+                                 << style::Type << arr->ElemType()->FriendlyName() << style::Plain
+                                 << " has a stride of " << arr->Stride() << " bytes. " << hint;
                 return false;
             }
         }
@@ -636,8 +630,9 @@
             if (IsValidationEnabled(var->attributes,
                                     ast::DisabledValidation::kIgnoreAddressSpace)) {
                 if (!local->Type()->UnwrapRef()->IsConstructible()) {
-                    AddError("function-scope 'var' must have a constructible type",
-                             var->type ? var->type->source : var->source);
+                    AddError(var->type ? var->type->source : var->source)
+                        << "function-scope " << style::Keyword << "var" << style::Plain
+                        << " must have a constructible type";
                     return false;
                 }
             }
@@ -663,16 +658,18 @@
         [&](const ast::Var* var) {
             if (auto* init = global->Initializer();
                 init && init->Stage() > core::EvaluationStage::kOverride) {
-                AddError("module-scope 'var' initializer must be a constant or override-expression",
-                         init->Declaration()->source);
+                AddError(init->Declaration()->source)
+                    << "module-scope " << style::Keyword << "var" << style::Plain
+                    << " initializer must be a constant or "
+                       "override-expression";
                 return false;
             }
 
             if (!var->declared_address_space && !global->Type()->UnwrapRef()->is_handle()) {
-                AddError(
-                    "module-scope 'var' declarations that are not of texture or sampler types must "
-                    "provide an address space",
-                    decl->source);
+                AddError(decl->source) << "module-scope " << style::Keyword << "var" << style::Plain
+                                       << " declarations that are not of texture "
+                                          "or sampler types must "
+                                          "provide an address space";
                 return false;
             }
 
@@ -687,7 +684,8 @@
     }
 
     if (global->AddressSpace() == core::AddressSpace::kFunction) {
-        AddError("module-scope 'var' must not use address space 'function'", decl->source);
+        AddError(decl->source) << "module-scope " << style::Keyword << "var" << style::Plain
+                               << " must not use address space " << style::Enum << "function";
         return false;
     }
 
@@ -698,7 +696,9 @@
             // https://gpuweb.github.io/gpuweb/wgsl/#resource-interface
             // Each resource variable must be declared with both group and binding attributes.
             if (!decl->HasBindingPoint()) {
-                AddError("resource variables require @group and @binding attributes", decl->source);
+                AddError(decl->source)
+                    << "resource variables require " << style::Attribute << "@group" << style::Plain
+                    << " and " << style::Attribute << "@binding" << style::Plain << " attributes";
                 return false;
             }
             break;
@@ -709,8 +709,10 @@
             if (binding_attr || group_attr) {
                 // https://gpuweb.github.io/gpuweb/wgsl/#attribute-binding
                 // Must only be applied to a resource variable
-                AddError("non-resource variables must not have @group or @binding attributes",
-                         decl->source);
+                AddError(decl->source)
+                    << "non-resource variables must not have " << style::Attribute << "@group"
+                    << style::Plain << " or " << style::Attribute << "@binding" << style::Plain
+                    << " attributes";
                 return false;
             }
         }
@@ -724,7 +726,8 @@
     auto* store_ty = v->Type()->UnwrapRef();
 
     if (!IsStorable(store_ty)) {
-        AddError(sem_.TypeNameOf(store_ty) + " cannot be used as the type of a var", var->source);
+        AddError(var->source) << sem_.TypeNameOf(store_ty)
+                              << " cannot be used as the type of a var";
         return false;
     }
 
@@ -732,9 +735,8 @@
         // https://gpuweb.github.io/gpuweb/wgsl/#module-scope-variables
         // If the store type is a texture type or a sampler type, then the variable declaration must
         // not have a address space attribute. The address space will always be handle.
-        AddError("variables of type '" + sem_.TypeNameOf(store_ty) +
-                     "' must not specifiy an address space",
-                 var->source);
+        AddError(var->source) << "variables of type " << style::Type << sem_.TypeNameOf(store_ty)
+                              << style::Plain << " must not specifiy an address space";
         return false;
     }
 
@@ -744,8 +746,8 @@
         // * For the storage address space, the access mode is optional, and defaults to read.
         // * For other address spaces, the access mode must not be written.
         if (v->AddressSpace() != core::AddressSpace::kStorage) {
-            AddError("only variables in <storage> address space may specify an access mode",
-                     var->source);
+            AddError(var->source)
+                << "only variables in <storage> address space may specify an access mode";
             return false;
         }
     }
@@ -759,10 +761,12 @@
                 // https://gpuweb.github.io/gpuweb/wgsl/#var-and-let
                 // Optionally has an initializer expression, if the variable is in the private or
                 // function address spaces.
-                AddError("var of address space '" + tint::ToString(v->AddressSpace()) +
-                             "' cannot have an initializer. var initializers are only supported "
-                             "for the address spaces 'private' and 'function'",
-                         var->source);
+                AddError(var->source)
+                    << "var of address space " << style::Enum << v->AddressSpace() << style::Plain
+                    << " cannot have an initializer. var initializers are only supported for "
+                       "the address spaces "
+                    << style::Enum << "private" << style::Plain << " and " << style::Enum
+                    << "function";
                 return false;
         }
     }
@@ -775,7 +779,7 @@
     if (IsValidationEnabled(var->attributes, ast::DisabledValidation::kIgnoreAddressSpace) &&
         (v->AddressSpace() == core::AddressSpace::kIn ||
          v->AddressSpace() == core::AddressSpace::kOut)) {
-        AddError("invalid use of input/output address space", var->source);
+        AddError(var->source) << "invalid use of input/output address space";
         return false;
     }
     return true;
@@ -786,8 +790,8 @@
     auto* storage_ty = v->Type()->UnwrapRef();
 
     if (!(storage_ty->IsConstructible() || storage_ty->Is<core::type::Pointer>())) {
-        AddError(sem_.TypeNameOf(storage_ty) + " cannot be used as the type of a 'let'",
-                 decl->source);
+        AddError(decl->source) << sem_.TypeNameOf(storage_ty) << " cannot be used as the type of a "
+                               << style::Keyword << "let";
         return false;
     }
     return true;
@@ -799,25 +803,25 @@
     auto* storage_ty = v->Type()->UnwrapRef();
 
     if (auto* init = v->Initializer(); init && init->Stage() > core::EvaluationStage::kOverride) {
-        AddError("'override' initializer must be an override-expression",
-                 init->Declaration()->source);
+        AddError(init->Declaration()->source) << style::Keyword << "override" << style::Plain
+                                              << " initializer must be an override-expression";
         return false;
     }
 
     if (auto id = v->Attributes().override_id) {
         if (auto var = override_ids.Get(*id); var && *var != v) {
             auto* attr = ast::GetAttribute<ast::IdAttribute>(v->Declaration()->attributes);
-            AddError("@id values must be unique", attr->source);
-            AddNote("a override with an ID of " + std::to_string(id->value) +
-                        " was previously declared here:",
-                    ast::GetAttribute<ast::IdAttribute>((*var)->Declaration()->attributes)->source);
+            AddError(attr->source)
+                << style::Attribute << "@id" << style::Plain << " values must be unique";
+            AddNote(ast::GetAttribute<ast::IdAttribute>((*var)->Declaration()->attributes)->source)
+                << "a override with an ID of " << id->value << " was previously declared here";
             return false;
         }
     }
 
     if (!storage_ty->Is<core::type::Scalar>()) {
-        AddError(sem_.TypeNameOf(storage_ty) + " cannot be used as the type of a 'override'",
-                 decl->source);
+        AddError(decl->source) << sem_.TypeNameOf(storage_ty) << " cannot be used as the type of a "
+                               << style::Keyword << "override";
         return false;
     }
 
@@ -855,10 +859,8 @@
                     break;
             }
             if (!ok) {
-                StringStream ss;
-                ss << "function parameter of pointer type cannot be in '" << sc
-                   << "' address space";
-                AddError(ss.str(), decl->source);
+                AddError(decl->source) << "function parameter of pointer type cannot be in "
+                                       << style::Enum << sc << style::Plain << " address space";
                 return false;
             }
         }
@@ -866,13 +868,13 @@
 
     if (IsPlain(var->Type())) {
         if (!var->Type()->IsConstructible()) {
-            AddError("type of function parameter must be constructible", decl->type->source);
+            AddError(decl->type->source) << "type of function parameter must be constructible";
             return false;
         }
     } else if (!var->Type()
                     ->IsAnyOf<core::type::Texture, core::type::Sampler, core::type::Pointer>()) {
-        AddError("type of function parameter cannot be " + sem_.TypeNameOf(var->Type()),
-                 decl->source);
+        AddError(decl->source) << "type of function parameter cannot be "
+                               << sem_.TypeNameOf(var->Type());
         return false;
     }
 
@@ -889,6 +891,13 @@
     bool is_stage_mismatch = false;
     bool is_output = !is_input;
     auto builtin = sem_.Get(attr)->Value();
+
+    auto err_builtin_type = [&](const char* required) {
+        AddError(attr->source) << "store type of " << style::Attribute << "@builtin" << style::Code
+                               << "(" << style::Enum << builtin << style::Code << ")"
+                               << style::Plain << " must be " << style::Type << required;
+    };
+
     switch (builtin) {
         case core::BuiltinValue::kPosition: {
             if (stage != ast::PipelineStage::kNone &&
@@ -898,9 +907,7 @@
             }
             auto* vec = type->As<core::type::Vector>();
             if (!(vec && vec->Width() == 4 && vec->type()->Is<core::type::F32>())) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'vec4<f32>'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("vec4<f32>");
                 return false;
             }
             break;
@@ -915,9 +922,7 @@
             }
             if (!(type->is_unsigned_integer_vector() &&
                   type->As<core::type::Vector>()->Width() == 3)) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'vec3<u32>'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("vec3<u32>");
                 return false;
             }
             break;
@@ -927,9 +932,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::F32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'f32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("f32");
                 return false;
             }
             break;
@@ -939,9 +942,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::Bool>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'bool'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("bool");
                 return false;
             }
             break;
@@ -951,9 +952,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::U32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'u32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("u32");
                 return false;
             }
             break;
@@ -964,9 +963,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::U32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'u32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("u32");
                 return false;
             }
             break;
@@ -975,9 +972,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::U32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'u32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("u32");
                 return false;
             }
             break;
@@ -987,31 +982,28 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<core::type::U32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'u32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("u32");
                 return false;
             }
             break;
         case core::BuiltinValue::kSubgroupInvocationId:
         case core::BuiltinValue::kSubgroupSize:
             if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumExperimentalSubgroups)) {
-                StringStream err;
-                err << "use of @builtin(" << builtin
-                    << ") attribute requires enabling extension 'chromium_experimental_subgroups'";
-                AddError(err.str(), attr->source);
+                AddError(attr->source) << "use of " << style::Attribute << "@builtin" << style::Code
+                                       << "(" << style::Enum << builtin << style::Code << ")"
+                                       << style::Plain << " attribute requires enabling extension "
+                                       << style::Code << "chromium_experimental_subgroups";
                 return false;
             }
             if (!type->Is<core::type::U32>()) {
-                StringStream err;
-                err << "store type of @builtin(" << builtin << ") must be 'u32'";
-                AddError(err.str(), attr->source);
+                err_builtin_type("u32");
                 return false;
             }
             if (stage != ast::PipelineStage::kNone && stage != ast::PipelineStage::kCompute) {
-                StringStream err;
-                err << "@builtin(" << builtin << ") is only valid as a compute shader input";
-                AddError(err.str(), attr->source);
+                AddError(attr->source)
+                    << style::Attribute << "@builtin" << style::Code << "(" << style::Enum
+                    << builtin << style::Code << ")" << style::Plain
+                    << " is only valid as a compute shader input";
                 return false;
             }
             break;
@@ -1020,10 +1012,10 @@
     }
 
     if (is_stage_mismatch) {
-        StringStream err;
-        err << "@builtin(" << builtin << ") cannot be used for " << stage_name.str() << " shader "
-            << (is_input ? "input" : "output");
-        AddError(err.str(), attr->source);
+        AddError(attr->source) << style::Attribute << "@builtin" << style::Code << "("
+                               << style::Enum << builtin << style::Code << ")" << style::Plain
+                               << " cannot be used for " << stage_name.str() << " shader "
+                               << (is_input ? "input" : "output");
         return false;
     }
 
@@ -1034,7 +1026,8 @@
                                      const core::type::Type* storage_ty,
                                      const ast::PipelineStage stage) const {
     if (stage == ast::PipelineStage::kCompute) {
-        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
+        AddError(attr->source) << style::Attribute << "@" << attr->Name() << style::Plain
+                               << " cannot be used by compute shaders";
         return false;
     }
 
@@ -1046,13 +1039,13 @@
     }
 
     if (type->is_integer_scalar_or_vector() && i_type->Value() != core::InterpolationType::kFlat) {
-        AddError("interpolation type must be 'flat' for integral user-defined IO types",
-                 attr->source);
+        AddError(attr->source) << "interpolation type must be " << style::Enum << "flat"
+                               << style::Plain << " for integral user-defined IO types";
         return false;
     }
 
     if (attr->sampling && i_type->Value() == core::InterpolationType::kFlat) {
-        AddError("flat interpolation attribute must not have a sampling parameter", attr->source);
+        AddError(attr->source) << "flat interpolation attribute must not have a sampling parameter";
         return false;
     }
 
@@ -1062,7 +1055,8 @@
 bool Validator::InvariantAttribute(const ast::InvariantAttribute* attr,
                                    const ast::PipelineStage stage) const {
     if (stage == ast::PipelineStage::kCompute) {
-        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
+        AddError(attr->source) << style::Attribute << "@" << attr->Name() << style::Plain
+                               << " cannot be used by compute shaders";
         return false;
     }
     return true;
@@ -1076,15 +1070,17 @@
             attr,  //
             [&](const ast::WorkgroupAttribute*) {
                 if (decl->PipelineStage() != ast::PipelineStage::kCompute) {
-                    AddError("@workgroup_size is only valid for compute stages", attr->source);
+                    AddError(attr->source) << style::Attribute << "@workgroup_size" << style::Plain
+                                           << " is only valid for compute stages";
                     return false;
                 }
                 return true;
             },
             [&](const ast::MustUseAttribute*) {
                 if (func->ReturnType()->Is<core::type::Void>()) {
-                    AddError("@must_use can only be applied to functions that return a value",
-                             attr->source);
+                    AddError(attr->source)
+                        << style::Attribute << "@must_use" << style::Plain
+                        << " can only be applied to functions that return a value";
                     return false;
                 }
                 return true;
@@ -1096,16 +1092,15 @@
     }
 
     if (decl->params.Length() > kMaxFunctionParameters) {
-        AddError("function declares " + std::to_string(decl->params.Length()) +
-                     " parameters, maximum is " + std::to_string(kMaxFunctionParameters),
-                 decl->source);
+        AddError(decl->source) << "function declares " << decl->params.Length()
+                               << " parameters, maximum is " << kMaxFunctionParameters;
         return false;
     }
 
     if (!func->ReturnType()->Is<core::type::Void>()) {
         if (!func->ReturnType()->IsConstructible()) {
-            AddError("function return type must be a constructible type",
-                     decl->return_type->source);
+            AddError(decl->return_type->source)
+                << "function return type must be a constructible type";
             return false;
         }
 
@@ -1115,12 +1110,12 @@
                 behaviors = sem_.Get(last)->Behaviors();
             }
             if (behaviors.Contains(sem::Behavior::kNext)) {
-                AddError("missing return at end of function", decl->source);
+                AddError(decl->source) << "missing return at end of function";
                 return false;
             }
         } else if (TINT_UNLIKELY(IsValidationEnabled(
                        decl->attributes, ast::DisabledValidation::kFunctionHasNoBody))) {
-            TINT_ICE() << "Function " << decl->name->symbol.Name() << " has no body";
+            TINT_ICE() << "function " << decl->name->symbol.Name() << " has no body";
         }
     }
 
@@ -1183,18 +1178,20 @@
                     auto builtin = sem_.Get(builtin_attr)->Value();
 
                     if (pipeline_io_attribute) {
-                        AddError("multiple entry point IO attributes", attr->source);
-                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
-                                pipeline_io_attribute->source);
+                        AddError(attr->source) << "multiple entry point IO attributes";
+                        AddNote(pipeline_io_attribute->source)
+                            << "previously consumed " << style::Attribute << "@"
+                            << pipeline_io_attribute->Name();
                         return false;
                     }
                     pipeline_io_attribute = attr;
 
                     if (builtins.Contains(builtin)) {
-                        StringStream err;
-                        err << "@builtin(" << builtin << ") appears multiple times as pipeline "
+                        AddError(decl->source)
+                            << style::Attribute << "@builtin" << style::Code << "(" << style::Enum
+                            << builtin << style::Code << ")" << style::Plain
+                            << " appears multiple times as pipeline "
                             << (param_or_ret == ParamOrRetType::kParameter ? "input" : "output");
-                        AddError(err.str(), decl->source);
                         return false;
                     }
 
@@ -1209,9 +1206,10 @@
                 [&](const ast::LocationAttribute* loc_attr) {
                     location_attribute = loc_attr;
                     if (pipeline_io_attribute) {
-                        AddError("multiple entry point IO attributes", attr->source);
-                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
-                                pipeline_io_attribute->source);
+                        AddError(attr->source) << "multiple entry point IO attributes";
+                        AddNote(pipeline_io_attribute->source)
+                            << "previously consumed " << style::Attribute << "@"
+                            << pipeline_io_attribute->Name();
                         return false;
                     }
                     pipeline_io_attribute = attr;
@@ -1236,9 +1234,10 @@
                 [&](const ast::ColorAttribute* col_attr) {
                     color_attribute = col_attr;
                     if (pipeline_io_attribute) {
-                        AddError("multiple entry point IO attributes", attr->source);
-                        AddNote("previously consumed " + AttrToStr(pipeline_io_attribute),
-                                pipeline_io_attribute->source);
+                        AddError(attr->source) << "multiple entry point IO attributes";
+                        AddNote(pipeline_io_attribute->source)
+                            << "previously consumed " << style::Attribute << "@"
+                            << pipeline_io_attribute->Name();
                         return false;
                     }
                     pipeline_io_attribute = attr;
@@ -1269,17 +1268,16 @@
 
         if (IsValidationEnabled(attrs, ast::DisabledValidation::kEntryPointParameter)) {
             if (is_struct_member && ty->Is<core::type::Struct>()) {
-                AddError("nested structures cannot be used for entry point IO", source);
+                AddError(source) << "nested structures cannot be used for entry point IO";
                 return false;
             }
 
             if (!ty->Is<core::type::Struct>() && !pipeline_io_attribute) {
-                std::string err = "missing entry point IO attribute";
+                auto& err = AddError(source) << "missing entry point IO attribute";
                 if (!is_struct_member) {
-                    err += (param_or_ret == ParamOrRetType::kParameter ? " on parameter"
+                    err << (param_or_ret == ParamOrRetType::kParameter ? " on parameter"
                                                                        : " on return type");
                 }
-                AddError(err, source);
                 return false;
             }
 
@@ -1287,18 +1285,18 @@
                 if (ty->is_integer_scalar_or_vector() && !interpolate_attribute) {
                     if (decl->PipelineStage() == ast::PipelineStage::kVertex &&
                         param_or_ret == ParamOrRetType::kReturnType) {
-                        AddError(
-                            "integral user-defined vertex outputs must have a flat interpolation "
-                            "attribute",
-                            source);
+                        AddError(source) << "integral user-defined vertex outputs must have a "
+                                         << style::Attribute << "@interpolate" << style::Code << "("
+                                         << style::Enum << "flat" << style::Code << ")"
+                                         << style::Plain << " attribute";
                         return false;
                     }
                     if (decl->PipelineStage() == ast::PipelineStage::kFragment &&
                         param_or_ret == ParamOrRetType::kParameter) {
-                        AddError(
-                            "integral user-defined fragment inputs must have a flat interpolation "
-                            "attribute",
-                            source);
+                        AddError(source) << "integral user-defined fragment inputs must have a "
+                                         << style::Attribute << "@interpolate" << style::Code << "("
+                                         << style::Enum << "flat" << style::Code << ")"
+                                         << style::Plain << " attribute";
                         return false;
                     }
                 }
@@ -1309,8 +1307,10 @@
                 // should restrict targets with @blend_src to location 0 for easy translation
                 // in the backend writers.
                 if (location.value_or(1) != 0) {
-                    AddError("@blend_src can only be used with @location(0)",
-                             blend_src_attribute->source);
+                    AddError(blend_src_attribute->source)
+                        << style::Attribute << "@blend_src" << style::Plain
+                        << " can only be used with " << style::Attribute << "@location"
+                        << style::Code << "(" << style::Literal << "0" << style::Code << ")";
                     return false;
                 }
             }
@@ -1322,11 +1322,13 @@
             }
 
             if (first_blend_src && first_location_without_blend_src) {
-                AddError(
-                    "use of @blend_src requires all the output @location attributes of the entry "
-                    "point to be paired with a @blend_src attribute",
-                    first_location_without_blend_src->source);
-                AddNote("use of @blend_src here", first_blend_src->source);
+                AddError(first_location_without_blend_src->source)
+                    << "use of " << style::Attribute << "@blend_src" << style::Plain
+                    << " requires all the output " << style::Attribute << "@location"
+                    << style::Plain << " attributes of the entry point to be paired with a "
+                    << style::Attribute << "@blend_src" << style::Plain << " attribute";
+                AddNote(first_blend_src->source)
+                    << "use of " << style::Attribute << "@blend_src" << style::Plain << " here";
                 return false;
             }
 
@@ -1335,38 +1337,44 @@
                     first_nonzero_location = location_attribute;
                 }
                 if (first_nonzero_location && first_blend_src) {
-                    AddError("pipeline cannot use both a @blend_src and non-zero @location",
-                             first_blend_src->source);
-                    AddNote("non-zero @location declared here", first_nonzero_location->source);
+                    AddError(first_blend_src->source)
+                        << "pipeline cannot use both a " << style::Attribute << "@blend_src"
+                        << style::Plain << " and non-zero " << style::Attribute << "@location";
+                    AddNote(first_nonzero_location->source)
+                        << "non-zero " << style::Attribute << "@location" << style::Plain
+                        << " declared here";
                     return false;
                 }
 
                 std::pair<uint32_t, uint32_t> location_and_blend_src(location.value(),
                                                                      blend_src.value_or(0));
                 if (!locations_and_blend_srcs.Add(location_and_blend_src)) {
-                    StringStream err;
-                    err << "@location(" << location.value() << ") ";
+                    auto& err = AddError(location_attribute->source)
+                                << style::Attribute << "@location" << style::Code << "("
+                                << style::Literal << location.value() << style::Code << ")";
                     if (blend_src_attribute) {
-                        err << "@blend_src(" << blend_src.value() << ") ";
+                        err << style::Attribute << " @blend_src" << style::Code << "("
+                            << style::Literal << blend_src.value() << style::Code << ")";
                     }
-                    err << "appears multiple times";
-                    AddError(err.str(), location_attribute->source);
+                    err << style::Plain << " appears multiple times";
                     return false;
                 }
             }
 
             if (color_attribute && !colors.Add(color.value())) {
-                StringStream err;
-                err << "@color(" << color.value() << ") appears multiple times";
-                AddError(err.str(), color_attribute->source);
+                AddError(color_attribute->source)
+                    << style::Attribute << "@color" << style::Code << "(" << style::Literal
+                    << color.value() << style::Code << ")" << style::Plain
+                    << " appears multiple times";
                 return false;
             }
 
             if (interpolate_attribute) {
                 if (!pipeline_io_attribute ||
                     !pipeline_io_attribute->Is<ast::LocationAttribute>()) {
-                    AddError("@interpolate can only be used with @location",
-                             interpolate_attribute->source);
+                    AddError(interpolate_attribute->source)
+                        << style::Attribute << "@interpolate" << style::Plain
+                        << " can only be used with " << style::Attribute << "@location";
                     return false;
                 }
             }
@@ -1380,8 +1388,10 @@
                     }
                 }
                 if (!has_position) {
-                    AddError("@invariant must be applied to a position builtin",
-                             invariant_attribute->source);
+                    AddError(invariant_attribute->source)
+                        << style::Attribute << "@invariant" << style::Plain
+                        << " must be applied to a " << style::Attribute << "@builtin" << style::Code
+                        << "(" << style::Enum << "position" << style::Code << ")";
                     return false;
                 }
             }
@@ -1407,8 +1417,8 @@
                             member->Declaration()->source, param_or_ret,
                             /*is_struct_member*/ true, member->Attributes().location,
                             member->Attributes().blend_src, member->Attributes().color)) {
-                        AddNote("while analyzing entry point '" + decl->name->symbol.Name() + "'",
-                                decl->source);
+                        AddNote(decl->source) << "while analyzing entry point " << style::Function
+                                              << decl->name->symbol.Name();
                         return false;
                     }
                 }
@@ -1459,16 +1469,16 @@
             }
         }
         if (!found) {
-            AddError("a vertex shader must include the 'position' builtin in its return type",
-                     decl->source);
+            AddError(decl->source) << "a vertex shader must include the " << style::Enum
+                                   << "position" << style::Plain << " builtin in its return type";
             return false;
         }
     }
 
     if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
         if (!ast::HasAttribute<ast::WorkgroupAttribute>(decl->attributes)) {
-            AddError("a compute shader must include 'workgroup_size' in its attributes",
-                     decl->source);
+            AddError(decl->source) << "a compute shader must include " << style::Attribute
+                                   << "@workgroup_size" << style::Plain << " in its attributes";
             return false;
         }
     }
@@ -1495,12 +1505,13 @@
             // resource interface of a given shader must not have the same group and binding values,
             // when considered as a pair of values.
             auto func_name = decl->name->symbol.Name();
-            AddError(
-                "entry point '" + func_name +
-                    "' references multiple variables that use the same resource binding @group(" +
-                    std::to_string(bp->group) + "), @binding(" + std::to_string(bp->binding) + ")",
-                var_decl->source);
-            AddNote("first resource binding usage declared here", added.value->source);
+            AddError(var_decl->source)
+                << "entry point " << style::Function << func_name << style::Plain
+                << " references multiple variables that use the same resource binding "
+                << style::Attribute << "@group" << style::Code << "(" << style::Literal << bp->group
+                << style::Code << ")" << style::Plain << ", " << style::Attribute << "@binding"
+                << style::Code << "(" << style::Literal << bp->binding << style::Code << ")";
+            AddNote(added.value->source) << "first resource binding usage declared here";
             return false;
         }
     }
@@ -1529,14 +1540,15 @@
             return "<unknown>";
         };
 
-        AddError(std::string(constraint) + " requires " + stage_name(latest_stage) +
-                     ", but expression is " + stage_name(expr->Stage()),
-                 expr->Declaration()->source);
+        AddError(expr->Declaration()->source)
+            << constraint << " requires " << stage_name(latest_stage) << ", but expression is "
+            << stage_name(expr->Stage());
 
         if (auto* stmt = expr->Stmt()) {
             if (auto* decl = As<ast::VariableDeclStatement>(stmt->Declaration())) {
                 if (decl->variable->Is<ast::Const>()) {
-                    AddNote("consider changing 'const' to 'let'", decl->source);
+                    AddNote(decl->source) << "consider changing " << style::Keyword << "const"
+                                          << style::Plain << " to " << style::Keyword << "let";
                 }
             }
         }
@@ -1548,9 +1560,12 @@
 bool Validator::Statements(VectorRef<const ast::Statement*> stmts) const {
     for (auto* stmt : stmts) {
         if (!sem_.Get(stmt)->IsReachable()) {
-            if (!AddDiagnostic(wgsl::ChromiumDiagnosticRule::kUnreachableCode,
-                               "code is unreachable", stmt->source)) {
-                return false;
+            if (auto* d = MaybeAddDiagnostic(wgsl::ChromiumDiagnosticRule::kUnreachableCode,
+                                             stmt->source)) {
+                *d << "code is unreachable";
+                if (d->severity >= diag::Severity::Error) {
+                    return false;
+                }
             }
             break;
         }
@@ -1561,14 +1576,14 @@
 bool Validator::BreakStatement(const sem::Statement* stmt,
                                sem::Statement* current_statement) const {
     if (!stmt->FindFirstParent<sem::LoopBlockStatement, sem::CaseStatement>()) {
-        AddError("break statement must be in a loop or switch case", stmt->Declaration()->source);
+        AddError(stmt->Declaration()->source) << "break statement must be in a loop or switch case";
         return false;
     }
     if (ClosestContinuing(/*stop_at_loop*/ true, /* stop_at_switch */ true, current_statement) !=
         nullptr) {
-        AddError(
-            "`break` must not be used to exit from a continuing block. Use `break-if` instead.",
-            stmt->Declaration()->source);
+        AddError(stmt->Declaration()->source)
+            << "`break` must not be used to exit from a continuing block. Use "
+               "`break-if` instead.";
         return false;
     }
     return true;
@@ -1578,16 +1593,16 @@
                                   sem::Statement* current_statement) const {
     if (auto* continuing = ClosestContinuing(/*stop_at_loop*/ true, /* stop_at_switch */ false,
                                              current_statement)) {
-        AddError("continuing blocks must not contain a continue statement",
-                 stmt->Declaration()->source);
+        AddError(stmt->Declaration()->source)
+            << "continuing blocks must not contain a continue statement";
         if (continuing != stmt->Declaration() && continuing != stmt->Parent()->Declaration()) {
-            AddNote("see continuing block here", continuing->source);
+            AddNote(continuing->source) << "see continuing block here";
         }
         return false;
     }
 
     if (!stmt->FindFirstParent<sem::LoopBlockStatement>()) {
-        AddError("continue statement must be in a loop", stmt->Declaration()->source);
+        AddError(stmt->Declaration()->source) << "continue statement must be in a loop";
         return false;
     }
 
@@ -1608,23 +1623,24 @@
         Switch(
             call->Target(),  //
             [&](const sem::Function* fn) {
-                AddError("ignoring return value of function '" +
-                             fn->Declaration()->name->symbol.Name() + "' annotated with @must_use",
-                         call->Declaration()->source);
+                AddError(call->Declaration()->source)
+                    << "ignoring return value of function " << style::Function
+                    << fn->Declaration()->name->symbol.Name() << style::Plain << " annotated with "
+                    << style::Attribute << "@must_use";
                 sem_.NoteDeclarationSource(fn->Declaration());
             },
             [&](const sem::BuiltinFn* b) {
-                AddError("ignoring return value of builtin '" + tint::ToString(b->Fn()) + "'",
-                         call->Declaration()->source);
+                AddError(call->Declaration()->source)
+                    << "ignoring return value of builtin " << style::Function << b->Fn();
             },
             [&](const sem::ValueConversion*) {
-                AddError("value conversion evaluated but not used", call->Declaration()->source);
+                AddError(call->Declaration()->source) << "value conversion evaluated but not used";
             },
             [&](const sem::ValueConstructor*) {
-                AddError("value constructor evaluated but not used", call->Declaration()->source);
+                AddError(call->Declaration()->source) << "value constructor evaluated but not used";
             },
             [&](Default) {
-                AddError("return value of call not used", call->Declaration()->source);
+                AddError(call->Declaration()->source) << "return value of call not used";
             });
         return false;
     }
@@ -1634,7 +1650,7 @@
 
 bool Validator::LoopStatement(const sem::LoopStatement* stmt) const {
     if (stmt->Behaviors().Empty()) {
-        AddError("loop does not exit", stmt->Declaration()->source.Begin());
+        AddError(stmt->Declaration()->source.Begin()) << "loop does not exit";
         return false;
     }
     return true;
@@ -1642,14 +1658,14 @@
 
 bool Validator::ForLoopStatement(const sem::ForLoopStatement* stmt) const {
     if (stmt->Behaviors().Empty()) {
-        AddError("for-loop does not exit", stmt->Declaration()->source.Begin());
+        AddError(stmt->Declaration()->source.Begin()) << "for-loop does not exit";
         return false;
     }
     if (auto* cond = stmt->Condition()) {
         auto* cond_ty = cond->Type()->UnwrapRef();
         if (!cond_ty->Is<core::type::Bool>()) {
-            AddError("for-loop condition must be bool, got " + sem_.TypeNameOf(cond_ty),
-                     stmt->Condition()->Declaration()->source);
+            AddError(stmt->Condition()->Declaration()->source)
+                << "for-loop condition must be bool, got " << sem_.TypeNameOf(cond_ty);
             return false;
         }
     }
@@ -1658,14 +1674,14 @@
 
 bool Validator::WhileStatement(const sem::WhileStatement* stmt) const {
     if (stmt->Behaviors().Empty()) {
-        AddError("while does not exit", stmt->Declaration()->source.Begin());
+        AddError(stmt->Declaration()->source.Begin()) << "while does not exit";
         return false;
     }
     if (auto* cond = stmt->Condition()) {
         auto* cond_ty = cond->Type()->UnwrapRef();
         if (!cond_ty->Is<core::type::Bool>()) {
-            AddError("while condition must be bool, got " + sem_.TypeNameOf(cond_ty),
-                     stmt->Condition()->Declaration()->source);
+            AddError(stmt->Condition()->Declaration()->source)
+                << "while condition must be bool, got " << sem_.TypeNameOf(cond_ty);
             return false;
         }
     }
@@ -1676,8 +1692,8 @@
                                  sem::Statement* current_statement) const {
     auto* cond_ty = stmt->Condition()->Type()->UnwrapRef();
     if (!cond_ty->Is<core::type::Bool>()) {
-        AddError("break-if statement condition must be bool, got " + sem_.TypeNameOf(cond_ty),
-                 stmt->Condition()->Declaration()->source);
+        AddError(stmt->Condition()->Declaration()->source)
+            << "break-if statement condition must be bool, got " << sem_.TypeNameOf(cond_ty);
         return false;
     }
 
@@ -1687,24 +1703,24 @@
         }
         if (auto* continuing = s->As<sem::LoopContinuingBlockStatement>()) {
             if (continuing->Declaration()->statements.Back() != stmt->Declaration()) {
-                AddError("break-if must be the last statement in a continuing block",
-                         stmt->Declaration()->source);
-                AddNote("see continuing block here", s->Declaration()->source);
+                AddError(stmt->Declaration()->source)
+                    << "break-if must be the last statement in a continuing block";
+                AddNote(s->Declaration()->source) << "see continuing block here";
                 return false;
             }
             return true;
         }
     }
 
-    AddError("break-if must be in a continuing block", stmt->Declaration()->source);
+    AddError(stmt->Declaration()->source) << "break-if must be in a continuing block";
     return false;
 }
 
 bool Validator::IfStatement(const sem::IfStatement* stmt) const {
     auto* cond_ty = stmt->Condition()->Type()->UnwrapRef();
     if (!cond_ty->Is<core::type::Bool>()) {
-        AddError("if statement condition must be bool, got " + sem_.TypeNameOf(cond_ty),
-                 stmt->Condition()->Declaration()->source);
+        AddError(stmt->Condition()->Declaration()->source)
+            << "if statement condition must be bool, got " << sem_.TypeNameOf(cond_ty);
         return false;
     }
     return true;
@@ -1728,9 +1744,9 @@
             // If the called function does not return a value, a function call statement should be
             // used instead.
             auto* builtin = call->Target()->As<sem::BuiltinFn>();
-            auto name = tint::ToString(builtin->Fn());
-            AddError("builtin function '" + name + "' does not return a value",
-                     call->Declaration()->source);
+            AddError(call->Declaration()->source)
+                << "builtin function " << style::Function << builtin->Fn() << style::Plain
+                << " does not return a value";
             return false;
         }
     }
@@ -1753,35 +1769,32 @@
             return true;
         }
         auto index = static_cast<size_t>(signed_index);
-        std::string name{core::ToString(usage)};
         auto* arg = call->Arguments()[index];
         if (auto values = arg->ConstantValue()) {
             if (auto* vector = values->Type()->As<core::type::Vector>()) {
                 for (size_t i = 0; i < vector->Width(); i++) {
                     auto value = values->Index(i)->ValueAs<AInt>();
                     if (value < min || value > max) {
-                        AddError("each component of the " + name + " argument must be at least " +
-                                     std::to_string(min) + " and at most " + std::to_string(max) +
-                                     ". " + name + " component " + std::to_string(i) + " is " +
-                                     std::to_string(value),
-                                 arg->Declaration()->source);
+                        AddError(arg->Declaration()->source)
+                            << "each component of the " << usage << " argument must be at least "
+                            << min << " and at most " << max << ". " << usage << " component " << i
+                            << " is " << value;
                         return false;
                     }
                 }
             } else {
                 auto value = values->ValueAs<AInt>();
                 if (value < min || value > max) {
-                    AddError("the " + name + " argument must be at least " + std::to_string(min) +
-                                 " and at most " + std::to_string(max) + ". " + name + " is " +
-                                 std::to_string(value),
-                             arg->Declaration()->source);
+                    AddError(arg->Declaration()->source)
+                        << "the " << usage << " argument must be at least " << min
+                        << " and at most " << max << ". " << usage << " is " << value;
                     return false;
                 }
             }
             return true;
         }
-        AddError("the " + name + " argument must be a const-expression",
-                 arg->Declaration()->source);
+        AddError(arg->Declaration()->source)
+            << "the " << usage << " argument must be a const-expression";
         return false;
     };
 
@@ -1802,9 +1815,9 @@
     auto* ty = ptr->StoreType();
 
     if (ty->Is<core::type::Atomic>() || atomic_composite_info_.Contains(ty)) {
-        AddError(
-            "workgroupUniformLoad must not be called with an argument that contains an atomic type",
-            arg->Declaration()->source);
+        AddError(arg->Declaration()->source)
+            << "workgroupUniformLoad must not be called with an argument that "
+               "contains an atomic type";
         return false;
     }
 
@@ -1820,8 +1833,8 @@
     TINT_ASSERT(call->Arguments().Length() == 2);
     auto* laneArg = call->Arguments()[1];
     if (!laneArg->ConstantValue()) {
-        AddError("the sourceLaneIndex argument of subgroupBroadcast must be a const-expression",
-                 laneArg->Declaration()->source);
+        AddError(laneArg->Declaration()->source)
+            << "the sourceLaneIndex argument of subgroupBroadcast must be a const-expression";
         return false;
     }
 
@@ -1837,9 +1850,9 @@
     const auto extension = builtin->RequiredExtension();
     if (extension != wgsl::Extension::kUndefined) {
         if (!enabled_extensions_.Contains(extension)) {
-            AddError("cannot call built-in function '" + std::string(builtin->str()) +
-                         "' without extension " + tint::ToString(extension),
-                     call->Declaration()->source);
+            AddError(call->Declaration()->source)
+                << "cannot call built-in function " << style::Function << builtin->Fn()
+                << style::Plain << " without extension " << extension;
             return false;
         }
     }
@@ -1847,10 +1860,10 @@
     const auto feature = builtin->RequiredLanguageFeature();
     if (feature != wgsl::LanguageFeature::kUndefined) {
         if (!allowed_features_.features.count(feature)) {
-            AddError("built-in function '" + std::string(builtin->str()) + "' requires the " +
-                         std::string(wgsl::ToString(feature)) +
-                         " language feature, which is not allowed in the current environment",
-                     call->Declaration()->source);
+            AddError(call->Declaration()->source)
+                << "built-in function " << style::Function << builtin->Fn() << style::Plain
+                << " requires the " << style::Code << wgsl::ToString(feature) << style::Plain
+                << " language feature, which is not allowed in the current environment";
             return false;
         }
     }
@@ -1861,7 +1874,8 @@
 bool Validator::CheckF16Enabled(const Source& source) const {
     // Validate if f16 type is allowed.
     if (!enabled_extensions_.Contains(wgsl::Extension::kF16)) {
-        AddError("f16 type used without 'f16' extension enabled", source);
+        AddError(source) << style::Type << "f16" << style::Plain << " type used without "
+                         << style::Code << "f16" << style::Plain << " extension enabled";
         return false;
     }
     return true;
@@ -1874,24 +1888,25 @@
     auto name = sym.Name();
 
     if (!current_statement) {  // Function call at module-scope.
-        AddError("functions cannot be called at module-scope", decl->source);
+        AddError(decl->source) << "functions cannot be called at module-scope";
         return false;
     }
 
     if (target->Declaration()->IsEntryPoint()) {
         // https://www.w3.org/TR/WGSL/#function-restriction
         // An entry point must never be the target of a function call.
-        AddError("entry point functions cannot be the target of a function call", decl->source);
+        AddError(decl->source) << "entry point functions cannot be the target of a function call";
         return false;
     }
 
     if (decl->args.Length() != target->Parameters().Length()) {
         bool more = decl->args.Length() > target->Parameters().Length();
-        AddError("too " + (more ? std::string("many") : std::string("few")) +
-                     " arguments in call to '" + name + "', expected " +
-                     std::to_string(target->Parameters().Length()) + ", got " +
-                     std::to_string(call->Arguments().Length()),
-                 decl->source);
+        AddError(decl->source) << "too "
+                               << (more ? std::string("many") : std::string("few")) +
+                                      " arguments in call to "
+                               << style::Function << name << style::Plain << ", expected "
+                               << target->Parameters().Length() << ", got "
+                               << call->Arguments().Length();
         return false;
     }
 
@@ -1902,10 +1917,10 @@
         auto* arg_type = sem_.TypeOf(arg_expr)->UnwrapRef();
 
         if (param_type != arg_type) {
-            AddError("type mismatch for argument " + std::to_string(i + 1) + " in call to '" +
-                         name + "', expected '" + sem_.TypeNameOf(param_type) + "', got '" +
-                         sem_.TypeNameOf(arg_type) + "'",
-                     arg_expr->source);
+            AddError(arg_expr->source) << "type mismatch for argument " << (i + 1) << " in call to "
+                                       << style::Function << name << style::Plain << ", expected "
+                                       << style::Type << sem_.TypeNameOf(param_type) << style::Plain
+                                       << ", got " << style::Type << sem_.TypeNameOf(arg_type);
             return false;
         }
 
@@ -1931,10 +1946,8 @@
             if (root_store_type != arg_store_type &&
                 IsValidationEnabled(param->Declaration()->attributes,
                                     ast::DisabledValidation::kIgnoreInvalidPointerArgument)) {
-                AddError(
-                    "arguments of pointer type must not point to a subset of the originating "
-                    "variable",
-                    arg_expr->source);
+                AddError(arg_expr->source) << "arguments of pointer type must not point to a "
+                                              "subset of the originating variable";
                 return false;
             }
         }
@@ -1951,7 +1964,8 @@
             // https://gpuweb.github.io/gpuweb/wgsl/#function-call-expr
             // If the called function does not return a value, a function call
             // statement should be used instead.
-            AddError("function '" + name + "' does not return a value", decl->source);
+            AddError(decl->source) << "function " << style::Function << name << style::Plain
+                                   << " does not return a value";
             return false;
         }
     }
@@ -1962,28 +1976,26 @@
 bool Validator::StructureInitializer(const ast::CallExpression* ctor,
                                      const core::type::Struct* struct_type) const {
     if (!struct_type->IsConstructible()) {
-        AddError("structure constructor has non-constructible type", ctor->source);
+        AddError(ctor->source) << "structure constructor has non-constructible type";
         return false;
     }
 
     if (ctor->args.Length() > 0) {
         if (ctor->args.Length() != struct_type->Members().Length()) {
             std::string fm = ctor->args.Length() < struct_type->Members().Length() ? "few" : "many";
-            AddError("structure constructor has too " + fm + " inputs: expected " +
-                         std::to_string(struct_type->Members().Length()) + ", found " +
-                         std::to_string(ctor->args.Length()),
-                     ctor->source);
+            AddError(ctor->source)
+                << "structure constructor has too " << fm << " inputs: expected "
+                << struct_type->Members().Length() << ", found " << ctor->args.Length();
             return false;
         }
         for (auto* member : struct_type->Members()) {
             auto* value = ctor->args[member->Index()];
             auto* value_ty = sem_.TypeOf(value);
             if (member->Type() != value_ty->UnwrapRef()) {
-                AddError(
-                    "type in structure constructor does not match struct member type: expected '" +
-                        sem_.TypeNameOf(member->Type()) + "', found '" + sem_.TypeNameOf(value_ty) +
-                        "'",
-                    value->source);
+                AddError(value->source)
+                    << "type in structure constructor does not match struct member type: expected "
+                    << style::Type << sem_.TypeNameOf(member->Type()) << style::Plain << ", found "
+                    << style::Type << sem_.TypeNameOf(value_ty);
                 return false;
             }
         }
@@ -1999,27 +2011,26 @@
         auto* value_ty = sem_.TypeOf(value)->UnwrapRef();
         if (core::type::Type::ConversionRank(value_ty, elem_ty) ==
             core::type::Type::kNoConversion) {
-            AddError("'" + sem_.TypeNameOf(value_ty) +
-                         "' cannot be used to construct an array of '" + sem_.TypeNameOf(elem_ty) +
-                         "'",
-                     value->source);
+            AddError(value->source) << style::Type << sem_.TypeNameOf(value_ty) << style::Plain
+                                    << " cannot be used to construct an array of " << style::Type
+                                    << sem_.TypeNameOf(elem_ty);
             return false;
         }
     }
 
     auto* c = array_type->Count();
     if (c->Is<core::type::RuntimeArrayCount>()) {
-        AddError("cannot construct a runtime-sized array", ctor->source);
+        AddError(ctor->source) << "cannot construct a runtime-sized array";
         return false;
     }
 
     if (c->IsAnyOf<sem::NamedOverrideArrayCount, sem::UnnamedOverrideArrayCount>()) {
-        AddError("cannot construct an array that has an override-expression count", ctor->source);
+        AddError(ctor->source) << "cannot construct an array that has an override-expression count";
         return false;
     }
 
     if (!elem_ty->IsConstructible()) {
-        AddError("array constructor has non-constructible element type", ctor->source);
+        AddError(ctor->source) << "array constructor has non-constructible element type";
         return false;
     }
 
@@ -2031,9 +2042,8 @@
     const auto count = c->As<core::type::ConstantArrayCount>()->value;
     if (!values.IsEmpty() && (values.Length() != count)) {
         std::string fm = values.Length() < count ? "few" : "many";
-        AddError("array constructor has too " + fm + " elements: expected " +
-                     std::to_string(count) + ", found " + std::to_string(values.Length()),
-                 ctor->source);
+        AddError(ctor->source) << "array constructor has too " << fm << " elements: expected "
+                               << count << ", found " << values.Length();
         return false;
     }
     return true;
@@ -2041,7 +2051,10 @@
 
 bool Validator::Vector(const core::type::Type* el_ty, const Source& source) const {
     if (!el_ty->Is<core::type::Scalar>()) {
-        AddError("vector element type must be 'bool', 'f32', 'f16', 'i32' or 'u32'", source);
+        AddError(source) << "vector element type must be " << style::Type << "bool" << style::Plain
+                         << ", " << style::Type << "f32" << style::Plain << ", " << style::Type
+                         << "f16" << style::Plain << ", " << style::Type << "i32" << style::Plain
+                         << " or " << style::Type << "u32";
         return false;
     }
     return true;
@@ -2049,7 +2062,8 @@
 
 bool Validator::Matrix(const core::type::Type* el_ty, const Source& source) const {
     if (!el_ty->is_float_scalar()) {
-        AddError("matrix element type must be 'f32' or 'f16'", source);
+        AddError(source) << "matrix element type must be " << style::Type << "f32" << style::Plain
+                         << " or " << style::Type << "f16";
         return false;
     }
     return true;
@@ -2059,12 +2073,12 @@
     auto backtrace = [&](const sem::Function* func, const sem::Function* entry_point) {
         if (func != entry_point) {
             TraverseCallChain(entry_point, func, [&](const sem::Function* f) {
-                AddNote("called by function '" + f->Declaration()->name->symbol.Name() + "'",
-                        f->Declaration()->source);
+                AddNote(f->Declaration()->source) << "called by function " << style::Function
+                                                  << f->Declaration()->name->symbol.Name();
             });
-            AddNote(
-                "called by entry point '" + entry_point->Declaration()->name->symbol.Name() + "'",
-                entry_point->Declaration()->source);
+            AddNote(entry_point->Declaration()->source)
+                << "called by entry point " << style::Function
+                << entry_point->Declaration()->name->symbol.Name();
         }
     };
 
@@ -2077,11 +2091,9 @@
                     break;
                 }
             }
-            StringStream msg;
-            msg << "var with '" << var->AddressSpace() << "' address space cannot be used by "
-                << stage << " pipeline stage";
-            AddError(msg.str(), source);
-            AddNote("variable is declared here", var->Declaration()->source);
+            AddError(source) << "var with " << style::Enum << var->AddressSpace() << style::Plain
+                             << " address space cannot be used by " << stage << " pipeline stage";
+            AddNote(var->Declaration()->source) << "variable is declared here";
             backtrace(func, entry_point);
             return false;
         };
@@ -2105,10 +2117,8 @@
         for (auto* builtin : func->DirectlyCalledBuiltins()) {
             if (!builtin->SupportedStages().Contains(stage)) {
                 auto* call = func->FindDirectCallTo(builtin);
-                StringStream err;
-                err << "built-in cannot be used by " << stage << " pipeline stage";
-                AddError(err.str(),
-                         call ? call->Declaration()->source : func->Declaration()->source);
+                AddError(call ? call->Declaration()->source : func->Declaration()->source)
+                    << "built-in cannot be used by " << stage << " pipeline stage";
                 backtrace(func, entry_point);
                 return false;
             }
@@ -2119,9 +2129,8 @@
     auto check_no_discards = [&](const sem::Function* func, const sem::Function* entry_point) {
         if (auto* discard = func->DiscardStatement()) {
             auto stage = entry_point->Declaration()->PipelineStage();
-            StringStream err;
-            err << "discard statement cannot be used in " << stage << " pipeline stage";
-            AddError(err.str(), discard->Declaration()->source);
+            AddError(discard->Declaration()->source)
+                << "discard statement cannot be used in " << stage << " pipeline stage";
             backtrace(func, entry_point);
             return false;
         }
@@ -2176,13 +2185,13 @@
     auto* el_ty = arr->ElemType();
 
     if (!IsPlain(el_ty)) {
-        AddError(sem_.TypeNameOf(el_ty) + " cannot be used as an element type of an array",
-                 el_source);
+        AddError(el_source) << sem_.TypeNameOf(el_ty)
+                            << " cannot be used as an element type of an array";
         return false;
     }
 
     if (!IsFixedFootprint(el_ty)) {
-        AddError("an array element type cannot contain a runtime-sized array", el_source);
+        AddError(el_source) << "an array element type cannot contain a runtime-sized array";
         return false;
     }
 
@@ -2204,10 +2213,9 @@
         // Arrays decorated with the stride attribute must have a stride that is
         // at least the size of the element type, and be a multiple of the
         // element type's alignment value.
-        AddError(
-            "arrays decorated with the stride attribute must have a stride that is at least the "
-            "size of the element type, and be a multiple of the element type's alignment value",
-            attr->source);
+        AddError(attr->source)
+            << "arrays decorated with the stride attribute must have a stride that is at least the "
+               "size of the element type, and be a multiple of the element type's alignment value";
         return false;
     }
     return true;
@@ -2219,7 +2227,7 @@
 
 bool Validator::Structure(const sem::Struct* str, ast::PipelineStage stage) const {
     if (str->Members().IsEmpty()) {
-        AddError("structures must have at least one member", str->Declaration()->source);
+        AddError(str->Declaration()->source) << "structures must have at least one member";
         return false;
     }
 
@@ -2229,8 +2237,8 @@
         if (auto* r = member->Type()->As<sem::Array>()) {
             if (r->Count()->Is<core::type::RuntimeArrayCount>()) {
                 if (member != str->Members().Back()) {
-                    AddError("runtime arrays may only appear as the last member of a struct",
-                             member->Declaration()->source);
+                    AddError(member->Declaration()->source)
+                        << "runtime arrays may only appear as the last member of a struct";
                     return false;
                 }
             }
@@ -2240,9 +2248,8 @@
                 return false;
             }
         } else if (!IsFixedFootprint(member->Type())) {
-            AddError(
-                "a struct that contains a runtime array cannot be nested inside another struct",
-                member->Declaration()->source);
+            AddError(member->Declaration()->source)
+                << "a struct that contains a runtime array cannot be nested inside another struct";
             return false;
         }
 
@@ -2291,10 +2298,10 @@
                 },
                 [&](const ast::StructMemberSizeAttribute*) {
                     if (!member->Type()->HasCreationFixedFootprint()) {
-                        AddError(
-                            "@size can only be applied to members where the member's type size can "
-                            "be fully determined at shader creation time",
-                            attr->source);
+                        AddError(attr->source)
+                            << style::Attribute << "@size" << style::Plain
+                            << " can only be applied to members where the member's type size can "
+                               "be fully determined at shader creation time";
                         return false;
                     }
                     return true;
@@ -2306,8 +2313,9 @@
         }
 
         if (invariant_attribute && !has_position) {
-            AddError("@invariant must be applied to a position builtin",
-                     invariant_attribute->source);
+            AddError(invariant_attribute->source)
+                << style::Attribute << "@invariant" << style::Plain
+                << " must be applied to a position builtin";
             return false;
         }
 
@@ -2316,14 +2324,18 @@
             // restrict targets with index attributes to location 0 for easy translation in the
             // backend writers.
             if (member->Attributes().location.value_or(1) != 0) {
-                AddError("@blend_src can only be used with @location(0)",
-                         blend_src_attribute->source);
+                AddError(blend_src_attribute->source)
+                    << style::Attribute << "@blend_src" << style::Plain << " can only be used with "
+                    << style::Attribute << "@location" << style::Code << "(" << style::Literal
+                    << "0" << style::Code << ")";
                 return false;
             }
         }
 
         if (interpolate_attribute && !location_attribute) {
-            AddError("@interpolate can only be used with @location", interpolate_attribute->source);
+            AddError(interpolate_attribute->source)
+                << style::Attribute << "@interpolate" << style::Plain << " can only be used with "
+                << style::Attribute << "@location";
             return false;
         }
 
@@ -2333,13 +2345,14 @@
             std::optional<uint32_t> blend_src = member->Attributes().blend_src;
 
             if (!locations_and_blend_srcs.Add(std::make_pair(location, blend_src))) {
-                StringStream err;
-                err << "@location(" << location << ") ";
+                auto& err = AddError(location_attribute->source)
+                            << style::Attribute << "@location" << style::Code << "("
+                            << style::Literal << location << style::Code << ")";
                 if (blend_src) {
-                    err << "@blend_src(" << blend_src.value() << ") ";
+                    err << style::Attribute << " @blend_src" << style::Code << "(" << style::Literal
+                        << blend_src.value() << style::Code << ")";
                 }
-                err << "appears multiple times";
-                AddError(err.str(), location_attribute->source);
+                err << style::Plain << " appears multiple times";
                 return false;
             }
         }
@@ -2347,9 +2360,9 @@
         if (color_attribute) {
             uint32_t color = member->Attributes().color.value();
             if (!colors.Add(color)) {
-                StringStream err;
-                err << "@color(" << color << ") appears multiple times";
-                AddError(err.str(), color_attribute->source);
+                AddError(color_attribute->source)
+                    << style::Attribute << "@color" << style::Code << "(" << style::Literal << color
+                    << style::Code << ")" << style::Plain << " appears multiple times";
                 return false;
             }
         }
@@ -2363,17 +2376,18 @@
                                   ast::PipelineStage stage,
                                   const Source& source) const {
     if (stage == ast::PipelineStage::kCompute) {
-        AddError(AttrToStr(attr) + " cannot be used by compute shaders", attr->source);
+        AddError(attr->source) << style::Attribute << "@" << attr->Name() << style::Plain
+                               << " cannot be used by compute shaders";
         return false;
     }
 
     if (!type->is_numeric_scalar_or_vector()) {
         std::string invalid_type = sem_.TypeNameOf(type);
-        AddError("cannot apply @location to declaration of type '" + invalid_type + "'", source);
-        AddNote(
-            "@location must only be applied to declarations of numeric scalar or numeric vector "
-            "type",
-            attr->source);
+        AddError(source) << "cannot apply " << style::Attribute << "@location" << style::Plain
+                         << " to declaration of type " << style::Type << invalid_type;
+        AddNote(attr->source)
+            << style::Attribute << "@location" << style::Plain
+            << " must only be applied to declarations of numeric scalar or numeric vector type";
         return false;
     }
 
@@ -2386,9 +2400,9 @@
                                const Source& source,
                                const std::optional<bool> is_input) const {
     if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumExperimentalFramebufferFetch)) {
-        AddError(
-            "use of @color requires enabling extension 'chromium_experimental_framebuffer_fetch'",
-            attr->source);
+        AddError(attr->source) << "use of " << style::Attribute << "@color" << style::Plain
+                               << " requires enabling extension " << style::Code
+                               << "chromium_experimental_framebuffer_fetch";
         return false;
     }
 
@@ -2396,16 +2410,18 @@
         stage != ast::PipelineStage::kNone && stage != ast::PipelineStage::kFragment;
     bool is_output = !is_input.value_or(true);
     if (is_stage_non_fragment || is_output) {
-        AddError("@color can only be used for fragment shader input", attr->source);
+        AddError(attr->source) << style::Attribute << "@color" << style::Plain
+                               << " can only be used for fragment shader input";
         return false;
     }
 
     if (!type->is_numeric_scalar_or_vector()) {
         std::string invalid_type = sem_.TypeNameOf(type);
-        AddError("cannot apply @color to declaration of type '" + invalid_type + "'", source);
-        AddNote(
-            "@color must only be applied to declarations of numeric scalar or numeric vector type",
-            attr->source);
+        AddError(source) << "cannot apply " << style::Attribute << "@color" << style::Plain
+                         << " to declaration of type " << style::Type << invalid_type;
+        AddNote(attr->source)
+            << style::Attribute << "@color" << style::Plain
+            << " must only be applied to declarations of numeric scalar or numeric vector type";
         return false;
     }
 
@@ -2416,10 +2432,9 @@
                                   ast::PipelineStage stage,
                                   const std::optional<bool> is_input) const {
     if (!enabled_extensions_.Contains(wgsl::Extension::kChromiumInternalDualSourceBlending)) {
-        AddError(
-            "use of @blend_src requires enabling extension "
-            "'chromium_internal_dual_source_blending'",
-            attr->source);
+        AddError(attr->source) << "use of " << style::Attribute << "@blend_src" << style::Plain
+                               << " requires enabling extension " << style::Code
+                               << "chromium_internal_dual_source_blending";
         return false;
     }
 
@@ -2427,7 +2442,8 @@
         stage != ast::PipelineStage::kNone && stage != ast::PipelineStage::kFragment;
     bool is_output = is_input.value_or(false);
     if (is_stage_non_fragment || is_output) {
-        AddError(AttrToStr(attr) + " can only be used for fragment shader output", attr->source);
+        AddError(attr->source) << style::Attribute << "@" << attr->Name() << style::Plain
+                               << " can only be used for fragment shader output";
         return false;
     }
 
@@ -2439,18 +2455,19 @@
                        const core::type::Type* ret_type,
                        sem::Statement* current_statement) const {
     if (func_type->UnwrapRef() != ret_type) {
-        AddError("return statement type must match its function return type, returned '" +
-                     sem_.TypeNameOf(ret_type) + "', expected '" + sem_.TypeNameOf(func_type) + "'",
-                 ret->source);
+        AddError(ret->source)
+            << "return statement type must match its function return type, returned " << style::Type
+            << sem_.TypeNameOf(ret_type) << style::Plain << ", expected " << style::Type
+            << sem_.TypeNameOf(func_type);
         return false;
     }
 
     auto* sem = sem_.Get(ret);
     if (auto* continuing = ClosestContinuing(/*stop_at_loop*/ false, /* stop_at_switch */ false,
                                              current_statement)) {
-        AddError("continuing blocks must not contain a return statement", ret->source);
+        AddError(ret->source) << "continuing blocks must not contain a return statement";
         if (continuing != sem->Declaration() && continuing != sem->Parent()->Declaration()) {
-            AddNote("see continuing block here", continuing->source);
+            AddNote(continuing->source) << "see continuing block here";
         }
         return false;
     }
@@ -2460,16 +2477,15 @@
 
 bool Validator::SwitchStatement(const ast::SwitchStatement* s) {
     if (s->body.Length() > kMaxSwitchCaseSelectors) {
-        AddError("switch statement has " + std::to_string(s->body.Length()) +
-                     " case selectors, max is " + std::to_string(kMaxSwitchCaseSelectors),
-                 s->source);
+        AddError(s->source) << "switch statement has " << s->body.Length()
+                            << " case selectors, max is " << kMaxSwitchCaseSelectors;
         return false;
     }
 
     auto* cond_ty = sem_.TypeOf(s->condition);
     if (!cond_ty->is_integer_scalar()) {
-        AddError("switch statement selector expression must be of a scalar integer type",
-                 s->condition->source);
+        AddError(s->condition->source)
+            << "switch statement selector expression must be of a scalar integer type";
         return false;
     }
 
@@ -2482,10 +2498,10 @@
             if (selector->IsDefault()) {
                 if (default_selector != nullptr) {
                     // More than one default clause
-                    AddError("switch statement must have exactly one default clause",
-                             selector->Declaration()->source);
+                    AddError(selector->Declaration()->source)
+                        << "switch statement must have exactly one default clause";
 
-                    AddNote("previous default case", default_selector->Declaration()->source);
+                    AddNote(default_selector->Declaration()->source) << "previous default case";
                     return false;
                 }
                 default_selector = selector;
@@ -2494,21 +2510,22 @@
 
             auto* decl_ty = selector->Value()->Type();
             if (cond_ty != decl_ty) {
-                AddError(
-                    "the case selector values must have the same type as the selector expression.",
-                    selector->Declaration()->source);
+                AddError(selector->Declaration()->source)
+                    << "the case selector values must have the same type as the "
+                       "selector expression.";
                 return false;
             }
 
             auto value = selector->Value()->ValueAs<u32>();
             if (auto added = selectors.Add(value, selector->Declaration()->source); !added) {
-                AddError("duplicate switch case '" +
-                             (decl_ty->IsAnyOf<core::type::I32, core::type::AbstractNumeric>()
-                                  ? std::to_string(i32(value))
-                                  : std::to_string(value)) +
-                             "'",
-                         selector->Declaration()->source);
-                AddNote("previous case declared here", added.value);
+                auto& err = AddError(selector->Declaration()->source)
+                            << "duplicate switch case " << style::Literal;
+                if (decl_ty->IsAnyOf<core::type::I32, core::type::AbstractNumeric>()) {
+                    err << i32(value);
+                } else {
+                    err << value;
+                }
+                AddNote(added.value) << "previous case declared here";
                 return false;
             }
         }
@@ -2516,7 +2533,7 @@
 
     if (default_selector == nullptr) {
         // No default clause
-        AddError("switch statement must have a default clause", s->source);
+        AddError(s->source) << "switch statement must have a default clause";
         return false;
     }
 
@@ -2543,10 +2560,11 @@
         if (!ty->IsConstructible() &&
             !ty->IsAnyOf<core::type::Pointer, core::type::Texture, core::type::Sampler,
                          core::type::AbstractNumeric>()) {
-            AddError("cannot assign '" + sem_.TypeNameOf(rhs_ty) +
-                         "' to '_'. '_' can only be assigned a constructible, pointer, texture or "
-                         "sampler type",
-                     rhs->source);
+            AddError(rhs->source)
+                << "cannot assign " << style::Type << sem_.TypeNameOf(rhs_ty) << style::Plain
+                << " to " << style::Code << "_" << style::Plain << ". " << style::Code << "_"
+                << style::Plain
+                << " can only be assigned a constructible, pointer, texture or sampler type";
             return false;
         }
         return true;  // RHS can be anything.
@@ -2559,7 +2577,7 @@
     auto* lhs_ref = lhs_ty->As<core::type::Reference>();
     if (!lhs_ref) {
         // LHS is not a reference, so it has no storage.
-        AddError("cannot assign to " + sem_.Describe(lhs_sem), lhs->source);
+        AddError(lhs->source) << "cannot assign to " << sem_.Describe(lhs_sem);
 
         auto* expr = lhs;
         while (expr) {
@@ -2571,22 +2589,25 @@
                         Switch(
                             user->Variable()->Declaration(),  //
                             [&](const ast::Let* v) {
-                                AddNote("'let' variables are immutable",
-                                        user->Declaration()->source);
+                                AddNote(user->Declaration()->source)
+                                    << style::Variable << "let" << style::Plain
+                                    << " variables are immutable";
                                 sem_.NoteDeclarationSource(v);
                             },
                             [&](const ast::Const* v) {
-                                AddNote("'const' variables are immutable",
-                                        user->Declaration()->source);
+                                AddNote(user->Declaration()->source)
+                                    << style::Variable << "const" << style::Plain
+                                    << " variables are immutable";
                                 sem_.NoteDeclarationSource(v);
                             },
                             [&](const ast::Override* v) {
-                                AddNote("'override' variables are immutable",
-                                        user->Declaration()->source);
+                                AddNote(user->Declaration()->source)
+                                    << style::Variable << "override" << style::Plain
+                                    << " variables are immutable";
                                 sem_.NoteDeclarationSource(v);
                             },
                             [&](const ast::Parameter* v) {
-                                AddNote("parameters are immutable", user->Declaration()->source);
+                                AddNote(user->Declaration()->source) << "parameters are immutable";
                                 sem_.NoteDeclarationSource(v);
                             });
                     }
@@ -2602,18 +2623,17 @@
 
     // Value type has to match storage type
     if (storage_ty != value_type) {
-        AddError(
-            "cannot assign '" + sem_.TypeNameOf(rhs_ty) + "' to '" + sem_.TypeNameOf(lhs_ty) + "'",
-            a->source);
+        AddError(a->source) << "cannot assign " << style::Type << sem_.TypeNameOf(rhs_ty)
+                            << style::Plain << " to " << style::Type << sem_.TypeNameOf(lhs_ty);
         return false;
     }
     if (!storage_ty->IsConstructible()) {
-        AddError("storage type of assignment must be constructible", a->source);
+        AddError(a->source) << "storage type of assignment must be constructible";
         return false;
     }
     if (lhs_ref->Access() == core::Access::kRead) {
-        AddError("cannot store into a read-only type '" + sem_.RawTypeNameOf(lhs_ty) + "'",
-                 a->source);
+        AddError(a->source) << "cannot store into a read-only type " << style::Type
+                            << sem_.RawTypeNameOf(lhs_ty);
         return false;
     }
     return true;
@@ -2626,14 +2646,22 @@
 
     if (auto* var_user = sem_.Get<sem::VariableUser>(lhs)) {
         auto* v = var_user->Variable()->Declaration();
-        const char* err = Switch(
+        bool errored = Switch(
             v,  //
-            [&](const ast::Parameter*) { return "cannot modify function parameter"; },
-            [&](const ast::Let*) { return "cannot modify 'let'"; },
-            [&](const ast::Override*) { return "cannot modify 'override'"; });
-        if (err) {
-            AddError(err, lhs->source);
-            AddNote("'" + v->name->symbol.Name() + "' is declared here:", v->source);
+            [&](const ast::Parameter*) {
+                AddError(lhs->source) << "cannot modify function parameter";
+                return true;
+            },
+            [&](const ast::Let*) {
+                AddError(lhs->source) << "cannot modify " << style::Keyword << "let";
+                return true;
+            },
+            [&](const ast::Override*) {
+                AddError(lhs->source) << "cannot modify " << style::Keyword << "override";
+                return true;
+            });
+        if (errored) {
+            sem_.NoteDeclarationSource(v);
             return false;
         }
     }
@@ -2642,18 +2670,20 @@
     auto* lhs_ref = lhs_ty->As<core::type::Reference>();
     if (!lhs_ref) {
         // LHS is not a reference, so it has no storage.
-        AddError("cannot modify value of type '" + sem_.TypeNameOf(lhs_ty) + "'", lhs->source);
+        AddError(lhs->source) << "cannot modify value of type " << style::Type
+                              << sem_.TypeNameOf(lhs_ty);
         return false;
     }
 
     if (!lhs_ref->StoreType()->is_integer_scalar()) {
         const std::string kind = inc->increment ? "increment" : "decrement";
-        AddError(kind + " statement can only be applied to an integer scalar", lhs->source);
+        AddError(lhs->source) << kind << " statement can only be applied to an integer scalar";
         return false;
     }
 
     if (lhs_ref->Access() == core::Access::kRead) {
-        AddError("cannot modify read-only type '" + sem_.RawTypeNameOf(lhs_ty) + "'", inc->source);
+        AddError(inc->source) << "cannot modify read-only type " << style::Type
+                              << sem_.RawTypeNameOf(lhs_ty);
         return false;
     }
     return true;
@@ -2669,8 +2699,8 @@
         } else {
             auto added = seen.Add(&d->TypeInfo(), d->source);
             if (!added && !d->Is<ast::InternalAttribute>()) {
-                AddError("duplicate " + d->Name() + " attribute", d->source);
-                AddNote("first attribute declared here", added.value);
+                AddError(d->source) << "duplicate " << d->Name() << " attribute";
+                AddNote(added.value) << "first attribute declared here";
                 return false;
             }
         }
@@ -2689,17 +2719,10 @@
 
         auto diag_added = diagnostics.Add(std::make_pair(category, name), dc);
         if (!diag_added && diag_added.value->severity != dc->severity) {
-            {
-                StringStream ss;
-                ss << "conflicting diagnostic " << use;
-                AddError(ss.str(), dc->rule_name->source);
-            }
-            {
-                StringStream ss;
-                ss << "severity of '" << dc->rule_name->String() << "' set to '" << dc->severity
-                   << "' here";
-                AddNote(ss.str(), diag_added.value->rule_name->source);
-            }
+            AddError(dc->rule_name->source) << "conflicting diagnostic " << use;
+            AddNote(diag_added.value->rule_name->source)
+                << "severity of " << style::Code << dc->rule_name->String() << style::Plain
+                << " set to " << style::Code << dc->severity << style::Plain << " here";
             return false;
         }
     }
@@ -2733,10 +2756,11 @@
 }
 
 void Validator::RaiseArrayWithOverrideCountError(const Source& source) const {
-    AddError(
-        "array with an 'override' element count can only be used as the store type of a "
-        "'var<workgroup>'",
-        source);
+    AddError(source) << style::Type << "array" << style::Plain << " with an " << style::Keyword
+                     << "override" << style::Plain
+                     << " element count can only be used as the store type of a " << style::Keyword
+                     << "var" << style::Code << "<" << style::Enum << "workgroup" << style::Code
+                     << ">";
 }
 
 std::string Validator::VectorPretty(uint32_t size, const core::type::Type* element_type) const {
@@ -2759,18 +2783,22 @@
                 for (auto* member : str->Members()) {
                     using Allowed = std::tuple<core::type::I32, core::type::U32, core::type::F32>;
                     if (TINT_UNLIKELY(!member->Type()->TypeInfo().IsAnyOfTuple<Allowed>())) {
-                        AddError(
-                            "struct members used in the 'pixel_local' address space can only be of "
-                            "the type 'i32', 'u32' or 'f32'",
-                            member->Declaration()->source);
-                        AddNote("struct '" + str->Name().Name() +
-                                    "' used in the 'pixel_local' address space here",
-                                source);
+                        AddError(member->Declaration()->source)
+                            << style::Keyword << "struct" << style::Plain << " members used in the "
+                            << style::Enum << "pixel_local" << style::Plain
+                            << " address space can only be of the type " << style::Type << "i32"
+                            << style::Plain << ", " << style::Type << "u32" << style::Plain
+                            << " or " << style::Type << "f32";
+                        AddNote(source)
+                            << style::Keyword << "struct " << style::Type << str->Name().Name()
+                            << style::Plain << " used in the " << style::Enum << "pixel_local"
+                            << style::Plain << " address space here";
                         return false;
                     }
                 }
             } else if (TINT_UNLIKELY(!store_ty->TypeInfo().Is<core::type::Struct>())) {
-                AddError("'pixel_local' variable only support struct storage types", source);
+                AddError(source) << style::Enum << "pixel_local" << style::Plain
+                                 << " variable only support struct storage types";
                 return false;
             }
             break;
@@ -2779,18 +2807,19 @@
                                   wgsl::Extension::kChromiumExperimentalPushConstant) &&
                               IsValidationEnabled(attributes,
                                                   ast::DisabledValidation::kIgnoreAddressSpace))) {
-                AddError(
-                    "use of variable address space 'push_constant' requires enabling extension "
-                    "'chromium_experimental_push_constant'",
-                    source);
+                AddError(source) << "use of variable address space " << style::Enum
+                                 << "push_constant" << style::Plain
+                                 << " requires enabling extension " << style::Code
+                                 << "chromium_experimental_push_constant";
                 return false;
             }
             break;
         case core::AddressSpace::kStorage:
             if (TINT_UNLIKELY(access == core::Access::kWrite)) {
                 // The access mode for the storage address space can only be 'read' or 'read_write'.
-                AddError("access mode 'write' is not valid for the 'storage' address space",
-                         source);
+                AddError(source) << "access mode " << style::Enum << "write" << style::Plain
+                                 << " is not valid for the " << style::Enum << "storage"
+                                 << style::Plain << " address space";
                 return false;
             }
             break;
@@ -2798,24 +2827,30 @@
             break;
     }
 
-    auto atomic_error = [&]() -> const char* {
+    auto atomic_error = [&] {
+        StyledText err;
         if (address_space != core::AddressSpace::kStorage &&
             address_space != core::AddressSpace::kWorkgroup) {
-            return "atomic variables must have <storage> or <workgroup> address space";
+            AddError(source) << style::Type << "atomic" << style::Plain << " variables must have "
+                             << style::Enum << "storage" << style::Plain << " or " << style::Enum
+                             << "workgroup" << style::Plain << " address space";
+            return true;
         }
         if (address_space == core::AddressSpace::kStorage && access != core::Access::kReadWrite) {
-            return "atomic variables in <storage> address space must have read_write access "
-                   "mode";
+            AddError(source) << "atomic variables in " << style::Enum << "storage" << style::Plain
+                             << " address space must have " << style::Enum << "read_write"
+                             << style::Plain << " access mode";
+            return true;
         }
-        return nullptr;
+        return false;
     };
 
     auto check_sub_atomics = [&] {
         if (auto atomic_use = atomic_composite_info_.Get(store_ty)) {
-            if (auto* err = atomic_error()) {
-                AddError(err, source);
-                AddNote("atomic sub-type of '" + sem_.TypeNameOf(store_ty) + "' is declared here",
-                        **atomic_use);
+            if (TINT_UNLIKELY(atomic_error())) {
+                AddNote(**atomic_use)
+                    << "atomic sub-type of " << style::Type << sem_.TypeNameOf(store_ty)
+                    << style::Plain << " is declared here";
                 return false;
             }
         }
@@ -2825,8 +2860,7 @@
     return Switch(
         store_ty,  //
         [&](const core::type::Atomic*) {
-            if (auto* err = atomic_error()) {
-                AddError(err, source);
+            if (TINT_UNLIKELY(atomic_error())) {
                 return false;
             }
             return true;
@@ -2855,29 +2889,30 @@
                 continue;
             }
 
-            std::string s{core::ToString(space)};
-
-            AddError("entry point '" + ep->Declaration()->name->symbol.Name() +
-                         "' uses two different '" + s + "' variables.",
-                     ep->Declaration()->source);
-            AddNote("first '" + s + "' variable declaration is here", var->Declaration()->source);
+            AddError(ep->Declaration()->source)
+                << "entry point " << style::Function << ep->Declaration()->name->symbol.Name()
+                << style::Plain << " uses two different " << style::Enum << space << style::Plain
+                << " variables.";
+            AddNote(var->Declaration()->source) << "first " << style::Enum << space << style::Plain
+                                                << " variable declaration is here";
             if (func != ep) {
                 TraverseCallChain(ep, func, [&](const sem::Function* f) {
-                    AddNote("called by function '" + f->Declaration()->name->symbol.Name() + "'",
-                            f->Declaration()->source);
+                    AddNote(f->Declaration()->source) << "called by function " << style::Function
+                                                      << f->Declaration()->name->symbol.Name();
                 });
-                AddNote("called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
-                        ep->Declaration()->source);
+                AddNote(ep->Declaration()->source) << "called by entry point " << style::Function
+                                                   << ep->Declaration()->name->symbol.Name();
             }
-            AddNote("second '" + s + "' variable declaration is here",
-                    seen_var->Declaration()->source);
+            AddNote(seen_var->Declaration()->source)
+                << "second " << style::Enum << space << style::Plain
+                << " variable declaration is here";
             if (seen_func != ep) {
                 TraverseCallChain(ep, seen_func, [&](const sem::Function* f) {
-                    AddNote("called by function '" + f->Declaration()->name->symbol.Name() + "'",
-                            f->Declaration()->source);
+                    AddNote(f->Declaration()->source) << "called by function " << style::Function
+                                                      << f->Declaration()->name->symbol.Name();
                 });
-                AddNote("called by entry point '" + ep->Declaration()->name->symbol.Name() + "'",
-                        ep->Declaration()->source);
+                AddNote(ep->Declaration()->source) << "called by entry point " << style::Function
+                                                   << ep->Declaration()->name->symbol.Name();
             }
             return false;
         }
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index a33b702..0fa3bd9 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -42,6 +42,7 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/diagnostic/source.h"
 #include "src/tint/utils/math/hash.h"
+#include "src/tint/utils/text/styled_text.h"
 
 // Forward declarations
 namespace tint::ast {
@@ -125,29 +126,23 @@
               Hashset<TypeAndAddressSpace, 8>& valid_type_storage_layouts);
     ~Validator();
 
-    /// Adds the given error message to the diagnostics
-    /// @param msg the error message
+    /// @returns an error diagnostic
     /// @param source the error source
-    void AddError(const std::string& msg, const Source& source) const;
+    diag::Diagnostic& AddError(const Source& source) const;
 
-    /// Adds the given warning message to the diagnostics
-    /// @param msg the warning message
+    /// @returns an warning diagnostic
     /// @param source the warning source
-    void AddWarning(const std::string& msg, const Source& source) const;
+    diag::Diagnostic& AddWarning(const Source& source) const;
 
-    /// Adds the given note message to the diagnostics
-    /// @param msg the note message
+    /// @returns an note diagnostic
     /// @param source the note source
-    void AddNote(const std::string& msg, const Source& source) const;
+    diag::Diagnostic& AddNote(const Source& source) const;
 
-    /// Adds the given message to the diagnostics with current severity for the given rule.
+    /// Adds a diagnostic with current severity for the given rule.
     /// @param rule the diagnostic trigger rule
-    /// @param msg the diagnostic message
     /// @param source the diagnostic source
-    /// @returns false if the diagnostic is an error for the given trigger rule
-    bool AddDiagnostic(wgsl::DiagnosticRule rule,
-                       const std::string& msg,
-                       const Source& source) const;
+    /// @returns the diagnostic, if the diagnostic level isn't disabled
+    diag::Diagnostic* MaybeAddDiagnostic(wgsl::DiagnosticRule rule, const Source& source) const;
 
     /// @returns the diagnostic filter stack
     DiagnosticFilterStack& DiagnosticFilters() { return diagnostic_filters_; }
diff --git a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
index 027b471..0fb486e 100644
--- a/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/value_constructor_validation_test.cc
@@ -478,7 +478,8 @@
     WrapInFunction(a);
 
     ASSERT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for f32(f32, f32)"));
+    EXPECT_THAT(r()->error(),
+                HasSubstr("12:34 error: no matching constructor for 'f32(f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, ConversionConstructorInvalid_InvalidConstructor) {
@@ -487,7 +488,7 @@
 
     ASSERT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for f32(array<f32, 4>)"));
+                HasSubstr("12:34 error: no matching constructor for 'f32(array<f32, 4>)"));
 }
 
 }  // namespace ConversionConstructTest
@@ -1062,7 +1063,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(i32, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(i32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2F16_Error_ScalarArgumentTypeMismatch) {
@@ -1072,7 +1073,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f16>(f16, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f16>(f16, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2U32_Error_ScalarArgumentTypeMismatch) {
@@ -1080,7 +1081,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<u32>(u32, i32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<u32>(u32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2I32_Error_ScalarArgumentTypeMismatch) {
@@ -1088,7 +1089,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<i32>(u32, i32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<i32>(u32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2Bool_Error_ScalarArgumentTypeMismatch) {
@@ -1096,7 +1097,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<bool>(bool, i32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<bool>(bool, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_Vec3ArgumentCardinalityTooLarge) {
@@ -1104,7 +1105,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(vec3<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(vec3<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_Vec4ArgumentCardinalityTooLarge) {
@@ -1112,7 +1113,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(vec4<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(vec4<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsScalar) {
@@ -1120,7 +1121,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(f32, f32, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(f32, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsVector) {
@@ -1129,7 +1130,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec2<f32>(vec2<f32>, vec2<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(vec2<f32>, vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_TooManyArgumentsVectorAndScalar) {
@@ -1137,7 +1138,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(vec2<f32>, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(vec2<f32>, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Error_InvalidArgumentType) {
@@ -1145,7 +1146,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(mat2x2<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(mat2x2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec2_Success_ZeroValue) {
@@ -1319,7 +1320,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(f32, f32, i32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(f32, f32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3F16_Error_ScalarArgumentTypeMismatch) {
@@ -1329,7 +1330,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f16>(f16, f16, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f16>(f16, f16, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3U32_Error_ScalarArgumentTypeMismatch) {
@@ -1337,7 +1338,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<u32>(u32, i32, u32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<u32>(u32, i32, u32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3I32_Error_ScalarArgumentTypeMismatch) {
@@ -1345,15 +1346,16 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<i32>(i32, u32, i32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<i32>(i32, u32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3Bool_Error_ScalarArgumentTypeMismatch) {
     WrapInFunction(Call<vec3<bool>>(Source{{12, 34}}, false, 1_i, true));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<bool>(bool, i32, bool)"));
+    EXPECT_THAT(
+        r()->error(),
+        HasSubstr("12:34 error: no matching constructor for 'vec3<bool>(bool, i32, bool)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_Vec4ArgumentCardinalityTooLarge) {
@@ -1361,7 +1363,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(vec4<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(vec4<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooFewArgumentsScalar) {
@@ -1369,7 +1371,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(f32, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsScalar) {
@@ -1378,7 +1380,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec3<f32>(f32, f32, f32, f32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(f32, f32, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooFewArgumentsVec2) {
@@ -1386,7 +1388,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(vec2<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec2) {
@@ -1395,7 +1397,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec3<f32>(vec2<f32>, vec2<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(vec2<f32>, vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec2AndScalar) {
@@ -1404,7 +1406,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec3<f32>(vec2<f32>, f32, f32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(vec2<f32>, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_TooManyArgumentsVec3) {
@@ -1412,7 +1414,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(vec3<f32>, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(vec3<f32>, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Error_InvalidArgumentType) {
@@ -1420,7 +1422,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(mat2x2<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(mat2x2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec3_Success_ZeroValue) {
@@ -1642,7 +1644,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(f32, f32, i32, f32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(f32, f32, i32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4F16_Error_ScalarArgumentTypeMismatch) {
@@ -1653,7 +1655,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f16>(f16, f16, f32, f16)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f16>(f16, f16, f32, f16)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4U32_Error_ScalarArgumentTypeMismatch) {
@@ -1662,7 +1664,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<u32>(u32, u32, i32, u32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<u32>(u32, u32, i32, u32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4I32_Error_ScalarArgumentTypeMismatch) {
@@ -1671,7 +1673,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<i32>(i32, i32, u32, i32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<i32>(i32, i32, u32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4Bool_Error_ScalarArgumentTypeMismatch) {
@@ -1680,7 +1682,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<bool>(bool, bool, i32, bool)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<bool>(bool, bool, i32, bool)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsScalar) {
@@ -1688,7 +1690,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec4<f32>(f32, f32, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(f32, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsScalar) {
@@ -1697,7 +1699,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(f32, f32, f32, f32, f32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(f32, f32, f32, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsVec2AndScalar) {
@@ -1705,7 +1707,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec2<f32>, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec2<f32>, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2AndScalars) {
@@ -1714,7 +1716,8 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec2<f32>, f32, f32, f32)"));
+        HasSubstr(
+            "12:34 error: no matching constructor for 'vec4<f32>(vec2<f32>, f32, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2Vec2Scalar) {
@@ -1723,7 +1726,8 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec2<f32>, vec2<f32>, f32)"));
+        HasSubstr(
+            "12:34 error: no matching constructor for 'vec4<f32>(vec2<f32>, vec2<f32>, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2Vec2Vec2) {
@@ -1731,10 +1735,8 @@
         Call<vec4<f32>>(Source{{12, 34}}, Call<vec2<f32>>(), Call<vec2<f32>>(), Call<vec2<f32>>()));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(
-        r()->error(),
-        HasSubstr(
-            "12:34 error: no matching constructor for vec4<f32>(vec2<f32>, vec2<f32>, vec2<f32>)"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for "
+                                        "'vec4<f32>(vec2<f32>, vec2<f32>, vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooFewArgumentsVec3) {
@@ -1742,7 +1744,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec3<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec3<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndScalars) {
@@ -1751,7 +1753,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec3<f32>, f32, f32)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec3<f32>, f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndVec2) {
@@ -1760,7 +1762,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec3<f32>, vec2<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec3<f32>, vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec2AndVec3) {
@@ -1769,7 +1771,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec2<f32>, vec3<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec2<f32>, vec3<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_TooManyArgumentsVec3AndVec3) {
@@ -1778,7 +1780,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for vec4<f32>(vec3<f32>, vec3<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(vec3<f32>, vec3<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Error_InvalidArgumentType) {
@@ -1786,7 +1788,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec4<f32>(mat2x2<f32>)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec4<f32>(mat2x2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vec4_Success_ZeroValue) {
@@ -1966,7 +1968,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<f32>(f32, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<f32>(f32, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, NestedVectorConstructors_Success) {
@@ -1989,7 +1991,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for vec2<f32>(u32)"));
+    EXPECT_THAT(r()->error(),
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(u32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vector_Alias_Argument_Success) {
@@ -2012,7 +2015,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec2<f32>(f32, u32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec2<f32>(f32, u32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vector_ElementTypeAlias_Success) {
@@ -2035,7 +2038,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: no matching constructor for vec3<u32>(vec2<f32>, f32)"));
+                HasSubstr("12:34 error: no matching constructor for 'vec3<u32>(vec2<f32>, f32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, Vector_ArgumentElementTypeAlias_Success) {
@@ -2339,7 +2342,7 @@
                         Expr(Source{{1, 3}}, 2_u)));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("1:1 error: no matching constructor for vec2(i32, u32)"));
+    EXPECT_THAT(r()->error(), HasSubstr("1:1 error: no matching constructor for 'vec2(i32, u32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, CannotInferVec3ElementTypeFromScalarsMismatch) {
@@ -2350,7 +2353,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("1:1 error: no matching constructor for vec3(i32, u32, i32)"));
+                HasSubstr("1:1 error: no matching constructor for 'vec3(i32, u32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest,
@@ -2361,7 +2364,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("1:1 error: no matching constructor for vec3(i32, vec2<f32>)"));
+                HasSubstr("1:1 error: no matching constructor for 'vec3(i32, vec2<f32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, CannotInferVec4ElementTypeFromScalarsMismatch) {
@@ -2373,7 +2376,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("1:1 error: no matching constructor for vec4(i32, i32, f32, i32)"));
+                HasSubstr("1:1 error: no matching constructor for 'vec4(i32, i32, f32, i32)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest,
@@ -2384,7 +2387,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("1:1 error: no matching constructor for vec4(i32, vec3<u32>)"));
+                HasSubstr("1:1 error: no matching constructor for 'vec4(i32, vec3<u32>)'"));
 }
 
 TEST_F(ResolverValueConstructorValidationTest, CannotInferVec4ElementTypeFromVec2AndVec2Mismatch) {
@@ -2394,7 +2397,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(r()->error(),
-                HasSubstr("1:1 error: no matching constructor for vec4(vec2<i32>, vec2<u32>)"));
+                HasSubstr("1:1 error: no matching constructor for 'vec4(vec2<i32>, vec2<u32>)'"));
 }
 
 }  // namespace VectorConstructor
@@ -2458,8 +2461,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ElementConstructor_Error_TooFewArguments) {
@@ -2486,8 +2489,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ColumnConstructor_Error_TooManyArguments) {
@@ -2515,8 +2518,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ElementConstructor_Error_TooManyArguments) {
@@ -2543,8 +2546,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ColumnConstructor_Error_InvalidArgumentType) {
@@ -2571,8 +2574,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ElementConstructor_Error_InvalidArgumentType) {
@@ -2598,8 +2601,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ColumnConstructor_Error_TooFewRowsInVectorArgument) {
@@ -2636,8 +2639,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ColumnConstructor_Error_TooManyRowsInVectorArgument) {
@@ -2673,8 +2676,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ZeroValue_Success) {
@@ -2759,8 +2762,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ElementTypeAlias_Success) {
@@ -2794,7 +2797,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_THAT(
         r()->error(),
-        HasSubstr("12:34 error: no matching constructor for mat2x2<f32>(vec2<u32>, vec2<f32>)"));
+        HasSubstr("12:34 error: no matching constructor for 'mat2x2<f32>(vec2<u32>, vec2<f32>)'"));
 }
 
 TEST_P(MatrixConstructorTest, ArgumentTypeAlias_Success) {
@@ -2840,8 +2843,8 @@
     WrapInFunction(tc);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for " +
-                                        MatrixStr(param) + "(" + args_tys.str() + ")"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: no matching constructor for '" +
+                                        MatrixStr(param) + "(" + args_tys.str() + ")'"));
 }
 
 TEST_P(MatrixConstructorTest, ArgumentElementTypeAlias_Success) {
@@ -2903,7 +2906,7 @@
     Enable(wgsl::Extension::kF16);
 
     StringStream err;
-    err << "12:34 error: no matching constructor for mat" << param.columns << "x" << param.rows
+    err << "12:34 error: no matching constructor for 'mat" << param.columns << "x" << param.rows
         << "(";
 
     Vector<const ast::Expression*, 8> args;
@@ -2934,7 +2937,7 @@
     Enable(wgsl::Extension::kF16);
 
     StringStream err;
-    err << "12:34 error: no matching constructor for mat" << param.columns << "x" << param.rows
+    err << "12:34 error: no matching constructor for 'mat" << param.columns << "x" << param.rows
         << "(";
 
     Vector<const ast::Expression*, 16> args;
@@ -2951,7 +2954,7 @@
         }
     }
 
-    err << ")";
+    err << ")'";
 
     auto matrix_type = ty.mat<Infer>(param.columns, param.rows);
     WrapInFunction(Call(Source{{12, 34}}, matrix_type, std::move(args)));
diff --git a/src/tint/lang/wgsl/resolver/variable_test.cc b/src/tint/lang/wgsl/resolver/variable_test.cc
index d3d9f10..6f70683 100644
--- a/src/tint/lang/wgsl/resolver/variable_test.cc
+++ b/src/tint/lang/wgsl/resolver/variable_test.cc
@@ -1312,7 +1312,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: variable 'a' does not take template arguments
-56:78 note: var 'a' declared here)");
+56:78 note: 'var a' declared here)");
 }
 
 TEST_F(ResolverVariableTest, GlobalConst_UseTemplatedIdent) {
@@ -1330,7 +1330,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: variable 'a' does not take template arguments
-56:78 note: const 'a' declared here)");
+56:78 note: 'const a' declared here)");
 }
 
 TEST_F(ResolverVariableTest, GlobalOverride_UseTemplatedIdent) {
@@ -1348,7 +1348,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: variable 'a' does not take template arguments
-56:78 note: override 'a' declared here)");
+56:78 note: 'override a' declared here)");
 }
 
 TEST_F(ResolverVariableTest, Param_UseTemplatedIdent) {
@@ -1380,7 +1380,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: variable 'a' does not take template arguments
-56:78 note: var 'a' declared here)");
+56:78 note: 'var a' declared here)");
 }
 
 TEST_F(ResolverVariableTest, Let_UseTemplatedIdent) {
@@ -1397,7 +1397,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(12:34 error: variable 'a' does not take template arguments
-56:78 note: let 'a' declared here)");
+56:78 note: 'let a' declared here)");
 }
 
 }  // namespace
diff --git a/src/tint/lang/wgsl/resolver/variable_validation_test.cc b/src/tint/lang/wgsl/resolver/variable_validation_test.cc
index f333b64..89179ce 100644
--- a/src/tint/lang/wgsl/resolver/variable_validation_test.cc
+++ b/src/tint/lang/wgsl/resolver/variable_validation_test.cc
@@ -101,8 +101,8 @@
     GlobalVar("b", ty.i32(), core::AddressSpace::kPrivate, Expr(Source{{56, 78}}, "a"));
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(56:78 error: var 'a' cannot be referenced at module-scope
-12:34 note: var 'a' declared here)");
+    EXPECT_EQ(r()->error(), R"(56:78 error: 'var a' cannot be referenced at module-scope
+12:34 note: 'var a' declared here)");
 }
 
 TEST_F(ResolverVariableValidationTest, OverrideNoInitializerNoType) {
@@ -190,7 +190,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize const of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'const' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, LetInitializerWrongType) {
@@ -199,7 +199,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize let of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'let' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, VarInitializerWrongType) {
@@ -208,7 +208,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize var of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'var' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, ConstInitializerWrongTypeViaAlias) {
@@ -217,7 +217,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize const of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'const' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, LetInitializerWrongTypeViaAlias) {
@@ -226,7 +226,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize let of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'let' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, VarInitializerWrongTypeViaAlias) {
@@ -235,7 +235,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(3:3 error: cannot initialize var of type 'i32' with value of type 'u32')");
+              R"(3:3 error: cannot initialize 'var' of type 'i32' with value of type 'u32')");
 }
 
 TEST_F(ResolverVariableValidationTest, LetOfPtrConstructedWithRef) {
@@ -250,7 +250,7 @@
 
     EXPECT_EQ(
         r()->error(),
-        R"(12:34 error: cannot initialize let of type 'ptr<function, f32, read_write>' with value of type 'f32')");
+        R"(12:34 error: cannot initialize 'let' of type 'ptr<function, f32, read_write>' with value of type 'f32')");
 }
 
 TEST_F(ResolverVariableValidationTest, LocalLetRedeclared) {
@@ -338,10 +338,9 @@
     WrapInFunction(ptr);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: cannot initialize let of type "
-              "'ptr<storage, i32, read_write>' with value of type "
-              "'ptr<storage, i32, read>'");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: cannot initialize 'let' of type 'ptr<storage, i32, read_write>' with value of type 'ptr<storage, i32, read>')");
 }
 
 TEST_F(ResolverVariableValidationTest, NonConstructibleType_Atomic) {
diff --git a/src/tint/lang/wgsl/wgsl.def b/src/tint/lang/wgsl/wgsl.def
index c99f413..b851cdb 100644
--- a/src/tint/lang/wgsl/wgsl.def
+++ b/src/tint/lang/wgsl/wgsl.def
@@ -76,6 +76,8 @@
   chromium_experimental_push_constant
   // A Chromium-specific extension that adds basic subgroup functionality.
   chromium_experimental_subgroups
+  // A Chromium-specific extension that enables features for graphite
+  chromium_internal_graphite
   // A Chromium-specific extension that relaxes memory layout requirements for uniform storage.
   chromium_internal_relaxed_uniform_layout
   // A Chromium-specific extension that enables dual source blending.
diff --git a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
index 622f73f..8ad212a 100644
--- a/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/wgsl/writer/ast_printer/ast_printer.cc
@@ -338,7 +338,7 @@
 void ASTPrinter::EmitImageFormat(StringStream& out, const core::TexelFormat fmt) {
     switch (fmt) {
         case core::TexelFormat::kUndefined:
-            diagnostics_.AddError(diag::System::Writer, "unknown image format");
+            diagnostics_.AddError(diag::System::Writer, Source{}) << "unknown image format";
             break;
         default:
             out << fmt;
diff --git a/src/tint/utils/cli/cli.cc b/src/tint/utils/cli/cli.cc
index b6dc911..6b00638 100644
--- a/src/tint/utils/cli/cli.cc
+++ b/src/tint/utils/cli/cli.cc
@@ -174,8 +174,8 @@
                 return Failure{err};
             }
         } else if (!parse_options.ignore_unknown) {
-            StringStream err;
-            err << "unknown flag: " << arg << std::endl;
+            StyledText err;
+            err << "unknown flag: " << arg << "\n";
             auto names = options_by_name.Keys();
             auto alternatives =
                 Transform(names, [&](const std::string& s) { return std::string_view(s); });
@@ -183,7 +183,7 @@
             opts.prefix = "--";
             opts.list_possible_values = false;
             SuggestAlternatives(arg, alternatives.Slice(), err, opts);
-            return Failure{err.str()};
+            return Failure{err.Plain()};
         }
     }
 
diff --git a/src/tint/utils/diagnostic/BUILD.bazel b/src/tint/utils/diagnostic/BUILD.bazel
index 5cd5f48..568132a 100644
--- a/src/tint/utils/diagnostic/BUILD.bazel
+++ b/src/tint/utils/diagnostic/BUILD.bazel
@@ -41,28 +41,11 @@
   srcs = [
     "diagnostic.cc",
     "formatter.cc",
-    "printer.cc",
     "source.cc",
-  ] + select({
-    ":_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_": [
-      "printer_other.cc",
-    ],
-    "//conditions:default": [],
-  }) + select({
-    ":tint_build_is_linux_or_tint_build_is_mac": [
-      "printer_posix.cc",
-    ],
-    "//conditions:default": [],
-  }) + select({
-    ":tint_build_is_win": [
-      "printer_windows.cc",
-    ],
-    "//conditions:default": [],
-  }),
+  ],
   hdrs = [
     "diagnostic.h",
     "formatter.h",
-    "printer.h",
     "source.h",
   ],
   deps = [
@@ -84,7 +67,6 @@
   srcs = [
     "diagnostic_test.cc",
     "formatter_test.cc",
-    "printer_test.cc",
     "source_test.cc",
   ],
   deps = [
@@ -103,50 +85,3 @@
   visibility = ["//visibility:public"],
 )
 
-alias(
-  name = "tint_build_is_linux",
-  actual = "//src/tint:tint_build_is_linux_true",
-)
-
-alias(
-  name = "_not_tint_build_is_linux_",
-  actual = "//src/tint:tint_build_is_linux_false",
-)
-
-alias(
-  name = "tint_build_is_mac",
-  actual = "//src/tint:tint_build_is_mac_true",
-)
-
-alias(
-  name = "_not_tint_build_is_mac_",
-  actual = "//src/tint:tint_build_is_mac_false",
-)
-
-alias(
-  name = "tint_build_is_win",
-  actual = "//src/tint:tint_build_is_win_true",
-)
-
-alias(
-  name = "_not_tint_build_is_win_",
-  actual = "//src/tint:tint_build_is_win_false",
-)
-
-selects.config_setting_group(
-    name = "tint_build_is_linux_or_tint_build_is_mac",
-    match_any = [
-        "tint_build_is_linux",
-        "tint_build_is_mac",
-    ],
-)
-
-selects.config_setting_group(
-    name = "_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_",
-    match_all = [
-        ":_not_tint_build_is_linux_",
-        ":_not_tint_build_is_mac_",
-        ":_not_tint_build_is_win_",
-    ],
-)
-
diff --git a/src/tint/utils/diagnostic/BUILD.cmake b/src/tint/utils/diagnostic/BUILD.cmake
index 37b0742..c0c2bc6 100644
--- a/src/tint/utils/diagnostic/BUILD.cmake
+++ b/src/tint/utils/diagnostic/BUILD.cmake
@@ -43,8 +43,6 @@
   utils/diagnostic/diagnostic.h
   utils/diagnostic/formatter.cc
   utils/diagnostic/formatter.h
-  utils/diagnostic/printer.cc
-  utils/diagnostic/printer.h
   utils/diagnostic/source.cc
   utils/diagnostic/source.h
 )
@@ -60,24 +58,6 @@
   tint_utils_traits
 )
 
-if((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
-  tint_target_add_sources(tint_utils_diagnostic lib
-    "utils/diagnostic/printer_other.cc"
-  )
-endif((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
-
-if(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
-  tint_target_add_sources(tint_utils_diagnostic lib
-    "utils/diagnostic/printer_posix.cc"
-  )
-endif(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
-
-if(TINT_BUILD_IS_WIN)
-  tint_target_add_sources(tint_utils_diagnostic lib
-    "utils/diagnostic/printer_windows.cc"
-  )
-endif(TINT_BUILD_IS_WIN)
-
 ################################################################################
 # Target:    tint_utils_diagnostic_test
 # Kind:      test
@@ -85,7 +65,6 @@
 tint_add_target(tint_utils_diagnostic_test test
   utils/diagnostic/diagnostic_test.cc
   utils/diagnostic/formatter_test.cc
-  utils/diagnostic/printer_test.cc
   utils/diagnostic/source_test.cc
 )
 
diff --git a/src/tint/utils/diagnostic/BUILD.gn b/src/tint/utils/diagnostic/BUILD.gn
index 4bc6b72..ccb2594 100644
--- a/src/tint/utils/diagnostic/BUILD.gn
+++ b/src/tint/utils/diagnostic/BUILD.gn
@@ -48,8 +48,6 @@
     "diagnostic.h",
     "formatter.cc",
     "formatter.h",
-    "printer.cc",
-    "printer.h",
     "source.cc",
     "source.h",
   ]
@@ -63,25 +61,12 @@
     "${tint_src_dir}/utils/text",
     "${tint_src_dir}/utils/traits",
   ]
-
-  if (!tint_build_is_linux && !tint_build_is_mac && !tint_build_is_win) {
-    sources += [ "printer_other.cc" ]
-  }
-
-  if (tint_build_is_linux || tint_build_is_mac) {
-    sources += [ "printer_posix.cc" ]
-  }
-
-  if (tint_build_is_win) {
-    sources += [ "printer_windows.cc" ]
-  }
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
     sources = [
       "diagnostic_test.cc",
       "formatter_test.cc",
-      "printer_test.cc",
       "source_test.cc",
     ]
     deps = [
diff --git a/src/tint/utils/diagnostic/diagnostic.cc b/src/tint/utils/diagnostic/diagnostic.cc
index 404d9fa..2a21caa 100644
--- a/src/tint/utils/diagnostic/diagnostic.cc
+++ b/src/tint/utils/diagnostic/diagnostic.cc
@@ -30,6 +30,7 @@
 #include <unordered_map>
 
 #include "src/tint/utils/diagnostic/formatter.h"
+#include "src/tint/utils/text/styled_text.h"
 
 namespace tint::diag {
 
@@ -69,7 +70,7 @@
 std::string List::Str() const {
     diag::Formatter::Style style;
     style.print_newline_at_end = false;
-    return Formatter{style}.Format(*this);
+    return Formatter{style}.Format(*this).Plain();
 }
 
 }  // namespace tint::diag
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index ac5c42a..69f77d2 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -35,6 +35,7 @@
 
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/diagnostic/source.h"
+#include "src/tint/utils/text/styled_text.h"
 #include "src/tint/utils/traits/traits.h"
 
 namespace tint::diag {
@@ -85,12 +86,19 @@
     /// @return this diagnostic
     Diagnostic& operator=(const Diagnostic&);
 
+    /// Appends @p msg to the diagnostic's message
+    template <typename T>
+    Diagnostic& operator<<(T&& msg) {
+        message << std::forward<T>(msg);
+        return *this;
+    }
+
     /// severity is the severity of the diagnostic message.
     Severity severity = Severity::Error;
     /// source is the location of the diagnostic.
     Source source;
     /// message is the text associated with the diagnostic.
-    std::string message;
+    StyledText message;
     /// system is the Tint system that raised the diagnostic.
     System system;
     /// A shared pointer to a Source::File. Only used if the diagnostic Source
@@ -171,80 +179,56 @@
 
     /// Adds the note message with the given Source to the end of this list.
     /// @param system the system raising the note message
-    /// @param note_msg the note message
     /// @param source the source of the note diagnostic
     /// @returns a reference to the new diagnostic.
     /// @note The returned reference must not be used after the list is mutated again.
-    diag::Diagnostic& AddNote(System system, std::string_view note_msg, const Source& source) {
+    diag::Diagnostic& AddNote(System system, const Source& source) {
         diag::Diagnostic note{};
         note.severity = diag::Severity::Note;
         note.system = system;
         note.source = source;
-        note.message = note_msg;
         return Add(std::move(note));
     }
 
     /// Adds the warning message with the given Source to the end of this list.
     /// @param system the system raising the warning message
-    /// @param warning_msg the warning message
     /// @param source the source of the warning diagnostic
     /// @returns a reference to the new diagnostic.
     /// @note The returned reference must not be used after the list is mutated again.
-    diag::Diagnostic& AddWarning(System system,
-                                 std::string_view warning_msg,
-                                 const Source& source) {
+    diag::Diagnostic& AddWarning(System system, const Source& source) {
         diag::Diagnostic warning{};
         warning.severity = diag::Severity::Warning;
         warning.system = system;
         warning.source = source;
-        warning.message = warning_msg;
         return Add(std::move(warning));
     }
 
-    /// Adds the error message without a source to the end of this list.
-    /// @param system the system raising the error message
-    /// @param err_msg the error message
-    /// @returns a reference to the new diagnostic.
-    /// @note The returned reference must not be used after the list is mutated again.
-    diag::Diagnostic& AddError(System system, std::string_view err_msg) {
-        diag::Diagnostic error{};
-        error.severity = diag::Severity::Error;
-        error.system = system;
-        error.message = err_msg;
-        return Add(std::move(error));
-    }
-
     /// Adds the error message with the given Source to the end of this list.
     /// @param system the system raising the error message
-    /// @param err_msg the error message
     /// @param source the source of the error diagnostic
     /// @returns a reference to the new diagnostic.
     /// @note The returned reference must not be used after the list is mutated again.
-    diag::Diagnostic& AddError(System system, std::string_view err_msg, const Source& source) {
+    diag::Diagnostic& AddError(System system, const Source& source) {
         diag::Diagnostic error{};
         error.severity = diag::Severity::Error;
         error.system = system;
         error.source = source;
-        error.message = err_msg;
         return Add(std::move(error));
     }
 
     /// Adds an internal compiler error message to the end of this list.
     /// @param system the system raising the error message
-    /// @param err_msg the error message
     /// @param source the source of the internal compiler error
     /// @param file the Source::File owned by this diagnostic
     /// @returns a reference to the new diagnostic.
     /// @note The returned reference must not be used after the list is mutated again.
     diag::Diagnostic& AddIce(System system,
-                             std::string_view err_msg,
                              const Source& source,
                              std::shared_ptr<Source::File> file) {
         diag::Diagnostic ice{};
         ice.severity = diag::Severity::InternalCompilerError;
         ice.system = system;
         ice.source = source;
-        ice.message = err_msg;
         ice.owned_file = std::move(file);
         return Add(std::move(ice));
     }
diff --git a/src/tint/utils/diagnostic/formatter.cc b/src/tint/utils/diagnostic/formatter.cc
index a256a8a..664c4f8 100644
--- a/src/tint/utils/diagnostic/formatter.cc
+++ b/src/tint/utils/diagnostic/formatter.cc
@@ -33,13 +33,16 @@
 #include <vector>
 
 #include "src/tint/utils/diagnostic/diagnostic.h"
-#include "src/tint/utils/diagnostic/printer.h"
+#include "src/tint/utils/macros/defer.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/text_style.h"
 
 namespace tint::diag {
 namespace {
 
-const char* to_str(Severity severity) {
+const char* ToString(Severity severity) {
     switch (severity) {
         case Severity::Note:
             return "note";
@@ -55,7 +58,7 @@
     return "";
 }
 
-std::string to_str(const Source::Location& location) {
+std::string ToString(const Source::Location& location) {
     StringStream ss;
     if (location.line > 0) {
         ss << location.line;
@@ -68,138 +71,84 @@
 
 }  // namespace
 
-/// State holds the internal formatter state for a format() call.
-struct Formatter::State {
-    /// Constructs a State associated with the given printer.
-    /// @param p the printer to write formatted messages to.
-    explicit State(Printer* p) : printer(p) {}
-    ~State() { flush(); }
-
-    /// SetStyle sets the current style to new_style, flushing any pending messages to the printer
-    /// if the style changed.
-    /// @param new_style the new style to apply for future written messages.
-    void SetStyle(const diag::Style& new_style) {
-        if (style.color != new_style.color || style.bold != new_style.bold) {
-            flush();
-            style = new_style;
-        }
-    }
-
-    /// flush writes any pending messages to the printer, clearing the buffer.
-    void flush() {
-        auto str = stream.str();
-        if (str.length() > 0) {
-            printer->Write(str, style);
-            StringStream reset;
-            stream.swap(reset);
-        }
-    }
-
-    /// operator<< queues msg to be written to the printer.
-    /// @param msg the value or string to write to the printer
-    /// @returns this State so that calls can be chained
-    template <typename T>
-    State& operator<<(T&& msg) {
-        stream << std::forward<T>(msg);
-        return *this;
-    }
-
-    /// Newline queues a newline to be written to the printer.
-    void Newline() { stream << std::endl; }
-
-    /// repeat queues the character c to be written to the printer n times.
-    /// @param c the character to print `n` times
-    /// @param n the number of times to print character `c`
-    void repeat(char c, size_t n) { stream.repeat(c, n); }
-
-  private:
-    Printer* printer;
-    diag::Style style;
-    StringStream stream;
-};
-
 Formatter::Formatter() {}
 Formatter::Formatter(const Style& style) : style_(style) {}
 
-void Formatter::Format(const List& list, Printer* printer) const {
-    State state{printer};
+StyledText Formatter::Format(const List& list) const {
+    StyledText text;
 
     bool first = true;
     for (auto diag : list) {
-        state.SetStyle({});
         if (!first) {
-            state.Newline();
+            text << "\n";
         }
-        Format(diag, state);
+        Format(diag, text);
         first = false;
     }
 
     if (style_.print_newline_at_end) {
-        state.Newline();
+        text << "\n";
     }
+
+    return text;
 }
 
-void Formatter::Format(const Diagnostic& diag, State& state) const {
+void Formatter::Format(const Diagnostic& diag, StyledText& text) const {
     auto const& src = diag.source;
     auto const& rng = src.range;
 
-    state.SetStyle({Color::kDefault, true});
+    text << style::Plain;
+    TINT_DEFER(text << style::Plain);
 
-    struct TextAndColor {
+    struct TextAndStyle {
         std::string text;
-        Color color;
-        bool bold = false;
+        TextStyle style = {};
     };
-    std::vector<TextAndColor> prefix;
-    prefix.reserve(6);
+    Vector<TextAndStyle, 6> prefix;
 
     if (style_.print_file && src.file != nullptr) {
         if (rng.begin.line > 0) {
-            prefix.emplace_back(
-                TextAndColor{src.file->path + ":" + to_str(rng.begin), Color::kDefault});
+            prefix.Push(TextAndStyle{src.file->path + ":" + ToString(rng.begin)});
         } else {
-            prefix.emplace_back(TextAndColor{src.file->path, Color::kDefault});
+            prefix.Push(TextAndStyle{src.file->path});
         }
     } else if (rng.begin.line > 0) {
-        prefix.emplace_back(TextAndColor{to_str(rng.begin), Color::kDefault});
+        prefix.Push(TextAndStyle{ToString(rng.begin)});
     }
 
-    Color severity_color = Color::kDefault;
-    switch (diag.severity) {
-        case Severity::Note:
-            break;
-        case Severity::Warning:
-            severity_color = Color::kYellow;
-            break;
-        case Severity::Error:
-            severity_color = Color::kRed;
-            break;
-        case Severity::Fatal:
-        case Severity::InternalCompilerError:
-            severity_color = Color::kMagenta;
-            break;
-    }
     if (style_.print_severity) {
-        prefix.emplace_back(TextAndColor{to_str(diag.severity), severity_color, true});
-    }
-
-    for (size_t i = 0; i < prefix.size(); i++) {
-        if (i > 0) {
-            state << " ";
+        TextStyle style;
+        switch (diag.severity) {
+            case Severity::Note:
+                break;
+            case Severity::Warning:
+                style = style::Warning + style::Bold;
+                break;
+            case Severity::Error:
+                style = style::Error + style::Bold;
+                break;
+            case Severity::Fatal:
+            case Severity::InternalCompilerError:
+                style = style::Fatal + style::Bold;
+                break;
         }
-        state.SetStyle({prefix[i].color, prefix[i].bold});
-        state << prefix[i].text;
+        prefix.Push(TextAndStyle{ToString(diag.severity), style});
     }
 
-    state.SetStyle({Color::kDefault, true});
-    if (!prefix.empty()) {
-        state << ": ";
+    for (size_t i = 0; i < prefix.Length(); i++) {
+        if (i > 0) {
+            text << " ";
+        }
+        text << prefix[i].style << prefix[i].text;
     }
-    state << diag.message;
+
+    if (!prefix.IsEmpty()) {
+        text << style::Plain << ": ";
+    }
+    text << style::Bold << diag.message;
 
     if (style_.print_line && src.file && rng.begin.line > 0) {
-        state.Newline();
-        state.SetStyle({Color::kDefault, false});
+        text << style::Plain << "\n";
 
         for (size_t line_num = rng.begin.line;
              (line_num <= rng.end.line) && (line_num <= src.file->content.lines.size());
@@ -210,16 +159,16 @@
             bool is_ascii = true;
             for (auto c : line) {
                 if (c == '\t') {
-                    state.repeat(' ', style_.tab_width);
+                    text.Repeat(' ', style_.tab_width);
                 } else {
-                    state << c;
+                    text << c;
                 }
                 if (c & 0x80) {
                     is_ascii = false;
                 }
             }
 
-            state.Newline();
+            text << style::Plain << "\n";
 
             // If the line contains non-ascii characters, then we cannot assume that
             // a single utf8 code unit represents a single glyph, so don't attempt to
@@ -228,7 +177,7 @@
                 continue;
             }
 
-            state.SetStyle({Color::kCyan, false});
+            text << style::Squiggle;
 
             // Count the number of glyphs in the line span.
             // start and end use 1-based indexing.
@@ -244,33 +193,24 @@
 
             if (line_num == rng.begin.line && line_num == rng.end.line) {
                 // Single line
-                state.repeat(' ', num_glyphs(1, rng.begin.column));
-                state.repeat('^',
-                             std::max<size_t>(num_glyphs(rng.begin.column, rng.end.column), 1));
+                text.Repeat(' ', num_glyphs(1, rng.begin.column));
+                text.Repeat('^', std::max<size_t>(num_glyphs(rng.begin.column, rng.end.column), 1));
             } else if (line_num == rng.begin.line) {
                 // Start of multi-line
-                state.repeat(' ', num_glyphs(1, rng.begin.column));
-                state.repeat('^', num_glyphs(rng.begin.column, line_len + 1));
+                text.Repeat(' ', num_glyphs(1, rng.begin.column));
+                text.Repeat('^', num_glyphs(rng.begin.column, line_len + 1));
             } else if (line_num == rng.end.line) {
                 // End of multi-line
-                state.repeat('^', num_glyphs(1, rng.end.column));
+                text.Repeat('^', num_glyphs(1, rng.end.column));
             } else {
                 // Middle of multi-line
-                state.repeat('^', num_glyphs(1, line_len + 1));
+                text.Repeat('^', num_glyphs(1, line_len + 1));
             }
-            state.Newline();
+            text << style::Plain << "\n";
         }
-
-        state.SetStyle({});
     }
 }
 
-std::string Formatter::Format(const List& list) const {
-    StringPrinter printer;
-    Format(list, &printer);
-    return printer.str();
-}
-
 Formatter::~Formatter() = default;
 
 }  // namespace tint::diag
diff --git a/src/tint/utils/diagnostic/formatter.h b/src/tint/utils/diagnostic/formatter.h
index 63da5e2..b744ff6 100644
--- a/src/tint/utils/diagnostic/formatter.h
+++ b/src/tint/utils/diagnostic/formatter.h
@@ -30,12 +30,17 @@
 
 #include <string>
 
+// Forward declaration
+namespace tint {
+class StyledTextPrinter;
+class StyledText;
+}  // namespace tint
 namespace tint::diag {
-
 class Diagnostic;
 class List;
-class Printer;
+}  // namespace tint::diag
 
+namespace tint::diag {
 /// Formatter are used to print a list of diagnostics messages.
 class Formatter {
   public:
@@ -62,18 +67,14 @@
 
     ~Formatter();
 
-    /// @param list the list of diagnostic messages to format
-    /// @param printer the printer used to display the formatted diagnostics
-    void Format(const List& list, Printer* printer) const;
-
     /// @return the list of diagnostics `list` formatted to a string.
     /// @param list the list of diagnostic messages to format
-    std::string Format(const List& list) const;
+    StyledText Format(const List& list) const;
 
   private:
     struct State;
 
-    void Format(const Diagnostic& diag, State& state) const;
+    void Format(const Diagnostic& diag, StyledText& text) const;
 
     const Style style_;
 };
diff --git a/src/tint/utils/diagnostic/formatter_test.cc b/src/tint/utils/diagnostic/formatter_test.cc
index 44e63dc..6d77367 100644
--- a/src/tint/utils/diagnostic/formatter_test.cc
+++ b/src/tint/utils/diagnostic/formatter_test.cc
@@ -31,6 +31,7 @@
 
 #include "gtest/gtest.h"
 #include "src/tint/utils/diagnostic/diagnostic.h"
+#include "src/tint/utils/text/styled_text.h"
 
 namespace tint::diag {
 namespace {
@@ -106,7 +107,7 @@
 
 TEST_F(DiagFormatterTest, Simple) {
     Formatter fmt{{false, false, false, false}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(1:14: purr
 2:14: grrr
 3:16: hiss)";
@@ -115,7 +116,7 @@
 
 TEST_F(DiagFormatterTest, SimpleNewlineAtEnd) {
     Formatter fmt{{false, false, false, true}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(1:14: purr
 2:14: grrr
 3:16: hiss
@@ -126,14 +127,14 @@
 TEST_F(DiagFormatterTest, SimpleNoSource) {
     Formatter fmt{{false, false, false, false}};
     auto diag = Diag(Severity::Note, Source{}, "no source!", System::Test);
-    auto got = fmt.Format(List{diag});
+    auto got = fmt.Format(List{diag}).Plain();
     auto* expect = "no source!";
     ASSERT_EQ(expect, got);
 }
 
 TEST_F(DiagFormatterTest, WithFile) {
     Formatter fmt{{true, false, false, false}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(file.name:1:14: purr
 file.name:2:14: grrr
 file.name:3:16: hiss)";
@@ -142,7 +143,7 @@
 
 TEST_F(DiagFormatterTest, WithSeverity) {
     Formatter fmt{{false, true, false, false}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(1:14 note: purr
 2:14 warning: grrr
 3:16 error: hiss)";
@@ -151,7 +152,7 @@
 
 TEST_F(DiagFormatterTest, WithLine) {
     Formatter fmt{{false, false, true, false}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(1:14: purr
 the  cat  says  meow
                 ^
@@ -169,7 +170,7 @@
 
 TEST_F(DiagFormatterTest, UnicodeWithLine) {
     Formatter fmt{{false, false, true, false}};
-    auto got = fmt.Format(List{utf8_diag_note, utf8_diag_warn, utf8_diag_err});
+    auto got = fmt.Format(List{utf8_diag_note, utf8_diag_warn, utf8_diag_err}).Plain();
     auto* expect =
         "1:15: purr\n"
         "the  \xf0\x9f\x90\xb1  says  meow\n"
@@ -184,7 +185,7 @@
 
 TEST_F(DiagFormatterTest, BasicWithFileSeverityLine) {
     Formatter fmt{{true, true, true, false}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(file.name:1:14 note: purr
 the  cat  says  meow
                 ^
@@ -204,7 +205,7 @@
     auto multiline = Diag(Severity::Warning, Source{Source::Range{{2, 9}, {4, 15}}, &ascii_file},
                           "multiline", System::Test);
     Formatter fmt{{false, false, true, false}};
-    auto got = fmt.Format(List{multiline});
+    auto got = fmt.Format(List{multiline}).Plain();
     auto* expect = R"(2:9: multiline
 the  dog  says  woof
           ^^^^^^^^^^
@@ -220,7 +221,7 @@
     auto multiline = Diag(Severity::Warning, Source{Source::Range{{2, 9}, {4, 15}}, &utf8_file},
                           "multiline", System::Test);
     Formatter fmt{{false, false, true, false}};
-    auto got = fmt.Format(List{multiline});
+    auto got = fmt.Format(List{multiline}).Plain();
     auto* expect =
         "2:9: multiline\n"
         "the  \xf0\x9f\x90\x95  says  woof\n"
@@ -231,7 +232,7 @@
 
 TEST_F(DiagFormatterTest, BasicWithFileSeverityLineTab4) {
     Formatter fmt{{true, true, true, false, 4u}};
-    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+    auto got = fmt.Format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err}).Plain();
     auto* expect = R"(file.name:1:14 note: purr
 the    cat    says    meow
                       ^
@@ -251,7 +252,7 @@
     auto multiline = Diag(Severity::Warning, Source{Source::Range{{2, 9}, {4, 15}}, &ascii_file},
                           "multiline", System::Test);
     Formatter fmt{{false, false, true, false, 4u}};
-    auto got = fmt.Format(List{multiline});
+    auto got = fmt.Format(List{multiline}).Plain();
     auto* expect = R"(2:9: multiline
 the    dog    says    woof
               ^^^^^^^^^^^^
@@ -265,7 +266,7 @@
 
 TEST_F(DiagFormatterTest, ICE) {
     Formatter fmt{{}};
-    auto got = fmt.Format(List{ascii_diag_ice});
+    auto got = fmt.Format(List{ascii_diag_ice}).Plain();
     auto* expect = R"(file.name:4:16 internal compiler error: unreachable
 the  snail  says  ???
                   ^^^
@@ -276,7 +277,7 @@
 
 TEST_F(DiagFormatterTest, Fatal) {
     Formatter fmt{{}};
-    auto got = fmt.Format(List{ascii_diag_fatal});
+    auto got = fmt.Format(List{ascii_diag_fatal}).Plain();
     auto* expect = R"(file.name:4:16 fatal: nothing
 the  snail  says  ???
                   ^^^
@@ -288,8 +289,8 @@
 TEST_F(DiagFormatterTest, RangeOOB) {
     Formatter fmt{{true, true, true, true}};
     diag::List list;
-    list.AddError(System::Test, "oob", Source{{{10, 20}, {30, 20}}, &ascii_file});
-    auto got = fmt.Format(list);
+    list.AddError(System::Test, Source{{{10, 20}, {30, 20}}, &ascii_file}) << "oob";
+    auto got = fmt.Format(list).Plain();
     auto* expect = R"(file.name:10:20 error: oob
 
 )";
diff --git a/src/tint/utils/diagnostic/printer.h b/src/tint/utils/diagnostic/printer.h
deleted file mode 100644
index 2b361b8..0000000
--- a/src/tint/utils/diagnostic/printer.h
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2020 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#ifndef SRC_TINT_UTILS_DIAGNOSTIC_PRINTER_H_
-#define SRC_TINT_UTILS_DIAGNOSTIC_PRINTER_H_
-
-#include <memory>
-#include <sstream>
-#include <string>
-
-namespace tint::diag {
-
-class List;
-
-/// Color is an enumerator of colors used by Style.
-enum class Color {
-    kDefault,
-    kBlack,
-    kRed,
-    kGreen,
-    kYellow,
-    kBlue,
-    kMagenta,
-    kCyan,
-    kWhite,
-};
-
-/// Style describes how a diagnostic message should be printed.
-struct Style {
-    /// The foreground text color
-    Color color = Color::kDefault;
-    /// If true the text will be displayed with a strong weight
-    bool bold = false;
-};
-
-/// Printers are used to print formatted diagnostic messages to a stream.
-class Printer {
-  public:
-    /// @returns a diagnostic Printer
-    /// @param out the file to print to.
-    /// @param use_colors if true, the printer will use colors if `out` is a terminal and supports
-    /// them.
-    static std::unique_ptr<Printer> Create(FILE* out, bool use_colors);
-
-    virtual ~Printer();
-
-    /// writes the string str to the printer with the given style.
-    /// @param str the string to write to the printer
-    /// @param style the style used to print `str`
-    virtual void Write(const std::string& str, const Style& style) = 0;
-};
-
-/// StringPrinter is an implementation of Printer that writes to a std::string.
-class StringPrinter : public Printer {
-  public:
-    StringPrinter();
-    ~StringPrinter() override;
-
-    /// @returns the printed string.
-    std::string str() const;
-
-    void Write(const std::string& str, const Style&) override;
-
-  private:
-    std::stringstream stream;
-};
-
-}  // namespace tint::diag
-
-#endif  // SRC_TINT_UTILS_DIAGNOSTIC_PRINTER_H_
diff --git a/src/tint/utils/diagnostic/printer_posix.cc b/src/tint/utils/diagnostic/printer_posix.cc
deleted file mode 100644
index 156c3ce..0000000
--- a/src/tint/utils/diagnostic/printer_posix.cc
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright 2020 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
-
-#include <unistd.h>
-
-#include <cstring>
-
-#include "src/tint/utils/diagnostic/printer.h"
-
-namespace tint::diag {
-namespace {
-
-bool supports_colors(FILE* f) {
-    if (!isatty(fileno(f))) {
-        return false;
-    }
-
-    const char* cterm = getenv("TERM");
-    if (cterm == nullptr) {
-        return false;
-    }
-
-    std::string term = getenv("TERM");
-    if (term != "cygwin" && term != "linux" && term != "rxvt-unicode-256color" &&
-        term != "rxvt-unicode" && term != "screen-256color" && term != "screen" &&
-        term != "tmux-256color" && term != "tmux" && term != "xterm-256color" &&
-        term != "xterm-color" && term != "xterm") {
-        return false;
-    }
-
-    return true;
-}
-
-class PrinterPosix : public Printer {
-  public:
-    PrinterPosix(FILE* f, bool colors) : file(f), use_colors(colors && supports_colors(f)) {}
-
-    void Write(const std::string& str, const Style& style) override {
-        WriteColor(style.color, style.bold);
-        fwrite(str.data(), 1, str.size(), file);
-        WriteColor(Color::kDefault, false);
-    }
-
-  private:
-    constexpr const char* ColorCode(Color color, bool bold) {
-        switch (color) {
-            case Color::kDefault:
-                return bold ? "\u001b[1m" : "\u001b[0m";
-            case Color::kBlack:
-                return bold ? "\u001b[30;1m" : "\u001b[30m";
-            case Color::kRed:
-                return bold ? "\u001b[31;1m" : "\u001b[31m";
-            case Color::kGreen:
-                return bold ? "\u001b[32;1m" : "\u001b[32m";
-            case Color::kYellow:
-                return bold ? "\u001b[33;1m" : "\u001b[33m";
-            case Color::kBlue:
-                return bold ? "\u001b[34;1m" : "\u001b[34m";
-            case Color::kMagenta:
-                return bold ? "\u001b[35;1m" : "\u001b[35m";
-            case Color::kCyan:
-                return bold ? "\u001b[36;1m" : "\u001b[36m";
-            case Color::kWhite:
-                return bold ? "\u001b[37;1m" : "\u001b[37m";
-        }
-        return "";  // unreachable
-    }
-
-    void WriteColor(Color color, bool bold) {
-        if (use_colors) {
-            auto* code = ColorCode(color, bold);
-            fwrite(code, 1, strlen(code), file);
-        }
-    }
-
-    FILE* const file;
-    const bool use_colors;
-};
-
-}  // namespace
-
-std::unique_ptr<Printer> Printer::Create(FILE* out, bool use_colors) {
-    return std::make_unique<PrinterPosix>(out, use_colors);
-}
-
-}  // namespace tint::diag
diff --git a/src/tint/utils/diagnostic/printer_test.cc b/src/tint/utils/diagnostic/printer_test.cc
deleted file mode 100644
index aeb1fa2..0000000
--- a/src/tint/utils/diagnostic/printer_test.cc
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2020 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-#include "src/tint/utils/diagnostic/printer.h"
-
-#include "gtest/gtest.h"
-
-namespace tint::diag {
-namespace {
-
-// Actually verifying that the expected colors are printed is exceptionally
-// difficult as:
-// a) The color emission varies by OS.
-// b) The logic checks to see if the printer is writing to a terminal, making
-//    mocking hard.
-// c) Actually probing what gets written to a FILE* is notoriously tricky.
-//
-// The least we can do is to exersice the code - which is what we do here.
-// The test will print each of the colors, and can be examined with human
-// eyeballs.
-// This can be enabled or disabled with ENABLE_PRINTER_TESTS
-#define ENABLE_PRINTER_TESTS 0
-#if ENABLE_PRINTER_TESTS
-
-using PrinterTest = testing::Test;
-
-TEST_F(PrinterTest, WithColors) {
-    auto printer = Printer::Create(stdout, true);
-    printer->Write("Default", Style{Color::kDefault, false});
-    printer->Write("Black", Style{Color::kBlack, false});
-    printer->Write("Red", Style{Color::kRed, false});
-    printer->Write("Green", Style{Color::kGreen, false});
-    printer->Write("Yellow", Style{Color::kYellow, false});
-    printer->Write("Blue", Style{Color::kBlue, false});
-    printer->Write("Magenta", Style{Color::kMagenta, false});
-    printer->Write("Cyan", Style{Color::kCyan, false});
-    printer->Write("White", Style{Color::kWhite, false});
-    printf("\n");
-}
-
-TEST_F(PrinterTest, BoldWithColors) {
-    auto printer = Printer::Create(stdout, true);
-    printer->Write("Default", Style{Color::kDefault, true});
-    printer->Write("Black", Style{Color::kBlack, true});
-    printer->Write("Red", Style{Color::kRed, true});
-    printer->Write("Green", Style{Color::kGreen, true});
-    printer->Write("Yellow", Style{Color::kYellow, true});
-    printer->Write("Blue", Style{Color::kBlue, true});
-    printer->Write("Magenta", Style{Color::kMagenta, true});
-    printer->Write("Cyan", Style{Color::kCyan, true});
-    printer->Write("White", Style{Color::kWhite, true});
-    printf("\n");
-}
-
-TEST_F(PrinterTest, WithoutColors) {
-    auto printer = Printer::Create(stdout, false);
-    printer->Write("Default", Style{Color::kDefault, false});
-    printer->Write("Black", Style{Color::kBlack, false});
-    printer->Write("Red", Style{Color::kRed, false});
-    printer->Write("Green", Style{Color::kGreen, false});
-    printer->Write("Yellow", Style{Color::kYellow, false});
-    printer->Write("Blue", Style{Color::kBlue, false});
-    printer->Write("Magenta", Style{Color::kMagenta, false});
-    printer->Write("Cyan", Style{Color::kCyan, false});
-    printer->Write("White", Style{Color::kWhite, false});
-    printf("\n");
-}
-
-TEST_F(PrinterTest, BoldWithoutColors) {
-    auto printer = Printer::Create(stdout, false);
-    printer->Write("Default", Style{Color::kDefault, true});
-    printer->Write("Black", Style{Color::kBlack, true});
-    printer->Write("Red", Style{Color::kRed, true});
-    printer->Write("Green", Style{Color::kGreen, true});
-    printer->Write("Yellow", Style{Color::kYellow, true});
-    printer->Write("Blue", Style{Color::kBlue, true});
-    printer->Write("Magenta", Style{Color::kMagenta, true});
-    printer->Write("Cyan", Style{Color::kCyan, true});
-    printer->Write("White", Style{Color::kWhite, true});
-    printf("\n");
-}
-
-#endif  // ENABLE_PRINTER_TESTS
-}  // namespace
-}  // namespace tint::diag
diff --git a/src/tint/utils/diagnostic/printer_windows.cc b/src/tint/utils/diagnostic/printer_windows.cc
deleted file mode 100644
index 485e9a9..0000000
--- a/src/tint/utils/diagnostic/printer_windows.cc
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2020 The Dawn & Tint Authors
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// 3. Neither the name of the copyright holder nor the names of its
-//    contributors may be used to endorse or promote products derived from
-//    this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-// GEN_BUILD:CONDITION(tint_build_is_win)
-
-#include <cstring>
-
-#include "src/tint/utils/diagnostic/printer.h"
-
-#define WIN32_LEAN_AND_MEAN 1
-#include <Windows.h>
-
-namespace tint::diag {
-namespace {
-
-struct ConsoleInfo {
-    HANDLE handle = INVALID_HANDLE_VALUE;
-    WORD default_attributes = 0;
-    operator bool() const { return handle != INVALID_HANDLE_VALUE; }
-};
-
-ConsoleInfo ConsoleInfoFor(FILE* file) {
-    if (file == nullptr) {
-        return {};
-    }
-
-    ConsoleInfo console{};
-    if (file == stdout) {
-        console.handle = GetStdHandle(STD_OUTPUT_HANDLE);
-    } else if (file == stderr) {
-        console.handle = GetStdHandle(STD_ERROR_HANDLE);
-    } else {
-        return {};
-    }
-
-    CONSOLE_SCREEN_BUFFER_INFO info{};
-    if (GetConsoleScreenBufferInfo(console.handle, &info) == 0) {
-        return {};
-    }
-
-    console.default_attributes = info.wAttributes;
-    return console;
-}
-
-class PrinterWindows : public Printer {
-  public:
-    PrinterWindows(FILE* f, bool use_colors)
-        : file(f), console(ConsoleInfoFor(use_colors ? f : nullptr)) {}
-
-    void Write(const std::string& str, const Style& style) override {
-        WriteColor(style.color, style.bold);
-        fwrite(str.data(), 1, str.size(), file);
-        WriteColor(Color::kDefault, false);
-    }
-
-  private:
-    WORD Attributes(Color color, bool bold) {
-        switch (color) {
-            case Color::kDefault:
-                return console.default_attributes;
-            case Color::kBlack:
-                return 0;
-            case Color::kRed:
-                return FOREGROUND_RED | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kGreen:
-                return FOREGROUND_GREEN | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kYellow:
-                return FOREGROUND_RED | FOREGROUND_GREEN | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kBlue:
-                return FOREGROUND_BLUE | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kMagenta:
-                return FOREGROUND_RED | FOREGROUND_BLUE | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kCyan:
-                return FOREGROUND_GREEN | FOREGROUND_BLUE | (bold ? FOREGROUND_INTENSITY : 0);
-            case Color::kWhite:
-                return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE |
-                       (bold ? FOREGROUND_INTENSITY : 0);
-        }
-        return 0;  // unreachable
-    }
-
-    void WriteColor(Color color, bool bold) {
-        if (console) {
-            SetConsoleTextAttribute(console.handle, Attributes(color, bold));
-            fflush(file);
-        }
-    }
-
-    FILE* const file;
-    const ConsoleInfo console;
-};
-
-}  // namespace
-
-std::unique_ptr<Printer> Printer::Create(FILE* out, bool use_colors) {
-    return std::make_unique<PrinterWindows>(out, use_colors);
-}
-
-}  // namespace tint::diag
diff --git a/src/tint/utils/result/result.cc b/src/tint/utils/result/result.cc
index 016e2fb..a3ecfc3 100644
--- a/src/tint/utils/result/result.cc
+++ b/src/tint/utils/result/result.cc
@@ -32,7 +32,7 @@
 Failure::Failure() = default;
 
 Failure::Failure(std::string_view err) {
-    reason.AddError(diag::System::Unknown, err, Source{});
+    reason.AddError(diag::System::Unknown, Source{}) << err;
 }
 
 Failure::Failure(diag::Diagnostic diagnostic) : reason(diag::List{std::move(diagnostic)}) {}
diff --git a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
index c7a8ba5..0308082 100644
--- a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
+++ b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
@@ -285,19 +285,17 @@
 {{- end  }}
     return Build{{$name}}(state, ty{{range .TemplateParams}}, {{.GetName}}{{end}});
   },
-/* string */ [](MatchState*{{if .TemplateParams}} state{{end}}) -> std::string {
+/* print */ []([[maybe_unused]] MatchState* state, StyledText& out) {
 {{- range .TemplateParams }}
 {{-   template "DeclareLocalTemplateParamName" . }}
 {{- end  }}
 
 {{- if .DisplayName }}
-    StringStream ss;
-    ss{{range SplitDisplayName .DisplayName}} << {{.}}{{end}};
-    return ss.str();
+    out {{range SplitDisplayName .DisplayName}} << style::Type << {{.}}{{end}};
 {{- else if .TemplateParams }}
-    return "{{.Name}}<"{{template "AppendTemplateParamNames" .TemplateParams}} + ">";
+    out << style::Type << "{{.Name}}" << style::Code << "<"{{template "AppendTemplateParamNames" .TemplateParams}} << style::Code << ">";
 {{- else }}
-    return "{{.Name}}";
+    out << style::Type << "{{.Name}}";
 {{- end  }}
   }
 };
@@ -319,18 +317,15 @@
 {{- end }}
     return nullptr;
   },
-/* string */ [](MatchState*) -> std::string {
-    StringStream ss;
-    // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+/* print */ [](MatchState*, StyledText& out) {
+    // Note: We pass nullptr to the Matcher.print() functions, as matchers do not support
     // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
-    ss
-{{- range .Types -}}
-{{-   if      IsFirstIn . $.Types }} << k{{PascalCase .Name}}Matcher.string(nullptr)
-{{-   else if IsLastIn  . $.Types }} << " or " << k{{PascalCase .Name}}Matcher.string(nullptr)
-{{-   else                        }} << ", " << k{{PascalCase .Name}}Matcher.string(nullptr)
+{{  range .Types -}}
+{{-   if      IsFirstIn . $.Types }} k{{PascalCase .Name}}Matcher.print(nullptr, out);
+{{-   else if IsLastIn  . $.Types }} out << TextStyle{} << " or "; k{{PascalCase .Name}}Matcher.print(nullptr, out);
+{{-   else                        }} out << TextStyle{} << ", "; k{{PascalCase .Name}}Matcher.print(nullptr, out);
 {{-   end -}}
-{{- end -}};
-    return ss.str();
+{{- end -}}
   }
 };
 {{  end -}}
@@ -364,15 +359,14 @@
     }
   },
 {{- end }}
-/* string */ [](MatchState*) -> std::string {
-    return "
+/* print */ [](MatchState*, StyledText& out) {
+  out
 {{- range .Options -}}
-{{-   if      IsFirstIn . $.Options }}{{.Name}}
-{{-   else if IsLastIn  . $.Options }} or {{.Name}}
-{{-   else                          }}, {{.Name}}
+{{-   if      IsFirstIn . $.Options }}<< style::Enum << "{{.Name}}"
+{{-   else if IsLastIn  . $.Options }}<< TextStyle{} << " or " << style::Enum << "{{.Name}}"
+{{-   else                          }}<< TextStyle{} << ", " << style::Enum << "{{.Name}}"
 {{-   end -}}
-{{- end -}}
-";
+{{- end -}};
   }
 };
 {{  end -}}
@@ -444,12 +438,13 @@
 {{- /* ------------------------------------------------------------------ */ -}}
 {{-                   define "DeclareLocalTemplateParamName"                 -}}
 {{- /* ------------------------------------------------------------------ */ -}}
+  StyledText {{.Name}};
 {{-   if      IsTemplateTypeParam . }}
-  const std::string {{.Name}} = state->TypeName();
+  state->PrintType({{.Name}});
 {{-   else if IsTemplateNumberParam . }}
-  const std::string {{.Name}} = state->NumName();
+  state->PrintNum({{.Name}});
 {{-   else if IsTemplateEnumParam . }}
-  const std::string {{.Name}} = state->NumName();
+  state->PrintNum({{.Name}});
 {{-   end -}}
 {{- end -}}
 
@@ -484,9 +479,9 @@
 {{-                      define "AppendTemplateParamNames"                   -}}
 {{- /* ------------------------------------------------------------------ */ -}}
 {{-   range $i, $ := . -}}
-{{-     if $i }} + ", " + {{.Name}}
-{{-     else }} + {{.Name}}
-{{-     end -}}
+{{-     if $i }} << style::Code << ", " << style::Type << {{.Name}}
+{{-     else  }} << style::Type << {{.Name}}
+{{-     end  -}}
 {{-   end -}}
 {{- end -}}
 
diff --git a/src/tint/utils/text/BUILD.bazel b/src/tint/utils/text/BUILD.bazel
index 21ae073..0e7ee03 100644
--- a/src/tint/utils/text/BUILD.bazel
+++ b/src/tint/utils/text/BUILD.bazel
@@ -42,12 +42,35 @@
     "base64.cc",
     "string.cc",
     "string_stream.cc",
+    "styled_text.cc",
+    "styled_text_printer.cc",
+    "styled_text_printer_ansi.cc",
+    "styled_text_theme.cc",
     "unicode.cc",
-  ],
+  ] + select({
+    ":_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_": [
+      "styled_text_printer_other.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_is_linux_or_tint_build_is_mac": [
+      "styled_text_printer_posix.cc",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_is_win": [
+      "styled_text_printer_windows.cc",
+    ],
+    "//conditions:default": [],
+  }),
   hdrs = [
     "base64.h",
     "string.h",
     "string_stream.h",
+    "styled_text.h",
+    "styled_text_printer.h",
+    "styled_text_theme.h",
+    "text_style.h",
     "unicode.h",
   ],
   deps = [
@@ -69,6 +92,8 @@
     "base64_test.cc",
     "string_stream_test.cc",
     "string_test.cc",
+    "styled_text_printer_test.cc",
+    "text_style_test.cc",
     "unicode_test.cc",
   ],
   deps = [
@@ -86,3 +111,50 @@
   visibility = ["//visibility:public"],
 )
 
+alias(
+  name = "tint_build_is_linux",
+  actual = "//src/tint:tint_build_is_linux_true",
+)
+
+alias(
+  name = "_not_tint_build_is_linux_",
+  actual = "//src/tint:tint_build_is_linux_false",
+)
+
+alias(
+  name = "tint_build_is_mac",
+  actual = "//src/tint:tint_build_is_mac_true",
+)
+
+alias(
+  name = "_not_tint_build_is_mac_",
+  actual = "//src/tint:tint_build_is_mac_false",
+)
+
+alias(
+  name = "tint_build_is_win",
+  actual = "//src/tint:tint_build_is_win_true",
+)
+
+alias(
+  name = "_not_tint_build_is_win_",
+  actual = "//src/tint:tint_build_is_win_false",
+)
+
+selects.config_setting_group(
+    name = "tint_build_is_linux_or_tint_build_is_mac",
+    match_any = [
+        "tint_build_is_linux",
+        "tint_build_is_mac",
+    ],
+)
+
+selects.config_setting_group(
+    name = "_not_tint_build_is_linux__and__not_tint_build_is_mac__and__not_tint_build_is_win_",
+    match_all = [
+        ":_not_tint_build_is_linux_",
+        ":_not_tint_build_is_mac_",
+        ":_not_tint_build_is_win_",
+    ],
+)
+
diff --git a/src/tint/utils/text/BUILD.cmake b/src/tint/utils/text/BUILD.cmake
index f5f8c10..7b19a11 100644
--- a/src/tint/utils/text/BUILD.cmake
+++ b/src/tint/utils/text/BUILD.cmake
@@ -45,6 +45,14 @@
   utils/text/string.h
   utils/text/string_stream.cc
   utils/text/string_stream.h
+  utils/text/styled_text.cc
+  utils/text/styled_text.h
+  utils/text/styled_text_printer.cc
+  utils/text/styled_text_printer.h
+  utils/text/styled_text_printer_ansi.cc
+  utils/text/styled_text_theme.cc
+  utils/text/styled_text_theme.h
+  utils/text/text_style.h
   utils/text/unicode.cc
   utils/text/unicode.h
 )
@@ -59,6 +67,24 @@
   tint_utils_traits
 )
 
+if((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
+  tint_target_add_sources(tint_utils_text lib
+    "utils/text/styled_text_printer_other.cc"
+  )
+endif((NOT TINT_BUILD_IS_LINUX) AND (NOT TINT_BUILD_IS_MAC) AND (NOT TINT_BUILD_IS_WIN))
+
+if(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
+  tint_target_add_sources(tint_utils_text lib
+    "utils/text/styled_text_printer_posix.cc"
+  )
+endif(TINT_BUILD_IS_LINUX OR TINT_BUILD_IS_MAC)
+
+if(TINT_BUILD_IS_WIN)
+  tint_target_add_sources(tint_utils_text lib
+    "utils/text/styled_text_printer_windows.cc"
+  )
+endif(TINT_BUILD_IS_WIN)
+
 ################################################################################
 # Target:    tint_utils_text_test
 # Kind:      test
@@ -67,6 +93,8 @@
   utils/text/base64_test.cc
   utils/text/string_stream_test.cc
   utils/text/string_test.cc
+  utils/text/styled_text_printer_test.cc
+  utils/text/text_style_test.cc
   utils/text/unicode_test.cc
 )
 
diff --git a/src/tint/utils/text/BUILD.gn b/src/tint/utils/text/BUILD.gn
index c13c8c0..8aaadd2 100644
--- a/src/tint/utils/text/BUILD.gn
+++ b/src/tint/utils/text/BUILD.gn
@@ -50,6 +50,14 @@
     "string.h",
     "string_stream.cc",
     "string_stream.h",
+    "styled_text.cc",
+    "styled_text.h",
+    "styled_text_printer.cc",
+    "styled_text_printer.h",
+    "styled_text_printer_ansi.cc",
+    "styled_text_theme.cc",
+    "styled_text_theme.h",
+    "text_style.h",
     "unicode.cc",
     "unicode.h",
   ]
@@ -62,6 +70,18 @@
     "${tint_src_dir}/utils/rtti",
     "${tint_src_dir}/utils/traits",
   ]
+
+  if (!tint_build_is_linux && !tint_build_is_mac && !tint_build_is_win) {
+    sources += [ "styled_text_printer_other.cc" ]
+  }
+
+  if (tint_build_is_linux || tint_build_is_mac) {
+    sources += [ "styled_text_printer_posix.cc" ]
+  }
+
+  if (tint_build_is_win) {
+    sources += [ "styled_text_printer_windows.cc" ]
+  }
 }
 if (tint_build_unittests) {
   tint_unittests_source_set("unittests") {
@@ -69,6 +89,8 @@
       "base64_test.cc",
       "string_stream_test.cc",
       "string_test.cc",
+      "styled_text_printer_test.cc",
+      "text_style_test.cc",
       "unicode_test.cc",
     ]
     deps = [
diff --git a/src/tint/utils/text/string.cc b/src/tint/utils/text/string.cc
index 0812dad..374176e 100644
--- a/src/tint/utils/text/string.cc
+++ b/src/tint/utils/text/string.cc
@@ -30,6 +30,7 @@
 #include "src/tint/utils/containers/transform.h"
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/text/string.h"
+#include "src/tint/utils/text/styled_text.h"
 
 namespace tint {
 
@@ -64,7 +65,7 @@
 
 void SuggestAlternatives(std::string_view got,
                          Slice<const std::string_view> strings,
-                         StringStream& ss,
+                         StyledText& ss,
                          const SuggestAlternativeOptions& options /* = {} */) {
     // If the string typed was within kSuggestionDistance of one of the possible enum values,
     // suggest that. Don't bother with suggestions if the string was extremely long.
diff --git a/src/tint/utils/text/string.h b/src/tint/utils/text/string.h
index 6ae2894..6ccd38d 100644
--- a/src/tint/utils/text/string.h
+++ b/src/tint/utils/text/string.h
@@ -35,6 +35,11 @@
 #include "src/tint/utils/containers/vector.h"
 #include "src/tint/utils/text/string_stream.h"
 
+/// Forward declaration
+namespace tint {
+class StyledText;
+}
+
 namespace tint {
 
 /// @param str the string to apply replacements to
@@ -124,7 +129,7 @@
 /// @param options options for the suggestion
 void SuggestAlternatives(std::string_view got,
                          Slice<const std::string_view> strings,
-                         StringStream& ss,
+                         StyledText& ss,
                          const SuggestAlternativeOptions& options = {});
 
 /// @param str the input string
diff --git a/src/tint/utils/text/string_stream.cc b/src/tint/utils/text/string_stream.cc
index 5a6bfa4..269142f 100644
--- a/src/tint/utils/text/string_stream.cc
+++ b/src/tint/utils/text/string_stream.cc
@@ -30,13 +30,28 @@
 namespace tint {
 
 StringStream::StringStream() {
+    Reset();
+}
+
+StringStream::StringStream(const StringStream& other) {
+    Reset();
+    sstream_ << other.str();
+}
+
+StringStream::~StringStream() = default;
+
+StringStream& StringStream::operator=(const StringStream& other) {
+    Reset();
+    return *this << other.str();
+}
+
+void StringStream::Reset() {
+    sstream_.clear();
     sstream_.flags(sstream_.flags() | std::ios_base::showpoint | std::ios_base::fixed);
     sstream_.imbue(std::locale::classic());
     sstream_.precision(9);
 }
 
-StringStream::~StringStream() = default;
-
 StringStream& operator<<(StringStream& out, CodePoint code_point) {
     if (code_point < 0x7f) {
         // See https://en.cppreference.com/w/cpp/language/escape
diff --git a/src/tint/utils/text/string_stream.h b/src/tint/utils/text/string_stream.h
index 3de603e..ab96301 100644
--- a/src/tint/utils/text/string_stream.h
+++ b/src/tint/utils/text/string_stream.h
@@ -59,9 +59,14 @@
 
     /// Constructor
     StringStream();
+    /// Copy constructor
+    StringStream(const StringStream&);
     /// Destructor
     ~StringStream();
 
+    /// Copy assignment operator
+    StringStream& operator=(const StringStream&);
+
     /// @returns the format flags for the stream
     std::ios_base::fmtflags flags() const { return sstream_.flags(); }
 
@@ -199,6 +204,7 @@
     std::string str() const { return sstream_.str(); }
 
   private:
+    void Reset();
     std::stringstream sstream_;
 };
 
diff --git a/src/tint/utils/text/string_test.cc b/src/tint/utils/text/string_test.cc
index b14aaac..da6e242 100644
--- a/src/tint/utils/text/string_test.cc
+++ b/src/tint/utils/text/string_test.cc
@@ -29,6 +29,7 @@
 
 #include "gmock/gmock.h"
 #include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/styled_text.h"
 
 #include "src/tint/utils/containers/transform.h"  // Used by ToStringList()
 
@@ -95,33 +96,33 @@
 TEST(StringTest, SuggestAlternatives) {
     {
         std::string_view alternatives[] = {"hello world", "Hello World"};
-        StringStream ss;
+        StyledText ss;
         SuggestAlternatives("hello wordl", alternatives, ss);
-        EXPECT_EQ(ss.str(), R"(Did you mean 'hello world'?
+        EXPECT_EQ(ss.Plain(), R"(Did you mean 'hello world'?
 Possible values: 'hello world', 'Hello World')");
     }
     {
         std::string_view alternatives[] = {"foobar", "something else"};
-        StringStream ss;
+        StyledText ss;
         SuggestAlternatives("hello world", alternatives, ss);
-        EXPECT_EQ(ss.str(), R"(Possible values: 'foobar', 'something else')");
+        EXPECT_EQ(ss.Plain(), R"(Possible values: 'foobar', 'something else')");
     }
     {
         std::string_view alternatives[] = {"hello world", "Hello World"};
-        StringStream ss;
+        StyledText ss;
         SuggestAlternativeOptions opts;
         opts.prefix = "$";
         SuggestAlternatives("hello wordl", alternatives, ss, opts);
-        EXPECT_EQ(ss.str(), R"(Did you mean '$hello world'?
+        EXPECT_EQ(ss.Plain(), R"(Did you mean '$hello world'?
 Possible values: '$hello world', '$Hello World')");
     }
     {
         std::string_view alternatives[] = {"hello world", "Hello World"};
-        StringStream ss;
+        StyledText ss;
         SuggestAlternativeOptions opts;
         opts.list_possible_values = false;
         SuggestAlternatives("hello world", alternatives, ss, opts);
-        EXPECT_EQ(ss.str(), R"(Did you mean 'hello world'?)");
+        EXPECT_EQ(ss.Plain(), R"(Did you mean 'hello world'?)");
     }
 }
 
diff --git a/src/tint/utils/text/styled_text.cc b/src/tint/utils/text/styled_text.cc
new file mode 100644
index 0000000..448ecdf
--- /dev/null
+++ b/src/tint/utils/text/styled_text.cc
@@ -0,0 +1,98 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/text/styled_text.h"
+#include <string_view>
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/text_style.h"
+
+namespace tint {
+
+StyledText::StyledText() = default;
+
+StyledText::StyledText(const StyledText&) = default;
+
+StyledText::StyledText(const std::string& text) {
+    stream_ << text;
+}
+
+StyledText::StyledText(std::string_view text) {
+    stream_ << text;
+}
+
+StyledText::StyledText(StyledText&&) = default;
+
+StyledText& StyledText::operator=(const StyledText& other) = default;
+
+StyledText& StyledText::operator=(std::string_view text) {
+    Clear();
+    return *this << text;
+}
+
+void StyledText::Clear() {
+    *this = StyledText{};
+}
+
+StyledText& StyledText::SetStyle(TextStyle style) {
+    if (spans_.Back().style != style) {
+        if (spans_.Back().length == 0) {
+            spans_.Back().style = style;
+        } else {
+            spans_.Push(Span{style});
+        }
+    }
+    return *this;
+}
+
+std::string StyledText::Plain() const {
+    StringStream ss;
+    bool is_code = false;
+    Walk([&](std::string_view text, TextStyle style) {
+        if (is_code != style.IsCode()) {
+            ss << "'";
+        }
+        is_code = style.IsCode();
+
+        ss << text;
+    });
+    if (is_code) {
+        ss << "'";
+    }
+    return ss.str();
+}
+
+void StyledText::Append(const StyledText& other) {
+    other.Walk([&](std::string_view text, TextStyle style) { *this << style << text; });
+}
+
+StyledText& StyledText::Repeat(char c, size_t n) {
+    stream_.repeat(c, n);
+    spans_.Back().length += n;
+    return *this;
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text.h b/src/tint/utils/text/styled_text.h
new file mode 100644
index 0000000..f46ced1
--- /dev/null
+++ b/src/tint/utils/text/styled_text.h
@@ -0,0 +1,129 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_TEXT_STYLED_TEXT_H_
+#define SRC_TINT_UTILS_TEXT_STYLED_TEXT_H_
+
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/text/string_stream.h"
+#include "src/tint/utils/text/text_style.h"
+
+// Forward declarations
+namespace tint {
+class StyledTextPrinter;
+}
+
+namespace tint {
+
+/// StyledText is a string builder with support for styled text spans.
+class StyledText {
+  public:
+    /// Constructor - empty string
+    StyledText();
+
+    /// Copy constructor
+    StyledText(const StyledText&);
+
+    /// Constructor from unstyled text
+    explicit StyledText(const std::string&);
+
+    /// Constructor from unstyled text
+    explicit StyledText(std::string_view);
+
+    /// Move constructor
+    StyledText(StyledText&&);
+
+    /// Assignment copy operator
+    StyledText& operator=(const StyledText& other);
+
+    /// Assignment operator from unstyled text
+    StyledText& operator=(std::string_view text);
+
+    /// Clears the text and restore the default style
+    void Clear();
+
+    /// Sets the style for all future writes to this StyledText
+    StyledText& SetStyle(TextStyle style);
+
+    /// @returns the unstyled text.
+    std::string Plain() const;
+
+    /// Appends the styled text of @p other to this StyledText.
+    void Append(const StyledText& other);
+
+    /// repeat queues the character @p c to be written to the StyledText n times.
+    /// @param c the character to print @p n times
+    /// @param n the number of times to print character @p c
+    /// @returns this StyledText so calls can be chained.
+    StyledText& Repeat(char c, size_t n);
+
+    /// operator<<() appends @p value to the StyledText.
+    /// @p value can be a StyledText to change the style of future appends.
+    template <typename VALUE>
+    StyledText& operator<<(VALUE&& value) {
+        using T = std::decay_t<VALUE>;
+        if constexpr (std::is_same_v<T, TextStyle>) {
+            SetStyle(std::forward<VALUE>(value));
+        } else if constexpr (std::is_same_v<T, StyledText>) {
+            Append(value);
+        } else {
+            uint32_t offset = stream_.tellp();
+            stream_ << value;
+            spans_.Back().length += stream_.tellp() - offset;
+        }
+        return *this;
+    }
+
+    /// Walk calls @p callback with each styled span in the StyledText.
+    /// @param callback a function with the signature: void(std::string_view, TextStyle)
+    template <typename CB>
+    void Walk(CB&& callback) const {
+        std::string text = stream_.str();
+        size_t offset = 0;
+        for (auto& span : spans_) {
+            callback(text.substr(offset, span.length), span.style);
+            offset += span.length;
+        }
+    }
+
+  private:
+    struct Span {
+        TextStyle style;
+        size_t length = 0;
+    };
+
+    StringStream stream_;
+    Vector<Span, 1> spans_{Span{}};
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TEXT_STYLED_TEXT_H_
diff --git a/src/tint/utils/diagnostic/printer_other.cc b/src/tint/utils/text/styled_text_printer.cc
similarity index 69%
rename from src/tint/utils/diagnostic/printer_other.cc
rename to src/tint/utils/text/styled_text_printer.cc
index b311e5a..be83c03 100644
--- a/src/tint/utils/diagnostic/printer_other.cc
+++ b/src/tint/utils/text/styled_text_printer.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,31 +25,35 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-// GEN_BUILD:CONDITION((!tint_build_is_linux) && (!tint_build_is_mac) && (!tint_build_is_win))
-
 #include <cstring>
 
-#include "src/tint/utils/diagnostic/printer.h"
+#include "src/tint/utils/text/styled_text_printer.h"
 
-namespace tint::diag {
+namespace tint {
 namespace {
 
-class PrinterOther : public Printer {
+class Plain : public StyledTextPrinter {
   public:
-    explicit PrinterOther(FILE* f) : file(f) {}
+    explicit Plain(FILE* f) : file_(f) {}
 
-    void Write(const std::string& str, const Style&) override {
-        fwrite(str.data(), 1, str.size(), file);
+    void Print(const StyledText& text) override {
+        auto plain = text.Plain();
+        fwrite(plain.data(), 1, plain.size(), file_);
     }
 
   private:
-    FILE* file;
+    FILE* const file_;
 };
 
 }  // namespace
 
-std::unique_ptr<Printer> Printer::Create(FILE* out, bool) {
-    return std::make_unique<PrinterOther>(out);
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::CreatePlain(FILE* out) {
+    return std::make_unique<Plain>(out);
+}
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::Create(FILE* out) {
+    return Create(out, StyledTextTheme::kDefault);
 }
 
-}  // namespace tint::diag
+StyledTextPrinter::~StyledTextPrinter() = default;
+
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_printer.h b/src/tint/utils/text/styled_text_printer.h
new file mode 100644
index 0000000..468d906
--- /dev/null
+++ b/src/tint/utils/text/styled_text_printer.h
@@ -0,0 +1,75 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_TEXT_STYLED_TEXT_PRINTER_H_
+#define SRC_TINT_UTILS_TEXT_STYLED_TEXT_PRINTER_H_
+
+#include <memory>
+#include <string>
+
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_theme.h"
+
+/// Forward declarations
+namespace tint {
+class TextStyle;
+}
+
+namespace tint {
+
+/// StyledTextPrinter is the interface for printing text with a style.
+class StyledTextPrinter {
+  public:
+    /// @returns a Printer using the default theme.
+    /// @param out the file to print to.
+    static std::unique_ptr<StyledTextPrinter> Create(FILE* out);
+
+    /// @returns a Printer using the theme @p theme.
+    /// @param out the file to print to.
+    /// @param theme the custom theme to use.
+    static std::unique_ptr<StyledTextPrinter> Create(FILE* out, const StyledTextTheme& theme);
+
+    /// @returns a Printer that emits non-styled text.
+    /// @param out the file to print to.
+    static std::unique_ptr<StyledTextPrinter> CreatePlain(FILE* out);
+
+    /// @returns a Printer that uses ANSI escape sequences and theme @p theme.
+    /// @param out the file to print to.
+    /// @param theme the custom theme to use.
+    static std::unique_ptr<StyledTextPrinter> CreateANSI(FILE* out, const StyledTextTheme& theme);
+
+    /// Destructor
+    virtual ~StyledTextPrinter();
+
+    /// Prints the styled text to the printer.
+    /// @param text the text to print.
+    virtual void Print(const StyledText& text) = 0;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TEXT_STYLED_TEXT_PRINTER_H_
diff --git a/src/tint/utils/text/styled_text_printer_ansi.cc b/src/tint/utils/text/styled_text_printer_ansi.cc
new file mode 100644
index 0000000..8a6f39c
--- /dev/null
+++ b/src/tint/utils/text/styled_text_printer_ansi.cc
@@ -0,0 +1,116 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <cstring>
+
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/styled_text_theme.h"
+#include "src/tint/utils/text/text_style.h"
+
+namespace tint {
+namespace {
+
+template <typename T>
+bool Equal(const std::optional<T>& lhs, const std::optional<T>& rhs) {
+    if (lhs.has_value() != rhs.has_value()) {
+        return false;
+    }
+    if (!lhs.has_value()) {
+        return true;
+    }
+    return lhs.value() == rhs.value();
+}
+
+#define ESCAPE "\u001b"
+
+class Printer : public StyledTextPrinter {
+  public:
+    Printer(FILE* f, const StyledTextTheme& t) : file_(f), theme_(t) {}
+
+    void Print(const StyledText& style_text) override {
+        StyledTextTheme::Attributes current;
+
+        style_text.Walk([&](std::string_view text, TextStyle text_style) {
+            auto style = theme_.Get(text_style);
+            if (!Equal(current.foreground, style.foreground)) {
+                current.foreground = style.foreground;
+                if (current.foreground.has_value()) {
+                    fprintf(file_, ESCAPE "[38;2;%d;%d;%dm",  //
+                            static_cast<int>(style.foreground->r),
+                            static_cast<int>(style.foreground->g),
+                            static_cast<int>(style.foreground->b));
+                } else {
+                    fprintf(file_, ESCAPE "[39m");
+                }
+            }
+            if (!Equal(current.background, style.background)) {
+                current.background = style.background;
+                if (current.background.has_value()) {
+                    fprintf(file_, ESCAPE "[48;2;%d;%d;%dm",  //
+                            static_cast<int>(style.background->r),
+                            static_cast<int>(style.background->g),
+                            static_cast<int>(style.background->b));
+                } else {
+                    fprintf(file_, ESCAPE "[49m");
+                }
+            }
+            if (!Equal(current.underlined, style.underlined)) {
+                current.underlined = style.underlined;
+                if (current.underlined == true) {
+                    fprintf(file_, ESCAPE "[4m");
+                } else {
+                    fprintf(file_, ESCAPE "[24m");
+                }
+            }
+            if (!Equal(current.bold, style.bold)) {
+                current.bold = style.bold;
+                if (current.bold == true) {
+                    fprintf(file_, ESCAPE "[1m");
+                } else {
+                    fprintf(file_, ESCAPE "[22m");
+                }
+            }
+            fwrite(text.data(), 1, text.size(), file_);
+        });
+        fprintf(file_, ESCAPE "[m");
+        fflush(file_);
+    }
+
+  private:
+    FILE* const file_;
+    const StyledTextTheme& theme_;
+};
+
+}  // namespace
+
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::CreateANSI(FILE* out,
+                                                                 const StyledTextTheme& theme) {
+    return std::make_unique<Printer>(out, theme);
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/diagnostic/printer.cc b/src/tint/utils/text/styled_text_printer_other.cc
similarity index 77%
rename from src/tint/utils/diagnostic/printer.cc
rename to src/tint/utils/text/styled_text_printer_other.cc
index 819dc58..eec09c0 100644
--- a/src/tint/utils/diagnostic/printer.cc
+++ b/src/tint/utils/text/styled_text_printer_other.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Dawn & Tint Authors
+// Copyright 2024 The Dawn & Tint Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are met:
@@ -25,23 +25,16 @@
 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-#include "src/tint/utils/diagnostic/printer.h"
+// GEN_BUILD:CONDITION((!tint_build_is_linux) && (!tint_build_is_mac) && (!tint_build_is_win))
 
-#include <string>
+#include <cstring>
 
-namespace tint::diag {
+#include "src/tint/utils/text/styled_text_printer.h"
 
-Printer::~Printer() = default;
+namespace tint {
 
-StringPrinter::StringPrinter() = default;
-StringPrinter::~StringPrinter() = default;
-
-std::string StringPrinter::str() const {
-    return stream.str();
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::Create(FILE* out, const StyledTextTheme&) {
+    return CreatePlain(out);
 }
 
-void StringPrinter::Write(const std::string& str, const Style&) {
-    stream << str;
-}
-
-}  // namespace tint::diag
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_printer_posix.cc b/src/tint/utils/text/styled_text_printer_posix.cc
new file mode 100644
index 0000000..e03ab52
--- /dev/null
+++ b/src/tint/utils/text/styled_text_printer_posix.cc
@@ -0,0 +1,73 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac)
+
+#include <unistd.h>
+
+#include <cstring>
+
+#include "src/tint/utils/text/styled_text.h"
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/styled_text_theme.h"
+#include "src/tint/utils/text/text_style.h"
+
+namespace tint {
+namespace {
+
+bool SupportsANSIEscape(FILE* f) {
+    if (!isatty(fileno(f))) {
+        return false;
+    }
+
+    const char* cterm = getenv("TERM");
+    if (cterm == nullptr) {
+        return false;
+    }
+
+    std::string term = getenv("TERM");
+    if (term != "cygwin" && term != "linux" && term != "rxvt-unicode-256color" &&
+        term != "rxvt-unicode" && term != "screen-256color" && term != "screen" &&
+        term != "tmux-256color" && term != "tmux" && term != "xterm-256color" &&
+        term != "xterm-color" && term != "xterm") {
+        return false;
+    }
+
+    return true;
+}
+
+}  // namespace
+
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::Create(FILE* out,
+                                                             const StyledTextTheme& theme) {
+    if (SupportsANSIEscape(out)) {
+        return CreateANSI(out, theme);
+    }
+    return CreatePlain(out);
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_printer_test.cc b/src/tint/utils/text/styled_text_printer_test.cc
new file mode 100644
index 0000000..b9e53d3
--- /dev/null
+++ b/src/tint/utils/text/styled_text_printer_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/text/styled_text_printer.h"
+#include "src/tint/utils/text/text_style.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace {
+
+#define ENABLE_PRINTER_TESTS 0  // Print styled text as part of the unit tests
+#if ENABLE_PRINTER_TESTS
+
+using StyledTextPrinterTest = testing::Test;
+
+TEST_F(StyledTextPrinterTest, Themed) {
+    auto printer = StyledTextPrinter::Create(stdout);
+    printer->Print(StyledText{} << style::Plain << "Plain\n"
+                                << style::Bold << "Bold\n"
+                                << style::Underlined << "Underlined\n"
+                                << style::Success << "Success\n"
+                                << style::Warning << "Warning\n"
+                                << style::Error << "Error\n"
+                                << style::Fatal << "Fatal\n"
+                                << style::Code << "Code\n"
+                                << style::Keyword << "Keyword\n"
+                                << style::Variable << "Variable\n"
+                                << style::Type << "Type\n"
+                                << style::Function << "Function\n"
+                                << style::Enum << "Enum\n"
+                                << style::Literal << "Literal\n"
+                                << style::Attribute << "Attribute\n"
+                                << style::Squiggle << "Squiggle\n");
+}
+
+#endif  // ENABLE_PRINTER_TESTS
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_printer_windows.cc b/src/tint/utils/text/styled_text_printer_windows.cc
new file mode 100644
index 0000000..eb5b32b
--- /dev/null
+++ b/src/tint/utils/text/styled_text_printer_windows.cc
@@ -0,0 +1,69 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// GEN_BUILD:CONDITION(tint_build_is_win)
+
+#include <cstring>
+
+#include "src/tint/utils/text/styled_text_printer.h"
+
+#define WIN32_LEAN_AND_MEAN 1
+#include <Windows.h>
+
+namespace tint {
+namespace {
+
+HANDLE ConsoleHandleFrom(FILE* file) {
+    HANDLE handle = INVALID_HANDLE_VALUE;
+    if (file == stdout) {
+        handle = GetStdHandle(STD_OUTPUT_HANDLE);
+    } else if (file == stderr) {
+        handle = GetStdHandle(STD_ERROR_HANDLE);
+    } else {
+        return INVALID_HANDLE_VALUE;
+    }
+
+    CONSOLE_SCREEN_BUFFER_INFO info{};
+    if (GetConsoleScreenBufferInfo(handle, &info) == 0) {
+        return INVALID_HANDLE_VALUE;
+    }
+    return handle;
+}
+
+}  // namespace
+
+std::unique_ptr<StyledTextPrinter> StyledTextPrinter::Create(FILE* out,
+                                                             const StyledTextTheme& theme) {
+    if (HANDLE handle = ConsoleHandleFrom(out); handle != INVALID_HANDLE_VALUE) {
+        if (SetConsoleMode(handle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) {
+            return CreateANSI(out, theme);
+        }
+    }
+    return CreatePlain(out);
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_theme.cc b/src/tint/utils/text/styled_text_theme.cc
new file mode 100644
index 0000000..ba2944a
--- /dev/null
+++ b/src/tint/utils/text/styled_text_theme.cc
@@ -0,0 +1,215 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/text/styled_text_theme.h"
+#include "src/tint/utils/text/text_style.h"
+
+namespace tint {
+
+const StyledTextTheme StyledTextTheme::kDefault{
+    /* compare_match */ StyledTextTheme::Attributes{
+        /* foreground */ std::nullopt,
+        /* background */ Color{20, 100, 20},
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* compare_mismatch */
+    StyledTextTheme::Attributes{
+        /* foreground */ std::nullopt,
+        /* background */ Color{120, 20, 20},
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* severity_success */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{0, 200, 0},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* severity_warning */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{200, 200, 0},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* severity_failure */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{200, 0, 0},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* severity_fatal */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{200, 0, 200},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_code */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{212, 212, 212},
+        /* background */ Color{43, 43, 43},
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_keyword */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{197, 134, 192},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_variable */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{156, 220, 254},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_type */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{78, 201, 176},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_function */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{220, 220, 170},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_enum */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{79, 193, 255},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_literal */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{181, 206, 168},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_attribute */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{156, 220, 254},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+    /* kind_squiggle */
+    StyledTextTheme::Attributes{
+        /* foreground */ Color{0, 200, 255},
+        /* background */ std::nullopt,
+        /* bold */ std::nullopt,
+        /* underlined */ std::nullopt,
+    },
+};
+
+StyledTextTheme::Attributes StyledTextTheme::Get(TextStyle text_style) const {
+    Attributes out;
+    out.bold = false;
+    out.underlined = false;
+
+    auto apply = [&](const Attributes& in) {
+        if (in.foreground) {
+            out.foreground = in.foreground;
+        }
+        if (in.background) {
+            out.background = in.background;
+        }
+        if (in.bold) {
+            out.bold = in.bold;
+        }
+        if (in.underlined) {
+            out.underlined = in.underlined;
+        }
+    };
+
+    if (text_style.HasSeverity()) {
+        if (text_style.IsSuccess()) {
+            apply(severity_success);
+        } else if (text_style.IsWarning()) {
+            apply(severity_warning);
+        } else if (text_style.IsError()) {
+            apply(severity_failure);
+        } else if (text_style.IsFatal()) {
+            apply(severity_fatal);
+        }
+    }
+
+    if (text_style.HasKind()) {
+        if (text_style.IsCode()) {
+            apply(kind_code);
+
+            if (text_style.IsKeyword()) {
+                apply(kind_keyword);
+            } else if (text_style.IsVariable()) {
+                apply(kind_variable);
+            } else if (text_style.IsType()) {
+                apply(kind_type);
+            } else if (text_style.IsFunction()) {
+                apply(kind_function);
+            } else if (text_style.IsEnum()) {
+                apply(kind_enum);
+            } else if (text_style.IsLiteral()) {
+                apply(kind_literal);
+            } else if (text_style.IsAttribute()) {
+                apply(kind_attribute);
+            }
+        }
+        if (text_style.IsSquiggle()) {
+            apply(kind_squiggle);
+        }
+    }
+
+    if (text_style.HasCompare()) {
+        if (text_style.IsMatch()) {
+            apply(compare_match);
+        } else {
+            apply(compare_mismatch);
+        }
+    }
+
+    if (text_style.IsBold()) {
+        out.bold = true;
+    }
+    if (text_style.IsUnderlined()) {
+        out.underlined = true;
+    }
+
+    return out;
+}
+
+}  // namespace tint
diff --git a/src/tint/utils/text/styled_text_theme.h b/src/tint/utils/text/styled_text_theme.h
new file mode 100644
index 0000000..c9dcfbb
--- /dev/null
+++ b/src/tint/utils/text/styled_text_theme.h
@@ -0,0 +1,107 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_TEXT_STYLED_TEXT_THEME_H_
+#define SRC_TINT_UTILS_TEXT_STYLED_TEXT_THEME_H_
+
+#include <stdint.h>
+#include <optional>
+
+/// Forward declarations
+namespace tint {
+class TextStyle;
+}
+
+namespace tint {
+
+/// StyledTextTheme describes the display theming for TextStyles
+struct StyledTextTheme {
+    /// The default theme
+    static const StyledTextTheme kDefault;
+
+    /// Color holds a 24-bit RGB color
+    struct Color {
+        uint8_t r = 0;
+        uint8_t g = 0;
+        uint8_t b = 0;
+
+        /// Equality operator
+        bool operator==(const Color& other) const {
+            return r == other.r && g == other.g && b == other.b;
+        }
+    };
+
+    /// Attributes holds a number of optional attributes for a style of text.
+    /// Attributes that are std::nullopt do not change the default style.
+    struct Attributes {
+        std::optional<Color> foreground;
+        std::optional<Color> background;
+        std::optional<bool> bold;
+        std::optional<bool> underlined;
+    };
+
+    /// @returns Attributes from the text style @p text_style
+    Attributes Get(TextStyle text_style) const;
+
+    /// The theme's attributes for a compare-match
+    Attributes compare_match;
+    /// The theme's attributes for a compare-mismatch
+    Attributes compare_mismatch;
+
+    /// The theme's attributes for a success severity
+    Attributes severity_success;
+    /// The theme's attributes for a warning severity
+    Attributes severity_warning;
+    /// The theme's attributes for a failure severity
+    Attributes severity_failure;
+    /// The theme's attributes for a fatal severity
+    Attributes severity_fatal;
+
+    /// The theme's attributes for a code text style
+    Attributes kind_code;
+    /// The theme's attributes for a keyword token. This is applied on top #kind_code.
+    Attributes kind_keyword;
+    /// The theme's attributes for a variable token. This is applied on top #kind_code.
+    Attributes kind_variable;
+    /// The theme's attributes for a type token. This is applied on top #kind_code.
+    Attributes kind_type;
+    /// The theme's attributes for a function token. This is applied on top #kind_code.
+    Attributes kind_function;
+    /// The theme's attributes for a enum token. This is applied on top #kind_code.
+    Attributes kind_enum;
+    /// The theme's attributes for a literal token. This is applied on top #kind_code.
+    Attributes kind_literal;
+    /// The theme's attributes for a attribute token. This is applied on top #kind_code.
+    Attributes kind_attribute;
+
+    /// The theme's attributes for a squiggle-highlight.
+    Attributes kind_squiggle;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_UTILS_TEXT_STYLED_TEXT_THEME_H_
diff --git a/src/tint/utils/text/text_style.h b/src/tint/utils/text/text_style.h
new file mode 100644
index 0000000..84deef9
--- /dev/null
+++ b/src/tint/utils/text/text_style.h
@@ -0,0 +1,170 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_UTILS_TEXT_TEXT_STYLE_H_
+#define SRC_TINT_UTILS_TEXT_TEXT_STYLE_H_
+
+#include <cstdint>
+
+#include "src/tint/utils/containers/enum_set.h"
+
+namespace tint {
+
+/// TextStyle is a styling that can be applied to span of a StyledText.
+class TextStyle {
+  public:
+    /// Bits is the integer type used to store the text style.
+    using Bits = uint16_t;
+
+    /// Bit patterns
+
+    static constexpr Bits kStyleMask /*          */ = 0b0000'0000'0000'0011;
+    static constexpr Bits kStyleUnderlined /*    */ = 0b0000'0000'0000'0001;
+    static constexpr Bits kStyleBold /*          */ = 0b0000'0000'0000'0010;
+
+    static constexpr Bits kCompareMask /*        */ = 0b0000'0000'0000'1100;
+    static constexpr Bits kCompareMatch /*       */ = 0b0000'0000'0000'0100;
+    static constexpr Bits kCompareMismatch /*    */ = 0b0000'0000'0000'1000;
+
+    static constexpr Bits kSeverityMask /*       */ = 0b0000'0000'1111'0000;
+    static constexpr Bits kSeverityDefault /*    */ = 0b0000'0000'0000'0000;
+    static constexpr Bits kSeveritySuccess /*    */ = 0b0000'0000'0001'0000;
+    static constexpr Bits kSeverityWarning /*    */ = 0b0000'0000'0010'0000;
+    static constexpr Bits kSeverityError /*      */ = 0b0000'0000'0011'0000;
+    static constexpr Bits kSeverityFatal /*      */ = 0b0000'0000'0100'0000;
+
+    static constexpr Bits kKindMask /*           */ = 0b0000'1111'0000'0000;
+    static constexpr Bits kKindCode /*           */ = 0b0000'0001'0000'0000;
+    static constexpr Bits kKindKeyword /*        */ = 0b0000'0011'0000'0000;
+    static constexpr Bits kKindVariable /*       */ = 0b0000'0101'0000'0000;
+    static constexpr Bits kKindType /*           */ = 0b0000'0111'0000'0000;
+    static constexpr Bits kKindFunction /*       */ = 0b0000'1001'0000'0000;
+    static constexpr Bits kKindEnum /*           */ = 0b0000'1011'0000'0000;
+    static constexpr Bits kKindLiteral /*        */ = 0b0000'1101'0000'0000;
+    static constexpr Bits kKindAttribute /*      */ = 0b0000'1111'0000'0000;
+    static constexpr Bits kKindSquiggle /*       */ = 0b0000'0010'0000'0000;
+
+    /// Getters
+
+    bool IsBold() const { return (bits & kStyleBold) != 0; }
+    bool IsUnderlined() const { return (bits & kStyleUnderlined) != 0; }
+
+    bool HasCompare() const { return (bits & kCompareMask) != 0; }
+    bool IsMatch() const { return (bits & kCompareMask) == kCompareMatch; }
+    bool IsMismatch() const { return (bits & kCompareMask) == kCompareMismatch; }
+
+    bool HasSeverity() const { return (bits & kSeverityMask) != 0; }
+    bool IsSuccess() const { return (bits & kSeverityMask) == kSeveritySuccess; }
+    bool IsWarning() const { return (bits & kSeverityMask) == kSeverityWarning; }
+    bool IsError() const { return (bits & kSeverityMask) == kSeverityError; }
+    bool IsFatal() const { return (bits & kSeverityMask) == kSeverityFatal; }
+
+    bool HasKind() const { return (bits & kKindMask) != 0; }
+    bool IsCode() const { return (bits & kKindCode) != 0; }
+    bool IsKeyword() const { return (bits & kKindMask) == kKindKeyword; }
+    bool IsVariable() const { return (bits & kKindMask) == kKindVariable; }
+    bool IsType() const { return (bits & kKindMask) == kKindType; }
+    bool IsFunction() const { return (bits & kKindMask) == kKindFunction; }
+    bool IsEnum() const { return (bits & kKindMask) == kKindEnum; }
+    bool IsLiteral() const { return (bits & kKindMask) == kKindLiteral; }
+    bool IsAttribute() const { return (bits & kKindMask) == kKindAttribute; }
+    bool IsSquiggle() const { return (bits & kKindMask) == kKindSquiggle; }
+
+    /// Equality operator
+    bool operator==(TextStyle other) const { return bits == other.bits; }
+
+    /// Inequality operator
+    bool operator!=(TextStyle other) const { return bits != other.bits; }
+
+    /// @returns the combination of this TextStyle and @p other.
+    /// If both this TextStyle and @p other have a compare style, severity style or kind style, then
+    /// the style collision will resolve by using the style of @p other.
+    TextStyle operator+(TextStyle other) const {
+        Bits out = other.bits;
+        out |= bits & kStyleMask;
+        if (HasCompare() && !other.HasCompare()) {
+            out |= bits & kCompareMask;
+        }
+        if (HasSeverity() && !other.HasSeverity()) {
+            out |= bits & kSeverityMask;
+        }
+        if (HasKind() && !other.HasKind()) {
+            out |= bits & kKindMask;
+        }
+        return TextStyle{out};
+    }
+
+    /// The style bit pattern
+    Bits bits = 0;
+};
+
+}  // namespace tint
+
+namespace tint::style {
+
+/// Plain renders text without any styling
+static constexpr TextStyle Plain = TextStyle{};
+/// Bold renders text with a heavy weight
+static constexpr TextStyle Bold = TextStyle{TextStyle::kStyleBold};
+/// Underlined renders text with an underline
+static constexpr TextStyle Underlined = TextStyle{TextStyle::kStyleUnderlined};
+/// Underlined renders text with the compare-match style
+static constexpr TextStyle Match = TextStyle{TextStyle::kCompareMatch};
+/// Underlined renders text with the compare-mismatch style
+static constexpr TextStyle Mismatch = TextStyle{TextStyle::kCompareMismatch};
+/// Success renders text with the styling for a successful status
+static constexpr TextStyle Success = TextStyle{TextStyle::kSeveritySuccess};
+/// Warning renders text with the styling for a warning status
+static constexpr TextStyle Warning = TextStyle{TextStyle::kSeverityWarning};
+/// Error renders text with the styling for a error status
+static constexpr TextStyle Error = TextStyle{TextStyle::kSeverityError};
+/// Fatal renders text with the styling for a fatal status
+static constexpr TextStyle Fatal = TextStyle{TextStyle::kSeverityFatal};
+/// Code renders text with a 'code' style
+static constexpr TextStyle Code = TextStyle{TextStyle::kKindCode};
+/// Keyword renders text with a 'code' style that represents a 'keyword' token
+static constexpr TextStyle Keyword = TextStyle{TextStyle::kKindKeyword};
+/// Variable renders text with a 'code' style that represents a 'variable' token
+static constexpr TextStyle Variable = TextStyle{TextStyle::kKindVariable};
+/// Type renders text with a 'code' style that represents a 'type' token
+static constexpr TextStyle Type = TextStyle{TextStyle::kKindType};
+/// Function renders text with a 'code' style that represents a 'function' token
+static constexpr TextStyle Function = TextStyle{TextStyle::kKindFunction};
+/// Enum renders text with a 'code' style that represents a 'enum' token
+static constexpr TextStyle Enum = TextStyle{TextStyle::kKindEnum};
+/// Literal renders text with a 'code' style that represents a 'literal' token
+static constexpr TextStyle Literal = TextStyle{TextStyle::kKindLiteral};
+/// Attribute renders text with a 'code' style that represents an 'attribute' token
+static constexpr TextStyle Attribute = TextStyle{TextStyle::kKindAttribute};
+/// Squiggle renders text with a squiggle-highlight style (`^^^^^`)
+static constexpr TextStyle Squiggle = TextStyle{TextStyle::kKindSquiggle};
+
+}  // namespace tint::style
+
+#endif  // SRC_TINT_UTILS_TEXT_TEXT_STYLE_H_
diff --git a/src/tint/utils/text/text_style_test.cc b/src/tint/utils/text/text_style_test.cc
new file mode 100644
index 0000000..41f32b9
--- /dev/null
+++ b/src/tint/utils/text/text_style_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/utils/text/text_style.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace {
+
+using TextStyleTest = testing::Test;
+
+TEST_F(TextStyleTest, Add) {
+    // Plain + X = X
+    EXPECT_EQ(style::Plain + style::Plain, style::Plain);
+    EXPECT_EQ(style::Plain + style::Bold, style::Bold);
+    EXPECT_EQ(style::Plain + style::Underlined, style::Underlined);
+    EXPECT_EQ(style::Plain + style::Success, style::Success);
+    EXPECT_EQ(style::Plain + style::Warning, style::Warning);
+    EXPECT_EQ(style::Plain + style::Error, style::Error);
+    EXPECT_EQ(style::Plain + style::Fatal, style::Fatal);
+    EXPECT_EQ(style::Plain + style::Code, style::Code);
+    EXPECT_EQ(style::Plain + style::Keyword, style::Keyword);
+    EXPECT_EQ(style::Plain + style::Variable, style::Variable);
+    EXPECT_EQ(style::Plain + style::Type, style::Type);
+    EXPECT_EQ(style::Plain + style::Function, style::Function);
+    EXPECT_EQ(style::Plain + style::Enum, style::Enum);
+    EXPECT_EQ(style::Plain + style::Literal, style::Literal);
+    EXPECT_EQ(style::Plain + style::Attribute, style::Attribute);
+    EXPECT_EQ(style::Plain + style::Squiggle, style::Squiggle);
+
+    // Non-colliding combinations
+    EXPECT_EQ(style::Bold + style::Underlined, style::Bold + style::Underlined);
+    EXPECT_EQ(style::Underlined + style::Success, style::Underlined + style::Success);
+    EXPECT_EQ(style::Type + style::Error, style::Type + style::Error);
+    EXPECT_EQ(style::Bold + style::Underlined + style::Variable + style::Squiggle,
+              style::Bold + style::Underlined + style::Variable + style::Squiggle);
+
+    // Colliding combinations resolve to RHS
+    EXPECT_EQ(style::Error + style::Warning, style::Warning);
+    EXPECT_EQ(style::Warning + style::Error, style::Error);
+    EXPECT_EQ(style::Variable + style::Attribute + style::Type, style::Type);
+}
+
+}  // namespace
+}  // namespace tint