Import Tint changes from Dawn

Changes:
  - 6cc183c85a24e8dafbba87fd0f242ede0810f478 Generic template and forward in stringstream. by dan sinclair <dsinclair@chromium.org>
  - 03de0e83aeb62e8282c3a7b31d2ba01155ebae30 Move tint::transform::Robustness to a santizier transform by Ben Clayton <bclayton@google.com>
  - d7ee9c1510dc7b90457fd77c1cc4359492d416fa tint/sem: Add Declaration() override for IndexAccessorExp... by Ben Clayton <bclayton@google.com>
  - 1edc2729049705022c6aff1188bc32d2164af84a tint/transform: Implement CreateASTTypeFor() for pointers by Ben Clayton <bclayton@google.com>
  - 57be2ff2eb03eba86fb4b3e0b1bd9cc1869969fa tint: Change CloneContext::Replace() to replace the map e... by Ben Clayton <bclayton@google.com>
  - 26157557e86d8aa80e152660e4c0410a455d7a12 tint/transform/utils: Add HoistToDeclBefore::Replace() by Ben Clayton <bclayton@google.com>
  - c0c8abc56978905bf05fccafeeb406709d885366 tint/resolver: Add missing ResolvedIdentifier case for 'l... by Ben Clayton <bclayton@google.com>
  - 3cde73cb1a07f8dc88974ddd7324011956f592f1 tint/transform/utils: Correctly scope for-loop init by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 6cc183c85a24e8dafbba87fd0f242ede0810f478
Change-Id: Iac8248e5d7f6906ff486082f43466207fc64f6c4
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/122500
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 cdda85c..c405175 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -32,7 +32,6 @@
 #include "src/tint/transform/manager.h"
 #include "src/tint/transform/multiplanar_external_texture.h"
 #include "src/tint/transform/renamer.h"
-#include "src/tint/transform/robustness.h"
 #include "src/tint/transform/single_entry_point.h"
 #include "src/tint/transform/substitute_override.h"
 #include "src/tint/transform/vertex_pulling.h"
diff --git a/src/tint/ast/binary_expression.h b/src/tint/ast/binary_expression.h
index 35ca308..b314250 100644
--- a/src/tint/ast/binary_expression.h
+++ b/src/tint/ast/binary_expression.h
@@ -297,10 +297,10 @@
     return "<invalid>";
 }
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param op the BinaryOp
-/// @return the std::ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, BinaryOp op) {
+/// @return the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& out, BinaryOp op) {
     out << FriendlyName(op);
     return out;
 }
diff --git a/src/tint/ast/builtin_texture_helper_test.cc b/src/tint/ast/builtin_texture_helper_test.cc
index 6451bfe..658b650 100644
--- a/src/tint/ast/builtin_texture_helper_test.cc
+++ b/src/tint/ast/builtin_texture_helper_test.cc
@@ -23,6 +23,45 @@
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::ast::builtin::test {
+namespace {
+
+utils::StringStream& operator<<(utils::StringStream& out, const TextureKind& kind) {
+    switch (kind) {
+        case TextureKind::kRegular:
+            out << "regular";
+            break;
+        case TextureKind::kDepth:
+            out << "depth";
+            break;
+        case TextureKind::kDepthMultisampled:
+            out << "depth-multisampled";
+            break;
+        case TextureKind::kMultisampled:
+            out << "multisampled";
+            break;
+        case TextureKind::kStorage:
+            out << "storage";
+            break;
+    }
+    return out;
+}
+
+utils::StringStream& operator<<(utils::StringStream& out, const TextureDataType& ty) {
+    switch (ty) {
+        case TextureDataType::kF32:
+            out << "f32";
+            break;
+        case TextureDataType::kU32:
+            out << "u32";
+            break;
+        case TextureDataType::kI32:
+            out << "i32";
+            break;
+    }
+    return out;
+}
+
+}  // namespace
 
 TextureOverloadCase::TextureOverloadCase(ValidTextureOverload o,
                                          const char* desc,
@@ -80,57 +119,24 @@
 TextureOverloadCase::TextureOverloadCase(const TextureOverloadCase&) = default;
 TextureOverloadCase::~TextureOverloadCase() = default;
 
-std::ostream& operator<<(std::ostream& out, const TextureKind& kind) {
-    switch (kind) {
-        case TextureKind::kRegular:
-            out << "regular";
-            break;
-        case TextureKind::kDepth:
-            out << "depth";
-            break;
-        case TextureKind::kDepthMultisampled:
-            out << "depth-multisampled";
-            break;
-        case TextureKind::kMultisampled:
-            out << "multisampled";
-            break;
-        case TextureKind::kStorage:
-            out << "storage";
-            break;
-    }
-    return out;
-}
-
-std::ostream& operator<<(std::ostream& out, const TextureDataType& ty) {
-    switch (ty) {
-        case TextureDataType::kF32:
-            out << "f32";
-            break;
-        case TextureDataType::kU32:
-            out << "u32";
-            break;
-        case TextureDataType::kI32:
-            out << "i32";
-            break;
-    }
-    return out;
-}
-
 std::ostream& operator<<(std::ostream& out, const TextureOverloadCase& data) {
-    out << "TextureOverloadCase " << static_cast<int>(data.overload) << "\n";
-    out << data.description << "\n";
-    out << "texture_kind:      " << data.texture_kind << "\n";
-    out << "sampler_kind:      ";
+    utils::StringStream str;
+    str << "TextureOverloadCase " << static_cast<int>(data.overload) << "\n";
+    str << data.description << "\n";
+    str << "texture_kind:      " << data.texture_kind << "\n";
+    str << "sampler_kind:      ";
     if (data.texture_kind != TextureKind::kStorage) {
-        out << data.sampler_kind;
+        str << data.sampler_kind;
     } else {
-        out << "<unused>";
+        str << "<unused>";
     }
-    out << "\n";
-    out << "access:            " << data.access << "\n";
-    out << "texel_format:      " << data.texel_format << "\n";
-    out << "texture_dimension: " << data.texture_dimension << "\n";
-    out << "texture_data_type: " << data.texture_data_type << "\n";
+    str << "\n";
+    str << "access:            " << data.access << "\n";
+    str << "texel_format:      " << data.texel_format << "\n";
+    str << "texture_dimension: " << data.texture_dimension << "\n";
+    str << "texture_data_type: " << data.texture_data_type << "\n";
+
+    out << str.str();
     return out;
 }
 
diff --git a/src/tint/ast/diagnostic_control.h b/src/tint/ast/diagnostic_control.h
index 3a5cd1f..a0f5a9d 100644
--- a/src/tint/ast/diagnostic_control.h
+++ b/src/tint/ast/diagnostic_control.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_AST_DIAGNOSTIC_CONTROL_H_
 #define SRC_TINT_AST_DIAGNOSTIC_CONTROL_H_
 
-#include <ostream>
 #include <string>
 #include <unordered_map>
 
diff --git a/src/tint/ast/float_literal_expression.cc b/src/tint/ast/float_literal_expression.cc
index 524b56e..5de8821 100644
--- a/src/tint/ast/float_literal_expression.cc
+++ b/src/tint/ast/float_literal_expression.cc
@@ -37,7 +37,7 @@
     return ctx->dst->create<FloatLiteralExpression>(src, value, suffix);
 }
 
-std::ostream& operator<<(std::ostream& out, FloatLiteralExpression::Suffix suffix) {
+utils::StringStream& operator<<(utils::StringStream& out, FloatLiteralExpression::Suffix suffix) {
     switch (suffix) {
         default:
             return out;
diff --git a/src/tint/ast/float_literal_expression.h b/src/tint/ast/float_literal_expression.h
index 7f3cd12..7920c78 100644
--- a/src/tint/ast/float_literal_expression.h
+++ b/src/tint/ast/float_literal_expression.h
@@ -56,11 +56,11 @@
     const Suffix suffix;
 };
 
-/// Writes the float literal suffix to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the float literal suffix to the stream.
+/// @param out the stream to write to
 /// @param suffix the suffix to write
 /// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, FloatLiteralExpression::Suffix suffix);
+utils::StringStream& operator<<(utils::StringStream& out, FloatLiteralExpression::Suffix suffix);
 
 }  // namespace tint::ast
 
diff --git a/src/tint/ast/int_literal_expression.cc b/src/tint/ast/int_literal_expression.cc
index 502ea9d..b659bbb 100644
--- a/src/tint/ast/int_literal_expression.cc
+++ b/src/tint/ast/int_literal_expression.cc
@@ -35,7 +35,7 @@
     return ctx->dst->create<IntLiteralExpression>(src, value, suffix);
 }
 
-std::ostream& operator<<(std::ostream& out, IntLiteralExpression::Suffix suffix) {
+utils::StringStream& operator<<(utils::StringStream& out, IntLiteralExpression::Suffix suffix) {
     switch (suffix) {
         default:
             return out;
diff --git a/src/tint/ast/int_literal_expression.h b/src/tint/ast/int_literal_expression.h
index 10cbbee..0f50ad3 100644
--- a/src/tint/ast/int_literal_expression.h
+++ b/src/tint/ast/int_literal_expression.h
@@ -55,11 +55,11 @@
     const Suffix suffix;
 };
 
-/// Writes the integer literal suffix to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the integer literal suffix to the stream.
+/// @param out the stream to write to
 /// @param suffix the suffix to write
 /// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, IntLiteralExpression::Suffix suffix);
+utils::StringStream& operator<<(utils::StringStream& out, IntLiteralExpression::Suffix suffix);
 
 }  // namespace tint::ast
 
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
index e50d8dd..b3177ed 100644
--- a/src/tint/ast/interpolate_attribute.h
+++ b/src/tint/ast/interpolate_attribute.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
 #define SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
 
-#include <ostream>
 #include <string>
 
 #include "src/tint/ast/attribute.h"
diff --git a/src/tint/ast/pipeline_stage.cc b/src/tint/ast/pipeline_stage.cc
index 79157da..72d3aca 100644
--- a/src/tint/ast/pipeline_stage.cc
+++ b/src/tint/ast/pipeline_stage.cc
@@ -16,7 +16,7 @@
 
 namespace tint::ast {
 
-std::ostream& operator<<(std::ostream& out, PipelineStage stage) {
+utils::StringStream& operator<<(utils::StringStream& out, PipelineStage stage) {
     switch (stage) {
         case PipelineStage::kNone: {
             out << "none";
diff --git a/src/tint/ast/pipeline_stage.h b/src/tint/ast/pipeline_stage.h
index a1b9c0c..5344afd 100644
--- a/src/tint/ast/pipeline_stage.h
+++ b/src/tint/ast/pipeline_stage.h
@@ -15,17 +15,17 @@
 #ifndef SRC_TINT_AST_PIPELINE_STAGE_H_
 #define SRC_TINT_AST_PIPELINE_STAGE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ast {
 
 /// The pipeline stage
 enum class PipelineStage { kNone = -1, kVertex, kFragment, kCompute };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param stage the PipelineStage
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, PipelineStage stage);
+/// @return the stream so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, PipelineStage stage);
 
 }  // namespace tint::ast
 
diff --git a/src/tint/ast/unary_op.cc b/src/tint/ast/unary_op.cc
index e0afe8d..34d15f7 100644
--- a/src/tint/ast/unary_op.cc
+++ b/src/tint/ast/unary_op.cc
@@ -16,7 +16,7 @@
 
 namespace tint::ast {
 
-std::ostream& operator<<(std::ostream& out, UnaryOp mod) {
+utils::StringStream& operator<<(utils::StringStream& out, UnaryOp mod) {
     switch (mod) {
         case UnaryOp::kAddressOf: {
             out << "address-of";
diff --git a/src/tint/ast/unary_op.h b/src/tint/ast/unary_op.h
index a861af3..368803d 100644
--- a/src/tint/ast/unary_op.h
+++ b/src/tint/ast/unary_op.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_AST_UNARY_OP_H_
 #define SRC_TINT_AST_UNARY_OP_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ast {
 
@@ -28,10 +28,10 @@
     kNot,          // !EXPR
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param mod the UnaryOp
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, UnaryOp mod);
+/// @return the stream so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, UnaryOp mod);
 
 }  // namespace tint::ast
 
diff --git a/src/tint/bench/benchmark.cc b/src/tint/bench/benchmark.cc
index 6b072db..5a28e95 100644
--- a/src/tint/bench/benchmark.cc
+++ b/src/tint/bench/benchmark.cc
@@ -15,7 +15,7 @@
 #include "src/tint/bench/benchmark.h"
 
 #include <filesystem>
-#include <sstream>
+#include <iostream>
 #include <utility>
 #include <vector>
 
diff --git a/src/tint/builtin/access.cc b/src/tint/builtin/access.cc
index 56b326c..15bf700 100644
--- a/src/tint/builtin/access.cc
+++ b/src/tint/builtin/access.cc
@@ -40,7 +40,7 @@
     return Access::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, Access value) {
+utils::StringStream& operator<<(utils::StringStream& out, Access value) {
     switch (value) {
         case Access::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/access.h b/src/tint/builtin/access.h
index f59f50e..afc0867 100644
--- a/src/tint/builtin/access.h
+++ b/src/tint/builtin/access.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_ACCESS_H_
 #define SRC_TINT_BUILTIN_ACCESS_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -35,10 +35,10 @@
     kWrite,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the Access
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, Access value);
+utils::StringStream& operator<<(utils::StringStream& out, Access value);
 
 /// ParseAccess parses a Access from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/access.h.tmpl b/src/tint/builtin/access.h.tmpl
index 6e4587a..3b45390 100644
--- a/src/tint/builtin/access.h.tmpl
+++ b/src/tint/builtin/access.h.tmpl
@@ -17,7 +17,7 @@
 #ifndef SRC_TINT_BUILTIN_ACCESS_H_
 #define SRC_TINT_BUILTIN_ACCESS_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
diff --git a/src/tint/builtin/address_space.cc b/src/tint/builtin/address_space.cc
index 3b386b8..545d97a 100644
--- a/src/tint/builtin/address_space.cc
+++ b/src/tint/builtin/address_space.cc
@@ -55,7 +55,7 @@
     return AddressSpace::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, AddressSpace value) {
+utils::StringStream& operator<<(utils::StringStream& out, AddressSpace value) {
     switch (value) {
         case AddressSpace::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/address_space.h b/src/tint/builtin/address_space.h
index 55b1557..5334301 100644
--- a/src/tint/builtin/address_space.h
+++ b/src/tint/builtin/address_space.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_ADDRESS_SPACE_H_
 #define SRC_TINT_BUILTIN_ADDRESS_SPACE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -41,10 +41,10 @@
     kWorkgroup,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the AddressSpace
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, AddressSpace value);
+utils::StringStream& operator<<(utils::StringStream& out, AddressSpace value);
 
 /// ParseAddressSpace parses a AddressSpace from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/address_space.h.tmpl b/src/tint/builtin/address_space.h.tmpl
index 1dd7d35..42103c3 100644
--- a/src/tint/builtin/address_space.h.tmpl
+++ b/src/tint/builtin/address_space.h.tmpl
@@ -17,7 +17,7 @@
 #ifndef SRC_TINT_BUILTIN_ADDRESS_SPACE_H_
 #define SRC_TINT_BUILTIN_ADDRESS_SPACE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
diff --git a/src/tint/builtin/attribute.cc b/src/tint/builtin/attribute.cc
index d20b77c..33a5171 100644
--- a/src/tint/builtin/attribute.cc
+++ b/src/tint/builtin/attribute.cc
@@ -76,7 +76,7 @@
     return Attribute::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, Attribute value) {
+utils::StringStream& operator<<(utils::StringStream& out, Attribute value) {
     switch (value) {
         case Attribute::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/attribute.h b/src/tint/builtin/attribute.h
index bf95458..c975267 100644
--- a/src/tint/builtin/attribute.h
+++ b/src/tint/builtin/attribute.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_ATTRIBUTE_H_
 #define SRC_TINT_BUILTIN_ATTRIBUTE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 /// \cond DO_NOT_DOCUMENT
 /// There is a bug in doxygen where this enum conflicts with the ast::Attribute
@@ -50,10 +50,10 @@
     kWorkgroupSize,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the Attribute
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, Attribute value);
+utils::StringStream& operator<<(utils::StringStream& out, Attribute value);
 
 /// ParseAttribute parses a Attribute from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/attribute.h.tmpl b/src/tint/builtin/attribute.h.tmpl
index 32ca6a3..ca7443d 100644
--- a/src/tint/builtin/attribute.h.tmpl
+++ b/src/tint/builtin/attribute.h.tmpl
@@ -17,7 +17,7 @@
 #ifndef SRC_TINT_BUILTIN_ATTRIBUTE_H_
 #define SRC_TINT_BUILTIN_ATTRIBUTE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 /// \cond DO_NOT_DOCUMENT
 /// There is a bug in doxygen where this enum conflicts with the ast::Attribute
diff --git a/src/tint/builtin/builtin.cc b/src/tint/builtin/builtin.cc
index dc608d0..085a266 100644
--- a/src/tint/builtin/builtin.cc
+++ b/src/tint/builtin/builtin.cc
@@ -241,7 +241,7 @@
     return Builtin::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, Builtin value) {
+utils::StringStream& operator<<(utils::StringStream& out, Builtin value) {
     switch (value) {
         case Builtin::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/builtin.h b/src/tint/builtin/builtin.h
index 25bac8b..da5226d 100644
--- a/src/tint/builtin/builtin.h
+++ b/src/tint/builtin/builtin.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_BUILTIN_H_
 #define SRC_TINT_BUILTIN_BUILTIN_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -102,10 +102,10 @@
     kVec4U,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the Builtin
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, Builtin value);
+utils::StringStream& operator<<(utils::StringStream& out, Builtin value);
 
 /// ParseBuiltin parses a Builtin from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/builtin.h.tmpl b/src/tint/builtin/builtin.h.tmpl
index 4fce648..f63b20d 100644
--- a/src/tint/builtin/builtin.h.tmpl
+++ b/src/tint/builtin/builtin.h.tmpl
@@ -18,7 +18,7 @@
 #ifndef SRC_TINT_BUILTIN_BUILTIN_H_
 #define SRC_TINT_BUILTIN_BUILTIN_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
diff --git a/src/tint/builtin/builtin_value.cc b/src/tint/builtin/builtin_value.cc
index 150ac7b..5e06c50 100644
--- a/src/tint/builtin/builtin_value.cc
+++ b/src/tint/builtin/builtin_value.cc
@@ -70,7 +70,7 @@
     return BuiltinValue::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, BuiltinValue value) {
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinValue value) {
     switch (value) {
         case BuiltinValue::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/builtin_value.h b/src/tint/builtin/builtin_value.h
index 4f8753f..fc1f39b 100644
--- a/src/tint/builtin/builtin_value.h
+++ b/src/tint/builtin/builtin_value.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_BUILTIN_VALUE_H_
 #define SRC_TINT_BUILTIN_BUILTIN_VALUE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -45,10 +45,10 @@
     kWorkgroupId,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the BuiltinValue
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, BuiltinValue value);
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinValue value);
 
 /// ParseBuiltinValue parses a BuiltinValue from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/builtin_value.h.tmpl b/src/tint/builtin/builtin_value.h.tmpl
index 46e806e..dae642a 100644
--- a/src/tint/builtin/builtin_value.h.tmpl
+++ b/src/tint/builtin/builtin_value.h.tmpl
@@ -14,7 +14,7 @@
 #ifndef SRC_TINT_BUILTIN_BUILTIN_VALUE_H_
 #define SRC_TINT_BUILTIN_BUILTIN_VALUE_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
diff --git a/src/tint/builtin/diagnostic_rule.cc b/src/tint/builtin/diagnostic_rule.cc
index 7b020d5..69650cb 100644
--- a/src/tint/builtin/diagnostic_rule.cc
+++ b/src/tint/builtin/diagnostic_rule.cc
@@ -22,9 +22,10 @@
 
 #include "src/tint/builtin/diagnostic_rule.h"
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// ParseDiagnosticRule parses a DiagnosticRule from a string.
@@ -40,7 +41,7 @@
     return DiagnosticRule::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, DiagnosticRule value) {
+utils::StringStream& operator<<(utils::StringStream& out, DiagnosticRule value) {
     switch (value) {
         case DiagnosticRule::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/diagnostic_rule.cc.tmpl b/src/tint/builtin/diagnostic_rule.cc.tmpl
index 4cf05a4..8705485 100644
--- a/src/tint/builtin/diagnostic_rule.cc.tmpl
+++ b/src/tint/builtin/diagnostic_rule.cc.tmpl
@@ -12,9 +12,10 @@
 
 #include "src/tint/builtin/diagnostic_rule.h"
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 {{ Eval "ParseEnum" (Sem.Enum "diagnostic_rule")}}
diff --git a/src/tint/builtin/diagnostic_rule.h b/src/tint/builtin/diagnostic_rule.h
index 55a6aab..6b6d094 100644
--- a/src/tint/builtin/diagnostic_rule.h
+++ b/src/tint/builtin/diagnostic_rule.h
@@ -25,6 +25,8 @@
 
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The diagnostic rule.
@@ -34,10 +36,10 @@
     kDerivativeUniformity,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the DiagnosticRule
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, DiagnosticRule value);
+utils::StringStream& operator<<(utils::StringStream& out, DiagnosticRule value);
 
 /// ParseDiagnosticRule parses a DiagnosticRule from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/diagnostic_rule.h.tmpl b/src/tint/builtin/diagnostic_rule.h.tmpl
index 2e6f7f9..ba823e3 100644
--- a/src/tint/builtin/diagnostic_rule.h.tmpl
+++ b/src/tint/builtin/diagnostic_rule.h.tmpl
@@ -15,6 +15,8 @@
 
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The diagnostic rule.
diff --git a/src/tint/builtin/diagnostic_severity.cc b/src/tint/builtin/diagnostic_severity.cc
index 8c14a9a..5d68992 100644
--- a/src/tint/builtin/diagnostic_severity.cc
+++ b/src/tint/builtin/diagnostic_severity.cc
@@ -58,7 +58,7 @@
     return DiagnosticSeverity::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, DiagnosticSeverity value) {
+utils::StringStream& operator<<(utils::StringStream& out, DiagnosticSeverity value) {
     switch (value) {
         case DiagnosticSeverity::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/diagnostic_severity.h b/src/tint/builtin/diagnostic_severity.h
index daef0f7..44a3b41 100644
--- a/src/tint/builtin/diagnostic_severity.h
+++ b/src/tint/builtin/diagnostic_severity.h
@@ -23,12 +23,12 @@
 #ifndef SRC_TINT_BUILTIN_DIAGNOSTIC_SEVERITY_H_
 #define SRC_TINT_BUILTIN_DIAGNOSTIC_SEVERITY_H_
 
-#include <ostream>
 #include <string>
 #include <unordered_map>
 
 #include "src/tint/builtin/diagnostic_rule.h"
 #include "src/tint/diagnostic/diagnostic.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -41,10 +41,10 @@
     kWarning,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the DiagnosticSeverity
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, DiagnosticSeverity value);
+utils::StringStream& operator<<(utils::StringStream& out, DiagnosticSeverity value);
 
 /// ParseDiagnosticSeverity parses a DiagnosticSeverity from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/diagnostic_severity.h.tmpl b/src/tint/builtin/diagnostic_severity.h.tmpl
index 7580b2c..7773d6c 100644
--- a/src/tint/builtin/diagnostic_severity.h.tmpl
+++ b/src/tint/builtin/diagnostic_severity.h.tmpl
@@ -13,10 +13,10 @@
 #ifndef SRC_TINT_BUILTIN_DIAGNOSTIC_SEVERITY_H_
 #define SRC_TINT_BUILTIN_DIAGNOSTIC_SEVERITY_H_
 
-#include <ostream>
 #include <string>
 #include <unordered_map>
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/builtin/diagnostic_rule.h"
 #include "src/tint/diagnostic/diagnostic.h"
 
diff --git a/src/tint/builtin/extension.cc b/src/tint/builtin/extension.cc
index 36171db..aca6220 100644
--- a/src/tint/builtin/extension.cc
+++ b/src/tint/builtin/extension.cc
@@ -49,7 +49,7 @@
     return Extension::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, Extension value) {
+utils::StringStream& operator<<(utils::StringStream& out, Extension value) {
     switch (value) {
         case Extension::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/extension.h b/src/tint/builtin/extension.h
index aace504..6beeda3 100644
--- a/src/tint/builtin/extension.h
+++ b/src/tint/builtin/extension.h
@@ -23,8 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_EXTENSION_H_
 #define SRC_TINT_BUILTIN_EXTENSION_H_
 
-#include <ostream>
-
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
 namespace tint::builtin {
@@ -41,10 +40,10 @@
     kF16,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the Extension
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, Extension value);
+utils::StringStream& operator<<(utils::StringStream& out, Extension value);
 
 /// ParseExtension parses a Extension from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/extension.h.tmpl b/src/tint/builtin/extension.h.tmpl
index 3051ad4..07f3ee4 100644
--- a/src/tint/builtin/extension.h.tmpl
+++ b/src/tint/builtin/extension.h.tmpl
@@ -14,8 +14,7 @@
 #ifndef SRC_TINT_BUILTIN_EXTENSION_H_
 #define SRC_TINT_BUILTIN_EXTENSION_H_
 
-#include <ostream>
-
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
 namespace tint::builtin {
diff --git a/src/tint/builtin/interpolation_sampling.cc b/src/tint/builtin/interpolation_sampling.cc
index 8b39930..30d925a 100644
--- a/src/tint/builtin/interpolation_sampling.cc
+++ b/src/tint/builtin/interpolation_sampling.cc
@@ -43,7 +43,7 @@
     return InterpolationSampling::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, InterpolationSampling value) {
+utils::StringStream& operator<<(utils::StringStream& out, InterpolationSampling value) {
     switch (value) {
         case InterpolationSampling::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/interpolation_sampling.h b/src/tint/builtin/interpolation_sampling.h
index a3fc357..78a9d51 100644
--- a/src/tint/builtin/interpolation_sampling.h
+++ b/src/tint/builtin/interpolation_sampling.h
@@ -23,9 +23,10 @@
 #ifndef SRC_TINT_BUILTIN_INTERPOLATION_SAMPLING_H_
 #define SRC_TINT_BUILTIN_INTERPOLATION_SAMPLING_H_
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The interpolation sampling.
@@ -36,10 +37,10 @@
     kSample,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the InterpolationSampling
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, InterpolationSampling value);
+utils::StringStream& operator<<(utils::StringStream& out, InterpolationSampling value);
 
 /// ParseInterpolationSampling parses a InterpolationSampling from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/interpolation_sampling.h.tmpl b/src/tint/builtin/interpolation_sampling.h.tmpl
index 24b3b02..eacc2a1 100644
--- a/src/tint/builtin/interpolation_sampling.h.tmpl
+++ b/src/tint/builtin/interpolation_sampling.h.tmpl
@@ -13,9 +13,10 @@
 #ifndef SRC_TINT_BUILTIN_INTERPOLATION_SAMPLING_H_
 #define SRC_TINT_BUILTIN_INTERPOLATION_SAMPLING_H_
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The interpolation sampling.
diff --git a/src/tint/builtin/interpolation_type.cc b/src/tint/builtin/interpolation_type.cc
index ee3f68b..349d3e9 100644
--- a/src/tint/builtin/interpolation_type.cc
+++ b/src/tint/builtin/interpolation_type.cc
@@ -42,7 +42,7 @@
     return InterpolationType::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, InterpolationType value) {
+utils::StringStream& operator<<(utils::StringStream& out, InterpolationType value) {
     switch (value) {
         case InterpolationType::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/interpolation_type.h b/src/tint/builtin/interpolation_type.h
index ee54964..bd32923 100644
--- a/src/tint/builtin/interpolation_type.h
+++ b/src/tint/builtin/interpolation_type.h
@@ -23,9 +23,10 @@
 #ifndef SRC_TINT_BUILTIN_INTERPOLATION_TYPE_H_
 #define SRC_TINT_BUILTIN_INTERPOLATION_TYPE_H_
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The interpolation type.
@@ -36,10 +37,10 @@
     kPerspective,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the InterpolationType
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, InterpolationType value);
+utils::StringStream& operator<<(utils::StringStream& out, InterpolationType value);
 
 /// ParseInterpolationType parses a InterpolationType from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/interpolation_type.h.tmpl b/src/tint/builtin/interpolation_type.h.tmpl
index c869a96..7114fd1 100644
--- a/src/tint/builtin/interpolation_type.h.tmpl
+++ b/src/tint/builtin/interpolation_type.h.tmpl
@@ -13,9 +13,10 @@
 #ifndef SRC_TINT_BUILTIN_INTERPOLATION_TYPE_H_
 #define SRC_TINT_BUILTIN_INTERPOLATION_TYPE_H_
 
-#include <ostream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::builtin {
 
 /// The interpolation type.
diff --git a/src/tint/builtin/texel_format.cc b/src/tint/builtin/texel_format.cc
index dc4bbfe..4e1b8b7 100644
--- a/src/tint/builtin/texel_format.cc
+++ b/src/tint/builtin/texel_format.cc
@@ -82,7 +82,7 @@
     return TexelFormat::kUndefined;
 }
 
-std::ostream& operator<<(std::ostream& out, TexelFormat value) {
+utils::StringStream& operator<<(utils::StringStream& out, TexelFormat value) {
     switch (value) {
         case TexelFormat::kUndefined:
             return out << "undefined";
diff --git a/src/tint/builtin/texel_format.h b/src/tint/builtin/texel_format.h
index 9611e62..6643523 100644
--- a/src/tint/builtin/texel_format.h
+++ b/src/tint/builtin/texel_format.h
@@ -23,7 +23,7 @@
 #ifndef SRC_TINT_BUILTIN_TEXEL_FORMAT_H_
 #define SRC_TINT_BUILTIN_TEXEL_FORMAT_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
@@ -49,10 +49,10 @@
     kRgba8Unorm,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the TexelFormat
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, TexelFormat value);
+utils::StringStream& operator<<(utils::StringStream& out, TexelFormat value);
 
 /// ParseTexelFormat parses a TexelFormat from a string.
 /// @param str the string to parse
diff --git a/src/tint/builtin/texel_format.h.tmpl b/src/tint/builtin/texel_format.h.tmpl
index a3a9e31..b00e1f8 100644
--- a/src/tint/builtin/texel_format.h.tmpl
+++ b/src/tint/builtin/texel_format.h.tmpl
@@ -14,7 +14,7 @@
 #ifndef SRC_TINT_BUILTIN_TEXEL_FORMAT_H_
 #define SRC_TINT_BUILTIN_TEXEL_FORMAT_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
diff --git a/src/tint/clone_context.h b/src/tint/clone_context.h
index 188ee25..8e0e2ff 100644
--- a/src/tint/clone_context.h
+++ b/src/tint/clone_context.h
@@ -359,7 +359,7 @@
     CloneContext& Replace(const WHAT* what, const WITH* with) {
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, with);
-        replacements_.Add(what, [with]() -> const Cloneable* { return with; });
+        replacements_.Replace(what, [with]() -> const Cloneable* { return with; });
         return *this;
     }
 
@@ -379,7 +379,7 @@
     template <typename WHAT, typename WITH, typename = std::invoke_result_t<WITH>>
     CloneContext& Replace(const WHAT* what, WITH&& with) {
         TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
-        replacements_.Add(what, with);
+        replacements_.Replace(what, with);
         return *this;
     }
 
diff --git a/src/tint/clone_context_test.cc b/src/tint/clone_context_test.cc
index f2ed247..59c47c0 100644
--- a/src/tint/clone_context_test.cc
+++ b/src/tint/clone_context_test.cc
@@ -339,6 +339,45 @@
     EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
 }
 
+TEST_F(CloneContextNodeTest, CloneWithRepeatedImmediateReplacePointer) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    CloneContext ctx(&cloned, &original);
+
+    // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
+    // node.
+
+    auto* replacement_x =
+        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_x);
+
+    auto* replacement_y =
+        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_y);
+
+    auto* replacement_z =
+        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, replacement_z);
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement_z);
+    EXPECT_EQ(cloned_root->b, replacement_z);
+    EXPECT_NE(cloned_root->c, replacement_z);
+
+    EXPECT_EQ(replacement_z->a, replacement_y);
+    EXPECT_EQ(replacement_y->a, replacement_x);
+}
+
 TEST_F(CloneContextNodeTest, CloneWithReplaceFunction) {
     Allocator a;
 
@@ -371,6 +410,45 @@
     EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
 }
 
+TEST_F(CloneContextNodeTest, CloneWithRepeatedImmediateReplaceFunction) {
+    Allocator a;
+
+    ProgramBuilder builder;
+    auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+    original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+    original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+    original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+    Program original(std::move(builder));
+
+    ProgramBuilder cloned;
+
+    CloneContext ctx(&cloned, &original);
+
+    // Demonstrate that ctx.Replace() can be called multiple times to update the replacement of a
+    // node.
+
+    Node* replacement_x =
+        a.Create<Node>(cloned.Symbols().New("replacement_x"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_x; });
+
+    Node* replacement_y =
+        a.Create<Node>(cloned.Symbols().New("replacement_y"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_y; });
+
+    Node* replacement_z =
+        a.Create<Node>(cloned.Symbols().New("replacement_z"), ctx.Clone(original_root->b));
+    ctx.Replace(original_root->b, [&] { return replacement_z; });
+
+    auto* cloned_root = ctx.Clone(original_root);
+
+    EXPECT_NE(cloned_root->a, replacement_z);
+    EXPECT_EQ(cloned_root->b, replacement_z);
+    EXPECT_NE(cloned_root->c, replacement_z);
+
+    EXPECT_EQ(replacement_z->a, replacement_y);
+    EXPECT_EQ(replacement_y->a, replacement_x);
+}
+
 TEST_F(CloneContextNodeTest, CloneWithRemove) {
     Allocator a;
 
diff --git a/src/tint/cmd/helper.cc b/src/tint/cmd/helper.cc
index f3760da..c31178d 100644
--- a/src/tint/cmd/helper.cc
+++ b/src/tint/cmd/helper.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/cmd/helper.h"
 
+#include <iostream>
 #include <utility>
 #include <vector>
 
diff --git a/src/tint/cmd/info.cc b/src/tint/cmd/info.cc
index 6c244ad..c31095c 100644
--- a/src/tint/cmd/info.cc
+++ b/src/tint/cmd/info.cc
@@ -13,6 +13,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <iostream>
+
 #if TINT_BUILD_SPV_READER
 #include "spirv-tools/libspirv.hpp"
 #endif  // TINT_BUILD_SPV_READER
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index 12d5b8c..0b2aad2 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -79,6 +79,7 @@
     bool print_hash = false;
     bool demangle = false;
     bool dump_inspector_bindings = false;
+    bool enable_robustness = false;
 
     std::unordered_set<uint32_t> skip_hash;
 
@@ -533,6 +534,7 @@
 #if TINT_BUILD_SPV_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::writer::spirv::Options gen_options;
+    gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.generate_external_texture_bindings = true;
     auto result = tint::writer::spirv::Generate(program, gen_options);
@@ -639,6 +641,7 @@
 
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::writer::msl::Options gen_options;
+    gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.generate_external_texture_bindings = true;
     auto result = tint::writer::msl::Generate(input_program, gen_options);
@@ -699,6 +702,7 @@
 #if TINT_BUILD_HLSL_WRITER
     // TODO(jrprice): Provide a way for the user to set non-default options.
     tint::writer::hlsl::Options gen_options;
+    gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.generate_external_texture_bindings = true;
     gen_options.root_constant_binding_point = options.hlsl_root_constant_binding_point;
@@ -838,6 +842,7 @@
 
     auto generate = [&](const tint::Program* prg, const std::string entry_point_name) -> bool {
         tint::writer::glsl::Options gen_options;
+        gen_options.disable_robustness = !options.enable_robustness;
         gen_options.generate_external_texture_bindings = true;
         auto result = tint::writer::glsl::Generate(prg, gen_options, entry_point_name);
         if (!result.success) {
@@ -946,8 +951,9 @@
              return true;
          }},
         {"robustness",
-         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap&) {
-             m.Add<tint::transform::Robustness>();
+         [&](tint::inspector::Inspector&, tint::transform::Manager&,
+             tint::transform::DataMap&) {  // enabled via writer option
+             options.enable_robustness = true;
              return true;
          }},
         {"substitute_override",
diff --git a/src/tint/diagnostic/formatter.cc b/src/tint/diagnostic/formatter.cc
index 18520fc..9752838 100644
--- a/src/tint/diagnostic/formatter.cc
+++ b/src/tint/diagnostic/formatter.cc
@@ -16,6 +16,7 @@
 
 #include <algorithm>
 #include <iterator>
+#include <utility>
 #include <vector>
 
 #include "src/tint/diagnostic/diagnostic.h"
@@ -85,8 +86,8 @@
     /// @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<<(const T& msg) {
-        stream << msg;
+    State& operator<<(T&& msg) {
+        stream << std::forward<T>(msg);
         return *this;
     }
 
diff --git a/src/tint/diagnostic/printer.h b/src/tint/diagnostic/printer.h
index 9e4ce7c..5e7752c 100644
--- a/src/tint/diagnostic/printer.h
+++ b/src/tint/diagnostic/printer.h
@@ -16,7 +16,6 @@
 #define SRC_TINT_DIAGNOSTIC_PRINTER_H_
 
 #include <memory>
-#include <sstream>
 #include <string>
 
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
index 11cdf76..c4f1fd1 100644
--- a/src/tint/fuzzers/tint_common_fuzzer.cc
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -17,6 +17,7 @@
 #include <cassert>
 #include <cstring>
 #include <fstream>
+#include <iostream>
 #include <memory>
 #include <sstream>
 #include <string>
diff --git a/src/tint/fuzzers/tint_reader_writer_fuzzer.h b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
index 0104c9d..b5d0a95 100644
--- a/src/tint/fuzzers/tint_reader_writer_fuzzer.h
+++ b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
@@ -43,16 +43,14 @@
         CommonFuzzer::SetTransformManager(tm, inputs);
     }
 
-    /// Pass through to the CommonFuzzer implementation, but will setup a
-    /// robustness transform, if no other transforms have been set.
-    /// @param data buffer of data that will interpreted as a byte array or string
-    ///             depending on the shader input format.
+    /// Pass through to the CommonFuzzer implementation.
+    /// @param data buffer of data that will interpreted as a byte array or string depending on the
+    /// shader input format.
     /// @param size number of elements in buffer
     /// @returns 0, this is what libFuzzer expects
     int Run(const uint8_t* data, size_t size) {
         if (!tm_set_) {
             tb_ = std::make_unique<TransformBuilder>(data, size);
-            tb_->AddTransform<tint::transform::Robustness>();
             SetTransformManager(tb_->manager(), tb_->data_map());
         }
 
diff --git a/src/tint/fuzzers/tint_robustness_fuzzer.cc b/src/tint/fuzzers/tint_robustness_fuzzer.cc
index dfc9a03..4b5eaa6 100644
--- a/src/tint/fuzzers/tint_robustness_fuzzer.cc
+++ b/src/tint/fuzzers/tint_robustness_fuzzer.cc
@@ -15,6 +15,7 @@
 #include "src/tint/fuzzers/fuzzer_init.h"
 #include "src/tint/fuzzers/tint_common_fuzzer.h"
 #include "src/tint/fuzzers/transform_builder.h"
+#include "src/tint/transform/robustness.h"
 
 namespace tint::fuzzers {
 
diff --git a/src/tint/fuzzers/transform_builder.h b/src/tint/fuzzers/transform_builder.h
index 73b5b51..787abb9 100644
--- a/src/tint/fuzzers/transform_builder.h
+++ b/src/tint/fuzzers/transform_builder.h
@@ -22,6 +22,7 @@
 
 #include "src/tint/fuzzers/data_builder.h"
 #include "src/tint/fuzzers/shuffle_transform.h"
+#include "src/tint/transform/robustness.h"
 
 namespace tint::fuzzers {
 
@@ -61,7 +62,6 @@
     /// Helper that invokes Add*Transform for all of the platform independent
     /// passes.
     void AddPlatformIndependentPasses() {
-        AddTransform<transform::Robustness>();
         AddTransform<transform::FirstIndexOffset>();
         AddTransform<transform::BindingRemapper>();
         AddTransform<transform::Renamer>();
diff --git a/src/tint/inspector/test_inspector_builder.cc b/src/tint/inspector/test_inspector_builder.cc
index ad576b9..9f92d6d 100644
--- a/src/tint/inspector/test_inspector_builder.cc
+++ b/src/tint/inspector/test_inspector_builder.cc
@@ -273,7 +273,11 @@
         case type::TextureDimension::kCubeArray:
             return ty.vec3(scalar);
         default:
-            [=]() { FAIL() << "Unsupported texture dimension: " << dim; }();
+            [=]() {
+                utils::StringStream str;
+                str << dim;
+                FAIL() << "Unsupported texture dimension: " << str.str();
+            }();
     }
     return ast::Type{};
 }
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 7a5334f..45d77c8 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -544,8 +544,8 @@
 @must_use @const fn abs<N: num, T: fia_fiu32_f16>(vec<N, T>) -> vec<N, T>
 @must_use @const fn acos<T: fa_f32_f16>(@test_value(0.96891242171) T) -> T
 @must_use @const fn acos<N: num, T: fa_f32_f16>(@test_value(0.96891242171) vec<N, T>) -> vec<N, T>
-@must_use @const fn acosh<T: fa_f32_f16>(@test_value(2.0) T) -> T
-@must_use @const fn acosh<N: num, T: fa_f32_f16>(@test_value(2.0) vec<N, T>) -> vec<N, T>
+@must_use @const fn acosh<T: fa_f32_f16>(@test_value(1.5430806348) T) -> T
+@must_use @const fn acosh<N: num, T: fa_f32_f16>(@test_value(1.5430806348) vec<N, T>) -> vec<N, T>
 @must_use @const fn all(bool) -> bool
 @must_use @const fn all<N: num>(vec<N, bool>) -> bool
 @must_use @const fn any(bool) -> bool
@@ -658,8 +658,8 @@
 @must_use @const fn refract<N: num, T: fa_f32_f16>(vec<N, T>, vec<N, T>, T) -> vec<N, T>
 @must_use @const fn reverseBits<T: iu32>(T) -> T
 @must_use @const fn reverseBits<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
-@must_use @const fn round<T: fa_f32_f16>(@test_value(3.4) T) -> T
-@must_use @const fn round<N: num, T: fa_f32_f16>(@test_value(3.4) vec<N, T>) -> vec<N, T>
+@must_use @const fn round<T: fa_f32_f16>(@test_value(3.5) T) -> T
+@must_use @const fn round<N: num, T: fa_f32_f16>(@test_value(3.5) vec<N, T>) -> vec<N, T>
 @must_use @const fn saturate<T: fa_f32_f16>(@test_value(2) T) -> T
 @must_use @const fn saturate<T: fa_f32_f16, N: num>(@test_value(2) vec<N, T>) -> vec<N, T>
 @must_use @const("select_bool") fn select<T: scalar>(T, T, bool) -> T
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index be5d243..063bc93 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_BINARY_H_
 #define SRC_TINT_IR_BINARY_H_
 
-#include <ostream>
-
 #include "src/tint/castable.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index 103719c..9f4ba19 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <sstream>
-
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index 16d62f8..0178066 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_BITCAST_H_
 #define SRC_TINT_IR_BITCAST_H_
 
-#include <ostream>
-
 #include "src/tint/castable.h"
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index d190abb..38ae9b2 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <sstream>
-
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index b70c059..9af481b 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -1836,8 +1836,8 @@
     EXPECT_EQ(d.AsString(), R"(%1 (u32) = 3 >> 4
 %2 (u32) = %1 (u32) + 9
 %3 (bool) = 1 < %2 (u32)
-%4 (f32) = 2.299999952 * 5.5
-%5 (f32) = 6.699999809 / %4 (f32)
+%4 (f32) = 2.29999995231628417969 * 5.5
+%5 (f32) = 6.69999980926513671875 / %4 (f32)
 %6 (bool) = 2.5 > %5 (f32)
 %7 (bool) = %3 (bool) && %6 (bool)
 )");
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index e7d66a4..1c4088f 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_CONSTANT_H_
 #define SRC_TINT_IR_CONSTANT_H_
 
-#include <ostream>
-
 #include "src/tint/constant/value.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
diff --git a/src/tint/ir/constant_test.cc b/src/tint/ir/constant_test.cc
index 745ec54..f3a1dc0 100644
--- a/src/tint/ir/constant_test.cc
+++ b/src/tint/ir/constant_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <sstream>
-
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/utils/string_stream.h"
@@ -34,7 +32,7 @@
     EXPECT_EQ(1.2_f, c->value->As<constant::Scalar<f32>>()->ValueAs<f32>());
 
     c->ToString(str, b.builder.ir.symbols);
-    EXPECT_EQ("1.200000048", str.str());
+    EXPECT_EQ("1.20000004768371582031", str.str());
 
     EXPECT_TRUE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
index b541236..290aaeb8 100644
--- a/src/tint/ir/debug.cc
+++ b/src/tint/ir/debug.cc
@@ -14,7 +14,6 @@
 
 #include "src/tint/ir/debug.h"
 
-#include <sstream>
 #include <unordered_map>
 #include <unordered_set>
 
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index 1ed822c..eee6b76 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_IR_DISASSEMBLER_H_
 #define SRC_TINT_IR_DISASSEMBLER_H_
 
-#include <sstream>
 #include <string>
 #include <unordered_map>
 #include <unordered_set>
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index abd5179..9d09fcb 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_INSTRUCTION_H_
 #define SRC_TINT_IR_INSTRUCTION_H_
 
-#include <ostream>
-
 #include "src/tint/castable.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
diff --git a/src/tint/ir/temp.h b/src/tint/ir/temp.h
index 989e416..8532a45 100644
--- a/src/tint/ir/temp.h
+++ b/src/tint/ir/temp.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_TEMP_H_
 #define SRC_TINT_IR_TEMP_H_
 
-#include <ostream>
-
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/ir/temp_test.cc b/src/tint/ir/temp_test.cc
index 1a33769..e73dd34 100644
--- a/src/tint/ir/temp_test.cc
+++ b/src/tint/ir/temp_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <sstream>
-
 #include "src/tint/ir/temp.h"
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index 983eae0..b3ec7ef 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -15,8 +15,6 @@
 #ifndef SRC_TINT_IR_VALUE_H_
 #define SRC_TINT_IR_VALUE_H_
 
-#include <ostream>
-
 #include "src/tint/castable.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
diff --git a/src/tint/number.cc b/src/tint/number.cc
index 629091b..8b85670 100644
--- a/src/tint/number.cc
+++ b/src/tint/number.cc
@@ -17,10 +17,10 @@
 #include <algorithm>
 #include <cmath>
 #include <cstring>
-#include <ostream>
 
 #include "src/tint/debug.h"
 #include "src/tint/utils/bitcast.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint {
 namespace {
@@ -50,7 +50,7 @@
 
 }  // namespace
 
-std::ostream& operator<<(std::ostream& out, ConversionFailure failure) {
+utils::StringStream& operator<<(utils::StringStream& out, ConversionFailure failure) {
     switch (failure) {
         case ConversionFailure::kExceedsPositiveLimit:
             return out << "value exceeds positive limit for type";
diff --git a/src/tint/number.h b/src/tint/number.h
index 82a9963..69df284 100644
--- a/src/tint/number.h
+++ b/src/tint/number.h
@@ -20,11 +20,11 @@
 #include <functional>
 #include <limits>
 #include <optional>
-#include <ostream>
 
 #include "src/tint/traits.h"
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/result.h"
+#include "src/tint/utils/string_stream.h"
 
 // Forward declaration
 namespace tint {
@@ -175,11 +175,11 @@
 };
 
 /// Writes the number to the ostream.
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param num the Number
-/// @return the std::ostream so calls can be chained
+/// @return the stream so calls can be chained
 template <typename T>
-inline std::ostream& operator<<(std::ostream& out, Number<T> num) {
+inline utils::StringStream& operator<<(utils::StringStream& out, Number<T> num) {
     return out << num.value;
 }
 
@@ -314,10 +314,10 @@
 };
 
 /// Writes the conversion failure message to the ostream.
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param failure the ConversionFailure
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, ConversionFailure failure);
+/// @return the stream so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, ConversionFailure failure);
 
 /// Converts a number from one type to another, checking that the value fits in the target type.
 /// @returns the resulting value of the conversion, or a failure reason.
diff --git a/src/tint/program_id.h b/src/tint/program_id.h
index c018543..4169ba1 100644
--- a/src/tint/program_id.h
+++ b/src/tint/program_id.h
@@ -16,10 +16,10 @@
 #define SRC_TINT_PROGRAM_ID_H_
 
 #include <stdint.h>
-#include <iostream>
 #include <utility>
 
 #include "src/tint/debug.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint {
 
@@ -71,11 +71,11 @@
     return id;
 }
 
-/// Writes the ProgramID to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the ProgramID to the stream.
+/// @param out the stream to write to
 /// @param id the program identifier to write
 /// @returns out so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, ProgramID id) {
+inline utils::StringStream& operator<<(utils::StringStream& out, ProgramID id) {
     out << "Program<" << id.Value() << ">";
     return out;
 }
diff --git a/src/tint/reader/spirv/construct.h b/src/tint/reader/spirv/construct.h
index 06ae450..7377d83 100644
--- a/src/tint/reader/spirv/construct.h
+++ b/src/tint/reader/spirv/construct.h
@@ -16,7 +16,6 @@
 #define SRC_TINT_READER_SPIRV_CONSTRUCT_H_
 
 #include <memory>
-#include <sstream>
 #include <string>
 
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/reader/spirv/fail_stream.h b/src/tint/reader/spirv/fail_stream.h
index 6530eae..0a154f7 100644
--- a/src/tint/reader/spirv/fail_stream.h
+++ b/src/tint/reader/spirv/fail_stream.h
@@ -19,7 +19,7 @@
 
 namespace tint::reader::spirv {
 
-/// A FailStream object accumulates values onto a given std::ostream,
+/// A FailStream object accumulates values onto a given stream,
 /// and can be used to record failure by writing the false value
 /// to given a pointer-to-bool.
 class FailStream {
diff --git a/src/tint/reader/spirv/function.h b/src/tint/reader/spirv/function.h
index a07309d..718e8e3 100644
--- a/src/tint/reader/spirv/function.h
+++ b/src/tint/reader/spirv/function.h
@@ -28,6 +28,7 @@
 #include "src/tint/reader/spirv/attributes.h"
 #include "src/tint/reader/spirv/construct.h"
 #include "src/tint/reader/spirv/parser_impl.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 
@@ -178,11 +179,11 @@
     utils::Vector<uint32_t, 4> phis_needing_state_vars;
 };
 
-/// Writes the BlockInfo to the ostream
-/// @param o the ostream
+/// Writes the BlockInfo to the stream
+/// @param o the stream
 /// @param bi the BlockInfo
-/// @returns the ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& o, const BlockInfo& bi) {
+/// @returns the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& o, const BlockInfo& bi) {
     o << "BlockInfo{"
       << " id: " << bi.id << " pos: " << bi.pos << " merge_for_header: " << bi.merge_for_header
       << " continue_for_header: " << bi.continue_for_header
@@ -353,11 +354,11 @@
     SkipReason skip = SkipReason::kDontSkip;
 };
 
-/// Writes the DefInfo to the ostream
-/// @param o the ostream
+/// Writes the DefInfo to the stream
+/// @param o the stream
 /// @param di the DefInfo
-/// @returns the ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& o, const DefInfo& di) {
+/// @returns the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& o, const DefInfo& di) {
     o << "DefInfo{"
       << " inst.result_id: " << di.inst.result_id();
     if (di.local.has_value()) {
diff --git a/src/tint/reader/spirv/function_cfg_test.cc b/src/tint/reader/spirv/function_cfg_test.cc
index f4f1ce5..36d085a 100644
--- a/src/tint/reader/spirv/function_cfg_test.cc
+++ b/src/tint/reader/spirv/function_cfg_test.cc
@@ -2598,10 +2598,11 @@
     fe.ComputeBlockOrderAndPositions();
     fe.RegisterMerges();
     EXPECT_FALSE(fe.VerifyHeaderContinueMergeOrder());
+
+    utils::StringStream result;
+    result << *fe.GetBlockInfo(50) << std::endl << *fe.GetBlockInfo(20) << std::endl;
     EXPECT_THAT(p->error(), Eq("Header 50 does not strictly dominate its merge block 20"))
-        << *fe.GetBlockInfo(50) << std::endl
-        << *fe.GetBlockInfo(20) << std::endl
-        << Dump(fe.block_order());
+        << result.str() << Dump(fe.block_order());
 }
 
 TEST_F(SpvParserCFGTest,
@@ -2634,10 +2635,10 @@
     fe.ComputeBlockOrderAndPositions();
     fe.RegisterMerges();
     EXPECT_FALSE(fe.VerifyHeaderContinueMergeOrder());
+    utils::StringStream str;
+    str << *fe.GetBlockInfo(50) << std::endl << *fe.GetBlockInfo(20) << std::endl;
     EXPECT_THAT(p->error(), Eq("Loop header 50 does not dominate its continue target 20"))
-        << *fe.GetBlockInfo(50) << std::endl
-        << *fe.GetBlockInfo(20) << std::endl
-        << Dump(fe.block_order());
+        << str.str() << Dump(fe.block_order());
 }
 
 TEST_F(SpvParserCFGTest, VerifyHeaderContinueMergeOrder_MergeInsideContinueTarget) {
@@ -2752,10 +2753,15 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 2u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,4) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,3) begin_id:10 end_id:99 depth:1 parent:Function@10 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -2798,10 +2804,15 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 2u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:5 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [1,4) begin_id:10 end_id:99 depth:1 parent:Function@5 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(5)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -2842,10 +2853,15 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 2u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ SwitchSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -2879,12 +2895,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 2u);
+
+    utils::StringStream str;
+    str << constructs;
+
     // A single-block loop consists *only* of a continue target with one block in
     // it.
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,3) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [1,2) begin_id:20 end_id:99 depth:1 parent:Function@10 in-c:Continue@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -2925,11 +2946,16 @@
     fe.RegisterMerges();
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [3,5) begin_id:40 end_id:99 depth:1 parent:Function@10 in-c:Continue@40 }
   Construct{ Loop [1,3) begin_id:20 end_id:40 depth:1 parent:Function@10 scope:[1,5) in-l:Loop@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -2973,10 +2999,14 @@
     fe.RegisterMerges();
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [1,5) begin_id:20 end_id:99 depth:1 parent:Function@10 in-c:Continue@20 }
-})")) << constructs;
+})")) << str.str();
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -3020,13 +3050,18 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 3u);
+
+    utils::StringStream str;
+    str << constructs;
+
     // A single-block loop consists *only* of a continue target with one block in
     // it.
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,4) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,2) begin_id:10 end_id:50 depth:1 parent:Function@10 }
   Construct{ Continue [2,3) begin_id:50 end_id:99 depth:1 parent:Function@10 in-c:Continue@50 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -3068,12 +3103,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,2) begin_id:10 end_id:50 depth:1 parent:Function@10 }
   Construct{ Continue [3,4) begin_id:60 end_id:99 depth:1 parent:Function@10 in-c:Continue@60 }
   Construct{ Loop [2,3) begin_id:50 end_id:60 depth:1 parent:Function@10 scope:[2,4) in-l:Loop@50 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[1].get());
@@ -3127,12 +3167,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,9) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,8) begin_id:10 end_id:99 depth:1 parent:Function@10 }
   Construct{ IfSelection [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 }
   Construct{ IfSelection [5,7) begin_id:50 end_id:89 depth:2 parent:IfSelection@10 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3187,13 +3232,18 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     // The ordering among siblings depends on the computed block order.
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ SwitchSelection [0,7) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
   Construct{ IfSelection [1,3) begin_id:50 end_id:89 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
   Construct{ IfSelection [4,6) begin_id:20 end_id:49 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[3].get());
@@ -3237,11 +3287,16 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 3u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 }
   Construct{ SwitchSelection [1,3) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 in-c-l-s:SwitchSelection@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3291,12 +3346,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [4,6) begin_id:50 end_id:89 depth:1 parent:Function@10 in-c:Continue@50 }
   Construct{ Loop [1,4) begin_id:20 end_id:50 depth:1 parent:Function@10 scope:[1,6) in-l:Loop@20 }
   Construct{ Continue [2,3) begin_id:30 end_id:40 depth:2 parent:Loop@20 in-l:Loop@20 in-c:Continue@30 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3346,12 +3406,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [5,6) begin_id:80 end_id:99 depth:1 parent:Function@10 in-c:Continue@80 }
   Construct{ Loop [1,5) begin_id:20 end_id:80 depth:1 parent:Function@10 scope:[1,6) in-l:Loop@20 }
   Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Loop@20 in-l:Loop@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3397,12 +3462,16 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [2,5) begin_id:30 end_id:99 depth:1 parent:Function@10 in-c:Continue@30 }
   Construct{ Loop [1,2) begin_id:20 end_id:30 depth:1 parent:Function@10 scope:[1,5) in-l:Loop@20 }
   Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Continue@30 in-c:Continue@30 }
-})")) << constructs;
+})")) << str.str();
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3441,11 +3510,15 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 3u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,4) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,3) begin_id:10 end_id:99 depth:1 parent:Function@10 }
   Construct{ Continue [1,2) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 in-c:Continue@20 }
-})")) << constructs;
+})")) << str.str();
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[2].get());
@@ -3490,12 +3563,17 @@
     EXPECT_TRUE(fe.LabelControlFlowConstructs());
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,6) begin_id:10 end_id:99 depth:1 parent:Function@10 }
   Construct{ Continue [3,5) begin_id:40 end_id:89 depth:2 parent:IfSelection@10 in-c:Continue@40 }
   Construct{ Loop [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 scope:[1,5) in-l:Loop@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[3].get());
@@ -3540,12 +3618,17 @@
     ASSERT_TRUE(FlowLabelControlFlowConstructs(&fe)) << p->error();
     const auto& constructs = fe.constructs();
     EXPECT_EQ(constructs.Length(), 4u);
+
+    utils::StringStream str;
+    str << constructs;
+
     ASSERT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [4,5) begin_id:90 end_id:99 depth:1 parent:Function@10 in-c:Continue@90 }
   Construct{ Loop [1,4) begin_id:20 end_id:90 depth:1 parent:Function@10 scope:[1,5) in-l:Loop@20 }
   Construct{ IfSelection [1,4) begin_id:20 end_id:90 depth:2 parent:Loop@20 in-l:Loop@20 }
-})")) << constructs;
+})")) << str.str();
+
     // The block records the nearest enclosing construct.
     EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
     EXPECT_EQ(fe.GetBlockInfo(20)->construct, constructs[3].get());
diff --git a/src/tint/reader/spirv/function_logical_test.cc b/src/tint/reader/spirv/function_logical_test.cc
index a9ccf7d..2d362c4 100644
--- a/src/tint/reader/spirv/function_logical_test.cc
+++ b/src/tint/reader/spirv/function_logical_test.cc
@@ -532,9 +532,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = "
-                          "!((vec2<f32>(50.0f, 60.0f) != vec2<f32>(60.0f, 50.0f)));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) != vec2<f32>(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordNotEqual_Scalar) {
@@ -567,9 +568,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = "
-                          "!((vec2<f32>(50.0f, 60.0f) == vec2<f32>(60.0f, 50.0f)));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) == vec2<f32>(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThan_Scalar) {
@@ -602,9 +604,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = "
-                          "!((vec2<f32>(50.0f, 60.0f) >= vec2<f32>(60.0f, 50.0f)));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) >= vec2<f32>(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordLessThanEqual_Scalar) {
@@ -637,9 +640,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = "
-                          "!((vec2<f32>(50.0f, 60.0f) > vec2<f32>(60.0f, 50.0f)));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) > vec2<f32>(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThan_Scalar) {
@@ -672,9 +676,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = "
-                          "!((vec2<f32>(50.0f, 60.0f) <= vec2<f32>(60.0f, 50.0f)));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) <= vec2<f32>(60.0f, 50.0f)));"));
 }
 
 TEST_F(SpvFUnordTest, FUnordGreaterThanEqual_Scalar) {
@@ -707,10 +712,10 @@
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
-    EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr("let x_1 : vec2<bool> = !(("
-                          "vec2<f32>(50.0f, 60.0f) < vec2<f32>(60.0f, 50.0f)"
-                          "));"));
+    EXPECT_THAT(
+        test::ToString(p->program(), ast_body),
+        HasSubstr(
+            "let x_1 : vec2<bool> = !((vec2<f32>(50.0f, 60.0f) < vec2<f32>(60.0f, 50.0f)));"));
 }
 
 using SpvLogicalTest = SpvParserTestBase<::testing::Test>;
diff --git a/src/tint/reader/spirv/namer.cc b/src/tint/reader/spirv/namer.cc
index 378e133..6c73a34 100644
--- a/src/tint/reader/spirv/namer.cc
+++ b/src/tint/reader/spirv/namer.cc
@@ -15,7 +15,6 @@
 #include "src/tint/reader/spirv/namer.h"
 
 #include <algorithm>
-#include <sstream>
 #include <unordered_set>
 
 #include "src/tint/debug.h"
diff --git a/src/tint/reader/spirv/parser_impl_handle_test.cc b/src/tint/reader/spirv/parser_impl_handle_test.cc
index 17e6ed6..39d5498 100644
--- a/src/tint/reader/spirv/parser_impl_handle_test.cc
+++ b/src/tint/reader/spirv/parser_impl_handle_test.cc
@@ -1645,7 +1645,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
-                        "textureGatherCompare(x_20, x_10, coords12, 0.200000003f)"},
+                        "textureGatherCompare(x_20, x_10, coords12, 0.20000000298023223877f)"},
         // OpImageDrefGather 2DDepth ConstOffset signed
         ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1653,7 +1653,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
-                        "textureGatherCompare(x_20, x_10, coords12, 0.200000003f, "
+                        "textureGatherCompare(x_20, x_10, coords12, 0.20000000298023223877f, "
                         "vec2<i32>(3i, 4i))"},
         // OpImageDrefGather 2DDepth ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
@@ -1663,7 +1663,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;)",
-                        "textureGatherCompare(x_20, x_10, coords12, 0.200000003f, "
+                        "textureGatherCompare(x_20, x_10, coords12, 0.20000000298023223877f, "
                         "vec2<i32>(vec2<u32>(3u, 4u)))"},
         // OpImageDrefGather 2DDepth Array
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
@@ -1673,7 +1673,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGatherCompare(x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), 0.200000003f)"},
+                        "i32(round(coords123.z)), 0.20000000298023223877f)"},
         // OpImageDrefGather 2DDepth Array ConstOffset signed
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1682,7 +1682,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGatherCompare(x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), 0.200000003f, vec2<i32>(3i, 4i))"},
+                        "i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i))"},
         // OpImageDrefGather 2DDepth Array ConstOffset unsigned
         ImageAccessCase{"%float 2D 1 1 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1692,7 +1692,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
                         "textureGatherCompare(x_20, x_10, coords123.xy, "
-                        "i32(round(coords123.z)), 0.200000003f, "
+                        "i32(round(coords123.z)), 0.20000000298023223877f, "
                         "vec2<i32>(vec2<u32>(3u, 4u)))"},
         // OpImageDrefGather DepthCube
         ImageAccessCase{"%float Cube 1 0 0 1 Unknown",
@@ -1701,7 +1701,7 @@
                         R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_cube;)",
-                        "textureGatherCompare(x_20, x_10, coords123, 0.200000003f)"},
+                        "textureGatherCompare(x_20, x_10, coords123, 0.20000000298023223877f)"},
         // OpImageDrefGather DepthCube Array
         ImageAccessCase{"%float Cube 1 1 0 1 Unknown",
                         "%result = OpImageDrefGather "
@@ -1710,7 +1710,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_cube_array;)",
                         "textureGatherCompare(x_20, x_10, coords1234.xyz, "
-                        "i32(round(coords1234.w)), 0.200000003f)"}}));
+                        "i32(round(coords1234.w)), 0.20000000298023223877f)"}}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleImplicitLod,
@@ -1829,7 +1829,7 @@
 )",
                         R"(
   let x_200 : vec4<f32> = vec4<f32>(textureSample(x_20, x_10, coords12), 0.0f, 0.0f, 0.0f);
-  let x_210 : f32 = textureSampleCompare(x_20, x_30, coords12, 0.200000003f);
+  let x_210 : f32 = textureSampleCompare(x_20, x_30, coords12, 0.20000000298023223877f);
 )"}));
 
 INSTANTIATE_TEST_SUITE_P(
@@ -1844,7 +1844,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-                        R"(textureSampleCompare(x_20, x_10, coords12, 0.200000003f))"},
+                        R"(textureSampleCompare(x_20, x_10, coords12, 0.20000000298023223877f))"},
         // ImageSampleDrefImplicitLod - arrayed
         ImageAccessCase{
             "%float 2D 0 1 0 1 Unknown",
@@ -1853,7 +1853,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.200000003f))"},
+            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f))"},
         // ImageSampleDrefImplicitLod with ConstOffset
         ImageAccessCase{
             "%float 2D 0 0 0 1 Unknown",
@@ -1863,7 +1863,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompare(x_20, x_10, coords12, 0.200000003f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompare(x_20, x_10, coords12, 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
         // ImageSampleDrefImplicitLod arrayed with ConstOffset
         ImageAccessCase{
             "%float 2D 0 1 0 1 Unknown",
@@ -1872,7 +1872,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.200000003f, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleCompare(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i)))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleDrefExplicitLod,
@@ -1881,14 +1881,15 @@
     // Another test checks cases where the Lod is not float constant 0.
     ::testing::Values(
         // 2D
-        ImageAccessCase{"%float 2D 1 0 0 1 Unknown",
-                        "%result = OpImageSampleDrefExplicitLod "
-                        "%float %sampled_image %coords12 %depth Lod %float_0",
-                        R"(@group(0) @binding(0) var x_10 : sampler_comparison;
+        ImageAccessCase{
+            "%float 2D 1 0 0 1 Unknown",
+            "%result = OpImageSampleDrefExplicitLod "
+            "%float %sampled_image %coords12 %depth Lod %float_0",
+            R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-                        R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.200000003f))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.20000000298023223877f))"},
         // 2D array
         ImageAccessCase{
             "%float 2D 1 1 0 1 Unknown",
@@ -1897,7 +1898,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.200000003f))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f))"},
         // 2D, ConstOffset
         ImageAccessCase{
             "%float 2D 1 0 0 1 Unknown",
@@ -1908,7 +1909,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.200000003f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords12, 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
         // 2D array, ConstOffset
         ImageAccessCase{
             "%float 2D 1 1 0 1 Unknown",
@@ -1918,15 +1919,16 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_2d_array;)",
-            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.200000003f, vec2<i32>(3i, 4i)))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords123.xy, i32(round(coords123.z)), 0.20000000298023223877f, vec2<i32>(3i, 4i)))"},
         // Cube
-        ImageAccessCase{"%float Cube 1 0 0 1 Unknown",
-                        "%result = OpImageSampleDrefExplicitLod "
-                        "%float %sampled_image %coords123 %depth Lod %float_0",
-                        R"(@group(0) @binding(0) var x_10 : sampler_comparison;
+        ImageAccessCase{
+            "%float Cube 1 0 0 1 Unknown",
+            "%result = OpImageSampleDrefExplicitLod "
+            "%float %sampled_image %coords123 %depth Lod %float_0",
+            R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_cube;)",
-                        R"(textureSampleCompareLevel(x_20, x_10, coords123, 0.200000003f))"},
+            R"(textureSampleCompareLevel(x_20, x_10, coords123, 0.20000000298023223877f))"},
         // Cube array
         ImageAccessCase{
             "%float Cube 1 1 0 1 Unknown",
@@ -1935,7 +1937,7 @@
             R"(@group(0) @binding(0) var x_10 : sampler_comparison;
 
 @group(2) @binding(1) var x_20 : texture_depth_cube_array;)",
-            R"(textureSampleCompareLevel(x_20, x_10, coords1234.xyz, i32(round(coords1234.w)), 0.200000003f))"}));
+            R"(textureSampleCompareLevel(x_20, x_10, coords1234.xyz, i32(round(coords1234.w)), 0.20000000298023223877f))"}));
 
 INSTANTIATE_TEST_SUITE_P(
     ImageSampleExplicitLod_UsingLod,
@@ -2308,7 +2310,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompare(x_20, x_10, (coords123.xy / coords123.z), 0.200000003f, 0.0f))"},
+            R"(textureSampleCompare(x_20, x_10, (coords123.xy / coords123.z), 0.20000000298023223877f, 0.0f))"},
 
         // OpImageSampleProjDrefImplicitLod 2D depth-texture, Lod ConstOffset
         ImageAccessCase{
@@ -2320,7 +2322,7 @@
 
 @group(2) @binding(1) var x_20 : texture_depth_2d;
 )",
-            R"(textureSampleCompareLevel(x_20, x_10, (coords123.xy / coords123.z), 0.200000003f, 0.0f, vec2<i32>(3i, 4i)))"}));
+            R"(textureSampleCompareLevel(x_20, x_10, (coords123.xy / coords123.z), 0.20000000298023223877f, 0.0f, vec2<i32>(3i, 4i)))"}));
 
 /////
 // End projection sampling
diff --git a/src/tint/reader/spirv/usage.cc b/src/tint/reader/spirv/usage.cc
index 5256d4f..add944b 100644
--- a/src/tint/reader/spirv/usage.cc
+++ b/src/tint/reader/spirv/usage.cc
@@ -14,8 +14,6 @@
 
 #include "src/tint/reader/spirv/usage.h"
 
-#include <sstream>
-
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
@@ -24,7 +22,7 @@
 Usage::Usage(const Usage& other) = default;
 Usage::~Usage() = default;
 
-std::ostream& Usage::operator<<(std::ostream& out) const {
+utils::StringStream& Usage::operator<<(utils::StringStream& out) const {
     out << "Usage(";
     if (IsSampler()) {
         out << "Sampler(";
diff --git a/src/tint/reader/spirv/usage.h b/src/tint/reader/spirv/usage.h
index 4c2ccbb..63902ef 100644
--- a/src/tint/reader/spirv/usage.h
+++ b/src/tint/reader/spirv/usage.h
@@ -17,6 +17,8 @@
 
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::reader::spirv {
 
 /// Records the properties of a sampler or texture based on how it's used
@@ -73,7 +75,7 @@
     /// Emits this usage to the given stream
     /// @param out the output stream.
     /// @returns the modified stream.
-    std::ostream& operator<<(std::ostream& out) const;
+    utils::StringStream& operator<<(utils::StringStream& out) const;
 
     /// Equality operator
     /// @param other the RHS of the equality test.
@@ -122,11 +124,11 @@
     bool is_storage_write_ = false;
 };
 
-/// Writes the Usage to the ostream
-/// @param out the ostream
+/// Writes the Usage to the stream
+/// @param out the stream
 /// @param u the Usage
-/// @returns the ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, const Usage& u) {
+/// @returns the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& out, const Usage& u) {
     return u.operator<<(out);
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_expression_test.cc b/src/tint/reader/wgsl/parser_impl_expression_test.cc
index 9a6c917..000a24b 100644
--- a/src/tint/reader/wgsl/parser_impl_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_expression_test.cc
@@ -468,7 +468,7 @@
 static bool ParsedAsTemplateArgumentList(BinaryOperatorInfo lhs_op, BinaryOperatorInfo rhs_op) {
     return lhs_op.bit == kOpLt && rhs_op.bit & (kOpGt | kOpGe | kOpShr);
 }
-static std::ostream& operator<<(std::ostream& o, const Case& c) {
+static utils::StringStream& operator<<(utils::StringStream& o, const Case& c) {
     return o << "a " << c.lhs_op.symbol << " b " << c.rhs_op.symbol << " c ";
 }
 
diff --git a/src/tint/reader/wgsl/token.h b/src/tint/reader/wgsl/token.h
index 222f28e..43f6831 100644
--- a/src/tint/reader/wgsl/token.h
+++ b/src/tint/reader/wgsl/token.h
@@ -353,7 +353,7 @@
     std::variant<int64_t, double, std::string, std::string_view> value_;
 };
 
-inline std::ostream& operator<<(std::ostream& out, Token::Type type) {
+inline utils::StringStream& operator<<(utils::StringStream& out, Token::Type type) {
     out << Token::TypeToName(type);
     return out;
 }
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 0108e89..4e9e4bd 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -2070,7 +2070,7 @@
 namespace texture_builtin_tests {
 
 enum class Texture { kF32, kI32, kU32 };
-inline std::ostream& operator<<(std::ostream& out, Texture data) {
+inline utils::StringStream& operator<<(utils::StringStream& out, Texture data) {
     if (data == Texture::kF32) {
         out << "f32";
     } else if (data == Texture::kI32) {
@@ -2087,7 +2087,9 @@
     builtin::TexelFormat format = builtin::TexelFormat::kR32Float;
 };
 inline std::ostream& operator<<(std::ostream& out, TextureTestParams data) {
-    out << data.dim << "_" << data.type;
+    utils::StringStream str;
+    str << data.dim << "_" << data.type;
+    out << str.str();
     return out;
 }
 
@@ -2110,7 +2112,11 @@
             case type::TextureDimension::kCubeArray:
                 return ty.vec3(scalar);
             default:
-                [=]() { FAIL() << "Unsupported texture dimension: " << dim; }();
+                [=]() {
+                    utils::StringStream str;
+                    str << dim;
+                    FAIL() << "Unsupported texture dimension: " << str.str();
+                }();
         }
         return ast::Type{};
     }
@@ -2448,8 +2454,11 @@
 
     if (std::string(param.function) == "textureDimensions") {
         switch (param.texture_dimension) {
-            default:
-                FAIL() << "invalid texture dimensions: " << param.texture_dimension;
+            default: {
+                utils::StringStream str;
+                str << param.texture_dimension;
+                FAIL() << "invalid texture dimensions: " << str.str();
+            }
             case type::TextureDimension::k1d:
                 EXPECT_TRUE(TypeOf(call)->Is<type::U32>());
                 break;
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
index ff50a24..76441cd 100644
--- a/src/tint/resolver/const_eval_binary_op_test.cc
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -1607,7 +1607,7 @@
         "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558"
         "632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245"
         "490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168"
-        "738177180919299881250404026184124858368.000000000 cannot be represented as 'f32'");
+        "738177180919299881250404026184124858368.0 cannot be represented as 'f32'");
 }
 
 TEST_F(ResolverConstEvalTest, ShortCircuit_And_Error_Materialize) {
@@ -1658,7 +1658,7 @@
         "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558"
         "632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245"
         "490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168"
-        "738177180919299881250404026184124858368.000000000 cannot be represented as 'f32'");
+        "738177180919299881250404026184124858368.0 cannot be represented as 'f32'");
 }
 
 TEST_F(ResolverConstEvalTest, ShortCircuit_Or_Error_Materialize) {
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 6f53af1..bd32779 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -2025,10 +2025,10 @@
         C({Vec(f32(10), f32(-10.5))}, Val(u32(0xc940'4900))),
 
         E({Vec(f32(0), f32::Highest())},
-          "12:34 error: value 340282346638528859811704183484516925440.000000000 cannot be "
+          "12:34 error: value 340282346638528859811704183484516925440.0 cannot be "
           "represented as 'f16'"),
         E({Vec(f32::Lowest(), f32(0))},
-          "12:34 error: value -340282346638528859811704183484516925440.000000000 cannot be "
+          "12:34 error: value -340282346638528859811704183484516925440.0 cannot be "
           "represented as 'f16'"),
     };
 }
@@ -2850,16 +2850,15 @@
           Vec(0x0.034p-14_f, -0x0.034p-14_f, 0x0.068p-14_f, -0x0.068p-14_f)),
 
         // Value out of f16 range
-        E({65504.003_f}, "12:34 error: value 65504.003906250 cannot be represented as 'f16'"),
-        E({-65504.003_f}, "12:34 error: value -65504.003906250 cannot be represented as 'f16'"),
-        E({0x1.234p56_f},
-          "12:34 error: value 81979586966978560.000000000 cannot be represented as 'f16'"),
+        E({65504.003_f}, "12:34 error: value 65504.00390625 cannot be represented as 'f16'"),
+        E({-65504.003_f}, "12:34 error: value -65504.00390625 cannot be represented as 'f16'"),
+        E({0x1.234p56_f}, "12:34 error: value 81979586966978560.0 cannot be represented as 'f16'"),
         E({0x4.321p65_f},
-          "12:34 error: value 154788719192723947520.000000000 cannot be represented as 'f16'"),
+          "12:34 error: value 154788719192723947520.0 cannot be represented as 'f16'"),
         E({Vec(65504.003_f, 0_f)},
-          "12:34 error: value 65504.003906250 cannot be represented as 'f16'"),
+          "12:34 error: value 65504.00390625 cannot be represented as 'f16'"),
         E({Vec(0_f, -0x4.321p65_f)},
-          "12:34 error: value -154788719192723947520.000000000 cannot be represented as 'f16'"),
+          "12:34 error: value -154788719192723947520.0 cannot be represented as 'f16'"),
     };
 }
 INSTANTIATE_TEST_SUITE_P(  //
diff --git a/src/tint/resolver/const_eval_conversion_test.cc b/src/tint/resolver/const_eval_conversion_test.cc
index b6bcc56..92af2de 100644
--- a/src/tint/resolver/const_eval_conversion_test.cc
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -431,8 +431,7 @@
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: value 10000000000.000000000 cannot be represented as 'f16'");
+    EXPECT_EQ(r()->error(), "12:34 error: value 10000000000.0 cannot be represented as 'f16'");
 }
 
 TEST_F(ResolverConstEvalTest, Vec3_Convert_Small_f32_to_f16) {
diff --git a/src/tint/resolver/const_eval_runtime_semantics_test.cc b/src/tint/resolver/const_eval_runtime_semantics_test.cc
index 347e41d..eb0cf52 100644
--- a/src/tint/resolver/const_eval_runtime_semantics_test.cc
+++ b/src/tint/resolver/const_eval_runtime_semantics_test.cc
@@ -363,7 +363,7 @@
     auto result = const_eval.exp(a->Type(), utils::Vector{a}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
-    EXPECT_EQ(error(), R"(warning: e^1000.000000000 cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: e^1000.0 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Exp2_F32_Overflow) {
@@ -371,7 +371,7 @@
     auto result = const_eval.exp2(a->Type(), utils::Vector{a}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
-    EXPECT_EQ(error(), R"(warning: 2^1000.000000000 cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: 2^1000.0 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, ExtractBits_I32_TooManyBits) {
@@ -476,7 +476,7 @@
     auto result = const_eval.pack2x16float(create<type::U32>(), utils::Vector{vec}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<u32>(), 0x51430000);
-    EXPECT_EQ(error(), R"(warning: value 75250.000000000 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 75250.0 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Pow_F32_Overflow) {
@@ -502,7 +502,7 @@
     auto result = const_eval.quantizeToF16(create<type::U32>(), utils::Vector{a}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<u32>(), 0);
-    EXPECT_EQ(error(), R"(warning: value 75250.000000000 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 75250.0 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Sqrt_F32_OutOfRange) {
@@ -536,7 +536,7 @@
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f32::kHighestValue);
     EXPECT_EQ(
         error(),
-        R"(warning: value 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000000 cannot be represented as 'f32')");
+        R"(warning: value 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F32_TooLow) {
@@ -546,7 +546,7 @@
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f32::kLowestValue);
     EXPECT_EQ(
         error(),
-        R"(warning: value -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000000 cannot be represented as 'f32')");
+        R"(warning: value -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F16_TooHigh) {
@@ -554,7 +554,7 @@
     auto result = const_eval.Convert(create<type::F16>(), a, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f16::kHighestValue);
-    EXPECT_EQ(error(), R"(warning: value 1000000.000000000 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 1000000.0 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F16_TooLow) {
@@ -562,7 +562,7 @@
     auto result = const_eval.Convert(create<type::F16>(), a, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f16::kLowestValue);
-    EXPECT_EQ(error(), R"(warning: value -1000000.000000000 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value -1000000.0 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Vec_Overflow_SingleComponent) {
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 9b6d471..8292adb 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -37,6 +37,7 @@
 #include "src/tint/ast/internal_attribute.h"
 #include "src/tint/ast/interpolate_attribute.h"
 #include "src/tint/ast/invariant_attribute.h"
+#include "src/tint/ast/let.h"
 #include "src/tint/ast/location_attribute.h"
 #include "src/tint/ast/loop_statement.h"
 #include "src/tint/ast/must_use_attribute.h"
@@ -810,6 +811,9 @@
             [&](const ast::Var* n) {  //
                 return "var '" + symbols.NameFor(n->name->symbol) + "'";
             },
+            [&](const ast::Let* n) {  //
+                return "let '" + symbols.NameFor(n->name->symbol) + "'";
+            },
             [&](const ast::Const* n) {  //
                 return "const '" + symbols.NameFor(n->name->symbol) + "'";
             },
diff --git a/src/tint/resolver/intrinsic_table.inl b/src/tint/resolver/intrinsic_table.inl
index 9aa0b76..c383265 100644
--- a/src/tint/resolver/intrinsic_table.inl
+++ b/src/tint/resolver/intrinsic_table.inl
@@ -14092,8 +14092,8 @@
   },
   {
     /* [2] */
-    /* fn acosh<T : fa_f32_f16>(@test_value(2) T) -> T */
-    /* fn acosh<N : num, T : fa_f32_f16>(@test_value(2) vec<N, T>) -> vec<N, T> */
+    /* fn acosh<T : fa_f32_f16>(@test_value(1.5430806348) T) -> T */
+    /* fn acosh<N : num, T : fa_f32_f16>(@test_value(1.5430806348) vec<N, T>) -> vec<N, T> */
     /* num overloads */ 2,
     /* overloads */ &kOverloads[287],
   },
@@ -14526,8 +14526,8 @@
   },
   {
     /* [66] */
-    /* fn round<T : fa_f32_f16>(@test_value(3.4) T) -> T */
-    /* fn round<N : num, T : fa_f32_f16>(@test_value(3.4) vec<N, T>) -> vec<N, T> */
+    /* fn round<T : fa_f32_f16>(@test_value(3.5) T) -> T */
+    /* fn round<N : num, T : fa_f32_f16>(@test_value(3.5) vec<N, T>) -> vec<N, T> */
     /* num overloads */ 2,
     /* overloads */ &kOverloads[383],
   },
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index 93402b6..bc4ac1d 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -15,7 +15,6 @@
 #include "src/tint/resolver/uniformity.h"
 
 #include <limits>
-#include <sstream>
 #include <string>
 #include <utility>
 #include <vector>
diff --git a/src/tint/resolver/uniformity_test.cc b/src/tint/resolver/uniformity_test.cc
index 0a79058..51608bd 100644
--- a/src/tint/resolver/uniformity_test.cc
+++ b/src/tint/resolver/uniformity_test.cc
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 #include <memory>
-#include <sstream>
 #include <string>
 #include <tuple>
 #include <utility>
diff --git a/src/tint/sem/behavior.cc b/src/tint/sem/behavior.cc
index 670cc1d..b2c7897 100644
--- a/src/tint/sem/behavior.cc
+++ b/src/tint/sem/behavior.cc
@@ -16,7 +16,7 @@
 
 namespace tint::sem {
 
-std::ostream& operator<<(std::ostream& out, Behavior behavior) {
+utils::StringStream& operator<<(utils::StringStream& out, Behavior behavior) {
     switch (behavior) {
         case Behavior::kReturn:
             return out << "Return";
diff --git a/src/tint/sem/behavior.h b/src/tint/sem/behavior.h
index 011ca72..27eba3f 100644
--- a/src/tint/sem/behavior.h
+++ b/src/tint/sem/behavior.h
@@ -31,11 +31,11 @@
 /// Behaviors is a set of Behavior
 using Behaviors = utils::EnumSet<Behavior>;
 
-/// Writes the Behavior to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the Behavior to the stream.
+/// @param out the stream to write to
 /// @param behavior the Behavior to write
 /// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, Behavior behavior);
+utils::StringStream& operator<<(utils::StringStream& out, Behavior behavior);
 
 }  // namespace tint::sem
 
diff --git a/src/tint/sem/binding_point.h b/src/tint/sem/binding_point.h
index 78403ab..6837310 100644
--- a/src/tint/sem/binding_point.h
+++ b/src/tint/sem/binding_point.h
@@ -18,10 +18,10 @@
 #include <stdint.h>
 
 #include <functional>
-#include <ostream>
 
 #include "src/tint/reflection.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::sem {
 
@@ -49,10 +49,10 @@
 };
 
 /// Prints the BindingPoint @p bp to @p o
-/// @param o the std::ostream to write to
+/// @param o the stream to write to
 /// @param bp the BindingPoint
-/// @return the std::ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& o, const BindingPoint& bp) {
+/// @return the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& o, const BindingPoint& bp) {
     return o << "[group: " << bp.group << ", binding: " << bp.binding << "]";
 }
 
diff --git a/src/tint/sem/builtin_type.cc b/src/tint/sem/builtin_type.cc
index d9cb0c2..8abcec0 100644
--- a/src/tint/sem/builtin_type.cc
+++ b/src/tint/sem/builtin_type.cc
@@ -22,8 +22,6 @@
 
 #include "src/tint/sem/builtin_type.h"
 
-#include <sstream>
-
 namespace tint::sem {
 
 BuiltinType ParseBuiltinType(const std::string& name) {
@@ -608,7 +606,7 @@
     return "<unknown>";
 }
 
-std::ostream& operator<<(std::ostream& out, BuiltinType i) {
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinType i) {
     out << str(i);
     return out;
 }
diff --git a/src/tint/sem/builtin_type.cc.tmpl b/src/tint/sem/builtin_type.cc.tmpl
index af3b1fa..86a8623 100644
--- a/src/tint/sem/builtin_type.cc.tmpl
+++ b/src/tint/sem/builtin_type.cc.tmpl
@@ -13,8 +13,6 @@
 
 #include "src/tint/sem/builtin_type.h"
 
-#include <sstream>
-
 namespace tint::sem {
 
 BuiltinType ParseBuiltinType(const std::string& name) {
@@ -38,7 +36,7 @@
     return "<unknown>";
 }
 
-std::ostream& operator<<(std::ostream& out, BuiltinType i) {
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinType i) {
     out << str(i);
     return out;
 }
diff --git a/src/tint/sem/builtin_type.h b/src/tint/sem/builtin_type.h
index 114afb6..23f3749 100644
--- a/src/tint/sem/builtin_type.h
+++ b/src/tint/sem/builtin_type.h
@@ -23,9 +23,10 @@
 #ifndef SRC_TINT_SEM_BUILTIN_TYPE_H_
 #define SRC_TINT_SEM_BUILTIN_TYPE_H_
 
-#include <sstream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::sem {
 
 /// Enumerator of all builtin functions
@@ -159,7 +160,7 @@
 
 /// Emits the name of the builtin function type. The spelling, including case,
 /// matches the name in the WGSL spec.
-std::ostream& operator<<(std::ostream& out, BuiltinType i);
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinType i);
 
 /// All builtin function
 constexpr BuiltinType kBuiltinTypes[] = {
diff --git a/src/tint/sem/builtin_type.h.tmpl b/src/tint/sem/builtin_type.h.tmpl
index 7e574f5..366db95 100644
--- a/src/tint/sem/builtin_type.h.tmpl
+++ b/src/tint/sem/builtin_type.h.tmpl
@@ -14,9 +14,10 @@
 #ifndef SRC_TINT_SEM_BUILTIN_TYPE_H_
 #define SRC_TINT_SEM_BUILTIN_TYPE_H_
 
-#include <sstream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::sem {
 
 /// Enumerator of all builtin functions
@@ -39,7 +40,7 @@
 
 /// Emits the name of the builtin function type. The spelling, including case,
 /// matches the name in the WGSL spec.
-std::ostream& operator<<(std::ostream& out, BuiltinType i);
+utils::StringStream& operator<<(utils::StringStream& out, BuiltinType i);
 
 /// All builtin function
 constexpr BuiltinType kBuiltinTypes[] = {
diff --git a/src/tint/sem/index_accessor_expression.h b/src/tint/sem/index_accessor_expression.h
index 2375a9d..8118659 100644
--- a/src/tint/sem/index_accessor_expression.h
+++ b/src/tint/sem/index_accessor_expression.h
@@ -17,13 +17,9 @@
 
 #include <vector>
 
+#include "src/tint/ast/index_accessor_expression.h"
 #include "src/tint/sem/value_expression.h"
 
-// Forward declarations
-namespace tint::ast {
-class IndexAccessorExpression;
-}  // namespace tint::ast
-
 namespace tint::sem {
 
 /// IndexAccessorExpression holds the semantic information for a ast::IndexAccessorExpression node.
@@ -52,6 +48,11 @@
     /// Destructor
     ~IndexAccessorExpression() override;
 
+    /// @returns the AST node
+    const ast::IndexAccessorExpression* Declaration() const {
+        return static_cast<const ast::IndexAccessorExpression*>(declaration_);
+    }
+
     /// @returns the object expression that is being indexed
     ValueExpression const* Object() const { return object_; }
 
diff --git a/src/tint/sem/sampler_texture_pair.h b/src/tint/sem/sampler_texture_pair.h
index b3cf4f2..b0199bb 100644
--- a/src/tint/sem/sampler_texture_pair.h
+++ b/src/tint/sem/sampler_texture_pair.h
@@ -17,9 +17,9 @@
 
 #include <cstdint>
 #include <functional>
-#include <ostream>
 
 #include "src/tint/sem/binding_point.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::sem {
 
@@ -45,10 +45,10 @@
 };
 
 /// Prints the SamplerTexturePair @p stp to @p o
-/// @param o the std::ostream to write to
+/// @param o the stream to write to
 /// @param stp the SamplerTexturePair
-/// @return the std::ostream so calls can be chained
-inline std::ostream& operator<<(std::ostream& o, const SamplerTexturePair& stp) {
+/// @return the stream so calls can be chained
+inline utils::StringStream& operator<<(utils::StringStream& o, const SamplerTexturePair& stp) {
     return o << "[sampler: " << stp.sampler_binding_point
              << ", texture: " << stp.sampler_binding_point << "]";
 }
diff --git a/src/tint/source.cc b/src/tint/source.cc
index 6a10de5..3f3ec7c 100644
--- a/src/tint/source.cc
+++ b/src/tint/source.cc
@@ -15,7 +15,6 @@
 #include "src/tint/source.h"
 
 #include <algorithm>
-#include <sstream>
 #include <string_view>
 #include <utility>
 
@@ -122,7 +121,7 @@
 
 Source::File::~File() = default;
 
-std::ostream& operator<<(std::ostream& out, const Source& source) {
+utils::StringStream& operator<<(utils::StringStream& out, const Source& source) {
     auto rng = source.range;
 
     if (source.file) {
diff --git a/src/tint/source.h b/src/tint/source.h
index 7bf9735..cc13c5f 100644
--- a/src/tint/source.h
+++ b/src/tint/source.h
@@ -16,12 +16,13 @@
 #ifndef SRC_TINT_SOURCE_H_
 #define SRC_TINT_SOURCE_H_
 
-#include <iostream>
 #include <string>
 #include <string_view>
 #include <tuple>
 #include <vector>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint {
 
 /// Source describes a range of characters within a source file.
@@ -191,35 +192,36 @@
     const File* file = nullptr;
 };
 
-/// Writes the Source::Location to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the Source::Location to the stream.
+/// @param out the stream to write to
 /// @param loc the location to write
 /// @returns out so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, const Source::Location& loc) {
+inline utils::StringStream& operator<<(utils::StringStream& out, const Source::Location& loc) {
     out << loc.line << ":" << loc.column;
     return out;
 }
 
-/// Writes the Source::Range to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the Source::Range to the stream.
+/// @param out the stream to write to
 /// @param range the range to write
 /// @returns out so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, const Source::Range& range) {
+inline utils::StringStream& operator<<(utils::StringStream& out, const Source::Range& range) {
     out << "[" << range.begin << ", " << range.end << "]";
     return out;
 }
 
-/// Writes the Source to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the Source to the stream.
+/// @param out the stream to write to
 /// @param source the source to write
 /// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, const Source& source);
+utils::StringStream& operator<<(utils::StringStream& out, const Source& source);
 
-/// Writes the Source::FileContent to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the Source::FileContent to the stream.
+/// @param out the stream to write to
 /// @param content the file content to write
 /// @returns out so calls can be chained
-inline std::ostream& operator<<(std::ostream& out, const Source::FileContent& content) {
+inline utils::StringStream& operator<<(utils::StringStream& out,
+                                       const Source::FileContent& content) {
     out << content.data;
     return out;
 }
diff --git a/src/tint/templates/enums.tmpl.inc b/src/tint/templates/enums.tmpl.inc
index ab08163..ad46942 100644
--- a/src/tint/templates/enums.tmpl.inc
+++ b/src/tint/templates/enums.tmpl.inc
@@ -47,10 +47,10 @@
 {{-   end }}
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param value the {{$enum}}
 /// @returns `out` so calls can be chained
-std::ostream& operator<<(std::ostream& out, {{$enum}} value);
+utils::StringStream& operator<<(utils::StringStream& out, {{$enum}} value);
 
 /// Parse{{$enum}} parses a {{$enum}} from a string.
 /// @param str the string to parse
@@ -90,11 +90,11 @@
 
 {{- /* ------------------------------------------------------------------ */ -}}
 {{-                         define "EnumOStream"                             -}}
-{{- /* Implements the std::ostream 'operator<<()' function to print the   */ -}}
+{{- /* Implements the stream 'operator<<()' function to print the         */ -}}
 {{- /* provided sem.Enum.                                                 */ -}}
 {{- /* ------------------------------------------------------------------ */ -}}
 {{- $enum := Eval "EnumName" $ -}}
-std::ostream& operator<<(std::ostream& out, {{$enum}} value) {
+    utils::StringStream& operator<<(utils::StringStream& out, {{$enum}} value) {
     switch (value) {
         case {{$enum}}::kUndefined:
             return out << "undefined";
diff --git a/src/tint/text/unicode.cc b/src/tint/text/unicode.cc
index cc9a9d1..ee3092b 100644
--- a/src/tint/text/unicode.cc
+++ b/src/tint/text/unicode.cc
@@ -330,30 +330,6 @@
                                               kXIDContinueRanges + kNumXIDContinueRanges, *this);
 }
 
-std::ostream& operator<<(std::ostream& out, CodePoint code_point) {
-    if (code_point < 0x7f) {
-        // See https://en.cppreference.com/w/cpp/language/escape
-        switch (code_point) {
-            case '\a':
-                return out << R"('\a')";
-            case '\b':
-                return out << R"('\b')";
-            case '\f':
-                return out << R"('\f')";
-            case '\n':
-                return out << R"('\n')";
-            case '\r':
-                return out << R"('\r')";
-            case '\t':
-                return out << R"('\t')";
-            case '\v':
-                return out << R"('\v')";
-        }
-        return out << "'" << static_cast<char>(code_point) << "'";
-    }
-    return out << "'U+" << std::hex << code_point.value << "'";
-}
-
 namespace utf8 {
 
 std::pair<CodePoint, size_t> Decode(const uint8_t* ptr, size_t len) {
diff --git a/src/tint/text/unicode.h b/src/tint/text/unicode.h
index 0594d31..493cdf2 100644
--- a/src/tint/text/unicode.h
+++ b/src/tint/text/unicode.h
@@ -17,7 +17,7 @@
 
 #include <cstddef>
 #include <cstdint>
-#include <ostream>
+#include <string_view>
 #include <utility>
 
 namespace tint::text {
@@ -54,12 +54,6 @@
     uint32_t value = 0;
 };
 
-/// Writes the CodePoint to the std::ostream.
-/// @param out the std::ostream to write to
-/// @param codepoint the CodePoint to write
-/// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, CodePoint codepoint);
-
 namespace utf8 {
 
 /// Decodes the first code point in the utf8 string.
diff --git a/src/tint/transform/direct_variable_access_test.cc b/src/tint/transform/direct_variable_access_test.cc
index f017662..cc77748 100644
--- a/src/tint/transform/direct_variable_access_test.cc
+++ b/src/tint/transform/direct_variable_access_test.cc
@@ -436,16 +436,32 @@
 }
 
 fn b() {
-  let ptr_index_save = first();
-  for(let p1 = &(U[ptr_index_save]); true; ) {
-    a_U_X_X(10, U_X_X(u32(ptr_index_save), u32(second())), 20);
+  {
+    let ptr_index_save = first();
+    let p1 = &(U[ptr_index_save]);
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+        a_U_X_X(10, U_X_X(u32(ptr_index_save), u32(second())), 20);
+      }
+    }
   }
 }
 
 fn c_U() {
-  let ptr_index_save_1 = first();
-  for(let p1 = &(U[ptr_index_save_1]); true; ) {
-    a_U_X_X(10, U_X_X(u32(ptr_index_save_1), u32(second())), 20);
+  {
+    let ptr_index_save_1 = first();
+    let p1 = &(U[ptr_index_save_1]);
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+        a_U_X_X(10, U_X_X(u32(ptr_index_save_1), u32(second())), 20);
+      }
+    }
   }
 }
 
diff --git a/src/tint/transform/expand_compound_assignment_test.cc b/src/tint/transform/expand_compound_assignment_test.cc
index 6b18b40..c227398 100644
--- a/src/tint/transform/expand_compound_assignment_test.cc
+++ b/src/tint/transform/expand_compound_assignment_test.cc
@@ -391,10 +391,15 @@
 }
 
 fn main() {
-  let tint_symbol = &(a[idx1()]);
-  let tint_symbol_1 = idx2();
-  for((*(tint_symbol))[tint_symbol_1] = ((*(tint_symbol))[tint_symbol_1] + 1); ; ) {
-    break;
+  {
+    let tint_symbol = &(a[idx1()]);
+    let tint_symbol_1 = idx2();
+    (*(tint_symbol))[tint_symbol_1] = ((*(tint_symbol))[tint_symbol_1] + 1);
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
diff --git a/src/tint/transform/packed_vec3_test.cc b/src/tint/transform/packed_vec3_test.cc
index 670bc05..dbc28f7 100644
--- a/src/tint/transform/packed_vec3_test.cc
+++ b/src/tint/transform/packed_vec3_test.cc
@@ -289,7 +289,7 @@
 @group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
 
 fn f() {
-  v = __packed_vec3<f32>(vec3(1.23));
+  v = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -342,7 +342,7 @@
 @group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
 
 fn f() {
-  v.y = 1.23;
+  v.y = 1.22999999999999998224;
 }
 )";
 
@@ -367,7 +367,7 @@
 @group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
 
 fn f() {
-  v[1] = 1.23;
+  v[1] = 1.22999999999999998224;
 }
 )";
 
@@ -596,7 +596,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 4u>;
 
 fn f() {
-  arr[0].elements = __packed_vec3<f32>(vec3(1.23));
+  arr[0].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -664,7 +664,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 4u>;
 
 fn f() {
-  arr[0].elements.y = 1.23;
+  arr[0].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -694,7 +694,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 4u>;
 
 fn f() {
-  arr[0].elements[1] = 1.23;
+  arr[0].elements[1] = 1.22999999999999998224;
 }
 )";
 
@@ -923,7 +923,7 @@
 @group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
 
 fn f() {
-  m[1].elements = __packed_vec3<f32>(vec3(1.23));
+  m[1].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -991,7 +991,7 @@
 @group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
 
 fn f() {
-  m[1].elements.y = 1.23;
+  m[1].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -1021,7 +1021,7 @@
 @group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
 
 fn f() {
-  m[1].elements[2] = 1.23;
+  m[1].elements[2] = 1.22999999999999998224;
 }
 )";
 
@@ -1312,7 +1312,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
 
 fn f() {
-  arr[0] = tint_pack_vec3_in_composite(mat3x3(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9));
+  arr[0] = tint_pack_vec3_in_composite(mat3x3(1.10000000000000008882, 2.20000000000000017764, 3.29999999999999982236, 4.40000000000000035527, 5.5, 6.59999999999999964473, 7.70000000000000017764, 8.80000000000000071054, 9.90000000000000035527));
 }
 )";
 
@@ -1380,7 +1380,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
 
 fn f() {
-  arr[0][1].elements = __packed_vec3<f32>(vec3(1.23));
+  arr[0][1].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -1453,7 +1453,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
 
 fn f() {
-  arr[0][1].elements.y = 1.23;
+  arr[0][1].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -1483,7 +1483,7 @@
 @group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
 
 fn f() {
-  arr[0][1].elements[2] = 1.23;
+  arr[0][1].elements[2] = 1.22999999999999998224;
 }
 )";
 
@@ -1685,7 +1685,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P = tint_pack_vec3_in_composite(S(vec3(1.23)));
+  P = tint_pack_vec3_in_composite(S(vec3(1.22999999999999998224)));
 }
 )";
 
@@ -1764,7 +1764,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.v = __packed_vec3<f32>(vec3(1.23));
+  P.v = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -1852,7 +1852,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.v.y = 1.23;
+  P.v.y = 1.22999999999999998224;
 }
 )";
 
@@ -1890,7 +1890,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.v[1] = 1.23;
+  P.v[1] = 1.22999999999999998224;
 }
 )";
 
@@ -2380,7 +2380,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0].elements = __packed_vec3<f32>(vec3(1.23));
+  P.arr[0].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -2479,7 +2479,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0].elements.y = 1.23;
+  P.arr[0].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -2522,7 +2522,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0].elements[1] = 1.23;
+  P.arr[0].elements[1] = 1.22999999999999998224;
 }
 )";
 
@@ -3012,7 +3012,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.m[1].elements = __packed_vec3<f32>(vec3(1.23));
+  P.m[1].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -3111,7 +3111,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.m[1].elements.y = 1.23;
+  P.m[1].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -3154,7 +3154,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.m[1].elements[2] = 1.23;
+  P.m[1].elements[2] = 1.22999999999999998224;
 }
 )";
 
@@ -3834,7 +3834,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0][1].elements = __packed_vec3<f32>(vec3(1.23));
+  P.arr[0][1].elements = __packed_vec3<f32>(vec3(1.22999999999999998224));
 }
 )";
 
@@ -3938,7 +3938,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0][1].elements.y = 1.23;
+  P.arr[0][1].elements.y = 1.22999999999999998224;
 }
 )";
 
@@ -3981,7 +3981,7 @@
 @group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
-  P.arr[0][1].elements[2] = 1.23;
+  P.arr[0][1].elements[2] = 1.22999999999999998224;
 }
 )";
 
diff --git a/src/tint/transform/pad_structs.h b/src/tint/transform/pad_structs.h
index e9996d4..1add1d6 100644
--- a/src/tint/transform/pad_structs.h
+++ b/src/tint/transform/pad_structs.h
@@ -22,6 +22,8 @@
 /// This transform turns all explicit alignment and sizing into padding
 /// members of structs. This is required for GLSL ES, since it not support
 /// the offset= decoration.
+///
+/// @note This transform requires the CanonicalizeEntryPointIO transform to have been run first.
 class PadStructs final : public Castable<PadStructs, Transform> {
   public:
     /// Constructor
diff --git a/src/tint/transform/promote_initializers_to_let_test.cc b/src/tint/transform/promote_initializers_to_let_test.cc
index 539f513..fc593d5 100644
--- a/src/tint/transform/promote_initializers_to_let_test.cc
+++ b/src/tint/transform/promote_initializers_to_let_test.cc
@@ -272,9 +272,14 @@
     auto* expect = R"(
 fn f() {
   var insert_after = 1;
-  let tint_symbol : array<f32, 4u> = array<f32, 4u>(0.0, 1.0, 2.0, 3.0);
-  for(var i = tint_symbol[insert_after]; ; ) {
-    break;
+  {
+    let tint_symbol : array<f32, 4u> = array<f32, 4u>(0.0, 1.0, 2.0, 3.0);
+    var i = tint_symbol[insert_after];
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
@@ -301,9 +306,14 @@
   const arr = array<f32, 4u>(0.0, 1.0, 2.0, 3.0);
   let runtime_value = 1;
   var insert_after = 1;
-  let tint_symbol : array<f32, 4u> = arr;
-  for(var i = tint_symbol[runtime_value]; ; ) {
-    break;
+  {
+    let tint_symbol : array<f32, 4u> = arr;
+    var i = tint_symbol[runtime_value];
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
@@ -332,9 +342,14 @@
 fn f() {
   let runtime_value = 1;
   var insert_after = 1;
-  let tint_symbol : array<f32, 4u> = arr;
-  for(var i = tint_symbol[runtime_value]; ; ) {
-    break;
+  {
+    let tint_symbol : array<f32, 4u> = arr;
+    var i = tint_symbol[runtime_value];
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
@@ -377,9 +392,14 @@
 
 fn f() {
   var insert_after = 1;
-  let tint_symbol : S = S(1, 2.0, vec3<f32>());
-  for(var x = get_b_runtime(tint_symbol); ; ) {
-    break;
+  {
+    let tint_symbol : S = S(1, 2.0, vec3<f32>());
+    var x = get_b_runtime(tint_symbol);
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
@@ -412,9 +432,14 @@
     auto* expect = R"(
 fn f() {
   var insert_after = 1;
-  let tint_symbol : S = S(1, 2.0, vec3<f32>());
-  for(var x = get_b_runtime(tint_symbol); ; ) {
-    break;
+  {
+    let tint_symbol : S = S(1, 2.0, vec3<f32>());
+    var x = get_b_runtime(tint_symbol);
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 
@@ -683,8 +708,8 @@
     auto* expect = R"(
 fn f() {
   let runtime_value = 0;
-  let tint_symbol : array<f32, 1u> = array<f32, 1u>(0.0);
   {
+    let tint_symbol : array<f32, 1u> = array<f32, 1u>(0.0);
     var f = tint_symbol[runtime_value];
     loop {
       let tint_symbol_1 : array<f32, 1u> = array<f32, 1u>(1.0);
@@ -728,8 +753,8 @@
   const arr_a = array<f32, 1u>(0.0);
   const arr_b = array<f32, 1u>(1.0);
   const arr_c = array<f32, 1u>(2.0);
-  let tint_symbol : array<f32, 1u> = arr_a;
   {
+    let tint_symbol : array<f32, 1u> = arr_a;
     var f = tint_symbol[runtime_value];
     loop {
       let tint_symbol_1 : array<f32, 1u> = arr_b;
@@ -1301,9 +1326,21 @@
 
 fn Z() {
   var i = 10;
-  let tint_symbol_2 : array<i32, 1u> = array<i32, 1u>(i);
-  for(var f = tint_symbol_2[0]; (f < 10); f = (f + 1)) {
-    var i = 20;
+  {
+    let tint_symbol_2 : array<i32, 1u> = array<i32, 1u>(i);
+    var f = tint_symbol_2[0];
+    loop {
+      if (!((f < 10))) {
+        break;
+      }
+      {
+        var i = 20;
+      }
+
+      continuing {
+        f = (f + 1);
+      }
+    }
   }
 }
 )";
diff --git a/src/tint/transform/promote_side_effects_to_decl_test.cc b/src/tint/transform/promote_side_effects_to_decl_test.cc
index f064834..6ed4b27 100644
--- a/src/tint/transform/promote_side_effects_to_decl_test.cc
+++ b/src/tint/transform/promote_side_effects_to_decl_test.cc
@@ -848,10 +848,15 @@
 
 fn f() {
   var b = 1;
-  let tint_symbol = a(0);
-  for(var r = (tint_symbol + b); ; ) {
-    var marker = 0;
-    break;
+  {
+    let tint_symbol = a(0);
+    var r = (tint_symbol + b);
+    loop {
+      {
+        var marker = 0;
+        break;
+      }
+    }
   }
 }
 )";
@@ -2169,13 +2174,18 @@
 
 fn f() {
   var b = true;
-  var tint_symbol = a(0);
-  if (tint_symbol) {
-    tint_symbol = b;
-  }
-  for(var r = tint_symbol; ; ) {
-    var marker = 0;
-    break;
+  {
+    var tint_symbol = a(0);
+    if (tint_symbol) {
+      tint_symbol = b;
+    }
+    var r = tint_symbol;
+    loop {
+      {
+        var marker = 0;
+        break;
+      }
+    }
   }
 }
 )";
diff --git a/src/tint/transform/robustness.h b/src/tint/transform/robustness.h
index 14c5fe1..780eab9 100644
--- a/src/tint/transform/robustness.h
+++ b/src/tint/transform/robustness.h
@@ -31,6 +31,7 @@
 /// the bounds of the array. Any access before the start of the array will clamp
 /// to zero and any access past the end of the array will clamp to
 /// (array length - 1).
+/// @note This transform must come before the BuiltinPolyfill transform
 class Robustness final : public Castable<Robustness, Transform> {
   public:
     /// Address space to be skipped in the transform
diff --git a/src/tint/transform/substitute_override_test.cc b/src/tint/transform/substitute_override_test.cc
index abc67f5..1b4646f 100644
--- a/src/tint/transform/substitute_override_test.cc
+++ b/src/tint/transform/substitute_override_test.cc
@@ -110,9 +110,9 @@
 
 const i_height = 11i;
 
-const f_width : f32 = 22.299999237f;
+const f_width : f32 = 22.299999237060546875f;
 
-const f_height = 12.399999619f;
+const f_height = 12.3999996185302734375f;
 
 const b_width : bool = true;
 
@@ -175,9 +175,9 @@
 
 const i_height = 11i;
 
-const f_width : f32 = 22.299999237f;
+const f_width : f32 = 22.299999237060546875f;
 
-const f_height = 12.399999619f;
+const f_height = 12.3999996185302734375f;
 
 const b_width : bool = true;
 
diff --git a/src/tint/transform/texture_1d_to_2d_test.cc b/src/tint/transform/texture_1d_to_2d_test.cc
index 0196951..b68b51e 100644
--- a/src/tint/transform/texture_1d_to_2d_test.cc
+++ b/src/tint/transform/texture_1d_to_2d_test.cc
@@ -226,7 +226,7 @@
 @group(0) @binding(1) var samp : sampler;
 
 fn f(t : texture_2d<f32>, s : sampler) -> vec4<f32> {
-  return textureSample(t, s, vec2<f32>(0.7, 0.5));
+  return textureSample(t, s, vec2<f32>(0.69999999999999995559, 0.5));
 }
 
 fn main() -> vec4<f32> {
diff --git a/src/tint/transform/transform.cc b/src/tint/transform/transform.cc
index ad954ee..73ee3ae 100644
--- a/src/tint/transform/transform.cc
+++ b/src/tint/transform/transform.cc
@@ -173,6 +173,15 @@
     if (auto* s = ty->As<type::Sampler>()) {
         return ctx.dst->ty.sampler(s->kind());
     }
+    if (auto* p = ty->As<type::Pointer>()) {
+        // Note: type::Pointer always has an inferred access, but WGSL only allows an explicit
+        // access in the 'storage' address space.
+        auto address_space = p->AddressSpace();
+        auto access = address_space == builtin::AddressSpace::kStorage
+                          ? p->Access()
+                          : builtin::Access::kUndefined;
+        return ctx.dst->ty.pointer(CreateASTTypeFor(ctx, p->StoreType()), address_space, access);
+    }
     TINT_UNREACHABLE(Transform, ctx.dst->Diagnostics())
         << "Unhandled type: " << ty->TypeInfo().name;
     return ast::Type{};
diff --git a/src/tint/transform/transform_test.cc b/src/tint/transform/transform_test.cc
index f27a990..deffae3 100644
--- a/src/tint/transform/transform_test.cc
+++ b/src/tint/transform/transform_test.cc
@@ -127,5 +127,24 @@
     ast::CheckIdentifier(ast_type_builder.Symbols(), str, "S");
 }
 
+TEST_F(CreateASTTypeForTest, PrivatePointer) {
+    auto ptr = create([](ProgramBuilder& b) {
+        return b.create<type::Pointer>(b.create<type::I32>(), builtin::AddressSpace::kPrivate,
+                                       builtin::Access::kReadWrite);
+    });
+
+    ast::CheckIdentifier(ast_type_builder.Symbols(), ptr, ast::Template("ptr", "private", "i32"));
+}
+
+TEST_F(CreateASTTypeForTest, StorageReadWritePointer) {
+    auto ptr = create([](ProgramBuilder& b) {
+        return b.create<type::Pointer>(b.create<type::I32>(), builtin::AddressSpace::kStorage,
+                                       builtin::Access::kReadWrite);
+    });
+
+    ast::CheckIdentifier(ast_type_builder.Symbols(), ptr,
+                         ast::Template("ptr", "storage", "i32", "read_write"));
+}
+
 }  // namespace
 }  // namespace tint::transform
diff --git a/src/tint/transform/utils/hoist_to_decl_before.cc b/src/tint/transform/utils/hoist_to_decl_before.cc
index 0c2c6eb..5fa00c4 100644
--- a/src/tint/transform/utils/hoist_to_decl_before.cc
+++ b/src/tint/transform/utils/hoist_to_decl_before.cc
@@ -99,6 +99,21 @@
         return InsertBeforeImpl(before_stmt, std::move(builder));
     }
 
+    /// @copydoc HoistToDeclBefore::Replace(const sem::Statement* what, const ast::Statement* with)
+    bool Replace(const sem::Statement* what, const ast::Statement* with) {
+        auto builder = [with] { return with; };
+        return Replace(what, std::move(builder));
+    }
+
+    /// @copydoc HoistToDeclBefore::Replace(const sem::Statement* what, const StmtBuilder& with)
+    bool Replace(const sem::Statement* what, const StmtBuilder& with) {
+        if (!InsertBeforeImpl(what, Decompose{})) {
+            return false;
+        }
+        ctx.Replace(what->Declaration(), with);
+        return true;
+    }
+
     /// @copydoc HoistToDeclBefore::Prepare()
     bool Prepare(const sem::ValueExpression* before_expr) {
         return InsertBefore(before_expr->Stmt(), nullptr);
@@ -112,6 +127,7 @@
     /// loop, so that declaration statements can be inserted before the
     /// condition expression or continuing statement.
     struct LoopInfo {
+        utils::Vector<StmtBuilder, 8> init_decls;
         utils::Vector<StmtBuilder, 8> cond_decls;
         utils::Vector<StmtBuilder, 8> cont_decls;
     };
@@ -198,7 +214,7 @@
                     // Next emit the for-loop body
                     body_stmts.Push(ctx.Clone(for_loop->body));
 
-                    // Finally create the continuing block if there was one.
+                    // Create the continuing block if there was one.
                     const ast::BlockStatement* continuing = nullptr;
                     if (auto* cont = for_loop->continuing) {
                         // Continuing block starts with any let declarations used by
@@ -210,8 +226,17 @@
 
                     auto* body = b.Block(body_stmts);
                     auto* loop = b.Loop(body, continuing);
-                    if (auto* init = for_loop->initializer) {
-                        return b.Block(ctx.Clone(init), loop);
+
+                    // If the loop has no initializer statements, then we're done.
+                    // Otherwise, wrap loop with another block, prefixed with the initializer
+                    // statements
+                    if (!info->init_decls.IsEmpty() || for_loop->initializer) {
+                        auto stmts = Build(info->init_decls);
+                        if (auto* init = for_loop->initializer) {
+                            stmts.Push(ctx.Clone(init));
+                        }
+                        stmts.Push(loop);
+                        return b.Block(std::move(stmts));
                     }
                     return loop;
                 }
@@ -299,7 +324,7 @@
             // Need to convert 'else if' to 'else { if }'.
             auto else_if_info = ElseIf(else_if->Declaration());
 
-            // Index the map to convert this else if, even if `stmt` is nullptr.
+            // Index the map to decompose this else if, even if `stmt` is nullptr.
             auto& decls = else_if_info->cond_decls;
             if constexpr (!std::is_same_v<BUILDER, Decompose>) {
                 decls.Push(std::forward<BUILDER>(builder));
@@ -311,7 +336,7 @@
             // Insertion point is a for-loop condition.
             // For-loop needs to be decomposed to a loop.
 
-            // Index the map to convert this for-loop, even if `stmt` is nullptr.
+            // Index the map to decompose this for-loop, even if `stmt` is nullptr.
             auto& decls = ForLoop(fl)->cond_decls;
             if constexpr (!std::is_same_v<BUILDER, Decompose>) {
                 decls.Push(std::forward<BUILDER>(builder));
@@ -323,7 +348,7 @@
             // Insertion point is a while condition.
             // While needs to be decomposed to a loop.
 
-            // Index the map to convert this while, even if `stmt` is nullptr.
+            // Index the map to decompose this while, even if `stmt` is nullptr.
             auto& decls = WhileLoop(w)->cond_decls;
             if constexpr (!std::is_same_v<BUILDER, Decompose>) {
                 decls.Push(std::forward<BUILDER>(builder));
@@ -348,11 +373,14 @@
             // These require special care.
             if (fl->Declaration()->initializer == ip) {
                 // Insertion point is a for-loop initializer.
-                // Insert the new statement above the for-loop.
+                // For-loop needs to be decomposed to a loop.
+
+                // Index the map to decompose this for-loop, even if `stmt` is nullptr.
+                auto& decls = ForLoop(fl)->init_decls;
                 if constexpr (!std::is_same_v<BUILDER, Decompose>) {
-                    ctx.InsertBefore(fl->Block()->Declaration()->statements, fl->Declaration(),
-                                     std::forward<BUILDER>(builder));
+                    decls.Push(std::forward<BUILDER>(builder));
                 }
+
                 return true;
             }
 
@@ -360,11 +388,12 @@
                 // Insertion point is a for-loop continuing statement.
                 // For-loop needs to be decomposed to a loop.
 
-                // Index the map to convert this for-loop, even if `stmt` is nullptr.
+                // Index the map to decompose this for-loop, even if `stmt` is nullptr.
                 auto& decls = ForLoop(fl)->cont_decls;
                 if constexpr (!std::is_same_v<BUILDER, Decompose>) {
                     decls.Push(std::forward<BUILDER>(builder));
                 }
+
                 return true;
             }
 
@@ -399,6 +428,14 @@
     return state_->InsertBefore(before_stmt, builder);
 }
 
+bool HoistToDeclBefore::Replace(const sem::Statement* what, const ast::Statement* with) {
+    return state_->Replace(what, with);
+}
+
+bool HoistToDeclBefore::Replace(const sem::Statement* what, const StmtBuilder& with) {
+    return state_->Replace(what, with);
+}
+
 bool HoistToDeclBefore::Prepare(const sem::ValueExpression* before_expr) {
     return state_->Prepare(before_expr);
 }
diff --git a/src/tint/transform/utils/hoist_to_decl_before.h b/src/tint/transform/utils/hoist_to_decl_before.h
index 81c255f..c662b1e 100644
--- a/src/tint/transform/utils/hoist_to_decl_before.h
+++ b/src/tint/transform/utils/hoist_to_decl_before.h
@@ -76,6 +76,20 @@
     /// @return true on success
     bool InsertBefore(const sem::Statement* before_stmt, const StmtBuilder& builder);
 
+    /// Replaces the statement @p what with the statement @p stmt, possibly converting 'for-loop's
+    /// to 'loop's if necessary.
+    /// @param what the statement to replace
+    /// @param with the replacement statement
+    /// @return true on success
+    bool Replace(const sem::Statement* what, const ast::Statement* with);
+
+    /// Replaces the statement @p what with the statement returned by @p stmt, possibly converting
+    /// 'for-loop's to 'loop's if necessary.
+    /// @param what the statement to replace
+    /// @param with the replacement statement builder
+    /// @return true on success
+    bool Replace(const sem::Statement* what, const StmtBuilder& with);
+
     /// Use to signal that we plan on hoisting a decl before `before_expr`. This
     /// will convert 'for-loop's to 'loop's and 'else-if's to 'else {if}'s if
     /// needed.
diff --git a/src/tint/transform/utils/hoist_to_decl_before_test.cc b/src/tint/transform/utils/hoist_to_decl_before_test.cc
index e8d92ed..0abb809 100644
--- a/src/tint/transform/utils/hoist_to_decl_before_test.cc
+++ b/src/tint/transform/utils/hoist_to_decl_before_test.cc
@@ -82,8 +82,16 @@
 
     auto* expect = R"(
 fn f() {
-  var tint_symbol : i32 = 1i;
-  for(var a = tint_symbol; true; ) {
+  {
+    var tint_symbol : i32 = 1i;
+    var a = tint_symbol;
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+      }
+    }
   }
 }
 )";
@@ -545,8 +553,16 @@
 }
 
 fn f() {
-  foo();
-  for(var a = 1i; true; ) {
+  {
+    foo();
+    var a = 1i;
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+      }
+    }
   }
 }
 )";
@@ -584,8 +600,16 @@
 }
 
 fn f() {
-  foo();
-  for(var a = 1i; true; ) {
+  {
+    foo();
+    var a = 1i;
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+      }
+    }
   }
 }
 )";
@@ -853,5 +877,264 @@
     EXPECT_EQ(expect, str(cloned));
 }
 
+TEST_F(HoistToDeclBeforeTest, Replace_Block) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     var a = 1i;
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{var});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(var);
+    auto* new_stmt = ctx.dst->CallStmt(ctx.dst->Call("foo"));
+    hoistToDeclBefore.Replace(target_stmt, new_stmt);
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  foo();
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
+TEST_F(HoistToDeclBeforeTest, Replace_Block_Function) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     var a = 1i;
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{var});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(var);
+    hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  foo();
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
+TEST_F(HoistToDeclBeforeTest, Replace_ForLoopInit) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     for(var a = 1i; true;) {
+    //     }
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{s});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(var);
+    auto* new_stmt = ctx.dst->CallStmt(ctx.dst->Call("foo"));
+    hoistToDeclBefore.Replace(target_stmt, new_stmt);
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  {
+    foo();
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+      }
+    }
+  }
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
+TEST_F(HoistToDeclBeforeTest, Replace_ForLoopInit_Function) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     for(var a = 1i; true;) {
+    //     }
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    auto* s = b.For(var, b.Expr(true), nullptr, b.Block());
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{s});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(var);
+    hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  {
+    foo();
+    loop {
+      if (!(true)) {
+        break;
+      }
+      {
+      }
+    }
+  }
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
+TEST_F(HoistToDeclBeforeTest, Replace_ForLoopCont) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     var a = 1i;
+    //     for(; true; a+=1i) {
+    //     }
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    auto* cont = b.CompoundAssign("a", b.Expr(1_i), ast::BinaryOp::kAdd);
+    auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{var, s});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(cont->As<ast::Statement>());
+    auto* new_stmt = ctx.dst->CallStmt(ctx.dst->Call("foo"));
+    hoistToDeclBefore.Replace(target_stmt, new_stmt);
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  var a = 1i;
+  loop {
+    if (!(true)) {
+      break;
+    }
+    {
+    }
+
+    continuing {
+      foo();
+    }
+  }
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
+TEST_F(HoistToDeclBeforeTest, Replace_ForLoopCont_Function) {
+    // fn foo() {
+    // }
+    // fn f() {
+    //     var a = 1i;
+    //     for(; true; a+=1i) {
+    //     }
+    // }
+    ProgramBuilder b;
+    b.Func("foo", utils::Empty, b.ty.void_(), utils::Empty);
+    auto* var = b.Decl(b.Var("a", b.Expr(1_i)));
+    auto* cont = b.CompoundAssign("a", b.Expr(1_i), ast::BinaryOp::kAdd);
+    auto* s = b.For(nullptr, b.Expr(true), cont, b.Block());
+    b.Func("f", utils::Empty, b.ty.void_(), utils::Vector{var, s});
+
+    Program original(std::move(b));
+    ProgramBuilder cloned_b;
+    CloneContext ctx(&cloned_b, &original);
+
+    HoistToDeclBefore hoistToDeclBefore(ctx);
+    auto* target_stmt = ctx.src->Sem().Get(cont->As<ast::Statement>());
+    hoistToDeclBefore.Replace(target_stmt, [&] { return ctx.dst->CallStmt(ctx.dst->Call("foo")); });
+
+    ctx.Clone();
+    Program cloned(std::move(cloned_b));
+
+    auto* expect = R"(
+fn foo() {
+}
+
+fn f() {
+  var a = 1i;
+  loop {
+    if (!(true)) {
+      break;
+    }
+    {
+    }
+
+    continuing {
+      foo();
+    }
+  }
+}
+)";
+
+    EXPECT_EQ(expect, str(cloned));
+}
+
 }  // namespace
 }  // namespace tint::transform
diff --git a/src/tint/transform/var_for_dynamic_index_test.cc b/src/tint/transform/var_for_dynamic_index_test.cc
index 80a79d5..2130ae1 100644
--- a/src/tint/transform/var_for_dynamic_index_test.cc
+++ b/src/tint/transform/var_for_dynamic_index_test.cc
@@ -126,9 +126,14 @@
 fn f() {
   var i : i32;
   let p = array<array<i32, 2>, 2>(array<i32, 2>(1, 2), array<i32, 2>(3, 4));
-  var var_for_index : array<array<i32, 2u>, 2u> = p;
-  for(let x = var_for_index[i]; ; ) {
-    break;
+  {
+    var var_for_index : array<array<i32, 2u>, 2u> = p;
+    let x = var_for_index[i];
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
@@ -154,9 +159,14 @@
 fn f() {
   var i : i32;
   let p = mat2x2(1.0, 2.0, 3.0, 4.0);
-  var var_for_index : mat2x2<f32> = p;
-  for(let x = var_for_index[i]; ; ) {
-    break;
+  {
+    var var_for_index : mat2x2<f32> = p;
+    let x = var_for_index[i];
+    loop {
+      {
+        break;
+      }
+    }
   }
 }
 )";
diff --git a/src/tint/transform/vertex_pulling.cc b/src/tint/transform/vertex_pulling.cc
index 09a68bb..e07dcfe 100644
--- a/src/tint/transform/vertex_pulling.cc
+++ b/src/tint/transform/vertex_pulling.cc
@@ -56,8 +56,8 @@
     kFloat,  // unsigned normalized, signed normalized, and float
 };
 
-/// Writes the VertexFormat to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the VertexFormat to the stream.
+/// @param out the stream to write to
 /// @param format the VertexFormat to write
 /// @returns out so calls can be chained
 utils::StringStream& operator<<(utils::StringStream& out, VertexFormat format) {
diff --git a/src/tint/type/sampler_kind.cc b/src/tint/type/sampler_kind.cc
index 4e20700..0076287 100644
--- a/src/tint/type/sampler_kind.cc
+++ b/src/tint/type/sampler_kind.cc
@@ -16,7 +16,7 @@
 
 namespace tint::type {
 
-std::ostream& operator<<(std::ostream& out, SamplerKind kind) {
+utils::StringStream& operator<<(utils::StringStream& out, SamplerKind kind) {
     switch (kind) {
         case SamplerKind::kSampler:
             out << "sampler";
diff --git a/src/tint/type/sampler_kind.h b/src/tint/type/sampler_kind.h
index 3522fda..b5b01b9 100644
--- a/src/tint/type/sampler_kind.h
+++ b/src/tint/type/sampler_kind.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_TYPE_SAMPLER_KIND_H_
 #define SRC_TINT_TYPE_SAMPLER_KIND_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::type {
 
@@ -27,10 +27,10 @@
     kComparisonSampler
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param kind the SamplerKind
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, SamplerKind kind);
+/// @return the stream so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, SamplerKind kind);
 
 }  // namespace tint::type
 
diff --git a/src/tint/type/texture_dimension.cc b/src/tint/type/texture_dimension.cc
index 4b4fea9..c450516 100644
--- a/src/tint/type/texture_dimension.cc
+++ b/src/tint/type/texture_dimension.cc
@@ -16,7 +16,7 @@
 
 namespace tint::type {
 
-std::ostream& operator<<(std::ostream& out, type::TextureDimension dim) {
+utils::StringStream& operator<<(utils::StringStream& out, type::TextureDimension dim) {
     switch (dim) {
         case type::TextureDimension::kNone:
             out << "None";
diff --git a/src/tint/type/texture_dimension.h b/src/tint/type/texture_dimension.h
index 0da2b05..a315b7e 100644
--- a/src/tint/type/texture_dimension.h
+++ b/src/tint/type/texture_dimension.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_TYPE_TEXTURE_DIMENSION_H_
 #define SRC_TINT_TYPE_TEXTURE_DIMENSION_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::type {
 
@@ -37,10 +37,10 @@
     kCubeArray,
 };
 
-/// @param out the std::ostream to write to
+/// @param out the stream to write to
 /// @param dim the type::TextureDimension
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, type::TextureDimension dim);
+/// @return the stream so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, type::TextureDimension dim);
 
 }  // namespace tint::type
 
diff --git a/src/tint/utils/enum_set.h b/src/tint/utils/enum_set.h
index 9b75ba0..575868b 100644
--- a/src/tint/utils/enum_set.h
+++ b/src/tint/utils/enum_set.h
@@ -17,10 +17,11 @@
 
 #include <cstdint>
 #include <functional>
-#include <ostream>
 #include <type_traits>
 #include <utility>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::utils {
 
 /// EnumSet is a set of enum values.
@@ -216,12 +217,12 @@
     uint64_t set = 0;
 };
 
-/// Writes the EnumSet to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the EnumSet to the stream.
+/// @param out the stream to write to
 /// @param set the EnumSet to write
 /// @returns out so calls can be chained
 template <typename ENUM>
-inline std::ostream& operator<<(std::ostream& out, EnumSet<ENUM> set) {
+inline utils::StringStream& operator<<(utils::StringStream& out, EnumSet<ENUM> set) {
     out << "{";
     bool first = true;
     for (auto e : set) {
diff --git a/src/tint/utils/enum_set_test.cc b/src/tint/utils/enum_set_test.cc
index a6bb169..6e9b59c 100644
--- a/src/tint/utils/enum_set_test.cc
+++ b/src/tint/utils/enum_set_test.cc
@@ -14,7 +14,6 @@
 
 #include "src/tint/utils/enum_set.h"
 
-#include <sstream>
 #include <vector>
 
 #include "gmock/gmock.h"
@@ -27,7 +26,7 @@
 
 enum class E { A = 0, B = 3, C = 7 };
 
-std::ostream& operator<<(std::ostream& out, E e) {
+utils::StringStream& operator<<(utils::StringStream& out, E e) {
     switch (e) {
         case E::A:
             return out << "A";
diff --git a/src/tint/utils/hashmap_base.h b/src/tint/utils/hashmap_base.h
index 959bebe..12c1ed2 100644
--- a/src/tint/utils/hashmap_base.h
+++ b/src/tint/utils/hashmap_base.h
@@ -91,12 +91,12 @@
     operator KeyValue<KEY, VALUE>() const { return {key, value}; }
 };
 
-/// Writes the KeyValue to the std::ostream.
-/// @param out the std::ostream to write to
+/// Writes the KeyValue to the stream.
+/// @param out the stream to write to
 /// @param key_value the KeyValue to write
 /// @returns out so calls can be chained
 template <typename KEY, typename VALUE>
-std::ostream& operator<<(std::ostream& out, const KeyValue<KEY, VALUE>& key_value) {
+utils::StringStream& operator<<(utils::StringStream& out, const KeyValue<KEY, VALUE>& key_value) {
     return out << "[" << key_value.key << ": " << key_value.value << "]";
 }
 
diff --git a/src/tint/utils/io/command_windows.cc b/src/tint/utils/io/command_windows.cc
index 31d0308..fc5df2c 100644
--- a/src/tint/utils/io/command_windows.cc
+++ b/src/tint/utils/io/command_windows.cc
@@ -17,7 +17,6 @@
 #define WIN32_LEAN_AND_MEAN 1
 #include <Windows.h>
 #include <dbghelp.h>
-#include <sstream>
 #include <string>
 
 #include "src/tint/utils/defer.h"
diff --git a/src/tint/utils/io/tmpfile.h b/src/tint/utils/io/tmpfile.h
index 7949a37..016ac08 100644
--- a/src/tint/utils/io/tmpfile.h
+++ b/src/tint/utils/io/tmpfile.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_UTILS_IO_TMPFILE_H_
 #define SRC_TINT_UTILS_IO_TMPFILE_H_
 
-#include <sstream>
 #include <string>
 
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/utils/math.h b/src/tint/utils/math.h
index d882b34..c10a2c9 100644
--- a/src/tint/utils/math.h
+++ b/src/tint/utils/math.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_UTILS_MATH_H_
 #define SRC_TINT_UTILS_MATH_H_
 
-#include <sstream>
 #include <string>
 #include <type_traits>
 
diff --git a/src/tint/utils/result.h b/src/tint/utils/result.h
index 41fda19..2b433fb 100644
--- a/src/tint/utils/result.h
+++ b/src/tint/utils/result.h
@@ -15,11 +15,11 @@
 #ifndef SRC_TINT_UTILS_RESULT_H_
 #define SRC_TINT_UTILS_RESULT_H_
 
-#include <ostream>
 #include <utility>
 #include <variant>
 
 #include "src/tint/debug.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 
@@ -145,12 +145,12 @@
     std::variant<std::monostate, SUCCESS_TYPE, FAILURE_TYPE> value;
 };
 
-/// Writes the result to the ostream.
-/// @param out the std::ostream to write to
+/// Writes the result to the stream.
+/// @param out the stream to write to
 /// @param res the result
-/// @return the std::ostream so calls can be chained
+/// @return the stream so calls can be chained
 template <typename SUCCESS, typename FAILURE>
-inline std::ostream& operator<<(std::ostream& out, Result<SUCCESS, FAILURE> res) {
+inline utils::StringStream& operator<<(utils::StringStream& out, Result<SUCCESS, FAILURE> res) {
     return res ? (out << "success: " << res.Get()) : (out << "failure: " << res.Failure());
 }
 
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index e77e637..5c7255d 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_UTILS_STRING_H_
 #define SRC_TINT_UTILS_STRING_H_
 
-#include <sstream>
 #include <string>
 #include <variant>
 
@@ -40,7 +39,7 @@
 }
 
 /// @param value the value to be printed as a string
-/// @returns value printed as a string via the std::ostream `<<` operator
+/// @returns value printed as a string via the stream `<<` operator
 template <typename T>
 std::string ToString(const T& value) {
     utils::StringStream s;
@@ -49,7 +48,7 @@
 }
 
 /// @param value the variant to be printed as a string
-/// @returns value printed as a string via the std::ostream `<<` operator
+/// @returns value printed as a string via the stream `<<` operator
 template <typename... TYs>
 std::string ToString(const std::variant<TYs...>& value) {
     utils::StringStream s;
diff --git a/src/tint/utils/string_stream.cc b/src/tint/utils/string_stream.cc
index 2d1ed44..f0dbe0c 100644
--- a/src/tint/utils/string_stream.cc
+++ b/src/tint/utils/string_stream.cc
@@ -25,3 +25,31 @@
 StringStream::~StringStream() = default;
 
 }  // namespace tint::utils
+
+namespace tint::text {
+
+utils::StringStream& operator<<(utils::StringStream& out, CodePoint code_point) {
+    if (code_point < 0x7f) {
+        // See https://en.cppreference.com/w/cpp/language/escape
+        switch (code_point) {
+            case '\a':
+                return out << R"('\a')";
+            case '\b':
+                return out << R"('\b')";
+            case '\f':
+                return out << R"('\f')";
+            case '\n':
+                return out << R"('\n')";
+            case '\r':
+                return out << R"('\r')";
+            case '\t':
+                return out << R"('\t')";
+            case '\v':
+                return out << R"('\v')";
+        }
+        return out << "'" << static_cast<char>(code_point) << "'";
+    }
+    return out << "'U+" << std::hex << code_point.value << "'";
+}
+
+}  // namespace tint::text
diff --git a/src/tint/utils/string_stream.h b/src/tint/utils/string_stream.h
index 893d2b9..4703095 100644
--- a/src/tint/utils/string_stream.h
+++ b/src/tint/utils/string_stream.h
@@ -16,12 +16,15 @@
 #define SRC_TINT_UTILS_STRING_STREAM_H_
 
 #include <functional>
+#include <iomanip>
 #include <iterator>
 #include <limits>
 #include <sstream>
 #include <string>
 #include <utility>
 
+#include "src/tint/text/unicode.h"
+
 namespace tint::utils {
 
 /// Stringstream wrapper which automatically resets the locale and sets floating point emission
@@ -44,23 +47,57 @@
     /// @param value the value to emit
     /// @returns a reference to this
     template <typename T,
-              typename std::enable_if<!std::is_floating_point<T>::value>::type* = nullptr>
-    StringStream& operator<<(const T& value) {
-        sstream_ << value;
+              typename std::enable_if_t<std::is_integral_v<std::decay_t<T>>, bool> = true>
+    StringStream& operator<<(T&& value) {
+        return EmitValue(std::forward<T>(value));
+    }
+
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    StringStream& operator<<(const char* value) { return EmitValue(value); }
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    StringStream& operator<<(const std::string& value) { return EmitValue(value); }
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    StringStream& operator<<(std::string_view value) { return EmitValue(value); }
+
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    StringStream& operator<<(const void* value) { return EmitValue(value); }
+
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    template <typename T,
+              typename std::enable_if_t<std::is_floating_point_v<std::decay_t<T>>, bool> = true>
+    StringStream& operator<<(T&& value) {
+        return EmitFloat(std::forward<T>(value));
+    }
+
+    /// Emit `value` to the stream
+    /// @param value the value to emit
+    /// @returns a reference to this
+    template <typename T>
+    StringStream& EmitValue(T&& value) {
+        sstream_ << std::forward<T>(value);
         return *this;
     }
 
     /// Emit `value` to the stream
     /// @param value the value to emit
     /// @returns a reference to this
-    template <typename T,
-              typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
-    StringStream& operator<<(const T& value) {
+    template <typename T>
+    StringStream& EmitFloat(const T& value) {
         // Try printing the float in fixed point, with a smallish limit on the precision
         std::stringstream fixed;
         fixed.flags(fixed.flags() | std::ios_base::showpoint | std::ios_base::fixed);
         fixed.imbue(std::locale::classic());
-        fixed.precision(9);
+        fixed.precision(20);
         fixed << value;
 
         std::string str = fixed.str();
@@ -71,6 +108,7 @@
         double roundtripped;
         fixed >> roundtripped;
 
+        // Strip trailing zeros from the number.
         auto float_equal_no_warning = std::equal_to<T>();
         if (float_equal_no_warning(value, static_cast<T>(roundtripped))) {
             while (str.length() >= 2 && str[str.size() - 1] == '0' && str[str.size() - 2] != '.') {
@@ -111,6 +149,57 @@
         return *this;
     }
 
+    /// The callback to emit a `std::hex` to the stream
+    using StdHex = std::ios_base& (*)(std::ios_base&);
+
+    /// @param manipulator the callback to emit too
+    /// @returns a reference to this
+    StringStream& operator<<(StdHex manipulator) {
+        // call the function, and return it's value
+        manipulator(sstream_);
+        return *this;
+    }
+
+    /// @param value the value to emit
+    /// @returns a reference to this
+    template <typename T,
+              typename std::enable_if<std::is_same<decltype(std::setw(std::declval<int>())),
+                                                   typename std::decay<T>::type>::value,
+                                      int>::type = 0>
+    StringStream& operator<<(T&& value) {
+        // call the function, and return it's value
+        sstream_ << std::forward<T>(value);
+        return *this;
+    }
+
+    // On MSVC the type of `std::setw` and `std::setprecision` are the same. Can't check for
+    // _MSC_VER because this is also set by clang-cl on windows.
+#if defined(__GNUC__) || defined(__clang__)
+    /// @param value the value to emit
+    /// @returns a reference to this
+    template <typename T,
+              typename std::enable_if<std::is_same<decltype(std::setprecision(std::declval<int>())),
+                                                   typename std::decay<T>::type>::value,
+                                      int>::type = 0>
+    StringStream& operator<<(T&& value) {
+        // call the function, and return it's value
+        sstream_ << std::forward<T>(value);
+        return *this;
+    }
+#endif  // defined(_MSC_VER)
+
+    /// @param value the value to emit
+    /// @returns a reference to this
+    template <typename T,
+              typename std::enable_if<std::is_same<decltype(std::setfill(std::declval<char>())),
+                                                   typename std::decay<T>::type>::value,
+                                      char>::type = 0>
+    StringStream& operator<<(T&& value) {
+        // call the function, and return it's value
+        sstream_ << std::forward<T>(value);
+        return *this;
+    }
+
     /// @returns the string contents of the stream
     std::string str() const { return sstream_.str(); }
 
@@ -120,4 +209,14 @@
 
 }  // namespace tint::utils
 
+namespace tint::text {
+
+/// Writes the CodePoint to the stream.
+/// @param out the stream to write to
+/// @param codepoint the CodePoint to write
+/// @returns out so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, CodePoint codepoint);
+
+}  // namespace tint::text
+
 #endif  // SRC_TINT_UTILS_STRING_STREAM_H_
diff --git a/src/tint/utils/string_stream_test.cc b/src/tint/utils/string_stream_test.cc
index eebefd6..5f99026 100644
--- a/src/tint/utils/string_stream_test.cc
+++ b/src/tint/utils/string_stream_test.cc
@@ -88,22 +88,22 @@
     {
         StringStream s;
         s << 1e-8f;
-        EXPECT_EQ(s.str(), "0.00000001");
+        EXPECT_EQ(s.str(), "0.00000000999999993923");
     }
     {
         StringStream s;
         s << 1e-9f;
-        EXPECT_EQ(s.str(), "0.000000001");
+        EXPECT_EQ(s.str(), "0.00000000099999997172");
     }
     {
         StringStream s;
         s << 1e-10f;
-        EXPECT_EQ(s.str(), "1.00000001e-10");
+        EXPECT_EQ(s.str(), "0.00000000010000000134");
     }
     {
         StringStream s;
         s << 1e-20f;
-        EXPECT_EQ(s.str(), "9.99999968e-21");
+        EXPECT_EQ(s.str(), "0.00000000000000000001");
     }
 }
 
diff --git a/src/tint/utils/vector.h b/src/tint/utils/vector.h
index e39854a..ed4fcf1 100644
--- a/src/tint/utils/vector.h
+++ b/src/tint/utils/vector.h
@@ -20,7 +20,6 @@
 #include <algorithm>
 #include <iterator>
 #include <new>
-#include <ostream>
 #include <utility>
 #include <vector>
 
@@ -28,6 +27,7 @@
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/slice.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 
@@ -740,11 +740,11 @@
 }
 
 /// Prints the vector @p vec to @p o
-/// @param o the std::ostream to write to
+/// @param o the stream to write to
 /// @param vec the vector
-/// @return the std::ostream so calls can be chained
+/// @return the stream so calls can be chained
 template <typename T, size_t N>
-inline std::ostream& operator<<(std::ostream& o, const utils::Vector<T, N>& vec) {
+inline utils::StringStream& operator<<(utils::StringStream& o, const utils::Vector<T, N>& vec) {
     o << "[";
     bool first = true;
     for (auto& el : vec) {
@@ -759,11 +759,11 @@
 }
 
 /// Prints the vector @p vec to @p o
-/// @param o the std::ostream to write to
+/// @param o the stream to write to
 /// @param vec the vector reference
-/// @return the std::ostream so calls can be chained
+/// @return the stream so calls can be chained
 template <typename T>
-inline std::ostream& operator<<(std::ostream& o, utils::VectorRef<T> vec) {
+inline utils::StringStream& operator<<(utils::StringStream& o, utils::VectorRef<T> vec) {
     o << "[";
     bool first = true;
     for (auto& el : vec) {
diff --git a/src/tint/writer/float_to_string.cc b/src/tint/writer/float_to_string.cc
index 0ce4954..fdf4675 100644
--- a/src/tint/writer/float_to_string.cc
+++ b/src/tint/writer/float_to_string.cc
@@ -19,7 +19,6 @@
 #include <functional>
 #include <iomanip>
 #include <limits>
-#include <sstream>
 
 #include "src/tint/debug.h"
 #include "src/tint/utils/string_stream.h"
diff --git a/src/tint/writer/float_to_string_test.cc b/src/tint/writer/float_to_string_test.cc
index 901334e..eb70369 100644
--- a/src/tint/writer/float_to_string_test.cc
+++ b/src/tint/writer/float_to_string_test.cc
@@ -73,10 +73,10 @@
 }
 
 TEST(FloatToStringTest, Precision) {
-    EXPECT_EQ(FloatToString(1e-8f), "0.00000001");
-    EXPECT_EQ(FloatToString(1e-9f), "0.000000001");
-    EXPECT_EQ(FloatToString(1e-10f), "1.00000001e-10");
-    EXPECT_EQ(FloatToString(1e-20f), "9.99999968e-21");
+    EXPECT_EQ(FloatToString(1e-8f), "0.00000000999999993923");
+    EXPECT_EQ(FloatToString(1e-9f), "0.00000000099999997172");
+    EXPECT_EQ(FloatToString(1e-10f), "0.00000000010000000134");
+    EXPECT_EQ(FloatToString(1e-20f), "0.00000000000000000001");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -179,15 +179,15 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 TEST(DoubleToStringTest, Zero) {
-    EXPECT_EQ(DoubleToString(0.0), "0.0");
+    EXPECT_EQ(DoubleToString(0.000000000), "0.0");
 }
 
 TEST(DoubleToStringTest, One) {
-    EXPECT_EQ(DoubleToString(1.0), "1.0");
+    EXPECT_EQ(DoubleToString(1.000000000), "1.0");
 }
 
 TEST(DoubleToStringTest, MinusOne) {
-    EXPECT_EQ(DoubleToString(-1.0), "-1.0");
+    EXPECT_EQ(DoubleToString(-1.000000000), "-1.0");
 }
 
 TEST(DoubleToStringTest, Billion) {
@@ -231,8 +231,8 @@
 TEST(DoubleToStringTest, Precision) {
     EXPECT_EQ(DoubleToString(1e-8), "0.00000001");
     EXPECT_EQ(DoubleToString(1e-9), "0.000000001");
-    EXPECT_EQ(DoubleToString(1e-10), "1e-10");
-    EXPECT_EQ(DoubleToString(1e-15), "1.0000000000000001e-15");
+    EXPECT_EQ(DoubleToString(1e-10), "0.0000000001");
+    EXPECT_EQ(DoubleToString(1e-15), "0.000000000000001");
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/writer/glsl/generator.h b/src/tint/writer/glsl/generator.h
index 4d31bdf..2bb4b03 100644
--- a/src/tint/writer/glsl/generator.h
+++ b/src/tint/writer/glsl/generator.h
@@ -63,6 +63,9 @@
     /// transform
     std::unordered_map<sem::BindingPoint, builtin::Access> access_controls;
 
+    /// Set to `true` to disable software robustness that prevents out-of-bounds accesses.
+    bool disable_robustness = false;
+
     /// If true, then validation will be disabled for binding point collisions
     /// generated by the BindingRemapper transform
     bool allow_collisions = false;
@@ -75,6 +78,13 @@
 
     /// The GLSL version to emit
     Version version;
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(disable_robustness,
+                 allow_collisions,
+                 disable_workgroup_init,
+                 generate_external_texture_bindings,
+                 version);
 };
 
 /// The result produced when generating GLSL.
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 46428b0..24c6386 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -57,6 +57,7 @@
 #include "src/tint/transform/promote_side_effects_to_decl.h"
 #include "src/tint/transform/remove_phonies.h"
 #include "src/tint/transform/renamer.h"
+#include "src/tint/transform/robustness.h"
 #include "src/tint/transform/simplify_pointers.h"
 #include "src/tint/transform/single_entry_point.h"
 #include "src/tint/transform/std140.h"
@@ -149,6 +150,33 @@
     // ExpandCompoundAssignment must come before BuiltinPolyfill
     manager.Add<transform::ExpandCompoundAssignment>();
 
+    if (!entry_point.empty()) {
+        manager.Add<transform::SingleEntryPoint>();
+        data.Add<transform::SingleEntryPoint::Config>(entry_point);
+    }
+    manager.Add<transform::Renamer>();
+    data.Add<transform::Renamer::Config>(transform::Renamer::Target::kGlslKeywords,
+                                         /* preserve_unicode */ false);
+
+    manager.Add<transform::PreservePadding>();  // Must come before DirectVariableAccess
+
+    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
+    manager.Add<transform::DirectVariableAccess>();
+
+    manager.Add<transform::PromoteSideEffectsToDecl>();
+
+    if (!options.disable_robustness) {
+        // Robustness must come before BuiltinPolyfill
+        manager.Add<transform::Robustness>();
+    }
+
+    if (options.generate_external_texture_bindings) {
+        // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
+        auto new_bindings_map = writer::GenerateExternalTextureBindings(in);
+        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
+        manager.Add<transform::MultiplanarExternalTexture>();
+    }
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
@@ -169,26 +197,16 @@
         manager.Add<transform::BuiltinPolyfill>();
     }
 
-    if (!entry_point.empty()) {
-        manager.Add<transform::SingleEntryPoint>();
-        data.Add<transform::SingleEntryPoint::Config>(entry_point);
-    }
-    manager.Add<transform::Renamer>();
-    data.Add<transform::Renamer::Config>(transform::Renamer::Target::kGlslKeywords,
-                                         /* preserve_unicode */ false);
-
-    manager.Add<transform::PreservePadding>();  // Must come before DirectVariableAccess
-
-    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
-    manager.Add<transform::DirectVariableAccess>();
-
     if (!options.disable_workgroup_init) {
         // ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
         // ZeroInitWorkgroupMemory may inject new builtin parameters.
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
+
+    // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<transform::CanonicalizeEntryPointIO>();
-    manager.Add<transform::PromoteSideEffectsToDecl>();
+
+    // PadStructs must come after CanonicalizeEntryPointIO
     manager.Add<transform::PadStructs>();
 
     // DemoteToHelper must come after PromoteSideEffectsToDecl and ExpandCompoundAssignment.
@@ -196,12 +214,6 @@
 
     manager.Add<transform::RemovePhonies>();
 
-    if (options.generate_external_texture_bindings) {
-        auto new_bindings_map = writer::GenerateExternalTextureBindings(in);
-        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
-    }
-    manager.Add<transform::MultiplanarExternalTexture>();
-
     data.Add<transform::CombineSamplers::BindingInfo>(options.binding_map,
                                                       options.placeholder_binding_point);
     manager.Add<transform::CombineSamplers>();
diff --git a/src/tint/writer/glsl/generator_impl_binary_test.cc b/src/tint/writer/glsl/generator_impl_binary_test.cc
index 475ae46..1c03530 100644
--- a/src/tint/writer/glsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/glsl/generator_impl_binary_test.cc
@@ -29,7 +29,9 @@
     ast::BinaryOp op;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
-    out << data.op;
+    utils::StringStream str;
+    str << data.op;
+    out << str.str();
     return out;
 }
 
diff --git a/src/tint/writer/glsl/generator_impl_builtin_test.cc b/src/tint/writer/glsl/generator_impl_builtin_test.cc
index 3d14a68..d281cdb 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_test.cc
@@ -932,7 +932,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082323f;
+  return param_0 * 57.29577951308232286465f;
 }
 
 
@@ -960,7 +960,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_degrees(vec3 param_0) {
-  return param_0 * 57.295779513082323f;
+  return param_0 * 57.29577951308232286465f;
 }
 
 
@@ -991,7 +991,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 float16_t tint_degrees(float16_t param_0) {
-  return param_0 * 57.295779513082323hf;
+  return param_0 * 57.29577951308232286465hf;
 }
 
 
@@ -1022,7 +1022,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 f16vec3 tint_degrees(f16vec3 param_0) {
-  return param_0 * 57.295779513082323hf;
+  return param_0 * 57.29577951308232286465hf;
 }
 
 
@@ -1050,7 +1050,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295f;
+  return param_0 * 0.01745329251994329547f;
 }
 
 
@@ -1078,7 +1078,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_radians(vec3 param_0) {
-  return param_0 * 0.017453292519943295f;
+  return param_0 * 0.01745329251994329547f;
 }
 
 
@@ -1109,7 +1109,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 float16_t tint_radians(float16_t param_0) {
-  return param_0 * 0.017453292519943295hf;
+  return param_0 * 0.01745329251994329547hf;
 }
 
 
@@ -1140,7 +1140,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 f16vec3 tint_radians(f16vec3 param_0) {
-  return param_0 * 0.017453292519943295hf;
+  return param_0 * 0.01745329251994329547hf;
 }
 
 
diff --git a/src/tint/writer/glsl/generator_impl_constructor_test.cc b/src/tint/writer/glsl/generator_impl_constructor_test.cc
index 2f4b3d0..02d148c 100644
--- a/src/tint/writer/glsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_constructor_test.cc
@@ -79,7 +79,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("-0.000012f"));
+    EXPECT_THAT(gen.result(), HasSubstr("-0.00001200000042445026f"));
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_F16) {
@@ -90,7 +90,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("-0.00119972229hf"));
+    EXPECT_THAT(gen.result(), HasSubstr("-0.0011997222900390625hf"));
 }
 
 TEST_F(GlslGeneratorImplTest_Constructor, Type_Bool) {
diff --git a/src/tint/writer/glsl/generator_impl_import_test.cc b/src/tint/writer/glsl/generator_impl_import_test.cc
index 3201712..bf546ac 100644
--- a/src/tint/writer/glsl/generator_impl_import_test.cc
+++ b/src/tint/writer/glsl/generator_impl_import_test.cc
@@ -98,8 +98,10 @@
 
     utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
-    EXPECT_EQ(out.str(),
-              std::string(param.glsl_name) + "(vec3(0.100000001f, 0.200000003f, 0.300000012f))");
+    EXPECT_EQ(
+        out.str(),
+        std::string(param.glsl_name) +
+            "(vec3(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f))");
 }
 INSTANTIATE_TEST_SUITE_P(GlslGeneratorImplTest_Import,
                          GlslImportData_SingleVectorParamTest,
diff --git a/src/tint/writer/glsl/generator_impl_loop_test.cc b/src/tint/writer/glsl/generator_impl_loop_test.cc
index 0979f53..2ee83b8 100644
--- a/src/tint/writer/glsl/generator_impl_loop_test.cc
+++ b/src/tint/writer/glsl/generator_impl_loop_test.cc
@@ -134,7 +134,7 @@
 
 TEST_F(GlslGeneratorImplTest_Loop, Emit_LoopWithVarUsedInContinuing) {
     // loop {
-    //   var lhs : f32 = 2.4;
+    //   var lhs : f32 = 2.5;
     //   var other : f32;
     //   break;
     //   continuing {
@@ -144,7 +144,7 @@
 
     GlobalVar("rhs", ty.f32(), builtin::AddressSpace::kPrivate);
 
-    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.4_f))),  //
+    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.5_f))),  //
                        Decl(Var("other", ty.f32())),             //
                        Break());
     auto* continuing = Block(Assign("lhs", "rhs"));
@@ -157,7 +157,7 @@
 
     ASSERT_TRUE(gen.EmitStatement(outer)) << gen.error();
     EXPECT_EQ(gen.result(), R"(  while (true) {
-    float lhs = 2.400000095f;
+    float lhs = 2.5f;
     float other = 0.0f;
     break;
     {
diff --git a/src/tint/writer/glsl/generator_impl_type_test.cc b/src/tint/writer/glsl/generator_impl_type_test.cc
index 2cc13f6..2535efd 100644
--- a/src/tint/writer/glsl/generator_impl_type_test.cc
+++ b/src/tint/writer/glsl/generator_impl_type_test.cc
@@ -318,7 +318,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, GlslDepthTextureData data) {
-    out << data.dim;
+    utils::StringStream s;
+    s << data.dim;
+    out << s.str();
     return out;
 }
 using GlslDepthTexturesTest = TestParamHelper<GlslDepthTextureData>;
@@ -378,7 +380,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, GlslSampledTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using GlslSampledTexturesTest = TestParamHelper<GlslSampledTextureData>;
@@ -527,7 +531,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, GlslStorageTextureData data) {
-    return out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    return out << str.str();
 }
 using GlslStorageTexturesTest = TestParamHelper<GlslStorageTextureData>;
 TEST_P(GlslStorageTexturesTest, Emit) {
diff --git a/src/tint/writer/glsl/test_helper.h b/src/tint/writer/glsl/test_helper.h
index 2f806ce..98c7d8d 100644
--- a/src/tint/writer/glsl/test_helper.h
+++ b/src/tint/writer/glsl/test_helper.h
@@ -32,6 +32,14 @@
     TestHelperBase() = default;
     ~TestHelperBase() override = default;
 
+    /// @returns the default generator options for SanitizeAndBuild(), if no explicit options are
+    /// provided.
+    static Options DefaultOptions() {
+        Options opts;
+        opts.disable_robustness = true;
+        return opts;
+    }
+
     /// Builds the program and returns a GeneratorImpl from the program.
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same GeneratorImpl without rebuilding.
@@ -60,7 +68,8 @@
     /// @param version the GLSL version
     /// @param options the GLSL backend options
     /// @return the built generator
-    GeneratorImpl& SanitizeAndBuild(Version version = Version(), const Options& options = {}) {
+    GeneratorImpl& SanitizeAndBuild(Version version = Version(),
+                                    const Options& options = DefaultOptions()) {
         if (gen_) {
             return *gen_;
         }
diff --git a/src/tint/writer/hlsl/generator.h b/src/tint/writer/hlsl/generator.h
index 80dd518..74d79ba 100644
--- a/src/tint/writer/hlsl/generator.h
+++ b/src/tint/writer/hlsl/generator.h
@@ -49,23 +49,32 @@
     /// @returns this Options
     Options& operator=(const Options&);
 
+    /// Set to `true` to disable software robustness that prevents out-of-bounds accesses.
+    bool disable_robustness = false;
+
     /// The binding point to use for information passed via root constants.
     std::optional<sem::BindingPoint> root_constant_binding_point;
+
     /// Set to `true` to disable workgroup memory zero initialization
     bool disable_workgroup_init = false;
+
     /// Set to 'true' to generates binding mappings for external textures
     bool generate_external_texture_bindings = false;
+
     /// Options used to specify a mapping of binding points to indices into a UBO
     /// from which to load buffer sizes.
     ArrayLengthFromUniformOptions array_length_from_uniform = {};
+
     /// Interstage locations actually used as inputs in the next stage of the pipeline.
     /// This is potentially used for truncating unused interstage outputs at current shader stage.
     std::bitset<16> interstage_locations;
+
     /// Set to `true` to generate polyfill for `reflect` builtin for vec2<f32>
     bool polyfill_reflect_vec2_f32 = false;
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(root_constant_binding_point,
+    TINT_REFLECT(disable_robustness,
+                 root_constant_binding_point,
                  disable_workgroup_init,
                  generate_external_texture_bindings,
                  array_length_from_uniform);
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 2bb4a9f..823e6bd 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -57,6 +57,7 @@
 #include "src/tint/transform/promote_side_effects_to_decl.h"
 #include "src/tint/transform/remove_continue_in_switch.h"
 #include "src/tint/transform/remove_phonies.h"
+#include "src/tint/transform/robustness.h"
 #include "src/tint/transform/simplify_pointers.h"
 #include "src/tint/transform/truncate_interstage_variables.h"
 #include "src/tint/transform/unshadow.h"
@@ -165,6 +166,33 @@
     // ExpandCompoundAssignment must come before BuiltinPolyfill
     manager.Add<transform::ExpandCompoundAssignment>();
 
+    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
+
+    manager.Add<transform::DirectVariableAccess>();
+
+    // LocalizeStructArrayAssignment must come after:
+    // * SimplifyPointers, because it assumes assignment to arrays in structs are
+    // done directly, not indirectly.
+    // TODO(crbug.com/tint/1340): See if we can get rid of the duplicate
+    // SimplifyPointers transform. Can't do it right now because
+    // LocalizeStructArrayAssignment introduces pointers.
+    manager.Add<transform::SimplifyPointers>();
+    manager.Add<transform::LocalizeStructArrayAssignment>();
+
+    manager.Add<transform::PromoteSideEffectsToDecl>();
+
+    if (!options.disable_robustness) {
+        // Robustness must come before BuiltinPolyfill
+        manager.Add<transform::Robustness>();
+    }
+
+    if (options.generate_external_texture_bindings) {
+        // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
+        auto new_bindings_map = GenerateExternalTextureBindings(in);
+        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
+        manager.Add<transform::MultiplanarExternalTexture>();
+    }
+
     {  // Builtin polyfills
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kFull;
@@ -189,37 +217,13 @@
         manager.Add<transform::BuiltinPolyfill>();
     }
 
-    // Build the config for the internal ArrayLengthFromUniform transform.
-    auto& array_length_from_uniform = options.array_length_from_uniform;
-    transform::ArrayLengthFromUniform::Config array_length_from_uniform_cfg(
-        array_length_from_uniform.ubo_binding);
-    array_length_from_uniform_cfg.bindpoint_to_size_index =
-        array_length_from_uniform.bindpoint_to_size_index;
-
-    if (options.generate_external_texture_bindings) {
-        auto new_bindings_map = GenerateExternalTextureBindings(in);
-        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
-    }
-    manager.Add<transform::MultiplanarExternalTexture>();
-
-    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
-
-    manager.Add<transform::DirectVariableAccess>();
-
-    // LocalizeStructArrayAssignment must come after:
-    // * SimplifyPointers, because it assumes assignment to arrays in structs are
-    // done directly, not indirectly.
-    // TODO(crbug.com/tint/1340): See if we can get rid of the duplicate
-    // SimplifyPointers transform. Can't do it right now because
-    // LocalizeStructArrayAssignment introduces pointers.
-    manager.Add<transform::SimplifyPointers>();
-    manager.Add<transform::LocalizeStructArrayAssignment>();
-
     if (!options.disable_workgroup_init) {
         // ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
         // ZeroInitWorkgroupMemory may inject new builtin parameters.
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
+
+    // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<transform::CanonicalizeEntryPointIO>();
 
     if (options.interstage_locations.any()) {
@@ -246,11 +250,17 @@
     // assumes that num_workgroups builtins only appear as struct members and are
     // only accessed directly via member accessors.
     manager.Add<transform::NumWorkgroupsFromUniform>();
-    manager.Add<transform::PromoteSideEffectsToDecl>();
     manager.Add<transform::VectorizeScalarMatrixInitializers>();
     manager.Add<transform::SimplifyPointers>();
     manager.Add<transform::RemovePhonies>();
 
+    // Build the config for the internal ArrayLengthFromUniform transform.
+    auto& array_length_from_uniform = options.array_length_from_uniform;
+    transform::ArrayLengthFromUniform::Config array_length_from_uniform_cfg(
+        array_length_from_uniform.ubo_binding);
+    array_length_from_uniform_cfg.bindpoint_to_size_index =
+        array_length_from_uniform.bindpoint_to_size_index;
+
     // DemoteToHelper must come after CanonicalizeEntryPointIO, PromoteSideEffectsToDecl, and
     // ExpandCompoundAssignment.
     // TODO(crbug.com/tint/1752): This is only necessary when FXC is being used.
diff --git a/src/tint/writer/hlsl/generator_impl_binary_test.cc b/src/tint/writer/hlsl/generator_impl_binary_test.cc
index 630fc09..7221e1a 100644
--- a/src/tint/writer/hlsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_binary_test.cc
@@ -32,7 +32,9 @@
     Types valid_for = Types::All;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
-    out << data.op;
+    utils::StringStream str;
+    str << data.op;
+    out << str.str();
     return out;
 }
 
diff --git a/src/tint/writer/hlsl/generator_impl_builtin_test.cc b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
index 229213b..5054327 100644
--- a/src/tint/writer/hlsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
@@ -812,7 +812,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 [numthreads(1, 1, 1)]
@@ -833,7 +833,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float3 tint_degrees(float3 param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 [numthreads(1, 1, 1)]
@@ -856,7 +856,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float16_t tint_degrees(float16_t param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 [numthreads(1, 1, 1)]
@@ -879,7 +879,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(vector<float16_t, 3> tint_degrees(vector<float16_t, 3> param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 [numthreads(1, 1, 1)]
@@ -900,7 +900,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 [numthreads(1, 1, 1)]
@@ -921,7 +921,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float3 tint_radians(float3 param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 [numthreads(1, 1, 1)]
@@ -944,7 +944,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float16_t tint_radians(float16_t param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 [numthreads(1, 1, 1)]
@@ -967,7 +967,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(vector<float16_t, 3> tint_radians(vector<float16_t, 3> param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 [numthreads(1, 1, 1)]
diff --git a/src/tint/writer/hlsl/generator_impl_constructor_test.cc b/src/tint/writer/hlsl/generator_impl_constructor_test.cc
index 6cd264e..d285cd6 100644
--- a/src/tint/writer/hlsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_constructor_test.cc
@@ -79,7 +79,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("-0.000012f"));
+    EXPECT_THAT(gen.result(), HasSubstr("-0.00001200000042445026f"));
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_F16) {
@@ -90,7 +90,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("float16_t(-0.00119972229h)"));
+    EXPECT_THAT(gen.result(), HasSubstr("float16_t(-0.0011997222900390625h)"));
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Bool) {
@@ -402,8 +402,10 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("{float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f),"
-                                        " float3(7.0f, 8.0f, 9.0f)}"));
+    EXPECT_THAT(
+        gen.result(),
+        HasSubstr(
+            "{float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f), float3(7.0f, 8.0f, 9.0f)}"));
 }
 
 TEST_F(HlslGeneratorImplTest_Constructor, Type_Array_Empty) {
diff --git a/src/tint/writer/hlsl/generator_impl_import_test.cc b/src/tint/writer/hlsl/generator_impl_import_test.cc
index 28b72dc..133774a 100644
--- a/src/tint/writer/hlsl/generator_impl_import_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_import_test.cc
@@ -97,8 +97,10 @@
 
     utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
-    EXPECT_EQ(out.str(),
-              std::string(param.hlsl_name) + "(float3(0.100000001f, 0.200000003f, 0.300000012f))");
+    EXPECT_EQ(
+        out.str(),
+        std::string(param.hlsl_name) +
+            "(float3(0.10000000149011611938f, 0.20000000298023223877f, 0.30000001192092895508f))");
 }
 INSTANTIATE_TEST_SUITE_P(HlslGeneratorImplTest_Import,
                          HlslImportData_SingleVectorParamTest,
diff --git a/src/tint/writer/hlsl/generator_impl_loop_test.cc b/src/tint/writer/hlsl/generator_impl_loop_test.cc
index 45bf04a..98b7f79 100644
--- a/src/tint/writer/hlsl/generator_impl_loop_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_loop_test.cc
@@ -134,7 +134,7 @@
 
 TEST_F(HlslGeneratorImplTest_Loop, Emit_LoopWithVarUsedInContinuing) {
     // loop {
-    //   var lhs : f32 = 2.4;
+    //   var lhs : f32 = 2.5;
     //   var other : f32;
     //   break;
     //   continuing {
@@ -144,7 +144,7 @@
 
     GlobalVar("rhs", ty.f32(), builtin::AddressSpace::kPrivate);
 
-    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.4_f))),  //
+    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.5_f))),  //
                        Decl(Var("other", ty.f32())),             //
                        Break());
 
@@ -158,7 +158,7 @@
 
     ASSERT_TRUE(gen.EmitStatement(outer)) << gen.error();
     EXPECT_EQ(gen.result(), R"(  while (true) {
-    float lhs = 2.400000095f;
+    float lhs = 2.5f;
     float other = 0.0f;
     break;
     {
diff --git a/src/tint/writer/hlsl/generator_impl_test.cc b/src/tint/writer/hlsl/generator_impl_test.cc
index eeb9acb..0c39a31 100644
--- a/src/tint/writer/hlsl/generator_impl_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_test.cc
@@ -53,7 +53,9 @@
     const char* attribute_name;
 };
 inline std::ostream& operator<<(std::ostream& out, HlslBuiltinData data) {
-    out << data.builtin;
+    utils::StringStream str;
+    str << data.builtin;
+    out << str.str();
     return out;
 }
 using HlslBuiltinConversionTest = TestParamHelper<HlslBuiltinData>;
diff --git a/src/tint/writer/hlsl/generator_impl_type_test.cc b/src/tint/writer/hlsl/generator_impl_type_test.cc
index 4ee8a22..cb8d349 100644
--- a/src/tint/writer/hlsl/generator_impl_type_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_type_test.cc
@@ -313,7 +313,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, HlslDepthTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using HlslDepthTexturesTest = TestParamHelper<HlslDepthTextureData>;
@@ -376,7 +378,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, HlslSampledTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using HlslSampledTexturesTest = TestParamHelper<HlslSampledTextureData>;
@@ -525,7 +529,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, HlslStorageTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using HlslStorageTexturesTest = TestParamHelper<HlslStorageTextureData>;
diff --git a/src/tint/writer/hlsl/test_helper.h b/src/tint/writer/hlsl/test_helper.h
index 8fd6bae..24eb56a 100644
--- a/src/tint/writer/hlsl/test_helper.h
+++ b/src/tint/writer/hlsl/test_helper.h
@@ -34,6 +34,14 @@
     TestHelperBase() = default;
     ~TestHelperBase() override = default;
 
+    /// @returns the default generator options for SanitizeAndBuild(), if no explicit options are
+    /// provided.
+    static Options DefaultOptions() {
+        Options opts;
+        opts.disable_robustness = true;
+        return opts;
+    }
+
     /// Builds the program and returns a GeneratorImpl from the program.
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same GeneratorImpl without rebuilding.
@@ -60,7 +68,7 @@
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same GeneratorImpl without rebuilding.
     /// @return the built generator
-    GeneratorImpl& SanitizeAndBuild(const Options& options = {}) {
+    GeneratorImpl& SanitizeAndBuild(const Options& options = DefaultOptions()) {
         if (gen_) {
             return *gen_;
         }
diff --git a/src/tint/writer/msl/generator.h b/src/tint/writer/msl/generator.h
index bdc2be5..1add259 100644
--- a/src/tint/writer/msl/generator.h
+++ b/src/tint/writer/msl/generator.h
@@ -44,6 +44,9 @@
     /// @returns this Options
     Options& operator=(const Options&);
 
+    /// Set to `true` to disable software robustness that prevents out-of-bounds accesses.
+    bool disable_robustness = false;
+
     /// The index to use when generating a UBO to receive storage buffer sizes.
     /// Defaults to 30, which is the last valid buffer slot.
     uint32_t buffer_size_ubo_index = 30;
@@ -67,7 +70,8 @@
     ArrayLengthFromUniformOptions array_length_from_uniform = {};
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(buffer_size_ubo_index,
+    TINT_REFLECT(disable_robustness,
+                 buffer_size_ubo_index,
                  fixed_sample_mask,
                  emit_vertex_point_size,
                  disable_workgroup_init,
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 81b32c0..b2c9ffa 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -53,6 +53,7 @@
 #include "src/tint/transform/promote_initializers_to_let.h"
 #include "src/tint/transform/promote_side_effects_to_decl.h"
 #include "src/tint/transform/remove_phonies.h"
+#include "src/tint/transform/robustness.h"
 #include "src/tint/transform/simplify_pointers.h"
 #include "src/tint/transform/unshadow.h"
 #include "src/tint/transform/vectorize_scalar_matrix_initializers.h"
@@ -171,24 +172,6 @@
     // ExpandCompoundAssignment must come before BuiltinPolyfill
     manager.Add<transform::ExpandCompoundAssignment>();
 
-    {  // Builtin polyfills
-        transform::BuiltinPolyfill::Builtins polyfills;
-        polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
-        polyfills.atanh = transform::BuiltinPolyfill::Level::kRangeCheck;
-        polyfills.bitshift_modulo = true;  // crbug.com/tint/1543
-        polyfills.clamp_int = true;
-        polyfills.extract_bits = transform::BuiltinPolyfill::Level::kClampParameters;
-        polyfills.first_leading_bit = true;
-        polyfills.first_trailing_bit = true;
-        polyfills.insert_bits = transform::BuiltinPolyfill::Level::kClampParameters;
-        polyfills.int_div_mod = true;
-        polyfills.sign_int = true;
-        polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
-        polyfills.workgroup_uniform_load = true;
-        data.Add<transform::BuiltinPolyfill::Config>(polyfills);
-        manager.Add<transform::BuiltinPolyfill>();
-    }
-
     // Build the config for the internal ArrayLengthFromUniform transform.
     auto& array_length_from_uniform = options.array_length_from_uniform;
     transform::ArrayLengthFromUniform::Config array_length_from_uniform_cfg(
@@ -217,23 +200,51 @@
         transform::CanonicalizeEntryPointIO::ShaderStyle::kMsl, options.fixed_sample_mask,
         options.emit_vertex_point_size);
 
-    if (options.generate_external_texture_bindings) {
-        auto new_bindings_map = GenerateExternalTextureBindings(in);
-        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
-    }
-    manager.Add<transform::MultiplanarExternalTexture>();
-
     manager.Add<transform::PreservePadding>();
 
     manager.Add<transform::Unshadow>();
 
+    manager.Add<transform::PromoteSideEffectsToDecl>();
+
+    if (!options.disable_robustness) {
+        // Robustness must come before BuiltinPolyfill
+        manager.Add<transform::Robustness>();
+    }
+
+    {  // Builtin polyfills
+        transform::BuiltinPolyfill::Builtins polyfills;
+        polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
+        polyfills.atanh = transform::BuiltinPolyfill::Level::kRangeCheck;
+        polyfills.bitshift_modulo = true;  // crbug.com/tint/1543
+        polyfills.clamp_int = true;
+        polyfills.extract_bits = transform::BuiltinPolyfill::Level::kClampParameters;
+        polyfills.first_leading_bit = true;
+        polyfills.first_trailing_bit = true;
+        polyfills.insert_bits = transform::BuiltinPolyfill::Level::kClampParameters;
+        polyfills.int_div_mod = true;
+        polyfills.sign_int = true;
+        polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
+        polyfills.workgroup_uniform_load = true;
+        data.Add<transform::BuiltinPolyfill::Config>(polyfills);
+        manager.Add<transform::BuiltinPolyfill>();
+    }
+
+    if (options.generate_external_texture_bindings) {
+        // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
+        auto new_bindings_map = GenerateExternalTextureBindings(in);
+        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
+        manager.Add<transform::MultiplanarExternalTexture>();
+    }
+
     if (!options.disable_workgroup_init) {
         // ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
         // ZeroInitWorkgroupMemory may inject new builtin parameters.
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
+
+    // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<transform::CanonicalizeEntryPointIO>();
-    manager.Add<transform::PromoteSideEffectsToDecl>();
+
     manager.Add<transform::PromoteInitializersToLet>();
 
     // DemoteToHelper must come after PromoteSideEffectsToDecl and ExpandCompoundAssignment.
diff --git a/src/tint/writer/msl/generator_impl_binary_test.cc b/src/tint/writer/msl/generator_impl_binary_test.cc
index 8b7e4b5..0297d8a 100644
--- a/src/tint/writer/msl/generator_impl_binary_test.cc
+++ b/src/tint/writer/msl/generator_impl_binary_test.cc
@@ -23,7 +23,9 @@
     ast::BinaryOp op;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
-    out << data.op;
+    utils::StringStream str;
+    str << data.op;
+    out << str.str();
     return out;
 }
 using MslBinaryTest = TestParamHelper<BinaryData>;
diff --git a/src/tint/writer/msl/generator_impl_builtin_test.cc b/src/tint/writer/msl/generator_impl_builtin_test.cc
index 0c6ba28..08a2d25 100644
--- a/src/tint/writer/msl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/msl/generator_impl_builtin_test.cc
@@ -851,7 +851,7 @@
 using namespace metal;
 
 float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 kernel void test_function() {
@@ -876,7 +876,7 @@
 using namespace metal;
 
 float3 tint_degrees(float3 param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 kernel void test_function() {
@@ -903,7 +903,7 @@
 using namespace metal;
 
 half tint_degrees(half param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 kernel void test_function() {
@@ -930,7 +930,7 @@
 using namespace metal;
 
 half3 tint_degrees(half3 param_0) {
-  return param_0 * 57.295779513082323;
+  return param_0 * 57.29577951308232286465;
 }
 
 kernel void test_function() {
@@ -955,7 +955,7 @@
 using namespace metal;
 
 float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 kernel void test_function() {
@@ -980,7 +980,7 @@
 using namespace metal;
 
 float3 tint_radians(float3 param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 kernel void test_function() {
@@ -1007,7 +1007,7 @@
 using namespace metal;
 
 half tint_radians(half param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 kernel void test_function() {
@@ -1034,7 +1034,7 @@
 using namespace metal;
 
 half3 tint_radians(half3 param_0) {
-  return param_0 * 0.017453292519943295;
+  return param_0 * 0.01745329251994329547;
 }
 
 kernel void test_function() {
diff --git a/src/tint/writer/msl/generator_impl_constructor_test.cc b/src/tint/writer/msl/generator_impl_constructor_test.cc
index ba567ab..ec5646c 100644
--- a/src/tint/writer/msl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/msl/generator_impl_constructor_test.cc
@@ -79,7 +79,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("-0.000012f"));
+    EXPECT_THAT(gen.result(), HasSubstr("-0.00001200000042445026f"));
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_F16) {
@@ -90,7 +90,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("-0.00119972229h"));
+    EXPECT_THAT(gen.result(), HasSubstr("-0.0011997222900390625h"));
 }
 
 TEST_F(MslGeneratorImplTest_Constructor, Type_Bool) {
diff --git a/src/tint/writer/msl/generator_impl_loop_test.cc b/src/tint/writer/msl/generator_impl_loop_test.cc
index 6d2959c..a0c56f7 100644
--- a/src/tint/writer/msl/generator_impl_loop_test.cc
+++ b/src/tint/writer/msl/generator_impl_loop_test.cc
@@ -131,7 +131,7 @@
 
 TEST_F(MslGeneratorImplTest, Emit_LoopWithVarUsedInContinuing) {
     // loop {
-    //   var lhs : f32 = 2.4;
+    //   var lhs : f32 = 2.5;
     //   var other : f32;
     //   continuing {
     //     lhs = rhs
@@ -141,7 +141,7 @@
 
     GlobalVar("rhs", ty.f32(), builtin::AddressSpace::kPrivate);
 
-    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.4_f))),  //
+    auto* body = Block(Decl(Var("lhs", ty.f32(), Expr(2.5_f))),  //
                        Decl(Var("other", ty.f32())),             //
                        Break());
 
@@ -155,7 +155,7 @@
 
     ASSERT_TRUE(gen.EmitStatement(outer)) << gen.error();
     EXPECT_EQ(gen.result(), R"(  while (true) {
-    float lhs = 2.400000095f;
+    float lhs = 2.5f;
     float other = 0.0f;
     break;
     {
diff --git a/src/tint/writer/msl/generator_impl_test.cc b/src/tint/writer/msl/generator_impl_test.cc
index 52ae381..43eb558 100644
--- a/src/tint/writer/msl/generator_impl_test.cc
+++ b/src/tint/writer/msl/generator_impl_test.cc
@@ -65,7 +65,9 @@
     const char* attribute_name;
 };
 inline std::ostream& operator<<(std::ostream& out, MslBuiltinData data) {
-    out << data.builtin;
+    utils::StringStream str;
+    str << data.builtin;
+    out << str.str();
     return out;
 }
 using MslBuiltinConversionTest = TestParamHelper<MslBuiltinData>;
diff --git a/src/tint/writer/msl/generator_impl_type_test.cc b/src/tint/writer/msl/generator_impl_type_test.cc
index e1e18a3..ce1c215 100644
--- a/src/tint/writer/msl/generator_impl_type_test.cc
+++ b/src/tint/writer/msl/generator_impl_type_test.cc
@@ -763,7 +763,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, MslDepthTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using MslDepthTexturesTest = TestParamHelper<MslDepthTextureData>;
@@ -805,7 +807,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, MslTextureData data) {
-    out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    out << str.str();
     return out;
 }
 using MslSampledtexturesTest = TestParamHelper<MslTextureData>;
@@ -849,7 +853,9 @@
     std::string result;
 };
 inline std::ostream& operator<<(std::ostream& out, MslStorageTextureData data) {
-    return out << data.dim;
+    utils::StringStream str;
+    str << data.dim;
+    return out << str.str();
 }
 using MslStorageTexturesTest = TestParamHelper<MslStorageTextureData>;
 TEST_P(MslStorageTexturesTest, Emit) {
diff --git a/src/tint/writer/msl/test_helper.h b/src/tint/writer/msl/test_helper.h
index f1b530d..17802d7 100644
--- a/src/tint/writer/msl/test_helper.h
+++ b/src/tint/writer/msl/test_helper.h
@@ -33,6 +33,14 @@
     TestHelperBase() = default;
     ~TestHelperBase() override = default;
 
+    /// @returns the default generator options for SanitizeAndBuild(), if no explicit options are
+    /// provided.
+    static Options DefaultOptions() {
+        Options opts;
+        opts.disable_robustness = true;
+        return opts;
+    }
+
     /// Builds and returns a GeneratorImpl from the program.
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same GeneratorImpl without rebuilding.
@@ -59,7 +67,7 @@
     /// @note The generator is only built once. Multiple calls to Build() will
     /// return the same GeneratorImpl without rebuilding.
     /// @return the built generator
-    GeneratorImpl& SanitizeAndBuild(const Options& options = {}) {
+    GeneratorImpl& SanitizeAndBuild(const Options& options = DefaultOptions()) {
         if (gen_) {
             return *gen_;
         }
diff --git a/src/tint/writer/spirv/builder_binary_expression_test.cc b/src/tint/writer/spirv/builder_binary_expression_test.cc
index 7251929..705bf77 100644
--- a/src/tint/writer/spirv/builder_binary_expression_test.cc
+++ b/src/tint/writer/spirv/builder_binary_expression_test.cc
@@ -27,7 +27,9 @@
     std::string name;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
-    out << data.op;
+    utils::StringStream str;
+    str << data.op;
+    out << str.str();
     return out;
 }
 
diff --git a/src/tint/writer/spirv/builder_builtin_test.cc b/src/tint/writer/spirv/builder_builtin_test.cc
index 1645ee6..5db867a 100644
--- a/src/tint/writer/spirv/builder_builtin_test.cc
+++ b/src/tint/writer/spirv/builder_builtin_test.cc
@@ -2107,7 +2107,7 @@
 OpExecutionMode %24 OriginUpperLeft
 OpName %5 "v"
 OpName %8 "tint_quantizeToF16"
-OpName %9 "v_1"
+OpName %9 "v"
 OpName %24 "a_func"
 %2 = OpTypeFloat 32
 %1 = OpTypeVector %2 3
diff --git a/src/tint/writer/spirv/builder_format_conversion_test.cc b/src/tint/writer/spirv/builder_format_conversion_test.cc
index 63f2183..1efdcca 100644
--- a/src/tint/writer/spirv/builder_format_conversion_test.cc
+++ b/src/tint/writer/spirv/builder_format_conversion_test.cc
@@ -24,7 +24,9 @@
     bool extended_format = false;
 };
 inline std::ostream& operator<<(std::ostream& out, TestData data) {
-    out << data.ast_format;
+    utils::StringStream str;
+    str << data.ast_format;
+    out << str.str();
     return out;
 }
 using ImageFormatConversionTest = TestParamHelper<TestData>;
diff --git a/src/tint/writer/spirv/builder_function_attribute_test.cc b/src/tint/writer/spirv/builder_function_attribute_test.cc
index 555c9ea..1b23982 100644
--- a/src/tint/writer/spirv/builder_function_attribute_test.cc
+++ b/src/tint/writer/spirv/builder_function_attribute_test.cc
@@ -43,7 +43,9 @@
     SpvExecutionModel model;
 };
 inline std::ostream& operator<<(std::ostream& out, FunctionStageData data) {
-    out << data.stage;
+    utils::StringStream str;
+    str << data.stage;
+    out << str.str();
     return out;
 }
 using Attribute_StageTest = TestParamHelper<FunctionStageData>;
diff --git a/src/tint/writer/spirv/builder_global_variable_test.cc b/src/tint/writer/spirv/builder_global_variable_test.cc
index 2f551ff..569d5e9 100644
--- a/src/tint/writer/spirv/builder_global_variable_test.cc
+++ b/src/tint/writer/spirv/builder_global_variable_test.cc
@@ -256,7 +256,9 @@
     SpvBuiltIn result;
 };
 inline std::ostream& operator<<(std::ostream& out, BuiltinData data) {
-    out << data.builtin;
+    utils::StringStream str;
+    str << data.builtin;
+    out << str.str();
     return out;
 }
 using BuiltinDataTest = TestParamHelper<BuiltinData>;
diff --git a/src/tint/writer/spirv/builder_type_test.cc b/src/tint/writer/spirv/builder_type_test.cc
index 55b92c6..89ba34e 100644
--- a/src/tint/writer/spirv/builder_type_test.cc
+++ b/src/tint/writer/spirv/builder_type_test.cc
@@ -611,7 +611,9 @@
     SpvStorageClass result;
 };
 inline std::ostream& operator<<(std::ostream& out, PtrData data) {
-    out << data.ast_class;
+    utils::StringStream str;
+    str << data.ast_class;
+    out << str.str();
     return out;
 }
 using PtrDataTest = TestParamHelper<PtrData>;
diff --git a/src/tint/writer/spirv/generator.h b/src/tint/writer/spirv/generator.h
index e406f6e..8b34032 100644
--- a/src/tint/writer/spirv/generator.h
+++ b/src/tint/writer/spirv/generator.h
@@ -35,6 +35,9 @@
 
 /// Configuration options used for generating SPIR-V.
 struct Options {
+    /// Set to `true` to disable software robustness that prevents out-of-bounds accesses.
+    bool disable_robustness = false;
+
     /// Set to `true` to generate a PointSize builtin and have it set to 1.0
     /// from all vertex shaders in the module.
     bool emit_vertex_point_size = true;
@@ -50,7 +53,8 @@
     bool use_zero_initialize_workgroup_memory_extension = false;
 
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
-    TINT_REFLECT(emit_vertex_point_size,
+    TINT_REFLECT(disable_robustness,
+                 emit_vertex_point_size,
                  disable_workgroup_init,
                  generate_external_texture_bindings,
                  use_zero_initialize_workgroup_memory_extension);
diff --git a/src/tint/writer/spirv/generator_impl.cc b/src/tint/writer/spirv/generator_impl.cc
index ef6aa2d..c46bf9a 100644
--- a/src/tint/writer/spirv/generator_impl.cc
+++ b/src/tint/writer/spirv/generator_impl.cc
@@ -32,6 +32,7 @@
 #include "src/tint/transform/promote_side_effects_to_decl.h"
 #include "src/tint/transform/remove_phonies.h"
 #include "src/tint/transform/remove_unreachable_statements.h"
+#include "src/tint/transform/robustness.h"
 #include "src/tint/transform/simplify_pointers.h"
 #include "src/tint/transform/std140.h"
 #include "src/tint/transform/unshadow.h"
@@ -53,7 +54,34 @@
     // ExpandCompoundAssignment must come before BuiltinPolyfill
     manager.Add<transform::ExpandCompoundAssignment>();
 
+    manager.Add<transform::PreservePadding>();  // Must come before DirectVariableAccess
+
+    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
+
+    manager.Add<transform::RemoveUnreachableStatements>();
+    manager.Add<transform::PromoteSideEffectsToDecl>();
+    manager.Add<transform::SimplifyPointers>();  // Required for arrayLength()
+    manager.Add<transform::RemovePhonies>();
+    manager.Add<transform::VectorizeScalarMatrixInitializers>();
+    manager.Add<transform::VectorizeMatrixConversions>();
+    manager.Add<transform::WhileToLoop>();  // ZeroInitWorkgroupMemory
+    manager.Add<transform::MergeReturn>();
+
+    if (!options.disable_robustness) {
+        // Robustness must come before BuiltinPolyfill
+        manager.Add<transform::Robustness>();
+    }
+
+    if (options.generate_external_texture_bindings) {
+        // Note: it is more efficient for MultiplanarExternalTexture to come after Robustness
+        auto new_bindings_map = GenerateExternalTextureBindings(in);
+        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
+        manager.Add<transform::MultiplanarExternalTexture>();
+    }
+
     {  // Builtin polyfills
+        // BuiltinPolyfill must come before DirectVariableAccess, due to the use of pointer
+        // parameter for workgroupUniformLoad()
         transform::BuiltinPolyfill::Builtins polyfills;
         polyfills.acosh = transform::BuiltinPolyfill::Level::kRangeCheck;
         polyfills.atanh = transform::BuiltinPolyfill::Level::kRangeCheck;
@@ -75,18 +103,11 @@
         manager.Add<transform::BuiltinPolyfill>();
     }
 
-    if (options.generate_external_texture_bindings) {
-        auto new_bindings_map = GenerateExternalTextureBindings(in);
-        data.Add<transform::MultiplanarExternalTexture::NewBindingPoints>(new_bindings_map);
-    }
-    manager.Add<transform::MultiplanarExternalTexture>();
-
-    manager.Add<transform::PreservePadding>();  // Must come before DirectVariableAccess
-
-    manager.Add<transform::Unshadow>();  // Must come before DirectVariableAccess
     bool disable_workgroup_init_in_sanitizer =
         options.disable_workgroup_init || options.use_zero_initialize_workgroup_memory_extension;
     if (!disable_workgroup_init_in_sanitizer) {
+        // ZeroInitWorkgroupMemory must come before CanonicalizeEntryPointIO as
+        // ZeroInitWorkgroupMemory may inject new builtin parameters.
         manager.Add<transform::ZeroInitWorkgroupMemory>();
     }
 
@@ -98,16 +119,11 @@
         manager.Add<transform::DirectVariableAccess>();
     }
 
-    manager.Add<transform::RemoveUnreachableStatements>();
-    manager.Add<transform::PromoteSideEffectsToDecl>();
-    manager.Add<transform::SimplifyPointers>();  // Required for arrayLength()
-    manager.Add<transform::RemovePhonies>();
-    manager.Add<transform::VectorizeScalarMatrixInitializers>();
-    manager.Add<transform::VectorizeMatrixConversions>();
-    manager.Add<transform::WhileToLoop>();  // ZeroInitWorkgroupMemory
-    manager.Add<transform::MergeReturn>();
+    // CanonicalizeEntryPointIO must come after Robustness
     manager.Add<transform::CanonicalizeEntryPointIO>();
     manager.Add<transform::AddEmptyEntryPoint>();
+
+    // AddBlockAttribute must come after MultiplanarExternalTexture
     manager.Add<transform::AddBlockAttribute>();
 
     // DemoteToHelper must come after CanonicalizeEntryPointIO, PromoteSideEffectsToDecl, and
diff --git a/src/tint/writer/spirv/test_helper.h b/src/tint/writer/spirv/test_helper.h
index 2e58760..028d363 100644
--- a/src/tint/writer/spirv/test_helper.h
+++ b/src/tint/writer/spirv/test_helper.h
@@ -33,6 +33,14 @@
     TestHelperBase() = default;
     ~TestHelperBase() override = default;
 
+    /// @returns the default generator options for SanitizeAndBuild(), if no explicit options are
+    /// provided.
+    static Options DefaultOptions() {
+        Options opts;
+        opts.disable_robustness = true;
+        return opts;
+    }
+
     /// Builds and returns a spirv::Builder from the program.
     /// @note The spirv::Builder is only built once. Multiple calls to Build()
     /// will return the same spirv::Builder without rebuilding.
@@ -59,7 +67,7 @@
     /// @note The spirv::Builder is only built once. Multiple calls to Build()
     /// will return the same spirv::Builder without rebuilding.
     /// @return the built spirv::Builder
-    spirv::Builder& SanitizeAndBuild(const Options& options = {}) {
+    spirv::Builder& SanitizeAndBuild(const Options& options = DefaultOptions()) {
         if (spirv_builder) {
             return *spirv_builder;
         }
diff --git a/src/tint/writer/text_generator.h b/src/tint/writer/text_generator.h
index 74e3ecb..2bd725f 100644
--- a/src/tint/writer/text_generator.h
+++ b/src/tint/writer/text_generator.h
@@ -15,7 +15,6 @@
 #ifndef SRC_TINT_WRITER_TEXT_GENERATOR_H_
 #define SRC_TINT_WRITER_TEXT_GENERATOR_H_
 
-#include <sstream>
 #include <string>
 #include <unordered_map>
 #include <utility>
diff --git a/src/tint/writer/wgsl/generator_impl_binary_test.cc b/src/tint/writer/wgsl/generator_impl_binary_test.cc
index 88bf9f5..fbb6c32 100644
--- a/src/tint/writer/wgsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_binary_test.cc
@@ -23,7 +23,9 @@
     ast::BinaryOp op;
 };
 inline std::ostream& operator<<(std::ostream& out, BinaryData data) {
-    out << data.op;
+    utils::StringStream str;
+    str << data.op;
+    out << str.str();
     return out;
 }
 using WgslBinaryTest = TestParamHelper<BinaryData>;
diff --git a/src/tint/writer/wgsl/generator_impl_constructor_test.cc b/src/tint/writer/wgsl/generator_impl_constructor_test.cc
index c815f42..7278736 100644
--- a/src/tint/writer/wgsl/generator_impl_constructor_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_constructor_test.cc
@@ -79,7 +79,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("f32(-0.000012f)"));
+    EXPECT_THAT(gen.result(), HasSubstr("f32(-0.00001200000042445026f)"));
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_F16) {
@@ -90,7 +90,7 @@
     GeneratorImpl& gen = Build();
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
-    EXPECT_THAT(gen.result(), HasSubstr("f16(-1.19805336e-05h)"));
+    EXPECT_THAT(gen.result(), HasSubstr("f16(-0.00001198053359985352h)"));
 }
 
 TEST_F(WgslGeneratorImplTest_Constructor, Type_Bool) {
diff --git a/src/tint/writer/wgsl/generator_impl_literal_test.cc b/src/tint/writer/wgsl/generator_impl_literal_test.cc
index e947778..b70f535 100644
--- a/src/tint/writer/wgsl/generator_impl_literal_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_literal_test.cc
@@ -183,16 +183,16 @@
 INSTANTIATE_TEST_SUITE_P(Subnormal,
                          WgslGenerator_F16LiteralTest,
                          ::testing::ValuesIn(std::vector<F16Data>{
-                             {MakeF16(0, 0, 1), "5.96046448e-08h"},  // Smallest
-                             {MakeF16(1, 0, 1), "-5.96046448e-08h"},
-                             {MakeF16(0, 0, 2), "1.1920929e-07h"},
-                             {MakeF16(1, 0, 2), "-1.1920929e-07h"},
-                             {MakeF16(0, 0, 0x3ffu), "6.09755516e-05h"},   // Largest
-                             {MakeF16(1, 0, 0x3ffu), "-6.09755516e-05h"},  // Largest
-                             {MakeF16(0, 0, 0x3afu), "5.620718e-05h"},     // Scattered bits
-                             {MakeF16(1, 0, 0x3afu), "-5.620718e-05h"},    // Scattered bits
-                             {MakeF16(0, 0, 0x2c7u), "4.23789024e-05h"},   // Scattered bits
-                             {MakeF16(1, 0, 0x2c7u), "-4.23789024e-05h"},  // Scattered bits
+                             {MakeF16(0, 0, 1), "0.00000005960464477539h"},  // Smallest
+                             {MakeF16(1, 0, 1), "-0.00000005960464477539h"},
+                             {MakeF16(0, 0, 2), "0.00000011920928955078h"},
+                             {MakeF16(1, 0, 2), "-0.00000011920928955078h"},
+                             {MakeF16(0, 0, 0x3ffu), "0.00006097555160522461h"},   // Largest
+                             {MakeF16(1, 0, 0x3ffu), "-0.00006097555160522461h"},  // Largest
+                             {MakeF16(0, 0, 0x3afu), "0.00005620718002319336h"},   // Scattered bits
+                             {MakeF16(1, 0, 0x3afu), "-0.00005620718002319336h"},  // Scattered bits
+                             {MakeF16(0, 0, 0x2c7u), "0.00004237890243530273h"},   // Scattered bits
+                             {MakeF16(1, 0, 0x2c7u), "-0.00004237890243530273h"},  // Scattered bits
                          }));
 
 }  // namespace