Import Tint changes from Dawn

Changes:
  - d623182c336bad0be262a544d8b5865a1565c5da tint/PromoteInitializers: Do not hoist abstracts by James Price <jrprice@google.com>
  - dee884c9257cc3b2c533c6b0ec338296d3839632 Convert most remaining usages to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - 7c21fe5d92c87e7defd37a367de0ab933dad1973 tint/uniformity: Fix struct member partial pointers by James Price <jrprice@google.com>
  - b7c2aed189b5006628a6998d3d9aca783c89c864 tint: Skip short-circuited array init const eval by James Price <jrprice@google.com>
  - 0723a3c7f8f193db9e557edae9833f86c226539e Convert WGSL reader to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - 0c184c285621f91969c199e6da54beaeac6560f0 Convert SPIR-V reader to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - b23cda4bc24f931f8be3d8999dbbf67838c407eb Convert the resolver over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - b2ba57b15d6c935eaf8c2c4e6a6d6c6d5ab02566 Convert TextGenerator over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - 88fea2a9c33436d42f284948cc8fab2d31295116 Convert the WGSL generator to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - ec24bb2f0ce2eed088d0d1fc9dcd2a087d51f5f9 Convert SPIR-V Writer over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - 52fa68b1a748b90b3f646a46c98ce72607608c60 Convert MSL generator over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - 2b9d5b338a28cf6d919bcb51c84561f77d2b2f16 Convert HLSL generator over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - dba03d30fb141e94d7d823924fd9a7c3a642d095 Convert GLSL Generator over to utils::StringStream. by dan sinclair <dsinclair@chromium.org>
  - a4637ad8a314c50138c03500dfe76c3ee5ffde3e Convert IR over to `utils::StringStream`. by dan sinclair <dsinclair@chromium.org>
  - 7ca41fffb782f0f75203dbaf91a6594203894a0d Add a utils/string_stream class. by dan sinclair <dsinclair@chromium.org>
  - 4d3af66bbdafda4284a0eefb0d384ccdb77a7080 tint/msl: Preserve trailing vec3 padding by James Price <jrprice@google.com>
  - 43ffb09247e1e48f021abb63131bedd449e55209 tint: validate max number of case selectors in a switch s... by Antonio Maiorano <amaiorano@google.com>
  - 1bb5be9789272206ea23ebe6454fa8afca579722 tint: improve error message about function paramter limit by Antonio Maiorano <amaiorano@google.com>
  - 6b304e9ffd649602d8074d0362928479416c8d3e tint: validate max nesting depth of composite types by Antonio Maiorano <amaiorano@google.com>
  - 0b3400c56e1ac2782f8bca1854cab1a1c282b141 tint: Add chromium_internal_relaxed_uniform_layout by James Price <jrprice@google.com>
  - 806135658dbc91b5dd581cc11effca62455bd2ca tint: Pass constant::Values to ArrayOrStructCtor by James Price <jrprice@google.com>
  - c5ec169b890ac8a16bffbb4f063cbede6f34294e classify template args: add cases used to debug Treesitte... by David Neto <dneto@google.com>
  - 6176c85be83b08a7bf4f2e6ec6422d65a5ee5290 tint: Preserve padding in matrices with three rows by James Price <jrprice@google.com>
  - fe19fee3ea3ddf7d0ef1995f7e89739870c20a94 tint/const-eval: Fix runtime semantics for (x % 0) by James Price <jrprice@google.com>
  - 04529be9b74e20e515eb30ca14303063ae49df3f Dawn/Tint: Polyfill reflect vec2<f32> for D3D12 FXC on In... by Zhaoming Jiang <zhaoming.jiang@intel.com>
GitOrigin-RevId: d623182c336bad0be262a544d8b5865a1565c5da
Change-Id: Ie09db15507c3ff36b0e23aadb5ef1a70753dbe61
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/121680
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 662f90d..89eb6ee 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -221,6 +221,8 @@
     "utils/slice.h",
     "utils/string.cc",
     "utils/string.h",
+    "utils/string_stream.cc",
+    "utils/string_stream.h",
     "utils/unique_allocator.h",
     "utils/unique_vector.h",
     "utils/vector.h",
@@ -291,7 +293,10 @@
     "demangler.cc",
     "demangler.h",
   ]
-  deps = [ ":libtint_program_src" ]
+  deps = [
+    ":libtint_base_src",
+    ":libtint_program_src",
+  ]
 }
 
 libtint_source_set("libtint_initializer_src") {
@@ -1559,6 +1564,7 @@
       "utils/reverse_test.cc",
       "utils/scoped_assignment_test.cc",
       "utils/slice_test.cc",
+      "utils/string_stream_test.cc",
       "utils/string_test.cc",
       "utils/transform_test.cc",
       "utils/unique_allocator_test.cc",
@@ -2051,6 +2057,12 @@
       configs += [ "//build/config/compiler:no_chromium_code" ]
     }
 
+    if (is_win && is_debug) {
+      # TODO(crbug.com/tint/1749): both msvc and clang builds stack overflow on debug builds.
+      # Increase the initial stack size to 4 MB (default is 1MB).
+      ldflags = [ "/STACK:4194304" ]
+    }
+
     testonly = true
   }
 }
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 658d14f..1d31896 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -530,6 +530,8 @@
   utils/slice.h
   utils/string.cc
   utils/string.h
+  utils/string_stream.cc
+  utils/string_stream.h
   utils/unique_allocator.h
   utils/unique_vector.h
   utils/vector.h
@@ -985,6 +987,7 @@
     utils/reverse_test.cc
     utils/scoped_assignment_test.cc
     utils/slice_test.cc
+    utils/string_stream_test.cc
     utils/string_test.cc
     utils/transform_test.cc
     utils/unique_allocator_test.cc
diff --git a/src/tint/ast/float_literal_expression_test.cc b/src/tint/ast/float_literal_expression_test.cc
index 5ec5f0f..6374583 100644
--- a/src/tint/ast/float_literal_expression_test.cc
+++ b/src/tint/ast/float_literal_expression_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/ast/test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::ast {
 namespace {
 
@@ -42,7 +44,7 @@
 
 TEST_F(FloatLiteralExpressionTest, SuffixStringStream) {
     auto to_str = [](FloatLiteralExpression::Suffix suffix) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << suffix;
         return ss.str();
     };
diff --git a/src/tint/ast/int_literal_expression_test.cc b/src/tint/ast/int_literal_expression_test.cc
index 969e1b9..9481c30 100644
--- a/src/tint/ast/int_literal_expression_test.cc
+++ b/src/tint/ast/int_literal_expression_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/ast/test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::ast {
 namespace {
 
@@ -42,7 +44,7 @@
 
 TEST_F(IntLiteralExpressionTest, SuffixStringStream) {
     auto to_str = [](IntLiteralExpression::Suffix suffix) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << suffix;
         return ss.str();
     };
diff --git a/src/tint/bench/benchmark.cc b/src/tint/bench/benchmark.cc
index c00e51f..6b072db 100644
--- a/src/tint/bench/benchmark.cc
+++ b/src/tint/bench/benchmark.cc
@@ -19,6 +19,8 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::bench {
 namespace {
 
@@ -44,7 +46,7 @@
     fseek(file, 0, SEEK_END);
     const auto file_size = static_cast<size_t>(ftell(file));
     if (0 != (file_size % sizeof(T))) {
-        std::stringstream err;
+        utils::StringStream err;
         err << "File " << input_file
             << " does not contain an integral number of objects: " << file_size
             << " bytes in the file, require " << sizeof(T) << " bytes per object";
diff --git a/src/tint/builtin/builtin.cc b/src/tint/builtin/builtin.cc
index 60449b5..dc608d0 100644
--- a/src/tint/builtin/builtin.cc
+++ b/src/tint/builtin/builtin.cc
@@ -28,6 +28,9 @@
 /// @param str the string to parse
 /// @returns the parsed enum, or Builtin::kUndefined if the string could not be parsed.
 Builtin ParseBuiltin(std::string_view str) {
+    if (str == "__packed_vec3") {
+        return Builtin::kPackedVec3;
+    }
     if (str == "array") {
         return Builtin::kArray;
     }
@@ -242,6 +245,8 @@
     switch (value) {
         case Builtin::kUndefined:
             return out << "undefined";
+        case Builtin::kPackedVec3:
+            return out << "__packed_vec3";
         case Builtin::kArray:
             return out << "array";
         case Builtin::kAtomic:
diff --git a/src/tint/builtin/builtin.h b/src/tint/builtin/builtin.h
index f514109..25bac8b 100644
--- a/src/tint/builtin/builtin.h
+++ b/src/tint/builtin/builtin.h
@@ -30,6 +30,7 @@
 /// An enumerator of builtin builtin.
 enum class Builtin {
     kUndefined,
+    kPackedVec3,
     kArray,
     kAtomic,
     kBool,
@@ -112,6 +113,7 @@
 Builtin ParseBuiltin(std::string_view str);
 
 constexpr const char* kBuiltinStrings[] = {
+    "__packed_vec3",
     "array",
     "atomic",
     "bool",
diff --git a/src/tint/builtin/builtin_bench.cc b/src/tint/builtin/builtin_bench.cc
index 3c3134d..1f309f5 100644
--- a/src/tint/builtin/builtin_bench.cc
+++ b/src/tint/builtin/builtin_bench.cc
@@ -31,489 +31,496 @@
 
 void BuiltinParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "arccy",
-        "3a",
-        "aVray",
+        "__acked_veccc",
+        "_pac3ed_v3",
+        "__packeV_vec3",
+        "__packed_vec3",
+        "__pa1ked_vec3",
+        "_qqJcked_vec3",
+        "__pack77d_vllc3",
+        "arqHapp",
+        "vy",
+        "Grby",
         "array",
-        "arra1",
-        "qqrJy",
-        "arrll7y",
-        "atppmHHc",
-        "cto",
-        "abGmi",
+        "arviay",
+        "ar8WWy",
+        "Mxxra",
+        "atXggi",
+        "Xoic",
+        "ato3ic",
         "atomic",
-        "atvmiii",
-        "atWWm8c",
-        "xxtomc",
-        "bXgg",
-        "Xu",
-        "b3ol",
+        "aEomic",
+        "toTTiPP",
+        "ddtoxxi",
+        "44ool",
+        "VVSSol",
+        "RoRl",
         "bool",
-        "booE",
-        "TTPol",
-        "xxool",
-        "4416",
-        "fSVV6",
-        "RR2",
+        "oFl",
+        "boo",
+        "ORVHl",
+        "y1",
+        "l77rrn6",
+        "4016",
         "f16",
-        "96",
-        "f1",
-        "VOR6",
-        "y3",
-        "l77rrn2",
-        "4032",
-        "f32",
         "5",
-        "u377",
-        "kk2",
-        "ii",
-        "i3XX",
+        "u16",
+        "f",
+        "f3kk",
+        "fi",
+        "f3XX",
+        "f32",
         "55399II",
-        "i32",
-        "irSSHHa",
+        "frSSHHa",
         "U",
         "jV3",
-        "ax2",
-        "t2SGG",
-        "q2x2",
-        "mat2x2",
-        "at2",
-        "majjx",
+        "",
+        "GG",
+        "i32",
+        "2",
+        "",
+        "jj",
         "a2xrf",
-        "mat2xjf",
-        "mNNw2x28",
-        "matx2f",
-        "mat2x2f",
-        "mrrt2x2f",
-        "Gat2x2f",
+        "mat2j2",
+        "m82wNN2",
+        "mat2x2",
+        "mt2x2",
+        "rrat2x2",
+        "mGt2x2",
         "mat2x2FF",
-        "at2h",
-        "marrx2h",
-        "t2x2h",
-        "mat2x2h",
-        "Da2xJJh",
+        "at2f",
+        "marrx2f",
+        "mat2x2f",
+        "t2x2f",
+        "Da2xJJf",
         "ma82",
         "m11k2",
-        "matx3",
-        "maJx3",
-        "cat2x3",
-        "mat2x3",
-        "mat2O3",
-        "ttKavv2x__",
+        "matx2h",
+        "maJx2h",
+        "mat2x2h",
+        "mat2c2h",
+        "mat2x2O",
+        "KK_atvvtt2h",
         "5txxx8",
-        "__qatF3",
-        "matqx3f",
-        "33atOx3f",
-        "mat2x3f",
-        "mtt62x9oQQ",
-        "ma2x66f",
+        "a__xqq",
+        "maqq2x",
+        "mat2x3",
+        "ma32x66",
+        "mttQQo2x3",
+        "mat66x",
         "mtOxzz66",
-        "mat2yy3h",
+        "mat2yy3f",
         "ZaHH3Z",
-        "4WWt2q3h",
-        "mat2x3h",
-        "mOO2x3h",
-        "oatY3h",
+        "mat2x3f",
+        "4WWt2q3f",
+        "mOO2x3f",
+        "oatY3f",
         "matx",
-        "ma2x4",
-        "matw4",
-        "ma2Gf",
-        "mat2x4",
-        "qatKKx4",
-        "mmmt2x4",
+        "ma2xFh",
+        "at2x3w",
+        "mat2x3h",
+        "fGtxKh",
+        "matqKx3h",
+        "matmmxFh",
         "at2x4",
-        "mt2x4q",
-        "mat2xbb",
-        "mi2x4f",
-        "mat2x4f",
-        "maOO2xq",
-        "matTvvx4f",
+        "matxq",
+        "mb2bb4",
+        "mat2x4",
+        "it2x4",
+        "mOO2xq",
+        "mat2Tvv4",
         "maFF2x4f",
-        "Pa00xQh",
-        "mPt2x4h",
+        "Pa00xQf",
+        "mPt2x4f",
+        "mat2x4f",
         "ma772xss",
-        "mat2x4h",
-        "RRCbb2x4h",
-        "mXXt2x4h",
+        "RRCbb2x4f",
+        "mXXt2x4f",
         "qaCC2xOOh",
-        "mtsuL",
-        "mat3xX",
-        "mat3x",
-        "mat3x2",
-        "qqt2",
-        "mat3x22",
+        "ma2s4L",
+        "mXt2x4h",
+        "mat2x4h",
+        "mat24h",
+        "qa2O4",
+        "mat2x22h",
         "mzzyt3x",
-        "matVViP",
-        "mannC2f",
-        "atx2AHHq",
+        "atiVP2",
+        "mt3Cnn",
+        "mat3x2",
+        "AtqqHH2",
+        "at3x2",
+        "mafKK",
+        "ltgg2f",
+        "mat3xf",
+        "NTTtcx4f",
         "mat3x2f",
-        "may3x2",
-        "aOOOZZf",
-        "Vt12f",
-        "mff__3x2h",
-        "qaTMMl4h",
+        "ma7ppl2f",
         "mNNt3xg",
-        "mat3x2h",
-        "uub3XX2h",
+        "uub3XX2f",
         "matx2h",
         "Qt882h",
-        "maqx3",
-        "mat3113",
-        "Ft3xi22",
-        "mat3x3",
-        "m7t3x3",
+        "mt9q2h",
+        "mat3x2h",
+        "m11t3x2h",
+        "22at3iih",
+        "at3x277",
         "NNa323",
         "VVat3x3",
-        "FaWW3w11f",
-        "mawwx3f",
-        "Dat3x3f",
-        "mat3x3f",
-        "mt3x3K",
+        "ma11F3w3",
+        "mat3x3",
+        "matww3",
+        "mat3D3",
+        "maKx3",
         "mat31PPhf",
         "mat33f",
-        "mYYt3x3h",
+        "mYYt3x3f",
+        "mat3x3f",
         "mttHH3kk",
-        "mat3rr3h",
-        "mat3x3h",
-        "WWas3x3h",
+        "mat3rr3f",
+        "WWas3x3f",
         "Yt3x3h",
         "mt3qfh",
-        "vvafu224",
-        "mt34",
-        "maY34",
-        "mat3x4",
-        "YYa7y3E4",
+        "mav223xuh",
+        "mat3x3h",
+        "t3x3h",
+        "YYat3h",
+        "may3x3EYY",
         "Moatd4",
         "mt3xMM",
-        "mat3x55f",
-        "maN34",
-        "ma3Ox33",
+        "m55t3x4",
+        "mat3x4",
+        "maN4",
+        "ma33x4",
+        "mt3x3",
+        "mm66Issf",
+        "mat3x1f",
+        "Xt3x4",
         "mat3x4f",
-        "m3t3x4f",
-        "mam3xI",
-        "mnnt3r4K",
-        "m3XX",
-        "LatIx4h",
-        "at3fh",
-        "mat3x4h",
+        "LatIx4f",
+        "at3ff",
         "mYtURD4",
         "mah3x4h",
         "uuIqt3x",
-        "mat4xH",
-        "at4Qvv",
-        "66ate",
-        "mat4x2",
-        "mat7x",
+        "maH3x4h",
+        "mat3x4h",
+        "at3QQvv",
+        "at66eh",
+        "ma7O4h",
         "m0t55DD2",
         "IIaH4x2",
-        "at4x2",
-        "rat4x299",
-        "mGtt41W2f",
-        "mat4x2f",
-        "yatx2",
+        "mat4x",
+        "mat4x2",
+        "mt4r2",
+        "mat4xl",
+        "mGttx2",
+        "mat4y2",
         "mt4x2f",
         "IIaBB4x2f",
+        "mat4x2f",
         "TTat4x833",
-        "ddUUnntYYx2h",
+        "ddUUnntYYx2f",
         "m5CCxxdZ",
-        "mat4x2h",
         "matkkq2h",
         "005itpxh",
         "maIInnx2h",
-        "ccaKx",
-        "mtKK",
-        "ma664x3",
-        "mat4x3",
+        "mat4x2h",
+        "Ka4Wcc",
+        "m42KK",
+        "mat66x2h",
         "mKKtPx",
         "maxx43",
         "matqx3",
-        "MMayySrxf",
-        "mat3f",
-        "tx3f",
-        "mat4x3f",
+        "mat4x3",
+        "rMtyyxSS",
+        "uat4",
+        "tx3",
         "ma5F4x3f",
         "rra444z3f",
         "matWW",
-        "CatZJXx3h",
-        "maPPx3h",
-        "mat4c3h",
-        "mat4x3h",
+        "mat4x3f",
+        "CatZJXx3f",
+        "maPPx3f",
+        "mat4c3f",
         "matPPll6h",
         "mat993yy",
         "mat4JKKh",
-        "ma_x4",
-        "a4K4",
-        "kVt4xz",
-        "mat4x4",
+        "mat4x3h",
+        "mat4_h",
+        "ayK3h",
+        "mzt4V3k",
         "qaSKx4",
         "mat44",
         "ma4xVV",
-        "AAatIxUf",
-        "mbj4f",
-        "YY444x",
-        "mat4x4f",
+        "mat4x4",
+        "mAAt4xI",
+        "jb44",
+        "t4YYx",
         "mao4x4",
         "mtx114f",
         "mtmxccf",
-        "aJJ4x4h",
+        "mat4x4f",
+        "aJJ4x4f",
         "fCCDD4x4U",
-        "mgt4x4h",
-        "mat4x4h",
+        "mgt4x4f",
         "CCx4h",
         "mat4x66",
         "maN4M4h",
-        "pt",
-        "KW",
-        "pzzee",
-        "ptr",
+        "mat4x4h",
+        "mattth",
+        "maKWxh",
+        "mateezx4h",
         "",
         "w9",
         "4tnn",
-        "sllDler",
-        "oamp4er",
-        "wEaggler",
-        "sampler",
+        "ptr",
+        "tll",
+        "4to",
+        "wEgg",
         "gamler",
         "spleS",
         "aampl",
-        "sampZcRTr_comparison",
-        "sampler_88TmparisOn",
-        "sampler_comparim00n",
-        "sampler_comparison",
+        "sampler",
+        "TamplZRRr",
+        "sa8TplOr",
+        "m0ampler",
         "sampler_Bmomparison",
         "Mamper_ppomarison",
         "samper_compOOrison",
-        "teGtGre_1d",
-        "tex11ureHH1d",
-        "6exeeur_1FF",
-        "texture_1d",
+        "sampler_comparison",
+        "sampler_compGrGGon",
+        "sHHm11ler_comparison",
+        "sa6ler_FFeemparison",
         "texure_1",
         "tKiilure_1d",
         "exture_1d",
-        "99etvIIre_2d",
+        "texture_1d",
+        "99etvIIre_1d",
         "texture_d",
         "texture_hd",
-        "texture_2d",
         "llxzzure_PPd",
         "exue2d",
         "tffqqtre_2d",
-        "texJJre_2dd_arWay",
-        "teXXzzre_2darray",
-        "textu2_2d_array",
-        "texture_2d_array",
+        "texture_2d",
+        "texturJdd_d",
+        "trXXtu_2zz",
+        "textu2e2d",
         "tNyyture_2d_array",
         "txture_2d_rOOa",
         "textureErduaZPay",
-        "22lxtredd3ee",
-        "texVVe93d",
-        "teture_I1d",
-        "texture_3d",
+        "texture_2d_array",
+        "exl22re_2dd_areeay",
+        "mextureVV_ar9ay",
+        "teIItu1_2d_array",
         "tebture_3d",
         "ie7ure3d",
         "teotiire_3d",
-        "entre_cube",
-        "texturScube",
-        "tex22r_cube",
-        "texture_cube",
+        "texture_3d",
+        "extre_35",
+        "textre_iS",
+        "t22xtur_3",
         "teC711recuGe",
         "texture8cffbe",
         "textue_cue",
-        "tJJxture_SSube_array",
-        "texture_9ue_arry",
-        "TbbJJxture_cube_array",
-        "texture_cube_array",
+        "texture_cube",
+        "texture_SSJJbe",
+        "textrecu9e",
+        "TTeJJbture_cube",
         "t66ture_cube_aray",
         "textur66_cubu_arra",
         "textureWubeyarray",
-        "texture_deth_d",
-        "texture_epth_2d",
-        "texture_derth_2d",
-        "texture_depth_2d",
+        "texture_cube_array",
+        "texture_cube_ara",
+        "texture_ube_array",
+        "rexture_cube_array",
         "tex2ure_depth_2B",
         "texture_dpBBh_2d",
         "texture_dpth_RRd",
-        "tLLxture_deptVV0darray",
-        "textuOOe_dethKK2d_arra",
-        "textuwe_ggepth_2d_rray",
-        "texture_depth_2d_array",
+        "texture_depth_2d",
+        "tLL0Vure_deth_2d",
+        "tetKKredOOpth_2d",
+        "textgwre_dpth_2d",
         "textue_depthLh2d_arpay",
         "texture_depEh2diiKrray",
         "texture_dept_2d_array",
-        "textuUUe88dept_cbe",
-        "texrrure_depvvh_cube",
-        "texure_wepmmh_ube",
-        "texture_depth_cube",
+        "texture_depth_2d_array",
+        "t88xtuUUe_deph_2d_rray",
+        "texrruvve_depth_2d_array",
+        "texture_depmm_2d_wray",
         "tjture_d44pth_cube",
         "texture_depth_cXbe",
         "t8xture_depth_cube",
-        "textre_depth_cubeEEarrvvy",
-        "tzzture_d9pth_cuie_array",
-        "teAture_depth_QQube_GGrrJJy",
-        "texture_depth_cube_array",
+        "texture_depth_cube",
+        "textur_devvth_cubEE",
+        "tzxturi99epth_cube",
+        "teQQtuJJGe_nepth_cuAe",
         "texture_depth_cusse_array",
         "texture_Pepth_cKbe_array",
         "texture_dppp_cube_attray",
-        "texture_depth_multisample_2",
-        "texture_depth_multisamplMMd_2d",
-        "texJJure_de0th_multisampled_2d",
-        "texture_depth_multisampled_2d",
+        "texture_depth_cube_array",
+        "exture_deth_cube_array",
+        "texture_depth_MMube_array",
+        "tJJxture_depth_cube_a0ray",
         "textu8_dpth_mulisampled_2V",
         "texture_dhhpth_mKltisggmpled_2d",
         "texture_depth_multisampledf2d",
-        "tex77ure_exQernal",
-        "tYYxture_externa",
-        "tektur_exterSal",
-        "texture_external",
+        "texture_depth_multisampled_2d",
+        "te77ture_depth_multisamQled_2d",
+        "teture_depthYYmultisampled_2d",
+        "texture_deptk_multiampled_Sd",
         "txturn_ext2rnal",
         "txture_FFternal",
         "texUPPIre_GGxuernal",
-        "txtuEEe_mulaisFmpledv2d",
-        "ddexBBure_mltDDeampled_2d",
-        "teMture_EEulccisam55led_2",
-        "texture_multisampled_2d",
+        "texture_external",
+        "taxtuvEE_externl",
+        "textureexBddernDDl",
+        "tEEMtur_e55tccrnal",
         "texturemuKKtisample_d",
         "texture_multisRmpled_2d",
         "texturemulDisampl9d_2d",
-        "texturestorage_1d",
-        "textIre_storaa_1d",
-        "texture_sto77age_1d",
-        "texture_storage_1d",
+        "texture_multisampled_2d",
+        "teture_multisampled_2d",
+        "textuIa_multisampld_2d",
+        "texture_multisamp77ed_2d",
         "texIure_storage_1d",
         "texture_storagedd",
         "texture_storae_1d",
+        "texture_storage_1d",
         "texture_strate_d",
-        "texture33stoXXcge_2d",
-        "texturestorage_2E",
-        "texture_storage_2d",
+        "texture33stoXXcge_1d",
+        "texturestorage_1E",
         "textuXXestorage_2d",
         "texture_stoBaxxe_2d",
         "texte_storWge_2G",
-        "texture_storage_2d_ar66ay",
-        "t0xTTr_storave_2d_array",
-        "kexure_orage_2d_rray",
-        "texture_storage_2d_array",
+        "texture_storage_2d",
+        "texture_s66orage_2d",
+        "textvTr_so0age_2d",
+        "textureorgek2d",
         "textppre_stoae_2d_array",
         "textre_stora11e_d_array",
         "textureystorBEgeJ2d_array",
-        "textqreIImtxrage_3d",
-        "texture_toFage_3d",
-        "exture_Ytorage_3d",
-        "texture_storage_3d",
+        "texture_storage_2d_array",
+        "texture_mtorage_2dxIqrray",
+        "teture_storageF2d_array",
+        "textur_Ytorage_2d_array",
         "heDture_sHHorage_3d",
         "texturstorage23H",
         "teture_strage_3d",
-        "u2",
-        "u2",
-        "dd32",
-        "u32",
+        "texture_storage_3d",
+        "texture_storage_d",
+        "texturestorage_3d",
+        "ddexture_storage_3d",
         "uPO",
         "ba",
         "u02",
-        "veh2",
-        "vgY2",
-        "Oec2",
-        "vec2",
+        "u32",
+        "h32",
+        "gYY",
+        "O32",
         "eh",
         "ppfe2",
         "vev",
-        "vc2zz",
-        "vaac2",
-        "Ouuicf",
-        "vec2f",
+        "vec2",
+        "vzz2",
+        "vc2",
+        "OOii",
         "vGc2f",
         "22ecTTf",
         "dlc2f",
-        "vecbh",
+        "vec2f",
+        "vecbf",
         "ec2BB",
         "IIScXPP",
-        "vec2h",
         "jjec2h",
         "cc_c2h",
         "zz6xx2h",
+        "vec2h",
         "c2",
         "4xx2N",
-        "p0AAei",
-        "vec2i",
+        "p0AAeh",
         "vey2",
         "vbWW0i",
         "meMMtti",
-        "du",
+        "vec2i",
+        "di",
         "vvc_",
-        "VEEc2u",
-        "vec2u",
+        "VEEc2i",
         "vec24",
         "VVeX2u",
         "veVou",
-        "vec",
-        "KKc3",
-        "G",
-        "vec3",
+        "vec2u",
+        "ve2u",
+        "ecKKt",
+        "eG",
         "ea3",
         "OOc",
         "G",
-        "v5c3f",
-        "99jcfff",
-        "XXvYY3R",
-        "vec3f",
+        "vec3",
+        "ve53",
+        "9fjec3",
+        "vvXcRY",
         "ccf",
         "v8XX5",
         "ec3",
+        "vec3f",
         "ppc3cc",
-        "vecvh",
+        "vecvf",
         "eEE3SS",
-        "vec3h",
         "vec",
         "eh",
         "ec3ww",
-        "vecd99i",
+        "vec3h",
+        "vecd99h",
         "ve99P",
         "KKec3",
-        "vec3i",
         "ooMcDD",
         "vei",
         "vqi",
+        "vec3i",
         "veL30",
         "vncvv66",
         "vrrn3",
-        "vec3u",
         "vxxce",
         "NCCOc3u",
         "vc3u",
-        "veca",
-        "veNNN",
-        "vec",
-        "vec4",
+        "vec3u",
+        "aec4u",
+        "NNc3NN",
+        "ve3u",
         "vc",
         "vAYS4",
         "vec0",
-        "vecaaf",
-        "vmm4f",
-        "ec4f",
-        "vec4f",
+        "vec4",
+        "vecaa",
+        "mmcq",
+        "vc4",
         "vE4U",
         "veKD4",
         "v0t4__",
+        "vec4f",
         "cpA",
-        "ec4h",
-        "vBBc4h",
-        "vec4h",
+        "ec4f",
+        "vBBc4f",
         "vbnn99",
         "EEcAAh",
         "v5c66h",
-        "vHc4i",
-        "vecxi",
+        "vec4h",
+        "vHc4h",
+        "vecxh",
         "vzyn40",
-        "vec4i",
         "ve4i",
         "kH4i",
         "veci",
+        "vec4i",
         "oo4rr",
         "JJc4",
         "vcCC0",
-        "vec4u",
         "xAA99F",
         "veccu",
         "vec4S",
+        "vec4u",
+        "vocBB",
+        "ec4u",
+        "veemm",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/builtin_test.cc b/src/tint/builtin/builtin_test.cc
index 2908bc2..9ff8d99 100644
--- a/src/tint/builtin/builtin_test.cc
+++ b/src/tint/builtin/builtin_test.cc
@@ -43,6 +43,7 @@
 }
 
 static constexpr Case kValidCases[] = {
+    {"__packed_vec3", Builtin::kPackedVec3},
     {"array", Builtin::kArray},
     {"atomic", Builtin::kAtomic},
     {"bool", Builtin::kBool},
@@ -115,213 +116,216 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"arccy", Builtin::kUndefined},
-    {"3a", Builtin::kUndefined},
-    {"aVray", Builtin::kUndefined},
-    {"1tomic", Builtin::kUndefined},
-    {"aoqqic", Builtin::kUndefined},
-    {"atomll77", Builtin::kUndefined},
-    {"ppqooH", Builtin::kUndefined},
-    {"c", Builtin::kUndefined},
-    {"bGo", Builtin::kUndefined},
-    {"f1vi", Builtin::kUndefined},
-    {"f8WW", Builtin::kUndefined},
-    {"fxx", Builtin::kUndefined},
+    {"__acked_veccc", Builtin::kUndefined},
+    {"_pac3ed_v3", Builtin::kUndefined},
+    {"__packeV_vec3", Builtin::kUndefined},
+    {"arra1", Builtin::kUndefined},
+    {"qqrJy", Builtin::kUndefined},
+    {"arrll7y", Builtin::kUndefined},
+    {"atppmHHc", Builtin::kUndefined},
+    {"cto", Builtin::kUndefined},
+    {"abGmi", Builtin::kUndefined},
+    {"bovii", Builtin::kUndefined},
+    {"boWWl", Builtin::kUndefined},
+    {"Mxxl", Builtin::kUndefined},
     {"fgg", Builtin::kUndefined},
     {"X", Builtin::kUndefined},
-    {"332", Builtin::kUndefined},
-    {"iE2", Builtin::kUndefined},
-    {"iPTT", Builtin::kUndefined},
+    {"316", Builtin::kUndefined},
+    {"fE2", Builtin::kUndefined},
+    {"fPTT", Builtin::kUndefined},
     {"dxx2", Builtin::kUndefined},
-    {"44at2x2", Builtin::kUndefined},
-    {"mSSVV2x2", Builtin::kUndefined},
-    {"mat2R2", Builtin::kUndefined},
-    {"mF2x9f", Builtin::kUndefined},
-    {"matx2f", Builtin::kUndefined},
-    {"VOORRH2f", Builtin::kUndefined},
-    {"ma2xyh", Builtin::kUndefined},
-    {"llnarr2772h", Builtin::kUndefined},
+    {"4432", Builtin::kUndefined},
+    {"iSVV2", Builtin::kUndefined},
+    {"RR2", Builtin::kUndefined},
+    {"at292", Builtin::kUndefined},
+    {"mat2x", Builtin::kUndefined},
+    {"Vat2OR2", Builtin::kUndefined},
+    {"ma2xyf", Builtin::kUndefined},
+    {"llnarr2772f", Builtin::kUndefined},
     {"mat24200", Builtin::kUndefined},
-    {"m2oo", Builtin::kUndefined},
-    {"atzz3", Builtin::kUndefined},
-    {"1it2xpp", Builtin::kUndefined},
-    {"mat2xXXf", Builtin::kUndefined},
-    {"9II5ann2x3f", Builtin::kUndefined},
-    {"mataSSrHHYf", Builtin::kUndefined},
-    {"makkh", Builtin::kUndefined},
+    {"a2ooh", Builtin::kUndefined},
+    {"zz2x2h", Builtin::kUndefined},
+    {"miitppx1", Builtin::kUndefined},
+    {"maXX2x3", Builtin::kUndefined},
+    {"55IIt2nn99", Builtin::kUndefined},
+    {"aHHrrt2xSS", Builtin::kUndefined},
+    {"makkf", Builtin::kUndefined},
     {"jatgRx", Builtin::kUndefined},
     {"mb2x3", Builtin::kUndefined},
-    {"mat2j4", Builtin::kUndefined},
-    {"mt2x4", Builtin::kUndefined},
-    {"m2q4", Builtin::kUndefined},
-    {"matNN4f", Builtin::kUndefined},
-    {"at24vv", Builtin::kUndefined},
-    {"QQt2x4f", Builtin::kUndefined},
-    {"maffxr", Builtin::kUndefined},
     {"mat2xjh", Builtin::kUndefined},
+    {"at2x3h", Builtin::kUndefined},
+    {"q2x3h", Builtin::kUndefined},
+    {"mNN2x4", Builtin::kUndefined},
+    {"mavv4", Builtin::kUndefined},
+    {"maQQx4", Builtin::kUndefined},
+    {"maffxr", Builtin::kUndefined},
+    {"mat2xjf", Builtin::kUndefined},
     {"mNNw2x48", Builtin::kUndefined},
-    {"mt3x2", Builtin::kUndefined},
-    {"rrat3x2", Builtin::kUndefined},
-    {"mGt3x2", Builtin::kUndefined},
-    {"mat3x2FF", Builtin::kUndefined},
-    {"at3f", Builtin::kUndefined},
-    {"marrx2f", Builtin::kUndefined},
-    {"t3x2h", Builtin::kUndefined},
-    {"Da3xJJh", Builtin::kUndefined},
+    {"matx4h", Builtin::kUndefined},
+    {"mrrt2x4h", Builtin::kUndefined},
+    {"Gat2x4h", Builtin::kUndefined},
+    {"matFFx2", Builtin::kUndefined},
+    {"mtx", Builtin::kUndefined},
+    {"mrrt3x", Builtin::kUndefined},
+    {"t3x2f", Builtin::kUndefined},
+    {"Da3xJJf", Builtin::kUndefined},
     {"ma82", Builtin::kUndefined},
-    {"1k33", Builtin::kUndefined},
-    {"matx3", Builtin::kUndefined},
-    {"maJx3", Builtin::kUndefined},
-    {"mat3c3f", Builtin::kUndefined},
-    {"mat3x3O", Builtin::kUndefined},
-    {"KK_atvvtt3f", Builtin::kUndefined},
-    {"xx83x3h", Builtin::kUndefined},
+    {"m11k2", Builtin::kUndefined},
+    {"matx2h", Builtin::kUndefined},
+    {"maJx2h", Builtin::kUndefined},
+    {"cat3x3", Builtin::kUndefined},
+    {"mat3O3", Builtin::kUndefined},
+    {"ttKavv3x__", Builtin::kUndefined},
+    {"xx83x3f", Builtin::kUndefined},
     {"__qatF3", Builtin::kUndefined},
-    {"matqx3h", Builtin::kUndefined},
-    {"ma33x66", Builtin::kUndefined},
-    {"mttQQo3x4", Builtin::kUndefined},
-    {"mat66x", Builtin::kUndefined},
-    {"mtOxzz66", Builtin::kUndefined},
-    {"mat3yy4f", Builtin::kUndefined},
-    {"ZaHH4Z", Builtin::kUndefined},
-    {"4WWt3q4h", Builtin::kUndefined},
-    {"mOO3x4h", Builtin::kUndefined},
-    {"oatY4h", Builtin::kUndefined},
-    {"ax2", Builtin::kUndefined},
-    {"ma4x2", Builtin::kUndefined},
-    {"matw2", Builtin::kUndefined},
-    {"fGtxKf", Builtin::kUndefined},
-    {"matqKx2f", Builtin::kUndefined},
-    {"matmmxFf", Builtin::kUndefined},
-    {"at4x2h", Builtin::kUndefined},
+    {"matqx3f", Builtin::kUndefined},
+    {"33atOx3h", Builtin::kUndefined},
+    {"mtt63x9oQQ", Builtin::kUndefined},
+    {"ma3x66h", Builtin::kUndefined},
+    {"66aOzx4", Builtin::kUndefined},
+    {"myyt3x4", Builtin::kUndefined},
+    {"HHZx4", Builtin::kUndefined},
+    {"4WWt3q4f", Builtin::kUndefined},
+    {"mOO3x4f", Builtin::kUndefined},
+    {"oatY4f", Builtin::kUndefined},
+    {"matx", Builtin::kUndefined},
+    {"ma3xFh", Builtin::kUndefined},
+    {"at3x4w", Builtin::kUndefined},
+    {"ma4Gf", Builtin::kUndefined},
+    {"qatKKx2", Builtin::kUndefined},
+    {"mmmt4x2", Builtin::kUndefined},
+    {"at4x2f", Builtin::kUndefined},
     {"mt4x2q", Builtin::kUndefined},
     {"mat4xbb", Builtin::kUndefined},
-    {"it4x3", Builtin::kUndefined},
-    {"mOO4xq", Builtin::kUndefined},
-    {"mat4Tvv3", Builtin::kUndefined},
-    {"maFF4x3f", Builtin::kUndefined},
-    {"Pa00xQf", Builtin::kUndefined},
-    {"mPt4x3f", Builtin::kUndefined},
+    {"mi4x2h", Builtin::kUndefined},
+    {"maOO4xq", Builtin::kUndefined},
+    {"matTvvx2h", Builtin::kUndefined},
+    {"mat4FF3", Builtin::kUndefined},
+    {"mtQ00P", Builtin::kUndefined},
+    {"maP4x3", Builtin::kUndefined},
     {"ma774xss", Builtin::kUndefined},
-    {"RRCbb4x3h", Builtin::kUndefined},
-    {"mXXt4x3h", Builtin::kUndefined},
-    {"CCt4OOOO", Builtin::kUndefined},
-    {"mtsuL", Builtin::kUndefined},
-    {"mat4xX", Builtin::kUndefined},
-    {"mat44f", Builtin::kUndefined},
-    {"qa4O4", Builtin::kUndefined},
-    {"mat4x22f", Builtin::kUndefined},
+    {"RRCbb4x3f", Builtin::kUndefined},
+    {"mXXt4x3f", Builtin::kUndefined},
+    {"qaCC4xOOh", Builtin::kUndefined},
+    {"ma4s3L", Builtin::kUndefined},
+    {"mXt4x3h", Builtin::kUndefined},
+    {"mat4x", Builtin::kUndefined},
+    {"qqt4", Builtin::kUndefined},
+    {"mat4x22", Builtin::kUndefined},
     {"myzz40XX", Builtin::kUndefined},
     {"matVViP", Builtin::kUndefined},
-    {"mannC4h", Builtin::kUndefined},
-    {"pHAq", Builtin::kUndefined},
-    {"tr", Builtin::kUndefined},
-    {"Kf", Builtin::kUndefined},
-    {"lmgger", Builtin::kUndefined},
-    {"samplr", Builtin::kUndefined},
-    {"NTTmcl4r", Builtin::kUndefined},
-    {"sampler_clmppri77on", Builtin::kUndefined},
-    {"samplg_czzmparNNso", Builtin::kUndefined},
-    {"smpleuuXXomparibbon", Builtin::kUndefined},
-    {"texture_1", Builtin::kUndefined},
-    {"t88tueQ1K", Builtin::kUndefined},
-    {"texturq9d", Builtin::kUndefined},
-    {"text11re_2d", Builtin::kUndefined},
-    {"teiiu22eF2d", Builtin::kUndefined},
-    {"tex77ur_2d", Builtin::kUndefined},
-    {"textNNr2_d_array", Builtin::kUndefined},
-    {"textVVre_2d_array", Builtin::kUndefined},
-    {"texwure_WWF_11rray", Builtin::kUndefined},
-    {"txture_3ww", Builtin::kUndefined},
-    {"texturD_3d", Builtin::kUndefined},
-    {"teKture_d", Builtin::kUndefined},
-    {"11exPPufe_cubh", Builtin::kUndefined},
-    {"textue_cube", Builtin::kUndefined},
-    {"texture_cubYY", Builtin::kUndefined},
-    {"texttr_cube_HHkkVay", Builtin::kUndefined},
-    {"texture_crrbe_array", Builtin::kUndefined},
-    {"texturesscubeWWaray", Builtin::kUndefined},
-    {"texture_deptY_d", Builtin::kUndefined},
-    {"teLturq_defh_2d", Builtin::kUndefined},
-    {"texvvre_duu22th_2d", Builtin::kUndefined},
-    {"texure_deth_2d_array", Builtin::kUndefined},
-    {"texturYY_depth_2daray", Builtin::kUndefined},
-    {"texturE_77epth_2d_aryYay", Builtin::kUndefined},
-    {"Mexdoore_depth_cue", Builtin::kUndefined},
-    {"texturedepMMh_cube", Builtin::kUndefined},
-    {"texture55depth_cube", Builtin::kUndefined},
-    {"textue_depth_cbe_aNray", Builtin::kUndefined},
-    {"texture_dpth_c33be_array", Builtin::kUndefined},
-    {"texture_depth_cub3_array", Builtin::kUndefined},
-    {"texIure_mepth_mulisampled_2d", Builtin::kUndefined},
-    {"texture_depthrmKltisampled_2nn", Builtin::kUndefined},
-    {"textur_depth_multismXld_2d", Builtin::kUndefined},
-    {"texpure_exLLeIna", Builtin::kUndefined},
-    {"txture_exfrnal", Builtin::kUndefined},
-    {"teUture_extYRRDl", Builtin::kUndefined},
-    {"texturehmultisampled_2d", Builtin::kUndefined},
-    {"texturqmultsIImuuled_2d", Builtin::kUndefined},
-    {"Hexture_multisampled_2d", Builtin::kUndefined},
-    {"texQQur_storge_vvd", Builtin::kUndefined},
-    {"texeure_66oage_1d", Builtin::kUndefined},
-    {"texture_stoage71d", Builtin::kUndefined},
-    {"texture_s55or0ge_2DD", Builtin::kUndefined},
-    {"teHture_storIIge_2d", Builtin::kUndefined},
-    {"textue_storage_2d", Builtin::kUndefined},
-    {"texturestorage_2d_rrray", Builtin::kUndefined},
-    {"textule_storage_2d_array", Builtin::kUndefined},
-    {"tetture_JJtorage_Gd_arra", Builtin::kUndefined},
-    {"yexture_storage3d", Builtin::kUndefined},
-    {"texturestorage_3d", Builtin::kUndefined},
-    {"texture_IItorBBge_3d", Builtin::kUndefined},
-    {"TTK33", Builtin::kUndefined},
-    {"nnUYdSS2", Builtin::kUndefined},
-    {"x5dZ", Builtin::kUndefined},
-    {"veckq", Builtin::kUndefined},
-    {"ii500", Builtin::kUndefined},
-    {"vecIIn", Builtin::kUndefined},
-    {"cceW", Builtin::kUndefined},
-    {"cKK", Builtin::kUndefined},
-    {"vec66f", Builtin::kUndefined},
-    {"vePPK", Builtin::kUndefined},
-    {"vexxh", Builtin::kUndefined},
-    {"qec2h", Builtin::kUndefined},
+    {"mannC4f", Builtin::kUndefined},
+    {"atx4AHHq", Builtin::kUndefined},
+    {"may4x4", Builtin::kUndefined},
+    {"aOOOZZh", Builtin::kUndefined},
+    {"V", Builtin::kUndefined},
+    {"ptf__", Builtin::kUndefined},
+    {"4lMT", Builtin::kUndefined},
+    {"sNNmplg", Builtin::kUndefined},
+    {"uubpXXer", Builtin::kUndefined},
+    {"samler", Builtin::kUndefined},
+    {"m88ler_cQmparisoK", Builtin::kUndefined},
+    {"qa9ler_comparison", Builtin::kUndefined},
+    {"sampler_comparis11n", Builtin::kUndefined},
+    {"teiiu22eF1d", Builtin::kUndefined},
+    {"tex77ur_1d", Builtin::kUndefined},
+    {"te2urNN_1d", Builtin::kUndefined},
+    {"texturVV_2d", Builtin::kUndefined},
+    {"WWFxtu11e_wd", Builtin::kUndefined},
+    {"txture_2ww", Builtin::kUndefined},
+    {"texture_2d_arrDy", Builtin::kUndefined},
+    {"teKtre_2d_array", Builtin::kUndefined},
+    {"texhure_2fra11raPP", Builtin::kUndefined},
+    {"texture3d", Builtin::kUndefined},
+    {"texture_3YY", Builtin::kUndefined},
+    {"HHtxtrkk_3d", Builtin::kUndefined},
+    {"texrrure_cube", Builtin::kUndefined},
+    {"tssxturWW_cue", Builtin::kUndefined},
+    {"teYure_cube", Builtin::kUndefined},
+    {"txture_Lufe_arraq", Builtin::kUndefined},
+    {"te22ture_uuubevvfray", Builtin::kUndefined},
+    {"texturecube_aray", Builtin::kUndefined},
+    {"texture_Yepth_2", Builtin::kUndefined},
+    {"teytYYEe_77epth_2d", Builtin::kUndefined},
+    {"teMture_deootd2d", Builtin::kUndefined},
+    {"texMMre_depth_2d_array", Builtin::kUndefined},
+    {"texture_depth_2d_arra55", Builtin::kUndefined},
+    {"texture_deh_2d_aNray", Builtin::kUndefined},
+    {"te3ture_dpth_cO3be", Builtin::kUndefined},
+    {"texture_depth_cub3", Builtin::kUndefined},
+    {"Iexturedepth_cume", Builtin::kUndefined},
+    {"texture_depthnncube_Krrry", Builtin::kUndefined},
+    {"texture_dth_XXube_rra", Builtin::kUndefined},
+    {"textIre_depph_ubeLLarray", Builtin::kUndefined},
+    {"txtfre_depthmultisampled_2d", Builtin::kUndefined},
+    {"texURuYe_Depthmultisampled_2d", Builtin::kUndefined},
+    {"texture_depth_multisamphed_2d", Builtin::kUndefined},
+    {"teqtureuIIextnal", Builtin::kUndefined},
+    {"texture_externaH", Builtin::kUndefined},
+    {"texre_externaQvv", Builtin::kUndefined},
+    {"textureemultismp66ed_d", Builtin::kUndefined},
+    {"tW7trO_multisampled_2d", Builtin::kUndefined},
+    {"texture_mult550ampled_2DD", Builtin::kUndefined},
+    {"teHture_storIIge_1d", Builtin::kUndefined},
+    {"textue_storage_1d", Builtin::kUndefined},
+    {"rexture_storae_1d", Builtin::kUndefined},
+    {"texture_stolage_2d", Builtin::kUndefined},
+    {"txture_JJtGrgtt_2d", Builtin::kUndefined},
+    {"yexture_storage2d", Builtin::kUndefined},
+    {"texture_storage_2d_rray", Builtin::kUndefined},
+    {"texture_IItorage_2d_BBrray", Builtin::kUndefined},
+    {"33exture_TTtorge_Kd_ar88ay", Builtin::kUndefined},
+    {"texSnnYUUure_storage_3d", Builtin::kUndefined},
+    {"textuxe_5torCCdZ_3d", Builtin::kUndefined},
+    {"tkkxture_storaqe_3d", Builtin::kUndefined},
+    {"5i00", Builtin::kUndefined},
+    {"unII2", Builtin::kUndefined},
+    {"cc", Builtin::kUndefined},
+    {"KK", Builtin::kUndefined},
+    {"66ec2", Builtin::kUndefined},
+    {"PPEK", Builtin::kUndefined},
+    {"vexxf", Builtin::kUndefined},
+    {"qec2f", Builtin::kUndefined},
     {"veSyMMr", Builtin::kUndefined},
     {"v2u", Builtin::kUndefined},
     {"ec", Builtin::kUndefined},
-    {"5eFF2u", Builtin::kUndefined},
+    {"5eFF2h", Builtin::kUndefined},
     {"rrecz44", Builtin::kUndefined},
     {"vWW", Builtin::kUndefined},
-    {"ZJJCcX", Builtin::kUndefined},
-    {"vcPP", Builtin::kUndefined},
-    {"vec", Builtin::kUndefined},
-    {"3Le003f", Builtin::kUndefined},
-    {"MMec3RR", Builtin::kUndefined},
-    {"vec39K", Builtin::kUndefined},
-    {"yyecm", Builtin::kUndefined},
-    {"v__cD", Builtin::kUndefined},
-    {"vec3U", Builtin::kUndefined},
-    {"ze333i", Builtin::kUndefined},
-    {"eKti", Builtin::kUndefined},
-    {"ve3V", Builtin::kUndefined},
-    {"jbR3K", Builtin::kUndefined},
-    {"e44344", Builtin::kUndefined},
-    {"00u", Builtin::kUndefined},
-    {"WK4", Builtin::kUndefined},
-    {"m", Builtin::kUndefined},
-    {"vJJ", Builtin::kUndefined},
-    {"lDDcUfC", Builtin::kUndefined},
-    {"vec4g", Builtin::kUndefined},
-    {"CCe", Builtin::kUndefined},
-    {"ec4h", Builtin::kUndefined},
-    {"vIc__h", Builtin::kUndefined},
+    {"XJecCZZ", Builtin::kUndefined},
+    {"vePP2", Builtin::kUndefined},
+    {"vec2c", Builtin::kUndefined},
+    {"ve6ll2u", Builtin::kUndefined},
+    {"vey99", Builtin::kUndefined},
+    {"vKKc3", Builtin::kUndefined},
+    {"x_3", Builtin::kUndefined},
+    {"Ky3", Builtin::kUndefined},
+    {"zek3f", Builtin::kUndefined},
+    {"veKSf", Builtin::kUndefined},
+    {"vc3h", Builtin::kUndefined},
+    {"ec3VV", Builtin::kUndefined},
+    {"IAAc3h", Builtin::kUndefined},
+    {"jbR", Builtin::kUndefined},
+    {"veY4", Builtin::kUndefined},
+    {"ec3i", Builtin::kUndefined},
+    {"vc911", Builtin::kUndefined},
+    {"mmccu", Builtin::kUndefined},
+    {"vJJcu", Builtin::kUndefined},
+    {"lDCfcU", Builtin::kUndefined},
+    {"veg4", Builtin::kUndefined},
+    {"CC", Builtin::kUndefined},
+    {"ec4f", Builtin::kUndefined},
+    {"vIc__f", Builtin::kUndefined},
     {"ePPtt", Builtin::kUndefined},
-    {"v3dc4i", Builtin::kUndefined},
-    {"vcyyi", Builtin::kUndefined},
+    {"v3dc4h", Builtin::kUndefined},
+    {"vcyyh", Builtin::kUndefined},
     {"u4", Builtin::kUndefined},
-    {"v03nnu", Builtin::kUndefined},
+    {"v03nni", Builtin::kUndefined},
     {"Cuuecnv", Builtin::kUndefined},
     {"vX4ll", Builtin::kUndefined},
+    {"vocppu", Builtin::kUndefined},
+    {"vwwc4", Builtin::kUndefined},
+    {"veuug", Builtin::kUndefined},
 };
 
 using BuiltinParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/builtin/extension.cc b/src/tint/builtin/extension.cc
index 3d57867..36171db 100644
--- a/src/tint/builtin/extension.cc
+++ b/src/tint/builtin/extension.cc
@@ -40,6 +40,9 @@
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
+    if (str == "chromium_internal_relaxed_uniform_layout") {
+        return Extension::kChromiumInternalRelaxedUniformLayout;
+    }
     if (str == "f16") {
         return Extension::kF16;
     }
@@ -58,6 +61,8 @@
             return out << "chromium_experimental_full_ptr_parameters";
         case Extension::kChromiumExperimentalPushConstant:
             return out << "chromium_experimental_push_constant";
+        case Extension::kChromiumInternalRelaxedUniformLayout:
+            return out << "chromium_internal_relaxed_uniform_layout";
         case Extension::kF16:
             return out << "f16";
     }
diff --git a/src/tint/builtin/extension.h b/src/tint/builtin/extension.h
index f0ebeca..aace504 100644
--- a/src/tint/builtin/extension.h
+++ b/src/tint/builtin/extension.h
@@ -37,6 +37,7 @@
     kChromiumExperimentalDp4A,
     kChromiumExperimentalFullPtrParameters,
     kChromiumExperimentalPushConstant,
+    kChromiumInternalRelaxedUniformLayout,
     kF16,
 };
 
@@ -51,11 +52,9 @@
 Extension ParseExtension(std::string_view str);
 
 constexpr const char* kExtensionStrings[] = {
-    "chromium_disable_uniformity_analysis",
-    "chromium_experimental_dp4a",
-    "chromium_experimental_full_ptr_parameters",
-    "chromium_experimental_push_constant",
-    "f16",
+    "chromium_disable_uniformity_analysis",      "chromium_experimental_dp4a",
+    "chromium_experimental_full_ptr_parameters", "chromium_experimental_push_constant",
+    "chromium_internal_relaxed_uniform_layout",  "f16",
 };
 
 // A unique vector of extensions
diff --git a/src/tint/builtin/extension_bench.cc b/src/tint/builtin/extension_bench.cc
index 4619ce2..b3e410e 100644
--- a/src/tint/builtin/extension_bench.cc
+++ b/src/tint/builtin/extension_bench.cc
@@ -59,13 +59,20 @@
         "chromium_exp9rimFntal_ush_constant",
         "chrmium_experimental_push_constant",
         "cOOromium_experiVeHtal_puh_conRRtant",
-        "y1",
-        "l77rrn6",
-        "4016",
+        "chromium_internl_relaxyd_uniform_layout",
+        "chromnnum_internrr77_Gelaxell_uniform_layout",
+        "chromium_intern4l_relaxe00_uniform_layout",
+        "chromium_internal_relaxed_uniform_layout",
+        "chrmoom_internal_relaxed_uniform_lyout",
+        "chroium_internal_rlaxed_uniform_layzzut",
+        "chromium_internaii_r11axed_uppifor_layout",
+        "f1XX",
+        "55199II",
+        "frSSHHa",
         "f16",
-        "5",
-        "u16",
-        "f",
+        "U",
+        "jV3",
+        "",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
diff --git a/src/tint/builtin/extension_test.cc b/src/tint/builtin/extension_test.cc
index 01eb1ae..9ef8683 100644
--- a/src/tint/builtin/extension_test.cc
+++ b/src/tint/builtin/extension_test.cc
@@ -48,6 +48,7 @@
     {"chromium_experimental_full_ptr_parameters",
      Extension::kChromiumExperimentalFullPtrParameters},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
+    {"chromium_internal_relaxed_uniform_layout", Extension::kChromiumInternalRelaxedUniformLayout},
     {"f16", Extension::kF16},
 };
 
@@ -64,9 +65,12 @@
     {"chvomium_experimental_push_constiint", Extension::kUndefined},
     {"chromiu8WWexperimental_push_constant", Extension::kUndefined},
     {"chromium_experiMental_push_costanxx", Extension::kUndefined},
-    {"fgg", Extension::kUndefined},
-    {"X", Extension::kUndefined},
-    {"316", Extension::kUndefined},
+    {"chromium_internal_relaxed_unXform_layugg", Extension::kUndefined},
+    {"chromiuu_iVterna_relxed_unifXrm_layout", Extension::kUndefined},
+    {"chromium_internal_relaxed_uni3orm_layout", Extension::kUndefined},
+    {"fE6", Extension::kUndefined},
+    {"fPTT", Extension::kUndefined},
+    {"dxx6", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index cd46885..12d5b8c 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -37,6 +37,7 @@
 #include "src/tint/cmd/helper.h"
 #include "src/tint/utils/io/command.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/transform.h"
 #include "src/tint/val/val.h"
 #include "tint/tint.h"
@@ -245,7 +246,7 @@
 std::vector<std::string> split_on_char(std::string list, char c) {
     std::vector<std::string> res;
 
-    std::stringstream str(list);
+    std::istringstream str(list);
     while (str.good()) {
         std::string substr;
         getline(str, substr, c);
@@ -1034,7 +1035,7 @@
          }},
     };
     auto transform_names = [&] {
-        std::stringstream names;
+        tint::utils::StringStream names;
         for (auto& t : transforms) {
             names << "   " << t.name << std::endl;
         }
diff --git a/src/tint/debug.h b/src/tint/debug.h
index 43bc052..5caeb5d 100644
--- a/src/tint/debug.h
+++ b/src/tint/debug.h
@@ -21,6 +21,7 @@
 #include "src/tint/diagnostic/formatter.h"
 #include "src/tint/diagnostic/printer.h"
 #include "src/tint/utils/compiler_macros.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint {
 
@@ -71,7 +72,7 @@
     const size_t line_;
     diag::System system_;
     diag::List& diagnostics_;
-    std::stringstream msg_;
+    utils::StringStream msg_;
 };
 
 }  // namespace tint
diff --git a/src/tint/demangler.cc b/src/tint/demangler.cc
index d68a62a..146a31c 100644
--- a/src/tint/demangler.cc
+++ b/src/tint/demangler.cc
@@ -15,6 +15,7 @@
 #include "src/tint/demangler.h"
 
 #include "src/tint/program.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint {
 namespace {
@@ -29,7 +30,7 @@
 Demangler::~Demangler() = default;
 
 std::string Demangler::Demangle(const SymbolTable& symbols, const std::string& str) const {
-    std::stringstream out;
+    utils::StringStream out;
 
     size_t pos = 0;
     for (;;) {
diff --git a/src/tint/diagnostic/formatter.cc b/src/tint/diagnostic/formatter.cc
index db69397..18520fc 100644
--- a/src/tint/diagnostic/formatter.cc
+++ b/src/tint/diagnostic/formatter.cc
@@ -20,6 +20,7 @@
 
 #include "src/tint/diagnostic/diagnostic.h"
 #include "src/tint/diagnostic/printer.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::diag {
 namespace {
@@ -41,7 +42,7 @@
 }
 
 std::string to_str(const Source::Location& location) {
-    std::stringstream ss;
+    utils::StringStream ss;
     if (location.line > 0) {
         ss << location.line;
         if (location.column > 0) {
@@ -75,7 +76,7 @@
         auto str = stream.str();
         if (str.length() > 0) {
             printer->write(str, style);
-            std::stringstream reset;
+            utils::StringStream reset;
             stream.swap(reset);
         }
     }
@@ -95,12 +96,12 @@
     /// repeat queues the character c to be written to the printer n times.
     /// @param c the character to print `n` times
     /// @param n the number of times to print character `c`
-    void repeat(char c, size_t n) { std::fill_n(std::ostream_iterator<char>(stream), n, c); }
+    void repeat(char c, size_t n) { stream.repeat(c, n); }
 
   private:
     Printer* printer;
     diag::Style style;
-    std::stringstream stream;
+    utils::StringStream stream;
 };
 
 Formatter::Formatter() {}
diff --git a/src/tint/diagnostic/printer.h b/src/tint/diagnostic/printer.h
index b2ac105..9e4ce7c 100644
--- a/src/tint/diagnostic/printer.h
+++ b/src/tint/diagnostic/printer.h
@@ -19,6 +19,8 @@
 #include <sstream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::diag {
 
 class List;
@@ -73,7 +75,7 @@
     void write(const std::string& str, const Style&) override;
 
   private:
-    std::stringstream stream;
+    utils::StringStream stream;
 };
 
 }  // namespace tint::diag
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index 826a667..7a5334f 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -70,6 +70,8 @@
   // A Chromium-specific extension that enables passing of uniform, storage and workgroup
   // address-spaced pointers as parameters, as well as pointers into sub-objects.
   chromium_experimental_full_ptr_parameters
+  // A Chromium-specific extension that relaxes memory layout requirements for uniform storage.
+  chromium_internal_relaxed_uniform_layout
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#storage-class
@@ -209,6 +211,9 @@
   texture_storage_3d
   // https://www.w3.org/TR/WGSL/#external-texture-type
   texture_external
+
+  // Internal types.
+  __packed_vec3
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#attributes
@@ -287,6 +292,7 @@
 type texture_storage_2d_array<F: texel_format, A: access>
 type texture_storage_3d<F: texel_format, A: access>
 type texture_external
+type packedVec3<T>
 
 @display("__modf_result_{T}")        type __modf_result<T>
 @display("__modf_result_vec{N}_{T}") type __modf_result_vec<N: num, T>
@@ -999,6 +1005,9 @@
 @must_use @const conv mat4x4<T: f16>(mat4x4<f32>) -> mat4x4<f16>
 @must_use @const conv mat4x4<T: f32>(mat4x4<f16>) -> mat4x4<f32>
 
+// Conversion from vec3 to internal __packed_vec3 type.
+@must_use @const conv packedVec3<T: concrete_scalar>(vec3<T>) -> packedVec3<T>
+
 ////////////////////////////////////////////////////////////////////////////////
 // Operators                                                                  //
 //                                                                            //
diff --git a/src/tint/ir/binary.cc b/src/tint/ir/binary.cc
index e48ca86..cdec94d 100644
--- a/src/tint/ir/binary.cc
+++ b/src/tint/ir/binary.cc
@@ -29,7 +29,7 @@
 
 Binary::~Binary() = default;
 
-std::ostream& Binary::ToString(std::ostream& out, const SymbolTable& st) const {
+utils::StringStream& Binary::ToString(utils::StringStream& out, const SymbolTable& st) const {
     Result()->ToString(out, st) << " = ";
     lhs_->ToString(out, st) << " ";
 
diff --git a/src/tint/ir/binary.h b/src/tint/ir/binary.h
index 85ad9dc..be5d243 100644
--- a/src/tint/ir/binary.h
+++ b/src/tint/ir/binary.h
@@ -21,6 +21,7 @@
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -79,7 +80,7 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    std::ostream& ToString(std::ostream& out, const SymbolTable& st) const override;
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
 
   private:
     Kind kind_;
@@ -87,8 +88,6 @@
     Value* rhs_ = nullptr;
 };
 
-std::ostream& operator<<(std::ostream& out, const Binary&);
-
 }  // namespace tint::ir
 
 #endif  // SRC_TINT_IR_BINARY_H_
diff --git a/src/tint/ir/binary_test.cc b/src/tint/ir/binary_test.cc
index b5fba93..103719c 100644
--- a/src/tint/ir/binary_test.cc
+++ b/src/tint/ir/binary_test.cc
@@ -16,6 +16,7 @@
 
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 namespace {
@@ -47,7 +48,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 & 2");
 }
@@ -74,7 +75,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 | 2");
 }
@@ -101,7 +102,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 ^ 2");
 }
@@ -128,7 +129,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 && 2");
 }
@@ -155,7 +156,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 || 2");
 }
@@ -182,7 +183,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 == 2");
 }
@@ -209,7 +210,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 != 2");
 }
@@ -236,7 +237,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 < 2");
 }
@@ -263,7 +264,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 > 2");
 }
@@ -290,7 +291,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 <= 2");
 }
@@ -317,7 +318,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (bool) = 4 >= 2");
 }
@@ -344,7 +345,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 << 2");
 }
@@ -371,7 +372,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 >> 2");
 }
@@ -398,7 +399,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 + 2");
 }
@@ -425,7 +426,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 - 2");
 }
@@ -452,7 +453,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 * 2");
 }
@@ -479,7 +480,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 / 2");
 }
@@ -506,7 +507,7 @@
     ASSERT_TRUE(rhs->Is<constant::Scalar<i32>>());
     EXPECT_EQ(2_i, rhs->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = 4 % 2");
 }
diff --git a/src/tint/ir/bitcast.cc b/src/tint/ir/bitcast.cc
index 512d93a..3cf0c97 100644
--- a/src/tint/ir/bitcast.cc
+++ b/src/tint/ir/bitcast.cc
@@ -26,7 +26,7 @@
 
 Bitcast::~Bitcast() = default;
 
-std::ostream& Bitcast::ToString(std::ostream& out, const SymbolTable& st) const {
+utils::StringStream& Bitcast::ToString(utils::StringStream& out, const SymbolTable& st) const {
     Result()->ToString(out, st);
     out << " = bitcast(";
     val_->ToString(out, st);
diff --git a/src/tint/ir/bitcast.h b/src/tint/ir/bitcast.h
index df8a05e..16d62f8 100644
--- a/src/tint/ir/bitcast.h
+++ b/src/tint/ir/bitcast.h
@@ -21,6 +21,7 @@
 #include "src/tint/ir/instruction.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -45,14 +46,12 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    std::ostream& ToString(std::ostream& out, const SymbolTable& st) const override;
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
 
   private:
     Value* val_ = nullptr;
 };
 
-std::ostream& operator<<(std::ostream& out, const Bitcast&);
-
 }  // namespace tint::ir
 
 #endif  // SRC_TINT_IR_BITCAST_H_
diff --git a/src/tint/ir/bitcast_test.cc b/src/tint/ir/bitcast_test.cc
index ee7bd3d..d190abb 100644
--- a/src/tint/ir/bitcast_test.cc
+++ b/src/tint/ir/bitcast_test.cc
@@ -16,6 +16,7 @@
 
 #include "src/tint/ir/instruction.h"
 #include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 namespace {
@@ -40,7 +41,7 @@
     ASSERT_TRUE(val->Is<constant::Scalar<i32>>());
     EXPECT_EQ(4_i, val->As<constant::Scalar<i32>>()->ValueAs<i32>());
 
-    std::stringstream str;
+    utils::StringStream str;
     instr->ToString(str, b.builder.ir.symbols);
     EXPECT_EQ(str.str(), "%42 (i32) = bitcast(4)");
 }
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index ccf0ccf..b70c059 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.3 * 5.5
-%5 (f32) = 6.7 / %4 (f32)
+%4 (f32) = 2.299999952 * 5.5
+%5 (f32) = 6.699999809 / %4 (f32)
 %6 (bool) = 2.5 > %5 (f32)
 %7 (bool) = %3 (bool) && %6 (bool)
 )");
diff --git a/src/tint/ir/constant.cc b/src/tint/ir/constant.cc
index 98a5d3d..da4cb36 100644
--- a/src/tint/ir/constant.cc
+++ b/src/tint/ir/constant.cc
@@ -28,7 +28,7 @@
 
 Constant::~Constant() = default;
 
-std::ostream& Constant::ToString(std::ostream& out, const SymbolTable& st) const {
+utils::StringStream& Constant::ToString(utils::StringStream& out, const SymbolTable& st) const {
     std::function<void(const constant::Value*)> emit = [&](const constant::Value* c) {
         Switch(
             c,
diff --git a/src/tint/ir/constant.h b/src/tint/ir/constant.h
index 10b41d3..e7d66a4 100644
--- a/src/tint/ir/constant.h
+++ b/src/tint/ir/constant.h
@@ -20,6 +20,7 @@
 #include "src/tint/constant/value.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -38,7 +39,7 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    std::ostream& ToString(std::ostream& out, const SymbolTable& st) const override;
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
 
     /// The constants value
     const constant::Value* const value;
diff --git a/src/tint/ir/constant_test.cc b/src/tint/ir/constant_test.cc
index c6fe2e4..745ec54 100644
--- a/src/tint/ir/constant_test.cc
+++ b/src/tint/ir/constant_test.cc
@@ -16,6 +16,7 @@
 
 #include "src/tint/ir/test_helper.h"
 #include "src/tint/ir/value.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 namespace {
@@ -27,13 +28,13 @@
 TEST_F(IR_ConstantTest, f32) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    utils::StringStream str;
 
     auto* c = b.builder.Constant(1.2_f);
     EXPECT_EQ(1.2_f, c->value->As<constant::Scalar<f32>>()->ValueAs<f32>());
 
     c->ToString(str, b.builder.ir.symbols);
-    EXPECT_EQ("1.2", str.str());
+    EXPECT_EQ("1.200000048", str.str());
 
     EXPECT_TRUE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
@@ -45,13 +46,13 @@
 TEST_F(IR_ConstantTest, f16) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    utils::StringStream str;
 
     auto* c = b.builder.Constant(1.1_h);
     EXPECT_EQ(1.1_h, c->value->As<constant::Scalar<f16>>()->ValueAs<f16>());
 
     c->ToString(str, b.builder.ir.symbols);
-    EXPECT_EQ("1.09961", str.str());
+    EXPECT_EQ("1.099609375", str.str());
 
     EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
     EXPECT_TRUE(c->value->Is<constant::Scalar<f16>>());
@@ -63,7 +64,7 @@
 TEST_F(IR_ConstantTest, i32) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    utils::StringStream str;
 
     auto* c = b.builder.Constant(1_i);
     EXPECT_EQ(1_i, c->value->As<constant::Scalar<i32>>()->ValueAs<i32>());
@@ -81,7 +82,7 @@
 TEST_F(IR_ConstantTest, u32) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    utils::StringStream str;
 
     auto* c = b.builder.Constant(2_u);
     EXPECT_EQ(2_u, c->value->As<constant::Scalar<u32>>()->ValueAs<u32>());
@@ -99,26 +100,30 @@
 TEST_F(IR_ConstantTest, bool) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    {
+        utils::StringStream str;
 
-    auto* c = b.builder.Constant(false);
-    EXPECT_FALSE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
+        auto* c = b.builder.Constant(false);
+        EXPECT_FALSE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-    c->ToString(str, b.builder.ir.symbols);
-    EXPECT_EQ("false", str.str());
+        c->ToString(str, b.builder.ir.symbols);
+        EXPECT_EQ("false", str.str());
+    }
 
-    str.str("");
-    c = b.builder.Constant(true);
-    EXPECT_TRUE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
+    {
+        utils::StringStream str;
+        auto c = b.builder.Constant(true);
+        EXPECT_TRUE(c->value->As<constant::Scalar<bool>>()->ValueAs<bool>());
 
-    c->ToString(str, b.builder.ir.symbols);
-    EXPECT_EQ("true", str.str());
+        c->ToString(str, b.builder.ir.symbols);
+        EXPECT_EQ("true", str.str());
 
-    EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
-    EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
-    EXPECT_FALSE(c->value->Is<constant::Scalar<i32>>());
-    EXPECT_FALSE(c->value->Is<constant::Scalar<u32>>());
-    EXPECT_TRUE(c->value->Is<constant::Scalar<bool>>());
+        EXPECT_FALSE(c->value->Is<constant::Scalar<f32>>());
+        EXPECT_FALSE(c->value->Is<constant::Scalar<f16>>());
+        EXPECT_FALSE(c->value->Is<constant::Scalar<i32>>());
+        EXPECT_FALSE(c->value->Is<constant::Scalar<u32>>());
+        EXPECT_TRUE(c->value->Is<constant::Scalar<bool>>());
+    }
 }
 
 }  // namespace
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
index 617b207..b541236 100644
--- a/src/tint/ir/debug.cc
+++ b/src/tint/ir/debug.cc
@@ -23,6 +23,7 @@
 #include "src/tint/ir/loop.h"
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -33,7 +34,7 @@
     std::unordered_set<const FlowNode*> visited;
     std::unordered_set<const FlowNode*> merge_nodes;
     std::unordered_map<const FlowNode*, std::string> node_to_name;
-    std::stringstream out;
+    utils::StringStream out;
 
     auto name_for = [&](const FlowNode* node) -> std::string {
         if (node_to_name.count(node) > 0) {
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 4665c80..1bac411 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -53,7 +53,7 @@
 
 Disassembler::~Disassembler() = default;
 
-std::ostream& Disassembler::Indent() {
+utils::StringStream& Disassembler::Indent() {
     for (uint32_t i = 0; i < indent_size_; i++) {
         out_ << " ";
     }
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index 03cbea1..1ed822c 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -22,6 +22,7 @@
 
 #include "src/tint/ir/flow_node.h"
 #include "src/tint/ir/module.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -45,12 +46,12 @@
     std::string AsString() const { return out_.str(); }
 
   private:
-    std::ostream& Indent();
+    utils::StringStream& Indent();
     void Walk(const FlowNode* node);
     size_t GetIdForNode(const FlowNode* node);
 
     const Module& mod_;
-    std::stringstream out_;
+    utils::StringStream out_;
     std::unordered_set<const FlowNode*> visited_;
     std::unordered_set<const FlowNode*> stop_nodes_;
     std::unordered_map<const FlowNode*, size_t> flow_node_to_id_;
diff --git a/src/tint/ir/instruction.h b/src/tint/ir/instruction.h
index 8cd9ba8..abd5179 100644
--- a/src/tint/ir/instruction.h
+++ b/src/tint/ir/instruction.h
@@ -20,6 +20,7 @@
 #include "src/tint/castable.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -41,7 +42,8 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    virtual std::ostream& ToString(std::ostream& out, const SymbolTable& st) const = 0;
+    virtual utils::StringStream& ToString(utils::StringStream& out,
+                                          const SymbolTable& st) const = 0;
 
   protected:
     /// Constructor
diff --git a/src/tint/ir/temp.cc b/src/tint/ir/temp.cc
index e925561..3efae03 100644
--- a/src/tint/ir/temp.cc
+++ b/src/tint/ir/temp.cc
@@ -24,7 +24,7 @@
 
 Temp::~Temp() = default;
 
-std::ostream& Temp::ToString(std::ostream& out, const SymbolTable& st) const {
+utils::StringStream& Temp::ToString(utils::StringStream& out, const SymbolTable& st) const {
     out << "%" << std::to_string(AsId()) << " (" << type_->FriendlyName(st) << ")";
     return out;
 }
diff --git a/src/tint/ir/temp.h b/src/tint/ir/temp.h
index 1a4a38d..989e416 100644
--- a/src/tint/ir/temp.h
+++ b/src/tint/ir/temp.h
@@ -19,6 +19,7 @@
 
 #include "src/tint/ir/value.h"
 #include "src/tint/symbol_table.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 
@@ -52,7 +53,7 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    std::ostream& ToString(std::ostream& out, const SymbolTable& st) const override;
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
 
   private:
     const type::Type* type_ = nullptr;
diff --git a/src/tint/ir/temp_test.cc b/src/tint/ir/temp_test.cc
index ddb0124..1a33769 100644
--- a/src/tint/ir/temp_test.cc
+++ b/src/tint/ir/temp_test.cc
@@ -16,6 +16,7 @@
 
 #include "src/tint/ir/temp.h"
 #include "src/tint/ir/test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::ir {
 namespace {
@@ -27,7 +28,7 @@
 TEST_F(IR_TempTest, id) {
     auto& b = CreateEmptyBuilder();
 
-    std::stringstream str;
+    utils::StringStream str;
 
     b.builder.next_temp_id = Temp::Id(4);
     auto* val = b.builder.Temp(b.builder.ir.types.Get<type::I32>());
diff --git a/src/tint/ir/value.h b/src/tint/ir/value.h
index 1f9619f..983eae0 100644
--- a/src/tint/ir/value.h
+++ b/src/tint/ir/value.h
@@ -20,6 +20,7 @@
 #include "src/tint/castable.h"
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
 // Forward declarations
@@ -56,7 +57,8 @@
     /// @param out the stream to write to
     /// @param st the symbol table
     /// @returns the stream
-    virtual std::ostream& ToString(std::ostream& out, const SymbolTable& st) const = 0;
+    virtual utils::StringStream& ToString(utils::StringStream& out,
+                                          const SymbolTable& st) const = 0;
 
   protected:
     /// Constructor
diff --git a/src/tint/number_test.cc b/src/tint/number_test.cc
index fe03663..7ae5e37 100644
--- a/src/tint/number_test.cc
+++ b/src/tint/number_test.cc
@@ -19,6 +19,7 @@
 
 #include "src/tint/program_builder.h"
 #include "src/tint/utils/compiler_macros.h"
+#include "src/tint/utils/string_stream.h"
 
 #include "gtest/gtest.h"
 
@@ -254,7 +255,7 @@
     float input_value = GetParam().input_value;
     float quantized_value = GetParam().quantized_value;
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "input value = " << input_value << ", expected quantized value = " << quantized_value;
     SCOPED_TRACE(ss.str());
 
@@ -269,7 +270,7 @@
     float input_value = GetParam().input_value;
     uint16_t representation = GetParam().f16_bit_pattern;
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "input value = " << input_value
        << ", expected binary16 bits representation = " << std::hex << std::showbase
        << representation;
@@ -282,7 +283,7 @@
     float input_value = GetParam().quantized_value;
     uint16_t representation = GetParam().f16_bit_pattern;
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "binary16 bits representation = " << std::hex << std::showbase << representation
        << " expected value = " << input_value;
     SCOPED_TRACE(ss.str());
diff --git a/src/tint/reader/spirv/construct.h b/src/tint/reader/spirv/construct.h
index cd0804a..06ae450 100644
--- a/src/tint/reader/spirv/construct.h
+++ b/src/tint/reader/spirv/construct.h
@@ -19,6 +19,7 @@
 #include <sstream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/vector.h"
 
 namespace tint::reader::spirv {
@@ -173,7 +174,7 @@
 /// @returns a short summary string
 inline std::string ToStringBrief(const Construct* c) {
     if (c) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << ToString(c->kind) << "@" << c->begin_id;
         return ss.str();
     }
@@ -184,7 +185,7 @@
 /// @param o the stream
 /// @param c the structured construct
 /// @returns the stream
-inline std::ostream& operator<<(std::ostream& o, const Construct& c) {
+inline utils::StringStream& operator<<(utils::StringStream& o, const Construct& c) {
     o << "Construct{ " << ToString(c.kind) << " [" << c.begin_pos << "," << c.end_pos << ")"
       << " begin_id:" << c.begin_id << " end_id:" << c.end_id << " depth:" << c.depth;
 
@@ -215,7 +216,8 @@
 /// @param o the stream
 /// @param c the structured construct
 /// @returns the stream
-inline std::ostream& operator<<(std::ostream& o, const std::unique_ptr<Construct>& c) {
+inline utils::StringStream& operator<<(utils::StringStream& o,
+                                       const std::unique_ptr<Construct>& c) {
     return o << *(c.get());
 }
 
@@ -223,7 +225,7 @@
 /// @param c the construct
 /// @returns the string representation
 inline std::string ToString(const Construct& c) {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << c;
     return ss.str();
 }
@@ -246,7 +248,7 @@
 /// @param o the stream
 /// @param cl the construct list
 /// @returns the stream
-inline std::ostream& operator<<(std::ostream& o, const ConstructList& cl) {
+inline utils::StringStream& operator<<(utils::StringStream& o, const ConstructList& cl) {
     o << "ConstructList{\n";
     for (const auto& c : cl) {
         o << "  " << c << "\n";
@@ -259,7 +261,7 @@
 /// @param cl the construct list
 /// @returns the string representation
 inline std::string ToString(const ConstructList& cl) {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << cl;
     return ss.str();
 }
diff --git a/src/tint/reader/spirv/enum_converter_test.cc b/src/tint/reader/spirv/enum_converter_test.cc
index 407b121..854504d 100644
--- a/src/tint/reader/spirv/enum_converter_test.cc
+++ b/src/tint/reader/spirv/enum_converter_test.cc
@@ -18,6 +18,7 @@
 
 #include "gmock/gmock.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -45,7 +46,7 @@
 
   protected:
     bool success_ = true;
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     EnumConverter converter_;
 };
@@ -103,7 +104,7 @@
 
   protected:
     bool success_ = true;
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     EnumConverter converter_;
 };
@@ -164,7 +165,7 @@
 
   protected:
     bool success_ = true;
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     EnumConverter converter_;
 };
@@ -239,7 +240,7 @@
 
   protected:
     bool success_ = true;
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     EnumConverter converter_;
 };
@@ -311,7 +312,7 @@
 
   protected:
     bool success_ = true;
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     EnumConverter converter_;
 };
diff --git a/src/tint/reader/spirv/fail_stream.h b/src/tint/reader/spirv/fail_stream.h
index 382fe5f..6530eae 100644
--- a/src/tint/reader/spirv/fail_stream.h
+++ b/src/tint/reader/spirv/fail_stream.h
@@ -15,7 +15,7 @@
 #ifndef SRC_TINT_READER_SPIRV_FAIL_STREAM_H_
 #define SRC_TINT_READER_SPIRV_FAIL_STREAM_H_
 
-#include <ostream>
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 
@@ -29,7 +29,7 @@
     /// to be a valid pointer to bool.
     /// @param out output stream where a message should be written to explain
     /// the failure
-    FailStream(bool* status_ptr, std::ostream* out) : status_ptr_(status_ptr), out_(out) {}
+    FailStream(bool* status_ptr, utils::StringStream* out) : status_ptr_(status_ptr), out_(out) {}
     /// Copy constructor
     /// @param other the fail stream to clone
     FailStream(const FailStream& other) = default;
@@ -61,7 +61,7 @@
 
   private:
     bool* status_ptr_;
-    std::ostream* out_;
+    utils::StringStream* out_;
 };
 
 }  // namespace tint::reader::spirv
diff --git a/src/tint/reader/spirv/fail_stream_test.cc b/src/tint/reader/spirv/fail_stream_test.cc
index 5b8701d..02eb822 100644
--- a/src/tint/reader/spirv/fail_stream_test.cc
+++ b/src/tint/reader/spirv/fail_stream_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/reader/spirv/fail_stream.h"
 
 #include "gmock/gmock.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -56,7 +57,7 @@
 
 TEST_F(FailStreamTest, ShiftOperatorAccumulatesValues) {
     bool flag = true;
-    std::stringstream ss;
+    utils::StringStream ss;
     FailStream fs(&flag, &ss);
 
     ss << "prefix ";
diff --git a/src/tint/reader/spirv/function_arithmetic_test.cc b/src/tint/reader/spirv/function_arithmetic_test.cc
index 0597916..8f29a91 100644
--- a/src/tint/reader/spirv/function_arithmetic_test.cc
+++ b/src/tint/reader/spirv/function_arithmetic_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -307,7 +308,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
        << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
@@ -346,7 +347,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "let x_1 : " << GetParam().wgsl_type << " = " << GetParam().expected << ";";
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
diff --git a/src/tint/reader/spirv/function_bit_test.cc b/src/tint/reader/spirv/function_bit_test.cc
index a8e97b3..40f9162 100644
--- a/src/tint/reader/spirv/function_bit_test.cc
+++ b/src/tint/reader/spirv/function_bit_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -125,7 +126,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
        << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
@@ -163,7 +164,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error() << assembly;
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "let x_1 : " << GetParam().wgsl_type << " = " << GetParam().expected << ";\nreturn;\n";
     auto ast_body = fe.ast_body();
     auto got = test::ToString(p->program(), ast_body);
diff --git a/src/tint/reader/spirv/function_cfg_test.cc b/src/tint/reader/spirv/function_cfg_test.cc
index c5800fb..f4f1ce5 100644
--- a/src/tint/reader/spirv/function_cfg_test.cc
+++ b/src/tint/reader/spirv/function_cfg_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -27,7 +28,7 @@
 using SpvParserCFGTest = SpvParserTest;
 
 std::string Dump(const std::vector<uint32_t>& v) {
-    std::ostringstream o;
+    utils::StringStream o;
     o << "{";
     for (auto a : v) {
         o << a << " ";
diff --git a/src/tint/reader/spirv/function_decl_test.cc b/src/tint/reader/spirv/function_decl_test.cc
index 0c4834b..0eceba2 100644
--- a/src/tint/reader/spirv/function_decl_test.cc
+++ b/src/tint/reader/spirv/function_decl_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -34,7 +35,7 @@
 /// @returns a SPIR-V assembly segment which assigns debug names
 /// to particular IDs.
 std::string Names(std::vector<std::string> ids) {
-    std::ostringstream outs;
+    utils::StringStream outs;
     for (auto& id : ids) {
         outs << "    OpName %" << id << " \"" << id << "\"\n";
     }
diff --git a/src/tint/reader/spirv/function_logical_test.cc b/src/tint/reader/spirv/function_logical_test.cc
index 474af2e..a9ccf7d 100644
--- a/src/tint/reader/spirv/function_logical_test.cc
+++ b/src/tint/reader/spirv/function_logical_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -188,7 +189,7 @@
     ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error() << "\n" << assembly;
     auto fe = p->function_emitter(100);
     EXPECT_TRUE(fe.EmitBody()) << p->error();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "let x_1 : " << GetParam().ast_type << " = (" << GetParam().ast_lhs << " "
        << GetParam().ast_op << " " << GetParam().ast_rhs << ");";
     auto ast_body = fe.ast_body();
diff --git a/src/tint/reader/spirv/function_var_test.cc b/src/tint/reader/spirv/function_var_test.cc
index 1119667..681572b 100644
--- a/src/tint/reader/spirv/function_var_test.cc
+++ b/src/tint/reader/spirv/function_var_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -26,7 +27,7 @@
 /// @returns a SPIR-V assembly segment which assigns debug names
 /// to particular IDs.
 std::string Names(std::vector<std::string> ids) {
-    std::ostringstream outs;
+    utils::StringStream outs;
     for (auto& id : ids) {
         outs << "    OpName %" << id << " \"" << id << "\"\n";
     }
diff --git a/src/tint/reader/spirv/namer.cc b/src/tint/reader/spirv/namer.cc
index 3e1f880..378e133 100644
--- a/src/tint/reader/spirv/namer.cc
+++ b/src/tint/reader/spirv/namer.cc
@@ -19,6 +19,7 @@
 #include <unordered_set>
 
 #include "src/tint/debug.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 
@@ -219,7 +220,7 @@
     std::string derived_name;
     uint32_t& i = next_unusued_derived_name_id_[base_name];
     while (i != 0xffffffff) {
-        std::stringstream new_name_stream;
+        utils::StringStream new_name_stream;
         new_name_stream << base_name;
         if (i > 0) {
             new_name_stream << "_" << i;
@@ -305,7 +306,7 @@
         uint32_t i = 1;
         std::string new_name;
         do {
-            std::stringstream new_name_stream;
+            utils::StringStream new_name_stream;
             new_name_stream << suggestion << "_" << i;
             new_name = new_name_stream.str();
             ++i;
@@ -331,7 +332,7 @@
     uint32_t index = 0;
     for (auto& name : name_vector) {
         if (name.empty()) {
-            std::stringstream suggestion;
+            utils::StringStream suggestion;
             suggestion << "field" << index;
             // Again, modify the name-vector in-place.
             name = disambiguate_name(suggestion.str());
diff --git a/src/tint/reader/spirv/namer_test.cc b/src/tint/reader/spirv/namer_test.cc
index a24e03c..65a19bf 100644
--- a/src/tint/reader/spirv/namer_test.cc
+++ b/src/tint/reader/spirv/namer_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/reader/spirv/namer.h"
 
 #include "gmock/gmock.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -29,7 +30,7 @@
     std::string error() { return errors_.str(); }
 
   protected:
-    std::stringstream errors_;
+    utils::StringStream errors_;
     bool success_ = true;
     FailStream fail_stream_;
 };
@@ -351,7 +352,7 @@
 
 TEST_P(SpvNamerReservedWordTest, ReservedWordsAreUsed) {
     bool success;
-    std::stringstream errors;
+    utils::StringStream errors;
     FailStream fail_stream(&success, &errors);
     Namer namer(fail_stream);
     const std::string reserved = GetParam();
diff --git a/src/tint/reader/spirv/parser_impl.h b/src/tint/reader/spirv/parser_impl.h
index 36c24de..141df61 100644
--- a/src/tint/reader/spirv/parser_impl.h
+++ b/src/tint/reader/spirv/parser_impl.h
@@ -24,6 +24,7 @@
 
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/hashmap.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_BEGIN_DISABLE_WARNING(NEWLINE_EOF);
 TINT_BEGIN_DISABLE_WARNING(OLD_STYLE_CAST);
@@ -819,7 +820,7 @@
     // Is the parse successful?
     bool success_ = true;
     // Collector for diagnostic messages.
-    std::stringstream errors_;
+    utils::StringStream errors_;
     FailStream fail_stream_;
     spvtools::MessageConsumer message_consumer_;
 
diff --git a/src/tint/reader/spirv/parser_impl_function_decl_test.cc b/src/tint/reader/spirv/parser_impl_function_decl_test.cc
index 459460d..1c29649 100644
--- a/src/tint/reader/spirv/parser_impl_function_decl_test.cc
+++ b/src/tint/reader/spirv/parser_impl_function_decl_test.cc
@@ -15,6 +15,7 @@
 #include "gmock/gmock.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -47,7 +48,7 @@
 /// @returns a SPIR-V assembly segment which assigns debug names
 /// to particular IDs.
 std::string Names(std::vector<std::string> ids) {
-    std::ostringstream outs;
+    utils::StringStream outs;
     for (auto& id : ids) {
         outs << "    OpName %" << id << " \"" << id << "\"\n";
     }
diff --git a/src/tint/reader/spirv/parser_impl_handle_test.cc b/src/tint/reader/spirv/parser_impl_handle_test.cc
index 2413386..17e6ed6 100644
--- a/src/tint/reader/spirv/parser_impl_handle_test.cc
+++ b/src/tint/reader/spirv/parser_impl_handle_test.cc
@@ -18,6 +18,7 @@
 #include "src/tint/reader/spirv/function.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
 #include "src/tint/reader/spirv/spirv_tools_helpers_test.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -235,7 +236,7 @@
 }
 
 std::string Bindings(std::vector<uint32_t> ids) {
-    std::ostringstream os;
+    utils::StringStream os;
     int binding = 0;
     for (auto id : ids) {
         os << "  OpDecorate %" << id << " DescriptorSet 0\n"
diff --git a/src/tint/reader/spirv/parser_impl_test_helper.cc b/src/tint/reader/spirv/parser_impl_test_helper.cc
index d0b6ef7..754922c 100644
--- a/src/tint/reader/spirv/parser_impl_test_helper.cc
+++ b/src/tint/reader/spirv/parser_impl_test_helper.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/generator_impl.h"
 
 namespace tint::reader::spirv::test {
@@ -54,7 +55,7 @@
     return Switch(
         node,
         [&](const ast::Expression* expr) {
-            std::stringstream out;
+            utils::StringStream out;
             if (!writer.EmitExpression(out, expr)) {
                 return "WGSL writer error: " + writer.error();
             }
diff --git a/src/tint/reader/spirv/parser_type.cc b/src/tint/reader/spirv/parser_type.cc
index 0d4c58a..71482ff 100644
--- a/src/tint/reader/spirv/parser_type.cc
+++ b/src/tint/reader/spirv/parser_type.cc
@@ -23,6 +23,7 @@
 #include "src/tint/utils/hash.h"
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_allocator.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::reader::spirv::Type);
@@ -557,31 +558,31 @@
 }
 
 std::string Pointer::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "ptr<" << utils::ToString(address_space) << ", " << type->String() + ">";
     return ss.str();
 }
 
 std::string Reference::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "ref<" + utils::ToString(address_space) << ", " << type->String() << ">";
     return ss.str();
 }
 
 std::string Vector::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "vec" << size << "<" << type->String() << ">";
     return ss.str();
 }
 
 std::string Matrix::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "mat" << columns << "x" << rows << "<" << type->String() << ">";
     return ss.str();
 }
 
 std::string Array::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "array<" << type->String() << ", " << size << ", " << stride << ">";
     return ss.str();
 }
@@ -597,31 +598,31 @@
 }
 
 std::string DepthTexture::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "depth_" << dims;
     return ss.str();
 }
 
 std::string DepthMultisampledTexture::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "depth_multisampled_" << dims;
     return ss.str();
 }
 
 std::string MultisampledTexture::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "texture_multisampled_" << dims << "<" << type << ">";
     return ss.str();
 }
 
 std::string SampledTexture::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "texture_" << dims << "<" << type << ">";
     return ss.str();
 }
 
 std::string StorageTexture::String() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "texture_storage_" << dims << "<" << format << ", " << access << ">";
     return ss.str();
 }
diff --git a/src/tint/reader/spirv/spirv_tools_helpers_test.cc b/src/tint/reader/spirv/spirv_tools_helpers_test.cc
index 21a5be2..ff3db87 100644
--- a/src/tint/reader/spirv/spirv_tools_helpers_test.cc
+++ b/src/tint/reader/spirv/spirv_tools_helpers_test.cc
@@ -16,6 +16,7 @@
 
 #include "gtest/gtest.h"
 #include "spirv-tools/libspirv.hpp"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv::test {
 
@@ -24,7 +25,7 @@
 
     // (The target environment doesn't affect assembly.
     spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
-    std::stringstream errors;
+    utils::StringStream errors;
     std::vector<uint32_t> result;
     tools.SetMessageConsumer([&errors](spv_message_level_t, const char*,
                                        const spv_position_t& position, const char* message) {
@@ -40,7 +41,7 @@
 
 std::string Disassemble(const std::vector<uint32_t>& spirv_module) {
     spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
-    std::stringstream errors;
+    utils::StringStream errors;
     tools.SetMessageConsumer([&errors](spv_message_level_t, const char*,
                                        const spv_position_t& position, const char* message) {
         errors << "disassmbly error:" << position.line << ":" << position.column << ": " << message;
diff --git a/src/tint/reader/spirv/usage.cc b/src/tint/reader/spirv/usage.cc
index c120b2c..5256d4f 100644
--- a/src/tint/reader/spirv/usage.cc
+++ b/src/tint/reader/spirv/usage.cc
@@ -16,6 +16,8 @@
 
 #include <sstream>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::reader::spirv {
 
 Usage::Usage() {}
@@ -179,7 +181,7 @@
 }
 
 std::string Usage::to_str() const {
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << *this;
     return ss.str();
 }
diff --git a/src/tint/reader/spirv/usage_test.cc b/src/tint/reader/spirv/usage_test.cc
index bb64bb3..eb5eb5d 100644
--- a/src/tint/reader/spirv/usage_test.cc
+++ b/src/tint/reader/spirv/usage_test.cc
@@ -17,6 +17,7 @@
 
 #include "gmock/gmock.h"
 #include "src/tint/reader/spirv/parser_impl_test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::spirv {
 namespace {
@@ -38,7 +39,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_Trivial_Output) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     ss << u;
     EXPECT_THAT(ss.str(), Eq("Usage()"));
@@ -89,13 +90,13 @@
     EXPECT_TRUE(a.IsStorageReadTexture());
     EXPECT_FALSE(a.IsStorageWriteTexture());
 
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << a;
     EXPECT_THAT(ss.str(), Eq("Usage(Sampler( comparison )Texture( read ))"));
 }
 
 TEST_F(SpvParserTest, Usage_AddSampler) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddSampler();
 
@@ -120,7 +121,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddComparisonSampler) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddComparisonSampler();
 
@@ -144,7 +145,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddTexture();
 
@@ -168,7 +169,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddSampledTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddSampledTexture();
 
@@ -192,7 +193,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddMultisampledTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddMultisampledTexture();
 
@@ -216,7 +217,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddDepthTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddDepthTexture();
 
@@ -240,7 +241,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddStorageReadTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddStorageReadTexture();
 
@@ -264,7 +265,7 @@
 }
 
 TEST_F(SpvParserTest, Usage_AddStorageWriteTexture) {
-    std::ostringstream ss;
+    utils::StringStream ss;
     Usage u;
     u.AddStorageWriteTexture();
 
diff --git a/src/tint/reader/wgsl/classify_template_args_test.cc b/src/tint/reader/wgsl/classify_template_args_test.cc
index 9cf2c8f..e2833ee 100644
--- a/src/tint/reader/wgsl/classify_template_args_test.cc
+++ b/src/tint/reader/wgsl/classify_template_args_test.cc
@@ -479,5 +479,198 @@
                              },
                          }));
 
+INSTANTIATE_TEST_SUITE_P(TreesitterScannerSeparatingCases,
+                         ClassifyTemplateArgsTest,
+                         testing::ValuesIn(std::vector<Case>{
+                             // Treesitter had trouble missing '=' in its lookahead
+                             {
+                                 "a<b>=c",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEqual,              // =
+                                     T::kIdentifier,         // c
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>>=c",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kGreaterThanEqual,   // >=
+                                     T::kPlaceholder,        // <placeholder>
+                                     T::kIdentifier,         // c
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b==c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kEqualEqual,         // ==
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b==c)>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kEqualEqual,         // ==
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<=c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kLessThanEqual,      // <=
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b<=c)>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kLessThanEqual,      // <=
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>=c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEqual,              // =
+                                     T::kIdentifier,         // c
+                                     T::kGreaterThan,        // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b<=c)>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kLessThanEqual,      // <=
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b>>c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kTemplateArgsRight,  // >
+                                     T::kGreaterThan,        // >
+                                     T::kIdentifier,         // c
+                                     T::kGreaterThan,        // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<b<<c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // b
+                                     T::kShiftLeft,          // <<
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b<<c)>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kShiftLeft,          // <<
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<(b>>c)>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kParenLeft,          // (
+                                     T::kIdentifier,         // b
+                                     T::kShiftRight,         // >>
+                                     T::kPlaceholder,        // <placeholder>
+                                     T::kIdentifier,         // c
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<1<<c>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIntLiteral,         // 1
+                                     T::kShiftLeft,          // <<
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                             {
+                                 "a<1<<c<d>()>",
+                                 {
+                                     T::kIdentifier,         // a
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIntLiteral,         // 1
+                                     T::kShiftLeft,          // <<
+                                     T::kIdentifier,         // c
+                                     T::kTemplateArgsLeft,   // <
+                                     T::kIdentifier,         // d
+                                     T::kTemplateArgsRight,  // >
+                                     T::kParenLeft,          // (
+                                     T::kParenRight,         // )
+                                     T::kTemplateArgsRight,  // >
+                                     T::kEOF,
+                                 },
+                             },
+                         }));
+
 }  // namespace
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 53d29f1..e4eab29 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -44,6 +44,7 @@
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/reverse.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::reader::wgsl {
 namespace {
@@ -213,7 +214,7 @@
 ParserImpl::Failure::Errored ParserImpl::add_error(const Source& source,
                                                    std::string_view err,
                                                    std::string_view use) {
-    std::stringstream msg;
+    utils::StringStream msg;
     msg << err;
     if (!use.empty()) {
         msg << " for " << use;
@@ -911,7 +912,7 @@
     }
 
     /// Create a sensible error message
-    std::ostringstream err;
+    utils::StringStream err;
     err << "expected " << name;
 
     if (!use.empty()) {
@@ -3164,7 +3165,7 @@
         return false;
     }
 
-    std::stringstream err;
+    utils::StringStream err;
     if (tok == Token::Type::kTemplateArgsLeft && t.type() == Token::Type::kLessThan) {
         err << "missing closing '>'";
     } else {
diff --git a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
index fea7c2d..7ad39c2 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -62,7 +62,7 @@
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -76,7 +76,7 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -124,7 +124,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -135,7 +135,7 @@
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -147,7 +147,7 @@
         EXPECT_TRUE(p->has_error());
         EXPECT_EQ(p->error(), R"(1:8: expected extension
 Did you mean 'f16'?
-Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'chromium_internal_relaxed_uniform_layout', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
diff --git a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
index 85e55cb..af50894 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/reader/wgsl/parser_impl_test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::reader::wgsl {
 namespace {
 
@@ -513,8 +515,8 @@
 TEST_F(ParserImplErrorTest, GlobalDeclConstExprMaxDepth) {
     uint32_t kMaxDepth = 128;
 
-    std::stringstream src;
-    std::stringstream mkr;
+    utils::StringStream src;
+    utils::StringStream mkr;
     src << "const i : i32 = ";
     mkr << "                ";
     for (size_t i = 0; i < kMaxDepth + 8; i++) {
@@ -530,7 +532,7 @@
         src << ")";
     }
     src << ";";
-    std::stringstream err;
+    utils::StringStream err;
     err << "test.wgsl:1:529 error: maximum parser recursive depth reached\n"
         << src.str() << "\n"
         << mkr.str() << "\n";
diff --git a/src/tint/reader/wgsl/parser_impl_expression_test.cc b/src/tint/reader/wgsl/parser_impl_expression_test.cc
index 0de3dea..9a6c917 100644
--- a/src/tint/reader/wgsl/parser_impl_expression_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_expression_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/reader/wgsl/parser_impl_test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::reader::wgsl {
 namespace {
 
@@ -486,7 +488,7 @@
 using ParserImplMixedBinaryOpTest = ParserImplTestWithParam<Case>;
 
 TEST_P(ParserImplMixedBinaryOpTest, Test) {
-    std::stringstream wgsl;
+    utils::StringStream wgsl;
     wgsl << GetParam();
     auto p = parser(wgsl.str());
     auto e = p->expression();
@@ -498,7 +500,7 @@
         EXPECT_TRUE(e.errored);
         EXPECT_EQ(e.value, nullptr);
         EXPECT_TRUE(p->has_error());
-        std::stringstream expected;
+        utils::StringStream expected;
         expected << "1:3: mixing '" << GetParam().lhs_op.symbol << "' and '"
                  << GetParam().rhs_op.symbol << "' requires parenthesis";
         EXPECT_EQ(p->error(), expected.str());
diff --git a/src/tint/resolver/address_space_layout_validation_test.cc b/src/tint/resolver/address_space_layout_validation_test.cc
index 79e2781..9a621e3 100644
--- a/src/tint/resolver/address_space_layout_validation_test.cc
+++ b/src/tint/resolver/address_space_layout_validation_test.cc
@@ -603,5 +603,150 @@
     ASSERT_TRUE(r()->Resolve()) << r()->error();
 }
 
+TEST_F(ResolverAddressSpaceLayoutValidationTest, RelaxedUniformLayout_StructMemberOffset_Struct) {
+    // enable chromium_internal_relaxed_uniform_layout;
+    //
+    // struct Inner {
+    //   scalar : i32;
+    // };
+    //
+    // struct Outer {
+    //   scalar : f32;
+    //   inner : Inner;
+    // };
+    //
+    // @group(0) @binding(0)
+    // var<uniform> a : Outer;
+
+    Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+    Structure(Source{{12, 34}}, "Inner",
+              utils::Vector{
+                  Member("scalar", ty.i32()),
+              });
+
+    Structure(Source{{34, 56}}, "Outer",
+              utils::Vector{
+                  Member("scalar", ty.f32()),
+                  Member(Source{{56, 78}}, "inner", ty("Inner")),
+              });
+
+    GlobalVar(Source{{78, 90}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
+              Binding(0_a));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAddressSpaceLayoutValidationTest, RelaxedUniformLayout_StructMemberOffset_Array) {
+    // enable chromium_internal_relaxed_uniform_layout;
+    //
+    // type Inner = @stride(16) array<f32, 10u>;
+    //
+    // struct Outer {
+    //   scalar : f32;
+    //   inner : Inner;
+    // };
+    //
+    // @group(0) @binding(0)
+    // var<uniform> a : Outer;
+
+    Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+    Alias("Inner", ty.array<f32, 10>(utils::Vector{Stride(16)}));
+
+    Structure(Source{{12, 34}}, "Outer",
+              utils::Vector{
+                  Member("scalar", ty.f32()),
+                  Member(Source{{56, 78}}, "inner", ty("Inner")),
+              });
+
+    GlobalVar(Source{{78, 90}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
+              Binding(0_a));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAddressSpaceLayoutValidationTest, RelaxedUniformLayout_MemberOffsetNotMutipleOf16) {
+    // enable chromium_internal_relaxed_uniform_layout;
+    //
+    // struct Inner {
+    //   @align(1) @size(5) scalar : i32;
+    // };
+    //
+    // struct Outer {
+    //   inner : Inner;
+    //   scalar : i32;
+    // };
+    //
+    // @group(0) @binding(0)
+    // var<uniform> a : Outer;
+
+    Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+    Structure(Source{{12, 34}}, "Inner",
+              utils::Vector{
+                  Member("scalar", ty.i32(), utils::Vector{MemberAlign(1_i), MemberSize(5_a)}),
+              });
+
+    Structure(Source{{34, 56}}, "Outer",
+              utils::Vector{
+                  Member(Source{{56, 78}}, "inner", ty("Inner")),
+                  Member(Source{{78, 90}}, "scalar", ty.i32()),
+              });
+
+    GlobalVar(Source{{22, 24}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
+              Binding(0_a));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAddressSpaceLayoutValidationTest, RelaxedUniformLayout_ArrayStride_Scalar) {
+    // enable chromium_internal_relaxed_uniform_layout;
+    //
+    // struct Outer {
+    //   arr : array<f32, 10u>;
+    // };
+    //
+    // @group(0) @binding(0)
+    // var<uniform> a : Outer;
+
+    Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+    Structure(Source{{12, 34}}, "Outer",
+              utils::Vector{
+                  Member("arr", ty.array<f32, 10>()),
+              });
+
+    GlobalVar(Source{{78, 90}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
+              Binding(0_a));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverAddressSpaceLayoutValidationTest, RelaxedUniformLayout_ArrayStride_Vech) {
+    // enable f16;
+    // enable chromium_internal_relaxed_uniform_layout;
+    //
+    // struct Outer {
+    //   arr : array<vec3<f16>, 10u>;
+    // };
+    //
+    // @group(0) @binding(0)
+    // var<uniform> a : Outer;
+
+    Enable(builtin::Extension::kF16);
+    Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+    Structure(Source{{12, 34}}, "Outer",
+              utils::Vector{
+                  Member("arr", ty.array(ty.vec3<f16>(), 10_u)),
+              });
+
+    GlobalVar(Source{{78, 90}}, "a", ty("Outer"), builtin::AddressSpace::kUniform, Group(0_a),
+              Binding(0_a));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/alias_analysis_test.cc b/src/tint/resolver/alias_analysis_test.cc
index fa70a7f..895ed88 100644
--- a/src/tint/resolver/alias_analysis_test.cc
+++ b/src/tint/resolver/alias_analysis_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/resolver/resolver.h"
 #include "src/tint/resolver/resolver_test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 #include "gmock/gmock.h"
 
@@ -196,7 +197,7 @@
                                            TwoPointerConfig{builtin::AddressSpace::kPrivate, false},
                                            TwoPointerConfig{builtin::AddressSpace::kPrivate, true}),
                          [](const ::testing::TestParamInfo<TwoPointers::ParamType>& p) {
-                             std::stringstream ss;
+                             utils::StringStream ss;
                              ss << (p.param.aliased ? "Aliased" : "Unaliased") << "_"
                                 << p.param.address_space;
                              return ss.str();
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 43e9929..b0a863d 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -18,6 +18,7 @@
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/transform/add_block_attribute.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 
 #include "gmock/gmock.h"
 
@@ -1164,7 +1165,7 @@
     auto& params = GetParam();
     ast::Type el_ty = params.create_el_type(*this);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "el_ty: " << FriendlyName(el_ty) << ", stride: " << params.stride
        << ", should_pass: " << params.should_pass;
     SCOPED_TRACE(ss.str());
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 6a9e5a3..0108e89 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -38,6 +38,7 @@
 #include "src/tint/type/test_helper.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 
 using ::testing::ElementsAre;
 using ::testing::HasSubstr;
@@ -2185,7 +2186,7 @@
 
 static std::string to_str(const std::string& function,
                           utils::VectorRef<const sem::Parameter*> params) {
-    std::stringstream out;
+    utils::StringStream out;
     out << function << "(";
     bool first = true;
     for (auto* param : params) {
diff --git a/src/tint/resolver/builtin_validation_test.cc b/src/tint/resolver/builtin_validation_test.cc
index 967335d..d074703 100644
--- a/src/tint/resolver/builtin_validation_test.cc
+++ b/src/tint/resolver/builtin_validation_test.cc
@@ -17,6 +17,7 @@
 #include "src/tint/ast/builtin_texture_helper_test.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/sem/value_constructor.h"
+#include "src/tint/utils/string_stream.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -335,7 +336,7 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        std::stringstream err;
+        utils::StringStream err;
         if (is_vector) {
             err << "12:34 error: each component of the " << param.name
                 << " argument must be at least " << param.min << " and at most " << param.max
@@ -392,7 +393,7 @@
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
         EXPECT_FALSE(r()->Resolve());
-        std::stringstream err;
+        utils::StringStream err;
         if (is_vector) {
             err << "12:34 error: each component of the " << param.name
                 << " argument must be at least " << param.min << " and at most " << param.max
@@ -442,7 +443,7 @@
          });
 
     EXPECT_FALSE(r()->Resolve());
-    std::stringstream err;
+    utils::StringStream err;
     err << "12:34 error: the " << param.name << " argument must be a const-expression";
     EXPECT_EQ(r()->error(), err.str());
 }
diff --git a/src/tint/resolver/builtins_validation_test.cc b/src/tint/resolver/builtins_validation_test.cc
index 63e5abf..a985a1d 100644
--- a/src/tint/resolver/builtins_validation_test.cc
+++ b/src/tint/resolver/builtins_validation_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/builtin/builtin_value.h"
 #include "src/tint/resolver/resolver_test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -145,7 +146,7 @@
     if (params.is_valid) {
         EXPECT_TRUE(r()->Resolve()) << r()->error();
     } else {
-        std::stringstream err;
+        utils::StringStream err;
         err << "12:34 error: @builtin(" << params.builtin << ")";
         err << " cannot be used in input of " << params.stage << " pipeline stage";
         EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index dc59ccd..325ccb0 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -44,6 +44,7 @@
 #include "src/tint/utils/bitcast.h"
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/map.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/transform.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -184,8 +185,7 @@
 
 template <typename NumberT>
 std::string OverflowErrorMessage(NumberT lhs, const char* op, NumberT rhs) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << "'" << lhs.value << " " << op << " " << rhs.value << "' cannot be represented as '"
        << FriendlyName<NumberT>() << "'";
     return ss.str();
@@ -193,8 +193,7 @@
 
 template <typename VALUE_TY>
 std::string OverflowErrorMessage(VALUE_TY value, std::string_view target_ty) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << "value " << value << " cannot be represented as "
        << "'" << target_ty << "'";
     return ss.str();
@@ -202,8 +201,7 @@
 
 template <typename NumberT>
 std::string OverflowExpErrorMessage(std::string_view base, NumberT exp) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << base << "^" << exp << " cannot be represented as "
        << "'" << FriendlyName<NumberT>() << "'";
     return ss.str();
@@ -714,7 +712,7 @@
         } else {
             AddError(OverflowErrorMessage(a, "%", b), source);
             if (use_runtime_semantics_) {
-                return a;
+                return NumberT{0};
             } else {
                 return utils::Failure;
             }
@@ -727,7 +725,7 @@
             // lhs % 0 is an error
             AddError(OverflowErrorMessage(a, "%", b), source);
             if (use_runtime_semantics_) {
-                return a;
+                return NumberT{0};
             } else {
                 return utils::Failure;
             }
@@ -738,7 +736,7 @@
             if (rhs == -1 && lhs == std::numeric_limits<T>::min()) {
                 AddError(OverflowErrorMessage(a, "%", b), source);
                 if (use_runtime_semantics_) {
-                    return a;
+                    return NumberT{0};
                 } else {
                     return utils::Failure;
                 }
@@ -1229,23 +1227,18 @@
 }
 
 ConstEval::Result ConstEval::ArrayOrStructCtor(const type::Type* ty,
-                                               utils::VectorRef<const sem::ValueExpression*> args) {
+                                               utils::VectorRef<const constant::Value*> args) {
     if (args.IsEmpty()) {
         return ZeroValue(ty);
     }
 
     if (args.Length() == 1 && args[0]->Type() == ty) {
         // Identity constructor.
-        return args[0]->ConstantValue();
+        return args[0];
     }
 
     // Multiple arguments. Must be a value constructor.
-    utils::Vector<const constant::Value*, 4> els;
-    els.Reserve(args.Length());
-    for (auto* arg : args) {
-        els.Push(arg->ConstantValue());
-    }
-    return builder.create<constant::Composite>(ty, std::move(els));
+    return builder.create<constant::Composite>(ty, std::move(args));
 }
 
 ConstEval::Result ConstEval::Conv(const type::Type* ty,
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index 969cfe7..9303656 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -77,11 +77,10 @@
     // Constant value evaluation methods, to be called directly from Resolver
     ////////////////////////////////////////////////////////////////////////////////////////////////
 
-    /// @param ty the target type - must be an array or initializer
+    /// @param ty the target type - must be an array or struct
     /// @param args the input arguments
     /// @return the constructed value, or null if the value cannot be calculated
-    Result ArrayOrStructCtor(const type::Type* ty,
-                             utils::VectorRef<const sem::ValueExpression*> args);
+    Result ArrayOrStructCtor(const type::Type* ty, utils::VectorRef<const constant::Value*> args);
 
     /// @param ty the target type
     /// @param value the value being converted
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
index e2dec19..ff50a24 100644
--- a/src/tint/resolver/const_eval_binary_op_test.cc
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -1077,7 +1077,15 @@
     GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Highest()), AFloat::Highest()));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "1:1 error: '1.7976931348623157081e+308 + 1.7976931348623157081e+308' cannot be "
+              "1:1 error: "
+              "'17976931348623157081452742373170435679807056752584499659891747680315726078002853876"
+              "058955863276687817154045895351438246423432132688946418276846754670353751698604991057"
+              "655128207624549009038932894407586850845513394230458323690322294816580855933212334827"
+              "4797826204144723168738177180919299881250404026184124858368.0 + "
+              "179769313486231570814527423731704356798070567525844996598917476803157260780028538760"
+              "589558632766878171540458953514382464234321326889464182768467546703537516986049910576"
+              "551282076245490090389328944075868508455133942304583236903222948165808559332123348274"
+              "797826204144723168738177180919299881250404026184124858368.0' cannot be "
               "represented as 'abstract-float'");
 }
 
@@ -1085,7 +1093,16 @@
     GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Lowest()), AFloat::Lowest()));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "1:1 error: '-1.7976931348623157081e+308 + -1.7976931348623157081e+308' cannot be "
+              "1:1 error: "
+              "'-"
+              "179769313486231570814527423731704356798070567525844996598917476803157260780028538760"
+              "589558632766878171540458953514382464234321326889464182768467546703537516986049910576"
+              "551282076245490090389328944075868508455133942304583236903222948165808559332123348274"
+              "797826204144723168738177180919299881250404026184124858368.0 + "
+              "-17976931348623157081452742373170435679807056752584499659891747680315726078002853876"
+              "058955863276687817154045895351438246423432132688946418276846754670353751698604991057"
+              "655128207624549009038932894407586850845513394230458323690322294816580855933212334827"
+              "4797826204144723168738177180919299881250404026184124858368.0' cannot be "
               "represented as 'abstract-float'");
 }
 
@@ -1584,8 +1601,13 @@
     GlobalConst("result", binary);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: value 1.7976931348623157081e+308 cannot be represented as 'f32'");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: value "
+        "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558"
+        "632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245"
+        "490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168"
+        "738177180919299881250404026184124858368.000000000 cannot be represented as 'f32'");
 }
 
 TEST_F(ResolverConstEvalTest, ShortCircuit_And_Error_Materialize) {
@@ -1630,8 +1652,13 @@
     GlobalConst("result", binary);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "12:34 error: value 1.7976931348623157081e+308 cannot be represented as 'f32'");
+    EXPECT_EQ(
+        r()->error(),
+        "12:34 error: value "
+        "179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558"
+        "632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245"
+        "490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168"
+        "738177180919299881250404026184124858368.000000000 cannot be represented as 'f32'");
 }
 
 TEST_F(ResolverConstEvalTest, ShortCircuit_Or_Error_Materialize) {
@@ -1981,6 +2008,42 @@
               "expected 'f32', found 'bool'");
 }
 
+TEST_F(ResolverConstEvalTest, ShortCircuit_And_Error_ArrayInit) {
+    // const one = 1;
+    // const result = (one == 0) && array(4) == 0;
+    GlobalConst("one", Expr(1_a));
+    auto* lhs = Equal("one", 0_a);
+    auto* rhs = Equal(Call("array", Expr(4_a)), 0_a);
+    GlobalConst("result", LogicalAnd(lhs, rhs));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching overload for operator == (array<abstract-int, 1>, abstract-int)
+
+2 candidate operators:
+  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+)");
+}
+
+TEST_F(ResolverConstEvalTest, ShortCircuit_Or_Error_ArrayInit) {
+    // const one = 1;
+    // const result = (one == 1) || array(4) == 0;
+    GlobalConst("one", Expr(1_a));
+    auto* lhs = Equal("one", 1_a);
+    auto* rhs = Equal(Call("array", Expr(4_a)), 0_a);
+    GlobalConst("result", LogicalOr(lhs, rhs));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching overload for operator == (array<abstract-int, 1>, abstract-int)
+
+2 candidate operators:
+  operator == (T, T) -> bool  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+  operator == (vecN<T>, vecN<T>) -> vecN<bool>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
+)");
+}
+
 ////////////////////////////////////////////////
 // Short-Circuit Builtin Call
 ////////////////////////////////////////////////
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
index 4e3cb5a..6f53af1 100644
--- a/src/tint/resolver/const_eval_builtin_test.cc
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -2025,9 +2025,11 @@
         C({Vec(f32(10), f32(-10.5))}, Val(u32(0xc940'4900))),
 
         E({Vec(f32(0), f32::Highest())},
-          "12:34 error: value 3.4028234663852885981e+38 cannot be represented as 'f16'"),
+          "12:34 error: value 340282346638528859811704183484516925440.000000000 cannot be "
+          "represented as 'f16'"),
         E({Vec(f32::Lowest(), f32(0))},
-          "12:34 error: value -3.4028234663852885981e+38 cannot be represented as 'f16'"),
+          "12:34 error: value -340282346638528859811704183484516925440.000000000 cannot be "
+          "represented as 'f16'"),
     };
 }
 INSTANTIATE_TEST_SUITE_P(  //
@@ -2848,15 +2850,16 @@
           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.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 cannot be represented as 'f16'"),
+        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({0x4.321p65_f},
-          "12:34 error: value 1.5478871919272394752e+20 cannot be represented as 'f16'"),
+          "12:34 error: value 154788719192723947520.000000000 cannot be represented as 'f16'"),
         E({Vec(65504.003_f, 0_f)},
-          "12:34 error: value 65504.00390625 cannot be represented as 'f16'"),
+          "12:34 error: value 65504.003906250 cannot be represented as 'f16'"),
         E({Vec(0_f, -0x4.321p65_f)},
-          "12:34 error: value -1.5478871919272394752e+20 cannot be represented as 'f16'"),
+          "12:34 error: value -154788719192723947520.000000000 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 bcad648..b6bcc56 100644
--- a/src/tint/resolver/const_eval_conversion_test.cc
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -431,7 +431,8 @@
     WrapInFunction(expr);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: value 10000000000 cannot be represented as 'f16'");
+    EXPECT_EQ(r()->error(),
+              "12:34 error: value 10000000000.000000000 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 9de1866..347e41d 100644
--- a/src/tint/resolver/const_eval_runtime_semantics_test.cc
+++ b/src/tint/resolver/const_eval_runtime_semantics_test.cc
@@ -75,7 +75,7 @@
     EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '1.7976931348623157081e+308 + 1.7976931348623157081e+308' cannot be represented as 'abstract-float')");
+        R"(warning: '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 + 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0' cannot be represented as 'abstract-float')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Add_F32_Overflow) {
@@ -86,7 +86,7 @@
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '3.4028234663852885981e+38 + 3.4028234663852885981e+38' cannot be represented as 'f32')");
+        R"(warning: '340282346638528859811704183484516925440.0 + 340282346638528859811704183484516925440.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Sub_AInt_Overflow) {
@@ -107,7 +107,7 @@
     EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '-1.7976931348623157081e+308 - 1.7976931348623157081e+308' cannot be represented as 'abstract-float')");
+        R"(warning: '-179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 - 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0' cannot be represented as 'abstract-float')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Sub_F32_Overflow) {
@@ -118,7 +118,7 @@
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '-3.4028234663852885981e+38 - 3.4028234663852885981e+38' cannot be represented as 'f32')");
+        R"(warning: '-340282346638528859811704183484516925440.0 - 340282346638528859811704183484516925440.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Mul_AInt_Overflow) {
@@ -139,7 +139,7 @@
     EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '1.7976931348623157081e+308 * 1.7976931348623157081e+308' cannot be represented as 'abstract-float')");
+        R"(warning: '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 * 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0' cannot be represented as 'abstract-float')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Mul_F32_Overflow) {
@@ -150,7 +150,7 @@
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
     EXPECT_EQ(
         error(),
-        R"(warning: '3.4028234663852885981e+38 * 3.4028234663852885981e+38' cannot be represented as 'f32')");
+        R"(warning: '340282346638528859811704183484516925440.0 * 340282346638528859811704183484516925440.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Div_AInt_ZeroDenominator) {
@@ -186,7 +186,7 @@
     auto result = const_eval.OpDivide(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 42.f);
-    EXPECT_EQ(error(), R"(warning: '42 / 0' cannot be represented as 'abstract-float')");
+    EXPECT_EQ(error(), R"(warning: '42.0 / 0.0' cannot be represented as 'abstract-float')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Div_F32_ZeroDenominator) {
@@ -195,7 +195,7 @@
     auto result = const_eval.OpDivide(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 42.f);
-    EXPECT_EQ(error(), R"(warning: '42 / 0' cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: '42.0 / 0.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Div_I32_MostNegativeByMinInt) {
@@ -212,7 +212,7 @@
     auto* b = Scalar(AInt(0));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<AInt>(), 42);
+    EXPECT_EQ(result.Get()->ValueAs<AInt>(), 0);
     EXPECT_EQ(error(), R"(warning: '42 % 0' cannot be represented as 'abstract-int')");
 }
 
@@ -221,7 +221,7 @@
     auto* b = Scalar(i32(0));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<i32>(), 42);
+    EXPECT_EQ(result.Get()->ValueAs<i32>(), 0);
     EXPECT_EQ(error(), R"(warning: '42 % 0' cannot be represented as 'i32')");
 }
 
@@ -230,7 +230,7 @@
     auto* b = Scalar(u32(0));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<u32>(), 42);
+    EXPECT_EQ(result.Get()->ValueAs<u32>(), 0);
     EXPECT_EQ(error(), R"(warning: '42 % 0' cannot be represented as 'u32')");
 }
 
@@ -239,8 +239,8 @@
     auto* b = Scalar(AFloat(0));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 42.f);
-    EXPECT_EQ(error(), R"(warning: '42 % 0' cannot be represented as 'abstract-float')");
+    EXPECT_EQ(result.Get()->ValueAs<AFloat>(), 0.f);
+    EXPECT_EQ(error(), R"(warning: '42.0 % 0.0' cannot be represented as 'abstract-float')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Mod_F32_ZeroDenominator) {
@@ -248,8 +248,8 @@
     auto* b = Scalar(f32(0));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<f32>(), 42.f);
-    EXPECT_EQ(error(), R"(warning: '42 % 0' cannot be represented as 'f32')");
+    EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
+    EXPECT_EQ(error(), R"(warning: '42.0 % 0.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Mod_I32_MostNegativeByMinInt) {
@@ -257,7 +257,7 @@
     auto* b = Scalar(i32(-1));
     auto result = const_eval.OpModulo(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
-    EXPECT_EQ(result.Get()->ValueAs<i32>(), i32::Lowest());
+    EXPECT_EQ(result.Get()->ValueAs<i32>(), 0);
     EXPECT_EQ(error(), R"(warning: '-2147483648 % -1' cannot be represented as 'i32')");
 }
 
@@ -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 cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: e^1000.000000000 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 cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: 2^1000.000000000 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 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 75250.000000000 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Pow_F32_Overflow) {
@@ -485,7 +485,7 @@
     auto result = const_eval.pow(a->Type(), utils::Vector{a, b}, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), 0.f);
-    EXPECT_EQ(error(), R"(warning: '2 ^ 1000' cannot be represented as 'f32')");
+    EXPECT_EQ(error(), R"(warning: '2.0 ^ 1000.0' cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Unpack2x16Float_OutOfRange) {
@@ -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 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 75250.000000000 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Sqrt_F32_OutOfRange) {
@@ -534,8 +534,9 @@
     auto result = const_eval.Convert(create<type::F32>(), a, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f32::kHighestValue);
-    EXPECT_EQ(error(),
-              R"(warning: value 1.7976931348623157081e+308 cannot be represented as 'f32')");
+    EXPECT_EQ(
+        error(),
+        R"(warning: value 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000000 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F32_TooLow) {
@@ -543,8 +544,9 @@
     auto result = const_eval.Convert(create<type::F32>(), a, {});
     ASSERT_TRUE(result);
     EXPECT_EQ(result.Get()->ValueAs<f32>(), f32::kLowestValue);
-    EXPECT_EQ(error(),
-              R"(warning: value -1.7976931348623157081e+308 cannot be represented as 'f32')");
+    EXPECT_EQ(
+        error(),
+        R"(warning: value -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.000000000 cannot be represented as 'f32')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F16_TooHigh) {
@@ -552,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 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value 1000000.000000000 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Convert_F16_TooLow) {
@@ -560,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 cannot be represented as 'f16')");
+    EXPECT_EQ(error(), R"(warning: value -1000000.000000000 cannot be represented as 'f16')");
 }
 
 TEST_F(ResolverConstEvalRuntimeSemanticsTest, Vec_Overflow_SingleComponent) {
diff --git a/src/tint/resolver/const_eval_test.h b/src/tint/resolver/const_eval_test.h
index 001d3d1..de25b20 100644
--- a/src/tint/resolver/const_eval_test.h
+++ b/src/tint/resolver/const_eval_test.h
@@ -24,6 +24,7 @@
 #include "gtest/gtest.h"
 #include "src/tint/resolver/resolver_test_helper.h"
 #include "src/tint/type/test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::resolver {
 
@@ -218,8 +219,7 @@
 /// Returns the overflow error message for binary ops
 template <typename NumberT>
 inline std::string OverflowErrorMessage(NumberT lhs, const char* op, NumberT rhs) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << "'" << lhs.value << " " << op << " " << rhs.value << "' cannot be represented as '"
        << FriendlyName<NumberT>() << "'";
     return ss.str();
@@ -228,8 +228,7 @@
 /// Returns the overflow error message for conversions
 template <typename VALUE_TY>
 std::string OverflowErrorMessage(VALUE_TY value, std::string_view target_ty) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << "value " << value << " cannot be represented as "
        << "'" << target_ty << "'";
     return ss.str();
@@ -238,8 +237,7 @@
 /// Returns the overflow error message for exponentiation
 template <typename NumberT>
 std::string OverflowExpErrorMessage(std::string_view base, NumberT exp) {
-    std::stringstream ss;
-    ss << std::setprecision(20);
+    utils::StringStream ss;
     ss << base << "^" << exp << " cannot be represented as "
        << "'" << FriendlyName<NumberT>() << "'";
     return ss.str();
diff --git a/src/tint/resolver/control_block_validation_test.cc b/src/tint/resolver/control_block_validation_test.cc
index 2104321..ba7aa53 100644
--- a/src/tint/resolver/control_block_validation_test.cc
+++ b/src/tint/resolver/control_block_validation_test.cc
@@ -561,5 +561,36 @@
     EXPECT_EQ(r()->error(), "12:34 error: case selector must be a constant expression");
 }
 
+constexpr size_t kMaxSwitchCaseSelectors = 16383;
+
+TEST_F(ResolverControlBlockValidationTest, Switch_MaxSelectors_Valid) {
+    utils::Vector<const ast::CaseStatement*, 0> cases;
+    for (size_t i = 0; i < kMaxSwitchCaseSelectors - 1; ++i) {
+        cases.Push(Case(CaseSelector(Expr(i32(i)))));
+    }
+    cases.Push(DefaultCase());
+
+    auto* var = Var("a", ty.i32());
+    auto* s = Switch("a", std::move(cases));
+    WrapInFunction(var, s);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverControlBlockValidationTest, Switch_MaxSelectors_Invalid) {
+    utils::Vector<const ast::CaseStatement*, 0> cases;
+    for (size_t i = 0; i < kMaxSwitchCaseSelectors; ++i) {
+        cases.Push(Case(CaseSelector(Expr(i32(i)))));
+    }
+    cases.Push(DefaultCase());
+
+    auto* var = Var("a", ty.i32());
+    auto* s = Switch(Source{{12, 34}}, "a", std::move(cases));
+    WrapInFunction(var, s);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: switch statement has 16384 case selectors, max is 16383");
+}
+
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/ctor_conv_intrinsic.cc b/src/tint/resolver/ctor_conv_intrinsic.cc
index 3ac3a67..1609e49 100644
--- a/src/tint/resolver/ctor_conv_intrinsic.cc
+++ b/src/tint/resolver/ctor_conv_intrinsic.cc
@@ -62,6 +62,8 @@
             return "mat4x3";
         case CtorConvIntrinsic::kMat4x4:
             return "mat4x4";
+        case CtorConvIntrinsic::kPackedVec3:
+            return "packedVec3";
     }
     return "<unknown>";
 }
diff --git a/src/tint/resolver/ctor_conv_intrinsic.h b/src/tint/resolver/ctor_conv_intrinsic.h
index 4ee1c79..f294394 100644
--- a/src/tint/resolver/ctor_conv_intrinsic.h
+++ b/src/tint/resolver/ctor_conv_intrinsic.h
@@ -48,6 +48,7 @@
     kMat4x2,
     kMat4x3,
     kMat4x4,
+    kPackedVec3,
 };
 
 /// @returns the name of the type.
diff --git a/src/tint/resolver/dependency_graph.cc b/src/tint/resolver/dependency_graph.cc
index 0b8f728..9b6d471 100644
--- a/src/tint/resolver/dependency_graph.cc
+++ b/src/tint/resolver/dependency_graph.cc
@@ -65,6 +65,7 @@
 #include "src/tint/utils/defer.h"
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
 #define TINT_DUMP_DEPENDENCY_GRAPH 0
@@ -711,7 +712,7 @@
     /// found in `stack`.
     /// @param stack is the global dependency stack that contains a loop.
     void CyclicDependencyFound(const Global* root, utils::VectorRef<const Global*> stack) {
-        std::stringstream msg;
+        utils::StringStream msg;
         msg << "cyclic dependency found: ";
         constexpr size_t kLoopNotStarted = ~0u;
         size_t loop_start = kLoopNotStarted;
diff --git a/src/tint/resolver/function_validation_test.cc b/src/tint/resolver/function_validation_test.cc
index 3a0275f..6654c43 100644
--- a/src/tint/resolver/function_validation_test.cc
+++ b/src/tint/resolver/function_validation_test.cc
@@ -18,6 +18,7 @@
 #include "src/tint/builtin/builtin_value.h"
 #include "src/tint/resolver/resolver.h"
 #include "src/tint/resolver/resolver_test_helper.h"
+#include "src/tint/utils/string_stream.h"
 
 #include "gmock/gmock.h"
 
@@ -979,7 +980,7 @@
     EXPECT_EQ(r()->error(), "12:34 error: type of function parameter must be constructible");
 }
 
-TEST_F(ResolverFunctionValidationTest, ParameterSotreType_AtomicFree) {
+TEST_F(ResolverFunctionValidationTest, ParameterStoreType_AtomicFree) {
     Structure("S", utils::Vector{
                        Member("m", ty.i32()),
                    });
@@ -1008,7 +1009,7 @@
     Func(Source{{12, 34}}, "f", params, ty.void_(), utils::Empty);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "12:34 error: functions may declare at most 255 parameters");
+    EXPECT_EQ(r()->error(), "12:34 error: function declares 256 parameters, maximum is 255");
 }
 
 TEST_F(ResolverFunctionValidationTest, ParameterVectorNoType) {
@@ -1054,7 +1055,7 @@
     if (param.expectation == Expectation::kAlwaysPass) {
         ASSERT_TRUE(r()->Resolve()) << r()->error();
     } else {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << param.address_space;
         EXPECT_FALSE(r()->Resolve());
         if (param.expectation == Expectation::kInvalid) {
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index 17927db..0d2cc10 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -39,6 +39,7 @@
 #include "src/tint/utils/hashmap.h"
 #include "src/tint/utils/math.h"
 #include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::resolver {
 namespace {
@@ -456,6 +457,25 @@
 constexpr auto build_vec3 = build_vec<3>;
 constexpr auto build_vec4 = build_vec<4>;
 
+bool match_packedVec3(MatchState&, const type::Type* ty, const type::Type*& T) {
+    if (ty->Is<Any>()) {
+        T = ty;
+        return true;
+    }
+
+    if (auto* v = ty->As<type::Vector>()) {
+        if (v->Packed()) {
+            T = v->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+const type::Vector* build_packedVec3(MatchState& state, const type::Type* el) {
+    return state.builder.create<type::Vector>(el, 3u, /* packed */ true);
+}
+
 bool match_mat(MatchState&, const type::Type* ty, Number& M, Number& N, const type::Type*& T) {
     if (ty->Is<Any>()) {
         M = Number::any;
@@ -1183,12 +1203,12 @@
                      sem::EvaluationStage earliest_eval_stage) const;
 
     // Prints the overload for emitting diagnostics
-    void PrintOverload(std::ostream& ss,
+    void PrintOverload(utils::StringStream& ss,
                        const OverloadInfo* overload,
                        const char* intrinsic_name) const;
 
     // Prints the list of candidates for emitting diagnostics
-    void PrintCandidates(std::ostream& ss,
+    void PrintCandidates(utils::StringStream& ss,
                          utils::VectorRef<Candidate> candidates,
                          const char* intrinsic_name) const;
 
@@ -1213,7 +1233,7 @@
                           const char* intrinsic_name,
                           utils::VectorRef<const type::Type*> args,
                           const type::Type* template_arg = nullptr) {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << intrinsic_name;
     if (template_arg) {
         ss << "<" << template_arg->FriendlyName(builder.Symbols()) << ">";
@@ -1252,7 +1272,7 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&](utils::VectorRef<Candidate> candidates) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << "no matching call to " << CallSignature(builder, intrinsic_name, args) << std::endl;
         if (!candidates.IsEmpty()) {
             ss << std::endl
@@ -1321,7 +1341,7 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&, name = intrinsic_name](utils::VectorRef<Candidate> candidates) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << "no matching overload for " << CallSignature(builder, name, args) << std::endl;
         if (!candidates.IsEmpty()) {
             ss << std::endl
@@ -1399,7 +1419,7 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&, name = intrinsic_name](utils::VectorRef<Candidate> candidates) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << "no matching overload for " << CallSignature(builder, name, args) << std::endl;
         if (!candidates.IsEmpty()) {
             ss << std::endl
@@ -1434,7 +1454,7 @@
 
     // Generates an error when no overloads match the provided arguments
     auto on_no_match = [&](utils::VectorRef<Candidate> candidates) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << "no matching constructor for " << CallSignature(builder, name, args, template_arg)
            << std::endl;
         Candidates ctor, conv;
@@ -1736,7 +1756,7 @@
     return MatchState(builder, templates, matchers, overload, matcher_indices, earliest_eval_stage);
 }
 
-void Impl::PrintOverload(std::ostream& ss,
+void Impl::PrintOverload(utils::StringStream& ss,
                          const OverloadInfo* overload,
                          const char* intrinsic_name) const {
     TemplateState templates;
@@ -1808,7 +1828,7 @@
     }
 }
 
-void Impl::PrintCandidates(std::ostream& ss,
+void Impl::PrintCandidates(utils::StringStream& ss,
                            utils::VectorRef<Candidate> candidates,
                            const char* intrinsic_name) const {
     for (auto& candidate : candidates) {
@@ -1846,7 +1866,7 @@
                                 utils::VectorRef<const type::Type*> args,
                                 TemplateState templates,
                                 utils::VectorRef<Candidate> candidates) const {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << "ambiguous overload while attempting to match " << intrinsic_name;
     for (size_t i = 0; i < std::numeric_limits<size_t>::max(); i++) {
         if (auto* ty = templates.Type(i)) {
diff --git a/src/tint/resolver/intrinsic_table.inl b/src/tint/resolver/intrinsic_table.inl
index a4b5e14..9aa0b76 100644
--- a/src/tint/resolver/intrinsic_table.inl
+++ b/src/tint/resolver/intrinsic_table.inl
@@ -71,7 +71,7 @@
 }
 
 std::string Ia::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "abstract-int";
   return ss.str();
 }
@@ -99,7 +99,7 @@
 }
 
 std::string Fa::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "abstract-float";
   return ss.str();
 }
@@ -627,7 +627,7 @@
 std::string Vec::String(MatchState* state) const {
   const std::string N = state->NumName();
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "vec" << N << "<" << T << ">";
   return ss.str();
 }
@@ -673,7 +673,7 @@
   const std::string N = state->NumName();
   const std::string M = state->NumName();
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "mat" << N << "x" << M << "<" << T << ">";
   return ss.str();
 }
@@ -1370,6 +1370,38 @@
   return "texture_external";
 }
 
+/// TypeMatcher for 'type packedVec3'
+class PackedVec3 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may define and refine the template types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const type::Type* Match(MatchState& state,
+                         const type::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState* state) const override;
+};
+
+const type::Type* PackedVec3::Match(MatchState& state, const type::Type* ty) const {
+  const type::Type* T = nullptr;
+  if (!match_packedVec3(state, ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_packedVec3(state, T);
+}
+
+std::string PackedVec3::String(MatchState* state) const {
+  const std::string T = state->TypeName();
+  return "packedVec3<" + T + ">";
+}
+
 /// TypeMatcher for 'type __modf_result'
 class ModfResult : public TypeMatcher {
  public:
@@ -1399,7 +1431,7 @@
 
 std::string ModfResult::String(MatchState* state) const {
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "__modf_result_" << T;
   return ss.str();
 }
@@ -1439,7 +1471,7 @@
 std::string ModfResultVec::String(MatchState* state) const {
   const std::string N = state->NumName();
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "__modf_result_vec" << N << "_" << T;
   return ss.str();
 }
@@ -1473,7 +1505,7 @@
 
 std::string FrexpResult::String(MatchState* state) const {
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "__frexp_result_" << T;
   return ss.str();
 }
@@ -1513,7 +1545,7 @@
 std::string FrexpResultVec::String(MatchState* state) const {
   const std::string N = state->NumName();
   const std::string T = state->TypeName();
-  std::stringstream ss;
+  utils::StringStream ss;
   ss << "__frexp_result_vec" << N << "_" << T;
   return ss.str();
 }
@@ -1592,7 +1624,7 @@
 }
 
 std::string Scalar::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1635,7 +1667,7 @@
 }
 
 std::string ConcreteScalar::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1681,7 +1713,7 @@
 }
 
 std::string ScalarNoF32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << I32().String(nullptr) << ", " << F16().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1727,7 +1759,7 @@
 }
 
 std::string ScalarNoF16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1773,7 +1805,7 @@
 }
 
 std::string ScalarNoI32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1819,7 +1851,7 @@
 }
 
 std::string ScalarNoU32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << " or " << Bool().String(nullptr);
@@ -1865,7 +1897,7 @@
 }
 
 std::string ScalarNoBool::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << " or " << U32().String(nullptr);
@@ -1911,7 +1943,7 @@
 }
 
 std::string FiaFiu32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Fa().String(nullptr) << ", " << Ia().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << F16().String(nullptr);
@@ -1954,7 +1986,7 @@
 }
 
 std::string FiaFi32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Fa().String(nullptr) << ", " << Ia().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << " or " << F16().String(nullptr);
@@ -1997,7 +2029,7 @@
 }
 
 std::string FiaFiu32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Fa().String(nullptr) << ", " << Ia().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << " or " << U32().String(nullptr);
@@ -2031,7 +2063,7 @@
 }
 
 std::string FaF32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Fa().String(nullptr) << " or " << F32().String(nullptr);
@@ -2068,7 +2100,7 @@
 }
 
 std::string FaF32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Fa().String(nullptr) << ", " << F32().String(nullptr) << " or " << F16().String(nullptr);
@@ -2105,7 +2137,7 @@
 }
 
 std::string IaIu32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << ", " << I32().String(nullptr) << " or " << U32().String(nullptr);
@@ -2139,7 +2171,7 @@
 }
 
 std::string IaI32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << Ia().String(nullptr) << " or " << I32().String(nullptr);
@@ -2179,7 +2211,7 @@
 }
 
 std::string Fiu32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << F16().String(nullptr);
@@ -2216,7 +2248,7 @@
 }
 
 std::string Fiu32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << ", " << I32().String(nullptr) << " or " << U32().String(nullptr);
@@ -2253,7 +2285,7 @@
 }
 
 std::string Fi32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << ", " << I32().String(nullptr) << " or " << F16().String(nullptr);
@@ -2287,7 +2319,7 @@
 }
 
 std::string Fi32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << " or " << I32().String(nullptr);
@@ -2321,7 +2353,7 @@
 }
 
 std::string F32F16::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << F32().String(nullptr) << " or " << F16().String(nullptr);
@@ -2355,7 +2387,7 @@
 }
 
 std::string Iu32::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss << I32().String(nullptr) << " or " << U32().String(nullptr);
@@ -2667,6 +2699,7 @@
   TextureStorage2DArray TextureStorage2DArray_;
   TextureStorage3D TextureStorage3D_;
   TextureExternal TextureExternal_;
+  PackedVec3 PackedVec3_;
   ModfResult ModfResult_;
   ModfResultVec ModfResultVec_;
   FrexpResult FrexpResult_;
@@ -2709,7 +2742,7 @@
   ~Matchers();
 
   /// The template types, types, and type matchers
-  TypeMatcher const* const type[72] = {
+  TypeMatcher const* const type[73] = {
     /* [0] */ &template_type_0_,
     /* [1] */ &template_type_1_,
     /* [2] */ &template_type_2_,
@@ -2757,31 +2790,32 @@
     /* [44] */ &TextureStorage2DArray_,
     /* [45] */ &TextureStorage3D_,
     /* [46] */ &TextureExternal_,
-    /* [47] */ &ModfResult_,
-    /* [48] */ &ModfResultVec_,
-    /* [49] */ &FrexpResult_,
-    /* [50] */ &FrexpResultVec_,
-    /* [51] */ &AtomicCompareExchangeResult_,
-    /* [52] */ &Scalar_,
-    /* [53] */ &ConcreteScalar_,
-    /* [54] */ &ScalarNoF32_,
-    /* [55] */ &ScalarNoF16_,
-    /* [56] */ &ScalarNoI32_,
-    /* [57] */ &ScalarNoU32_,
-    /* [58] */ &ScalarNoBool_,
-    /* [59] */ &FiaFiu32F16_,
-    /* [60] */ &FiaFi32F16_,
-    /* [61] */ &FiaFiu32_,
-    /* [62] */ &FaF32_,
-    /* [63] */ &FaF32F16_,
-    /* [64] */ &IaIu32_,
-    /* [65] */ &IaI32_,
-    /* [66] */ &Fiu32F16_,
-    /* [67] */ &Fiu32_,
-    /* [68] */ &Fi32F16_,
-    /* [69] */ &Fi32_,
-    /* [70] */ &F32F16_,
-    /* [71] */ &Iu32_,
+    /* [47] */ &PackedVec3_,
+    /* [48] */ &ModfResult_,
+    /* [49] */ &ModfResultVec_,
+    /* [50] */ &FrexpResult_,
+    /* [51] */ &FrexpResultVec_,
+    /* [52] */ &AtomicCompareExchangeResult_,
+    /* [53] */ &Scalar_,
+    /* [54] */ &ConcreteScalar_,
+    /* [55] */ &ScalarNoF32_,
+    /* [56] */ &ScalarNoF16_,
+    /* [57] */ &ScalarNoI32_,
+    /* [58] */ &ScalarNoU32_,
+    /* [59] */ &ScalarNoBool_,
+    /* [60] */ &FiaFiu32F16_,
+    /* [61] */ &FiaFi32F16_,
+    /* [62] */ &FiaFiu32_,
+    /* [63] */ &FaF32_,
+    /* [64] */ &FaF32F16_,
+    /* [65] */ &IaIu32_,
+    /* [66] */ &IaI32_,
+    /* [67] */ &Fiu32F16_,
+    /* [68] */ &Fiu32_,
+    /* [69] */ &Fi32F16_,
+    /* [70] */ &Fi32_,
+    /* [71] */ &F32F16_,
+    /* [72] */ &Iu32_,
   };
 
   /// The template numbers, and number matchers
@@ -2848,13 +2882,13 @@
   /* [40] */ 23,
   /* [41] */ 0,
   /* [42] */ 9,
-  /* [43] */ 50,
+  /* [43] */ 51,
   /* [44] */ 0,
   /* [45] */ 0,
   /* [46] */ 23,
   /* [47] */ 0,
   /* [48] */ 1,
-  /* [49] */ 48,
+  /* [49] */ 49,
   /* [50] */ 0,
   /* [51] */ 0,
   /* [52] */ 42,
@@ -2913,9 +2947,9 @@
   /* [105] */ 8,
   /* [106] */ 12,
   /* [107] */ 0,
-  /* [108] */ 49,
+  /* [108] */ 50,
   /* [109] */ 0,
-  /* [110] */ 47,
+  /* [110] */ 48,
   /* [111] */ 0,
   /* [112] */ 11,
   /* [113] */ 9,
@@ -2967,7 +3001,7 @@
   /* [159] */ 1,
   /* [160] */ 12,
   /* [161] */ 1,
-  /* [162] */ 51,
+  /* [162] */ 52,
   /* [163] */ 0,
   /* [164] */ 11,
   /* [165] */ 10,
@@ -3037,14 +3071,16 @@
   /* [229] */ 9,
   /* [230] */ 22,
   /* [231] */ 10,
-  /* [232] */ 37,
-  /* [233] */ 38,
-  /* [234] */ 39,
-  /* [235] */ 40,
-  /* [236] */ 41,
-  /* [237] */ 46,
-  /* [238] */ 28,
-  /* [239] */ 29,
+  /* [232] */ 47,
+  /* [233] */ 0,
+  /* [234] */ 37,
+  /* [235] */ 38,
+  /* [236] */ 39,
+  /* [237] */ 40,
+  /* [238] */ 41,
+  /* [239] */ 46,
+  /* [240] */ 28,
+  /* [241] */ 29,
 };
 
 // Assert that the MatcherIndex is big enough to index all the matchers, plus
@@ -3387,7 +3423,7 @@
   {
     /* [66] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [67] */
@@ -3427,7 +3463,7 @@
   {
     /* [74] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [75] */
@@ -3447,12 +3483,12 @@
   {
     /* [78] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [79] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [80] */
@@ -3482,7 +3518,7 @@
   {
     /* [85] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [86] */
@@ -3507,12 +3543,12 @@
   {
     /* [90] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [91] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [92] */
@@ -3537,12 +3573,12 @@
   {
     /* [96] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [97] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [98] */
@@ -3572,7 +3608,7 @@
   {
     /* [103] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [104] */
@@ -3602,7 +3638,7 @@
   {
     /* [109] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [110] */
@@ -3632,7 +3668,7 @@
   {
     /* [115] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [116] */
@@ -3662,7 +3698,7 @@
   {
     /* [121] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [122] */
@@ -3692,7 +3728,7 @@
   {
     /* [127] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [128] */
@@ -3717,12 +3753,12 @@
   {
     /* [132] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [133] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [134] */
@@ -3817,7 +3853,7 @@
   {
     /* [152] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [153] */
@@ -3842,7 +3878,7 @@
   {
     /* [157] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [158] */
@@ -3867,7 +3903,7 @@
   {
     /* [162] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [163] */
@@ -3882,12 +3918,12 @@
   {
     /* [165] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [166] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [167] */
@@ -3907,12 +3943,12 @@
   {
     /* [170] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [171] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [172] */
@@ -3932,12 +3968,12 @@
   {
     /* [175] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [176] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [177] */
@@ -3957,12 +3993,12 @@
   {
     /* [180] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [181] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [182] */
@@ -3987,7 +4023,7 @@
   {
     /* [186] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [187] */
@@ -4007,12 +4043,12 @@
   {
     /* [190] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [191] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [192] */
@@ -4037,7 +4073,7 @@
   {
     /* [196] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [197] */
@@ -4062,7 +4098,7 @@
   {
     /* [201] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [202] */
@@ -4087,7 +4123,7 @@
   {
     /* [206] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [207] */
@@ -4112,7 +4148,7 @@
   {
     /* [211] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [212] */
@@ -4132,12 +4168,12 @@
   {
     /* [215] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [216] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [217] */
@@ -4157,12 +4193,12 @@
   {
     /* [220] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [221] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [222] */
@@ -4182,12 +4218,12 @@
   {
     /* [225] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [226] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [227] */
@@ -4207,12 +4243,12 @@
   {
     /* [230] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [231] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [232] */
@@ -4232,12 +4268,12 @@
   {
     /* [235] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [236] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [237] */
@@ -4257,12 +4293,12 @@
   {
     /* [240] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [241] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [242] */
@@ -4287,7 +4323,7 @@
   {
     /* [246] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [247] */
@@ -4312,7 +4348,7 @@
   {
     /* [251] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [252] */
@@ -4337,7 +4373,7 @@
   {
     /* [256] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [257] */
@@ -4362,7 +4398,7 @@
   {
     /* [261] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [262] */
@@ -4387,7 +4423,7 @@
   {
     /* [266] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [267] */
@@ -4412,7 +4448,7 @@
   {
     /* [271] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [272] */
@@ -4437,7 +4473,7 @@
   {
     /* [276] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [277] */
@@ -4457,12 +4493,12 @@
   {
     /* [280] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [281] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [282] */
@@ -4482,12 +4518,12 @@
   {
     /* [285] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [286] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [287] */
@@ -4507,12 +4543,12 @@
   {
     /* [290] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [291] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [292] */
@@ -4582,7 +4618,7 @@
   {
     /* [305] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [306] */
@@ -4602,7 +4638,7 @@
   {
     /* [309] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [310] */
@@ -4612,12 +4648,12 @@
   {
     /* [311] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [312] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [313] */
@@ -4632,12 +4668,12 @@
   {
     /* [315] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [316] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [317] */
@@ -4652,12 +4688,12 @@
   {
     /* [319] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [320] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [321] */
@@ -4672,12 +4708,12 @@
   {
     /* [323] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [324] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [325] */
@@ -4692,12 +4728,12 @@
   {
     /* [327] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [328] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [329] */
@@ -4717,7 +4753,7 @@
   {
     /* [332] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [333] */
@@ -4737,7 +4773,7 @@
   {
     /* [336] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [337] */
@@ -4757,7 +4793,7 @@
   {
     /* [340] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [341] */
@@ -4777,7 +4813,7 @@
   {
     /* [344] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [345] */
@@ -4792,12 +4828,12 @@
   {
     /* [347] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [348] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [349] */
@@ -4812,12 +4848,12 @@
   {
     /* [351] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [352] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [353] */
@@ -4832,12 +4868,12 @@
   {
     /* [355] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [356] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [357] */
@@ -4857,7 +4893,7 @@
   {
     /* [360] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [361] */
@@ -4877,7 +4913,7 @@
   {
     /* [364] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [365] */
@@ -4897,7 +4933,7 @@
   {
     /* [368] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [369] */
@@ -4912,12 +4948,12 @@
   {
     /* [371] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [372] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [373] */
@@ -4932,12 +4968,12 @@
   {
     /* [375] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [376] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [377] */
@@ -4952,12 +4988,12 @@
   {
     /* [379] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [380] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [381] */
@@ -4972,12 +5008,12 @@
   {
     /* [383] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [384] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[239],
+    /* matcher indices */ &kMatcherIndices[241],
   },
   {
     /* [385] */
@@ -4997,7 +5033,7 @@
   {
     /* [388] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [389] */
@@ -5017,7 +5053,7 @@
   {
     /* [392] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [393] */
@@ -5037,7 +5073,7 @@
   {
     /* [396] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [397] */
@@ -5052,12 +5088,12 @@
   {
     /* [399] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [400] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [401] */
@@ -5072,12 +5108,12 @@
   {
     /* [403] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [404] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [405] */
@@ -5172,7 +5208,7 @@
   {
     /* [423] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [424] */
@@ -5532,12 +5568,12 @@
   {
     /* [495] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [496] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [497] */
@@ -5547,12 +5583,12 @@
   {
     /* [498] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [499] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [500] */
@@ -5567,7 +5603,7 @@
   {
     /* [502] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [503] */
@@ -5582,7 +5618,7 @@
   {
     /* [505] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [506] */
@@ -5597,7 +5633,7 @@
   {
     /* [508] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [509] */
@@ -5612,7 +5648,7 @@
   {
     /* [511] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [512] */
@@ -5622,12 +5658,12 @@
   {
     /* [513] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [514] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [515] */
@@ -5637,12 +5673,12 @@
   {
     /* [516] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [517] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [518] */
@@ -5657,7 +5693,7 @@
   {
     /* [520] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [521] */
@@ -5667,12 +5703,12 @@
   {
     /* [522] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[237],
+    /* matcher indices */ &kMatcherIndices[239],
   },
   {
     /* [523] */
     /* usage */ ParameterUsage::kSampler,
-    /* matcher indices */ &kMatcherIndices[238],
+    /* matcher indices */ &kMatcherIndices[240],
   },
   {
     /* [524] */
@@ -5877,7 +5913,7 @@
   {
     /* [564] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [565] */
@@ -5892,7 +5928,7 @@
   {
     /* [567] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[236],
+    /* matcher indices */ &kMatcherIndices[238],
   },
   {
     /* [568] */
@@ -6277,7 +6313,7 @@
   {
     /* [644] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [645] */
@@ -6287,7 +6323,7 @@
   {
     /* [646] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [647] */
@@ -6297,7 +6333,7 @@
   {
     /* [648] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [649] */
@@ -6307,7 +6343,7 @@
   {
     /* [650] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [651] */
@@ -6317,7 +6353,7 @@
   {
     /* [652] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[237],
+    /* matcher indices */ &kMatcherIndices[239],
   },
   {
     /* [653] */
@@ -7657,27 +7693,27 @@
   {
     /* [920] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [921] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [922] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [923] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [924] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[236],
+    /* matcher indices */ &kMatcherIndices[238],
   },
   {
     /* [925] */
@@ -7702,7 +7738,7 @@
   {
     /* [929] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[237],
+    /* matcher indices */ &kMatcherIndices[239],
   },
   {
     /* [930] */
@@ -7717,12 +7753,12 @@
   {
     /* [932] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [933] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [934] */
@@ -7762,22 +7798,22 @@
   {
     /* [941] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[232],
+    /* matcher indices */ &kMatcherIndices[234],
   },
   {
     /* [942] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[233],
+    /* matcher indices */ &kMatcherIndices[235],
   },
   {
     /* [943] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[234],
+    /* matcher indices */ &kMatcherIndices[236],
   },
   {
     /* [944] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[235],
+    /* matcher indices */ &kMatcherIndices[237],
   },
   {
     /* [945] */
@@ -7787,7 +7823,7 @@
   {
     /* [946] */
     /* usage */ ParameterUsage::kTexture,
-    /* matcher indices */ &kMatcherIndices[236],
+    /* matcher indices */ &kMatcherIndices[238],
   },
   {
     /* [947] */
@@ -8119,78 +8155,83 @@
     /* usage */ ParameterUsage::kNone,
     /* matcher indices */ &kMatcherIndices[230],
   },
+  {
+    /* [1013] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[106],
+  },
 };
 
 constexpr TemplateTypeInfo kTemplateTypes[] = {
   {
     /* [0] */
     /* name */ "T",
-    /* matcher index */ 67,
+    /* matcher index */ 68,
   },
   {
     /* [1] */
     /* name */ "C",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [2] */
     /* name */ "A",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [3] */
     /* name */ "L",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [4] */
     /* name */ "T",
-    /* matcher index */ 67,
+    /* matcher index */ 68,
   },
   {
     /* [5] */
     /* name */ "C",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [6] */
     /* name */ "L",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [7] */
     /* name */ "T",
-    /* matcher index */ 67,
+    /* matcher index */ 68,
   },
   {
     /* [8] */
     /* name */ "C",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [9] */
     /* name */ "S",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [10] */
     /* name */ "T",
-    /* matcher index */ 63,
+    /* matcher index */ 64,
   },
   {
     /* [11] */
     /* name */ "U",
-    /* matcher index */ 65,
+    /* matcher index */ 66,
   },
   {
     /* [12] */
     /* name */ "T",
-    /* matcher index */ 67,
+    /* matcher index */ 68,
   },
   {
     /* [13] */
     /* name */ "L",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [14] */
@@ -8200,7 +8241,7 @@
   {
     /* [15] */
     /* name */ "U",
-    /* matcher index */ 54,
+    /* matcher index */ 55,
   },
   {
     /* [16] */
@@ -8210,7 +8251,7 @@
   {
     /* [17] */
     /* name */ "U",
-    /* matcher index */ 55,
+    /* matcher index */ 56,
   },
   {
     /* [18] */
@@ -8220,7 +8261,7 @@
   {
     /* [19] */
     /* name */ "U",
-    /* matcher index */ 56,
+    /* matcher index */ 57,
   },
   {
     /* [20] */
@@ -8230,7 +8271,7 @@
   {
     /* [21] */
     /* name */ "U",
-    /* matcher index */ 57,
+    /* matcher index */ 58,
   },
   {
     /* [22] */
@@ -8240,12 +8281,12 @@
   {
     /* [23] */
     /* name */ "U",
-    /* matcher index */ 58,
+    /* matcher index */ 59,
   },
   {
     /* [24] */
     /* name */ "T",
-    /* matcher index */ 59,
+    /* matcher index */ 60,
   },
   {
     /* [25] */
@@ -8255,62 +8296,62 @@
   {
     /* [26] */
     /* name */ "T",
-    /* matcher index */ 71,
+    /* matcher index */ 72,
   },
   {
     /* [27] */
     /* name */ "T",
-    /* matcher index */ 52,
+    /* matcher index */ 53,
   },
   {
     /* [28] */
     /* name */ "T",
-    /* matcher index */ 60,
+    /* matcher index */ 61,
   },
   {
     /* [29] */
     /* name */ "T",
-    /* matcher index */ 64,
+    /* matcher index */ 65,
   },
   {
     /* [30] */
     /* name */ "T",
-    /* matcher index */ 66,
+    /* matcher index */ 67,
   },
   {
     /* [31] */
     /* name */ "T",
-    /* matcher index */ 56,
+    /* matcher index */ 57,
   },
   {
     /* [32] */
     /* name */ "T",
-    /* matcher index */ 57,
+    /* matcher index */ 58,
   },
   {
     /* [33] */
     /* name */ "T",
-    /* matcher index */ 54,
+    /* matcher index */ 55,
   },
   {
     /* [34] */
     /* name */ "T",
-    /* matcher index */ 55,
+    /* matcher index */ 56,
   },
   {
     /* [35] */
     /* name */ "T",
-    /* matcher index */ 58,
+    /* matcher index */ 59,
   },
   {
     /* [36] */
     /* name */ "T",
-    /* matcher index */ 53,
+    /* matcher index */ 54,
   },
   {
     /* [37] */
     /* name */ "T",
-    /* matcher index */ 70,
+    /* matcher index */ 71,
   },
 };
 
@@ -8879,7 +8920,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[36],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[134],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -9515,7 +9556,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[36],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[106],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -9983,7 +10024,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[36],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[23],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10499,7 +10540,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[178],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10571,7 +10612,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[184],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10643,7 +10684,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[190],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10715,7 +10756,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[196],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10787,7 +10828,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[202],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10859,7 +10900,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[208],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -10931,7 +10972,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[214],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11003,7 +11044,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[220],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11075,7 +11116,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[37],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[226],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11591,7 +11632,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[9],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11627,7 +11668,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[105],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11663,7 +11704,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[42],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11699,7 +11740,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[1],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -11735,7 +11776,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ &kMatcherIndices[39],
     /* flags */ OverloadFlags(OverloadFlag::kIsConstructor, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::Zero,
@@ -13751,7 +13792,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ nullptr,
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsComputePipeline),
     /* const eval */ nullptr,
@@ -13835,7 +13876,7 @@
     /* num template numbers */ 0,
     /* template types */ &kTemplateTypes[38],
     /* template numbers */ &kTemplateNumbers[10],
-    /* parameters */ &kParameters[1013],
+    /* parameters */ &kParameters[1014],
     /* return matcher indices */ nullptr,
     /* flags */ OverloadFlags(OverloadFlag::kIsBuiltin, OverloadFlag::kSupportsComputePipeline),
     /* const eval */ nullptr,
@@ -14020,6 +14061,18 @@
     /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
     /* const eval */ &ConstEval::OpLogicalOr,
   },
+  {
+    /* [471] */
+    /* num parameters */ 1,
+    /* num template types */ 1,
+    /* num template numbers */ 0,
+    /* template types */ &kTemplateTypes[36],
+    /* template numbers */ &kTemplateNumbers[10],
+    /* parameters */ &kParameters[1013],
+    /* return matcher indices */ &kMatcherIndices[232],
+    /* flags */ OverloadFlags(OverloadFlag::kIsConverter, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline, OverloadFlag::kMustUse),
+    /* const eval */ &ConstEval::Conv,
+  },
 };
 
 constexpr IntrinsicInfo kBuiltins[] = {
@@ -15284,6 +15337,12 @@
     /* num overloads */ 6,
     /* overloads */ &kOverloads[225],
   },
+  {
+    /* [17] */
+    /* conv packedVec3<T : concrete_scalar>(vec3<T>) -> packedVec3<T> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[471],
+  },
 };
 
 // clang-format on
diff --git a/src/tint/resolver/intrinsic_table.inl.tmpl b/src/tint/resolver/intrinsic_table.inl.tmpl
index 92e7e31..4769a3c 100644
--- a/src/tint/resolver/intrinsic_table.inl.tmpl
+++ b/src/tint/resolver/intrinsic_table.inl.tmpl
@@ -221,7 +221,7 @@
 {{- end  }}
 
 {{- if .DisplayName }}
-  std::stringstream ss;
+  utils::StringStream ss;
   ss{{range SplitDisplayName .DisplayName}} << {{.}}{{end}};
   return ss.str();
 {{- else if .TemplateParams }}
@@ -264,7 +264,7 @@
 }
 
 std::string {{$class}}::String(MatchState*) const {
-  std::stringstream ss;
+  utils::StringStream ss;
   // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
   // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
   ss
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 4292438..cd426f7 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -84,6 +84,7 @@
 #include "src/tint/utils/reverse.h"
 #include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/transform.h"
 #include "src/tint/utils/vector.h"
 
@@ -99,6 +100,7 @@
 
 constexpr int64_t kMaxArrayElementCount = 65536;
 constexpr uint32_t kMaxStatementDepth = 127;
+constexpr size_t kMaxNestDepthOfCompositeType = 255;
 
 }  // namespace
 
@@ -1697,7 +1699,7 @@
                 target_el_ty = target_arr_ty->ElemType();
             }
             if (auto* el_ty = ConcreteType(a->ElemType(), target_el_ty, source)) {
-                return Array(source, source, el_ty, a->Count(), /* explicit_stride */ 0);
+                return Array(source, source, source, el_ty, a->Count(), /* explicit_stride */ 0);
             }
             return nullptr;
         },
@@ -2010,8 +2012,12 @@
 
         auto stage = args_stage;                 // The evaluation stage of the call
         const constant::Value* value = nullptr;  // The constant value for the call
+        if (stage == sem::EvaluationStage::kConstant && skip_const_eval_.Contains(expr)) {
+            stage = sem::EvaluationStage::kNotEvaluated;
+        }
         if (stage == sem::EvaluationStage::kConstant) {
-            if (auto r = const_eval_.ArrayOrStructCtor(ty, args)) {
+            auto els = utils::Transform(args, [&](auto* arg) { return arg->ConstantValue(); });
+            if (auto r = const_eval_.ArrayOrStructCtor(ty, std::move(els))) {
                 value = r.Get();
             } else {
                 return nullptr;
@@ -2043,6 +2049,10 @@
             [&](const type::F32*) { return ctor_or_conv(CtorConvIntrinsic::kF32, nullptr); },
             [&](const type::Bool*) { return ctor_or_conv(CtorConvIntrinsic::kBool, nullptr); },
             [&](const type::Vector* v) {
+                if (v->Packed()) {
+                    TINT_ASSERT(Resolver, v->Width() == 3u);
+                    return ctor_or_conv(CtorConvIntrinsic::kPackedVec3, v->type());
+                }
                 return ctor_or_conv(VectorCtorConvIntrinsic(v->Width()), v->type());
             },
             [&](const type::Matrix* m) {
@@ -2128,7 +2138,8 @@
             }
             return nullptr;
         }
-        auto* arr = Array(expr->source, expr->source, el_ty, el_count, /* explicit_stride */ 0);
+        auto* arr = Array(expr->source, expr->source, expr->source, el_ty, el_count,
+                          /* explicit_stride */ 0);
         if (!arr) {
             return nullptr;
         }
@@ -2445,7 +2456,8 @@
             return nullptr;
         }
 
-        auto* out = Array(ast_el_ty->source,                              //
+        auto* out = Array(tmpl_ident->source,                             //
+                          ast_el_ty->source,                              //
                           ast_count ? ast_count->source : ident->source,  //
                           el_ty, el_count, explicit_stride);
         if (!out) {
@@ -2570,6 +2582,24 @@
         }
         return tex;
     };
+    auto packed_vec3_t = [&]() -> type::Vector* {
+        auto* tmpl_ident = templated_identifier(1);
+        if (TINT_UNLIKELY(!tmpl_ident)) {
+            return nullptr;
+        }
+        auto* el_ty = Type(tmpl_ident->arguments[0]);
+        if (TINT_UNLIKELY(!el_ty)) {
+            return nullptr;
+        }
+
+        if (TINT_UNLIKELY(!el_ty)) {
+            return nullptr;
+        }
+        if (TINT_UNLIKELY(!validator_.Vector(el_ty, ident->source))) {
+            return nullptr;
+        }
+        return b.create<type::Vector>(el_ty, 3u, true);
+    };
 
     switch (builtin_ty) {
         case builtin::Builtin::kBool:
@@ -2716,6 +2746,9 @@
             return storage_texture(type::TextureDimension::k2dArray);
         case builtin::Builtin::kTextureStorage3D:
             return storage_texture(type::TextureDimension::k3d);
+        case builtin::Builtin::kPackedVec3: {
+            return packed_vec3_t();
+        }
         case builtin::Builtin::kUndefined:
             break;
     }
@@ -2725,6 +2758,19 @@
     return nullptr;
 }
 
+size_t Resolver::NestDepth(const type::Type* ty) const {
+    return Switch(
+        ty,  //
+        [](const type::Vector*) { return size_t{1}; },
+        [](const type::Matrix*) { return size_t{2}; },
+        [&](Default) {
+            if (auto d = nest_depth_.Get(ty)) {
+                return *d;
+            }
+            return size_t{0};
+        });
+}
+
 void Resolver::CollectTextureSamplerPairs(
     const sem::Builtin* builtin,
     utils::VectorRef<const sem::ValueExpression*> args) const {
@@ -3057,7 +3103,7 @@
                         filtered.Push(str);
                     }
                 }
-                std::ostringstream msg;
+                utils::StringStream msg;
                 utils::SuggestAlternatives(unresolved->name,
                                            filtered.Slice().Reinterpret<char const* const>(), msg);
                 AddNote(msg.str(), expr->source);
@@ -3422,7 +3468,7 @@
     if (rule != builtin::DiagnosticRule::kUndefined) {
         validator_.DiagnosticFilters().Set(rule, control.severity);
     } else {
-        std::ostringstream ss;
+        utils::StringStream ss;
         ss << "unrecognized diagnostic rule '" << rule_name << "'\n";
         utils::SuggestAlternatives(rule_name, builtin::kDiagnosticRuleStrings, ss);
         AddWarning(ss.str(), control.rule_name->source);
@@ -3527,7 +3573,8 @@
     return true;
 }
 
-type::Array* Resolver::Array(const Source& el_source,
+type::Array* Resolver::Array(const Source& array_source,
+                             const Source& el_source,
                              const Source& count_source,
                              const type::Type* el_ty,
                              const type::ArrayCount* el_count,
@@ -3541,7 +3588,7 @@
     if (auto const_count = el_count->As<type::ConstantArrayCount>()) {
         size = const_count->value * stride;
         if (size > std::numeric_limits<uint32_t>::max()) {
-            std::stringstream msg;
+            utils::StringStream msg;
             msg << "array byte size (0x" << std::hex << size
                 << ") must not exceed 0xffffffff bytes";
             AddError(msg.str(), count_source);
@@ -3554,6 +3601,17 @@
         el_ty, el_count, el_align, static_cast<uint32_t>(size), static_cast<uint32_t>(stride),
         static_cast<uint32_t>(implicit_stride));
 
+    // Maximum nesting depth of composite types
+    //  https://gpuweb.github.io/gpuweb/wgsl/#limits
+    const size_t nest_depth = 1 + NestDepth(el_ty);
+    if (nest_depth > kMaxNestDepthOfCompositeType) {
+        AddError("array has nesting depth of " + std::to_string(nest_depth) + ", maximum is " +
+                     std::to_string(kMaxNestDepthOfCompositeType),
+                 array_source);
+        return nullptr;
+    }
+    nest_depth_.Add(out, nest_depth);
+
     if (!validator_.Array(out, el_source)) {
         return nullptr;
     }
@@ -3573,15 +3631,18 @@
 }
 
 sem::Struct* Resolver::Structure(const ast::Struct* str) {
+    auto struct_name = [&] {  //
+        return builder_->Symbols().NameFor(str->name->symbol);
+    };
+
     if (validator_.IsValidationEnabled(str->attributes,
                                        ast::DisabledValidation::kIgnoreStructMemberLimit)) {
         // Maximum number of members in a structure type
         // https://gpuweb.github.io/gpuweb/wgsl/#limits
         const size_t kMaxNumStructMembers = 16383;
         if (str->members.Length() > kMaxNumStructMembers) {
-            AddError("struct '" + builder_->Symbols().NameFor(str->name->symbol) + "' has " +
-                         std::to_string(str->members.Length()) + " members, maximum is " +
-                         std::to_string(kMaxNumStructMembers),
+            AddError("struct '" + struct_name() + "' has " + std::to_string(str->members.Length()) +
+                         " members, maximum is " + std::to_string(kMaxNumStructMembers),
                      str->source);
             return nullptr;
         }
@@ -3607,6 +3668,7 @@
     uint64_t struct_align = 1;
     utils::Hashmap<Symbol, const ast::StructMember*, 8> member_map;
 
+    size_t members_nest_depth = 0;
     for (auto* member : str->members) {
         Mark(member);
         Mark(member->name);
@@ -3623,6 +3685,8 @@
             return nullptr;
         }
 
+        members_nest_depth = std::max(members_nest_depth, NestDepth(type));
+
         // validator_.Validate member type
         if (!validator_.IsPlain(type)) {
             AddError(sem_.TypeNameOf(type) + " cannot be used as the type of a structure member",
@@ -3762,7 +3826,7 @@
 
         offset = utils::RoundUp(align, offset);
         if (offset > std::numeric_limits<uint32_t>::max()) {
-            std::stringstream msg;
+            utils::StringStream msg;
             msg << "struct member offset (0x" << std::hex << offset << ") must not exceed 0x"
                 << std::hex << std::numeric_limits<uint32_t>::max() << " bytes";
             AddError(msg.str(), member->source);
@@ -3784,7 +3848,7 @@
     struct_size = utils::RoundUp(struct_align, struct_size);
 
     if (struct_size > std::numeric_limits<uint32_t>::max()) {
-        std::stringstream msg;
+        utils::StringStream msg;
         msg << "struct size (0x" << std::hex << struct_size << ") must not exceed 0xffffffff bytes";
         AddError(msg.str(), str->source);
         return nullptr;
@@ -3820,6 +3884,18 @@
         return nullptr;
     }
 
+    // Maximum nesting depth of composite types
+    //  https://gpuweb.github.io/gpuweb/wgsl/#limits
+    const size_t nest_depth = 1 + members_nest_depth;
+    if (nest_depth > kMaxNestDepthOfCompositeType) {
+        AddError("struct '" + struct_name() + "' has nesting depth of " +
+                     std::to_string(nest_depth) + ", maximum is " +
+                     std::to_string(kMaxNestDepthOfCompositeType),
+                 str->source);
+        return nullptr;
+    }
+    nest_depth_.Add(out, nest_depth);
+
     return out;
 }
 
@@ -4120,7 +4196,7 @@
             if (decl &&
                 !ApplyAddressSpaceUsageToType(
                     address_space, const_cast<type::Type*>(member->Type()), decl->type->source)) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "while analyzing structure member " << sem_.TypeNameOf(str) << "."
                     << builder_->Symbols().NameFor(member->Name());
                 AddNote(err.str(), member->Source());
@@ -4151,7 +4227,7 @@
     }
 
     if (builtin::IsHostShareable(address_space) && !validator_.IsHostShareable(ty)) {
-        std::stringstream err;
+        utils::StringStream err;
         err << "Type '" << sem_.TypeNameOf(ty) << "' cannot be used in address space '"
             << address_space << "' as it is non-host-shareable";
         AddError(err.str(), usage);
@@ -4176,7 +4252,7 @@
                     return false;
                 }
             } else {
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "attribute is not valid for " << use;
                 AddError(ss.str(), attr->source);
                 return false;
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 72f9e8f..fff7506 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -349,6 +349,7 @@
 
     /// Builds and returns the semantic information for an array.
     /// @returns the semantic Array information, or nullptr if an error is raised.
+    /// @param array_source the source of the array
     /// @param el_source the source of the array element, or the array if the array does not have a
     ///        locally-declared element AST node.
     /// @param count_source the source of the array count, or the array if the array does not have a
@@ -356,7 +357,8 @@
     /// @param el_ty the Array element type
     /// @param el_count the number of elements in the array.
     /// @param explicit_stride the explicit byte stride of the array. Zero means implicit stride.
-    type::Array* Array(const Source& el_source,
+    type::Array* Array(const Source& array_source,
+                       const Source& el_source,
                        const Source& count_source,
                        const type::Type* el_ty,
                        const type::ArrayCount* el_count,
@@ -496,6 +498,10 @@
     /// @note: Will raise an ICE if @p symbol is not a builtin type.
     type::Type* BuiltinType(builtin::Builtin builtin_ty, const ast::Identifier* ident);
 
+    /// @returns the nesting depth of @ty as defined in
+    /// https://gpuweb.github.io/gpuweb/wgsl/#composite-types
+    size_t NestDepth(const type::Type* ty) const;
+
     // ArrayConstructorSig represents a unique array constructor signature.
     // It is a tuple of the array type, number of arguments provided and earliest evaluation stage.
     using ArrayConstructorSig =
@@ -566,6 +572,7 @@
         logical_binary_lhs_to_parent_;
     utils::Hashset<const ast::Expression*, 8> skip_const_eval_;
     IdentifierResolveHint identifier_resolve_hint_;
+    utils::Hashmap<const type::Type*, size_t, 8> nest_depth_;
 };
 
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index 25155ff..a601ef0 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -46,6 +46,7 @@
 #include "src/tint/type/reference.h"
 #include "src/tint/type/sampled_texture.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 
 using ::testing::ElementsAre;
 using ::testing::HasSubstr;
@@ -1647,7 +1648,7 @@
     ast::Type rhs_type = params.create_rhs_type(*this);
     auto* result_type = params.create_result_type(*this);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << FriendlyName(lhs_type) << " " << params.op << " " << FriendlyName(rhs_type);
     SCOPED_TRACE(ss.str());
 
@@ -1679,7 +1680,7 @@
     ast::Type lhs_type = create_lhs_type(*this);
     ast::Type rhs_type = create_rhs_type(*this);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << FriendlyName(lhs_type) << " " << params.op << " " << FriendlyName(rhs_type);
 
     ss << ", After aliasing: " << FriendlyName(lhs_type) << " " << params.op << " "
@@ -1728,7 +1729,7 @@
     ast::Type lhs_type = lhs_create_type_func(*this);
     ast::Type rhs_type = rhs_create_type_func(*this);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << FriendlyName(lhs_type) << " " << op << " " << FriendlyName(rhs_type);
     SCOPED_TRACE(ss.str());
 
@@ -2434,5 +2435,195 @@
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
+size_t kMaxNestDepthOfCompositeType = 255;
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Structs_Valid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.i32())});
+    size_t depth = 1;  // Depth of struct
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        s = Structure("S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Structs_Invalid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.i32())});
+    size_t depth = 1;  // Depth of struct
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
+        s = Structure(source, "S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: struct 'S254' has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithVector_Valid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.vec3<i32>())});
+    size_t depth = 2;  // Despth of struct + vector
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        s = Structure("S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithVector_Invalid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.vec3<i32>())});
+    size_t depth = 2;  // Despth of struct + vector
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
+        s = Structure(source, "S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: struct 'S253' has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithMatrix_Valid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.mat3x3<f32>())});
+    size_t depth = 3;  // Depth of struct + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        s = Structure("S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsWithMatrix_Invalid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.mat3x3<f32>())});
+    size_t depth = 3;  // Depth of struct + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
+        s = Structure(source, "S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: struct 'S252' has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Arrays_Valid) {
+    auto a = ty.array(ty.i32(), 10_u);
+    size_t depth = 1;  // Depth of array
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        a = ty.array(a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_Arrays_Invalid) {
+    auto a = ty.array(Source{{99, 88}}, ty.i32(), 10_u);
+    size_t depth = 1;  // Depth of array
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
+        a = ty.array(source, a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: array has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfVector_Valid) {
+    auto a = ty.array(ty.vec3<i32>(), 10_u);
+    size_t depth = 2;  // Depth of array + vector
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        a = ty.array(a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfVector_Invalid) {
+    auto a = ty.array(Source{{99, 88}}, ty.vec3<i32>(), 10_u);
+    size_t depth = 2;  // Depth of array + vector
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
+        a = ty.array(source, a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: array has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfMatrix_Valid) {
+    auto a = ty.array(ty.mat3x3<f32>(), 10_u);
+    size_t depth = 3;  // Depth of array + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        a = ty.array(a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfMatrix_Invalid) {
+    auto a = ty.array(ty.mat3x3<f32>(), 10_u);
+    size_t depth = 3;  // Depth of array + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
+        a = ty.array(source, a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: array has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsOfArray_Valid) {
+    auto a = ty.array(ty.mat3x3<f32>(), 10_u);
+    auto* s = Structure("S", utils::Vector{Member("m", a)});
+    size_t depth = 4;  // Depth of struct + array + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        s = Structure("S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_StructsOfArray_Invalid) {
+    auto a = ty.array(ty.mat3x3<f32>(), 10_u);
+    auto* s = Structure("S", utils::Vector{Member("m", a)});
+    size_t depth = 4;  // Depth of struct + array + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = i == iterations - 1 ? Source{{12, 34}} : Source{{0, i}};
+        s = Structure(source, "S" + std::to_string(i), utils::Vector{Member("m", ty.Of(s))});
+    }
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: struct 'S251' has nesting depth of 256, maximum is 255");
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfStruct_Valid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.mat3x3<f32>())});
+    auto a = ty.array(ty.Of(s), 10_u);
+    size_t depth = 4;  // Depth of array + struct + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth;
+    for (size_t i = 0; i < iterations; ++i) {
+        a = ty.array(a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverTest, MaxNestDepthOfCompositeType_ArraysOfStruct_Invalid) {
+    auto* s = Structure("S", utils::Vector{Member("m", ty.mat3x3<f32>())});
+    auto a = ty.array(ty.Of(s), 10_u);
+    size_t depth = 4;  // Depth of array + struct + matrix
+    size_t iterations = kMaxNestDepthOfCompositeType - depth + 1;
+    for (size_t i = 0; i < iterations; ++i) {
+        auto source = (i == iterations - 1) ? Source{{12, 34}} : Source{{0, i}};
+        a = ty.array(source, a, 1_u);
+    }
+    Alias("a", a);
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: array has nesting depth of 256, maximum is 255");
+}
+
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index 37fba4c..93402b6 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -40,6 +40,7 @@
 #include "src/tint/sem/while_statement.h"
 #include "src/tint/utils/block_allocator.h"
 #include "src/tint/utils/map.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/unique_vector.h"
 
 // Set to `1` to dump the uniformity graph for each function in graphviz format.
@@ -1352,14 +1353,14 @@
         // To determine if we're dereferencing a partial pointer, unwrap *&
         // chains; if the final expression is an identifier, see if it's a
         // partial pointer. If it's not an identifier, then it must be an
-        // index/accessor expression, and thus a partial pointer.
+        // index/member accessor expression, and thus a partial pointer.
         auto* e = UnwrapIndirectAndAddressOfChain(u);
         if (auto* var_user = sem_.Get<sem::VariableUser>(e)) {
             if (current_function_->partial_ptrs.Contains(var_user->Variable())) {
                 return true;
             }
         } else {
-            TINT_ASSERT(Resolver, e->Is<ast::IndexAccessorExpression>());
+            TINT_ASSERT(Resolver, e->Is<ast::AccessorExpression>());
             return true;
         }
         return false;
@@ -1778,7 +1779,7 @@
             non_uniform_source->ast,
             [&](const ast::IdentifierExpression* ident) {
                 auto* var = sem_.GetVal(ident)->UnwrapLoad()->As<sem::VariableUser>()->Variable();
-                std::ostringstream ss;
+                utils::StringStream ss;
                 if (auto* param = var->As<sem::Parameter>()) {
                     auto* func = param->Owner()->As<sem::Function>();
                     ss << param_type(param) << "'" << NameFor(ident) << "' of '" << NameFor(func)
@@ -1791,7 +1792,7 @@
             },
             [&](const ast::Variable* v) {
                 auto* var = sem_.Get(v);
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "reading from " << var_type(var) << "'" << NameFor(v)
                    << "' may result in a non-uniform value";
                 diagnostics_.add_note(diag::System::Resolver, ss.str(), v->source);
@@ -1808,7 +1809,7 @@
                     case Node::kFunctionCallArgumentContents: {
                         auto* arg = c->args[non_uniform_source->arg_index];
                         auto* var = sem_.GetVal(arg)->RootIdentifier();
-                        std::ostringstream ss;
+                        utils::StringStream ss;
                         ss << "reading from " << var_type(var) << "'" << NameFor(var)
                            << "' may result in a non-uniform value";
                         diagnostics_.add_note(diag::System::Resolver, ss.str(),
@@ -1895,7 +1896,7 @@
 
             // Show the place where the non-uniform argument was passed.
             // If this is a builtin, this will be the trigger location for the failure.
-            std::ostringstream ss;
+            utils::StringStream ss;
             ss << "possibly non-uniform value passed" << (is_value ? "" : " via pointer")
                << " here";
             report(call->args[cause->arg_index]->source, ss.str(), /* note */ user_func != nullptr);
@@ -1907,7 +1908,7 @@
             {
                 // Show a builtin was reachable from this call (which may be the call itself).
                 // This will be the trigger location for the failure.
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "'" << NameFor(builtin_call->target)
                    << "' must only be called from uniform control flow";
                 report(builtin_call->source, ss.str(), /* note */ false);
@@ -1915,7 +1916,7 @@
 
             if (builtin_call != call) {
                 // The call was to a user function, so show that call too.
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "called ";
                 if (target->As<sem::Function>() != SemCall(builtin_call)->Stmt()->Function()) {
                     ss << "indirectly ";
diff --git a/src/tint/resolver/uniformity_test.cc b/src/tint/resolver/uniformity_test.cc
index fec74b3..0a79058 100644
--- a/src/tint/resolver/uniformity_test.cc
+++ b/src/tint/resolver/uniformity_test.cc
@@ -21,6 +21,7 @@
 #include "src/tint/program_builder.h"
 #include "src/tint/reader/wgsl/parser.h"
 #include "src/tint/resolver/uniformity.h"
+#include "src/tint/utils/string_stream.h"
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -6374,6 +6375,43 @@
 )");
 }
 
+TEST_F(UniformityAnalysisTest, StructMember_MemberBecomesUniformThroughPartialPointer) {
+    // For aggregate types, we conservatively consider them to be non-uniform once they
+    // become non-uniform. Test that after assigning a uniform value to a member, that member is
+    // still considered to be non-uniform.
+    std::string src = R"(
+struct S {
+  a : i32,
+  b : i32,
+}
+@group(0) @binding(0) var<storage, read_write> rw : i32;
+
+fn foo() {
+  var s : S;
+  s.a = rw;
+  *&s.a = 0;
+  if (s.a == 0) {
+    workgroupBarrier();
+  }
+}
+)";
+
+    RunTest(src, false);
+    EXPECT_EQ(error_,
+              R"(test:13:5 error: 'workgroupBarrier' must only be called from uniform control flow
+    workgroupBarrier();
+    ^^^^^^^^^^^^^^^^
+
+test:12:3 note: control flow depends on possibly non-uniform value
+  if (s.a == 0) {
+  ^^
+
+test:10:9 note: reading from read_write storage buffer 'rw' may result in a non-uniform value
+  s.a = rw;
+        ^^
+)");
+}
+
 TEST_F(UniformityAnalysisTest, StructMember_MemberBecomesUniformThroughCapturedPartialPointer) {
     // For aggregate types, we conservatively consider them to be non-uniform once they
     // become non-uniform. Test that after assigning a uniform value to a member, that member is
@@ -7890,7 +7928,7 @@
 
 TEST_P(UniformityAnalysisDiagnosticFilterTest, Directive) {
     auto& param = GetParam();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << "diagnostic(" << param << ", derivative_uniformity);"
        << R"(
 @group(0) @binding(0) var<storage, read_write> non_uniform : i32;
@@ -7909,7 +7947,7 @@
     if (param == builtin::DiagnosticSeverity::kOff) {
         EXPECT_TRUE(error_.empty());
     } else {
-        std::ostringstream err;
+        utils::StringStream err;
         err << ToStr(param) << ": 'textureSample' must only be called";
         EXPECT_THAT(error_, ::testing::HasSubstr(err.str()));
     }
@@ -7917,7 +7955,7 @@
 
 TEST_P(UniformityAnalysisDiagnosticFilterTest, AttributeOnFunction) {
     auto& param = GetParam();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << R"(
 @group(0) @binding(0) var<storage, read_write> non_uniform : i32;
 @group(0) @binding(1) var t : texture_2d<f32>;
@@ -7936,7 +7974,7 @@
     if (param == builtin::DiagnosticSeverity::kOff) {
         EXPECT_TRUE(error_.empty());
     } else {
-        std::ostringstream err;
+        utils::StringStream err;
         err << ToStr(param) << ": 'textureSample' must only be called";
         EXPECT_THAT(error_, ::testing::HasSubstr(err.str()));
     }
@@ -7944,7 +7982,7 @@
 
 TEST_P(UniformityAnalysisDiagnosticFilterTest, AttributeOnBlock) {
     auto& param = GetParam();
-    std::ostringstream ss;
+    utils::StringStream ss;
     ss << R"(
 @group(0) @binding(0) var<storage, read_write> non_uniform : i32;
 @group(0) @binding(1) var t : texture_2d<f32>;
@@ -7962,7 +8000,7 @@
     if (param == builtin::DiagnosticSeverity::kOff) {
         EXPECT_TRUE(error_.empty());
     } else {
-        std::ostringstream err;
+        utils::StringStream err;
         err << ToStr(param) << ": 'textureSample' must only be called";
         EXPECT_THAT(error_, ::testing::HasSubstr(err.str()));
     }
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index b272b15..f8f4c4d 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -71,11 +71,15 @@
 #include "src/tint/utils/reverse.h"
 #include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/utils/transform.h"
 
 namespace tint::resolver {
 namespace {
 
+constexpr size_t kMaxFunctionParameters = 255;
+constexpr size_t kMaxSwitchCaseSelectors = 16383;
+
 bool IsValidStorageTextureDimension(type::TextureDimension dim) {
     switch (dim) {
         case type::TextureDimension::k1d:
@@ -377,7 +381,7 @@
 
     // Value type has to match storage type
     if (storage_ty != value_type) {
-        std::stringstream s;
+        utils::StringStream s;
         s << "cannot initialize " << v->Kind() << " of type '" << sem_.TypeNameOf(storage_ty)
           << "' with value of type '" << sem_.TypeNameOf(initializer_ty) << "'";
         AddError(s.str(), v->source);
@@ -467,7 +471,9 @@
             }
 
             // Validate that member is at a valid byte offset
-            if (m->Offset() % required_align != 0) {
+            if (m->Offset() % required_align != 0 &&
+                !enabled_extensions_.Contains(
+                    builtin::Extension::kChromiumInternalRelaxedUniformLayout)) {
                 AddError("the offset of a struct member of type '" +
                              m->Type()->UnwrapRef()->FriendlyName(symbols_) +
                              "' in address space '" + utils::ToString(address_space) +
@@ -493,7 +499,9 @@
             auto* const prev_member = (i == 0) ? nullptr : str->Members()[i - 1];
             if (prev_member && is_uniform_struct(prev_member->Type())) {
                 const uint32_t prev_to_curr_offset = m->Offset() - prev_member->Offset();
-                if (prev_to_curr_offset % 16 != 0) {
+                if (prev_to_curr_offset % 16 != 0 &&
+                    !enabled_extensions_.Contains(
+                        builtin::Extension::kChromiumInternalRelaxedUniformLayout)) {
                     AddError(
                         "uniform storage requires that the number of bytes between the "
                         "start of the previous member of type struct and the current "
@@ -526,7 +534,9 @@
             return false;
         }
 
-        if (address_space == builtin::AddressSpace::kUniform) {
+        if (address_space == builtin::AddressSpace::kUniform &&
+            !enabled_extensions_.Contains(
+                builtin::Extension::kChromiumInternalRelaxedUniformLayout)) {
             // We already validated that this array member is itself aligned to 16 bytes above, so
             // we only need to validate that stride is a multiple of 16 bytes.
             if (arr->Stride() % 16 != 0) {
@@ -833,7 +843,7 @@
                     break;
             }
             if (!ok) {
-                std::stringstream ss;
+                utils::StringStream ss;
                 ss << "function parameter of pointer type cannot be in '" << sc
                    << "' address space";
                 AddError(ss.str(), decl->source);
@@ -861,7 +871,7 @@
                                  ast::PipelineStage stage,
                                  const bool is_input) const {
     auto* type = storage_ty->UnwrapRef();
-    std::stringstream stage_name;
+    utils::StringStream stage_name;
     stage_name << stage;
     bool is_stage_mismatch = false;
     bool is_output = !is_input;
@@ -874,7 +884,7 @@
                 is_stage_mismatch = true;
             }
             if (!(type->is_float_vector() && type->As<type::Vector>()->Width() == 4)) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'vec4<f32>'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -889,7 +899,7 @@
                 is_stage_mismatch = true;
             }
             if (!(type->is_unsigned_integer_vector() && type->As<type::Vector>()->Width() == 3)) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'vec3<u32>'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -901,7 +911,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::F32>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'f32'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -913,7 +923,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::Bool>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'bool'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -925,7 +935,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::U32>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'u32'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -938,7 +948,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::U32>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'u32'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -949,7 +959,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::U32>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'u32'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -961,7 +971,7 @@
                 is_stage_mismatch = true;
             }
             if (!type->Is<type::U32>()) {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "store type of @builtin(" << builtin << ") must be 'u32'";
                 AddError(err.str(), attr->source);
                 return false;
@@ -972,7 +982,7 @@
     }
 
     if (is_stage_mismatch) {
-        std::stringstream err;
+        utils::StringStream err;
         err << "@builtin(" << builtin << ") cannot be used in "
             << (is_input ? "input of " : "output of ") << stage_name.str() << " pipeline stage";
         AddError(err.str(), attr->source);
@@ -1040,8 +1050,10 @@
         }
     }
 
-    if (decl->params.Length() > 255) {
-        AddError("functions may declare at most 255 parameters", decl->source);
+    if (decl->params.Length() > kMaxFunctionParameters) {
+        AddError("function declares " + std::to_string(decl->params.Length()) +
+                     " parameters, maximum is " + std::to_string(kMaxFunctionParameters),
+                 decl->source);
         return false;
     }
 
@@ -1145,7 +1157,7 @@
                 pipeline_io_attribute = attr;
 
                 if (builtins.Contains(builtin)) {
-                    std::stringstream err;
+                    utils::StringStream err;
                     err << "@builtin(" << builtin << ") appears multiple times as pipeline "
                         << (param_or_ret == ParamOrRetType::kParameter ? "input" : "output");
                     AddError(err.str(), decl->source);
@@ -1938,7 +1950,7 @@
         if (stage != ast::PipelineStage::kCompute) {
             for (auto* var : func->DirectlyReferencedGlobals()) {
                 if (var->AddressSpace() == builtin::AddressSpace::kWorkgroup) {
-                    std::stringstream stage_name;
+                    utils::StringStream stage_name;
                     stage_name << stage;
                     for (auto* user : var->Users()) {
                         if (func == user->Stmt()->Function()) {
@@ -1962,7 +1974,7 @@
         for (auto* builtin : func->DirectlyCalledBuiltins()) {
             if (!builtin->SupportedStages().Contains(stage)) {
                 auto* call = func->FindDirectCallTo(builtin);
-                std::stringstream err;
+                utils::StringStream err;
                 err << "built-in cannot be used by " << stage << " pipeline stage";
                 AddError(err.str(),
                          call ? call->Declaration()->source : func->Declaration()->source);
@@ -1976,7 +1988,7 @@
     auto check_no_discards = [&](const sem::Function* func, const sem::Function* entry_point) {
         if (auto* discard = func->DiscardStatement()) {
             auto stage = entry_point->Declaration()->PipelineStage();
-            std::stringstream err;
+            utils::StringStream err;
             err << "discard statement cannot be used in " << stage << " pipeline stage";
             AddError(err.str(), discard->Declaration()->source);
             backtrace(func, entry_point);
@@ -2272,7 +2284,7 @@
     }
 
     if (!locations.Add(location)) {
-        std::stringstream err;
+        utils::StringStream err;
         err << "@location(" << location << ") appears multiple times";
         AddError(err.str(), loc_attr->source);
         return false;
@@ -2305,6 +2317,13 @@
 }
 
 bool Validator::SwitchStatement(const ast::SwitchStatement* s) {
+    if (s->body.Length() > kMaxSwitchCaseSelectors) {
+        AddError("switch statement has " + std::to_string(s->body.Length()) +
+                     " case selectors, max is " + std::to_string(kMaxSwitchCaseSelectors),
+                 s->source);
+        return false;
+    }
+
     auto* cond_ty = sem_.TypeOf(s->condition);
     if (!cond_ty->is_integer_scalar()) {
         AddError("switch statement selector expression must be of a scalar integer type",
@@ -2525,12 +2544,12 @@
         auto diag_added = diagnostics.Add(dc->rule_name->symbol, dc);
         if (!diag_added && (*diag_added.value)->severity != dc->severity) {
             {
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "conflicting diagnostic " << use;
                 AddError(ss.str(), dc->rule_name->source);
             }
             {
-                std::ostringstream ss;
+                utils::StringStream ss;
                 ss << "severity of '" << symbols_.NameFor(dc->rule_name->symbol) << "' set to '"
                    << dc->severity << "' here";
                 AddNote(ss.str(), (*diag_added.value)->rule_name->source);
diff --git a/src/tint/resolver/value_constructor_validation_test.cc b/src/tint/resolver/value_constructor_validation_test.cc
index 8ab1cab..21603fe 100644
--- a/src/tint/resolver/value_constructor_validation_test.cc
+++ b/src/tint/resolver/value_constructor_validation_test.cc
@@ -17,6 +17,7 @@
 #include "src/tint/sem/value_constructor.h"
 #include "src/tint/sem/value_conversion.h"
 #include "src/tint/type/reference.h"
+#include "src/tint/utils/string_stream.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -354,7 +355,7 @@
     auto rhs_type = params.rhs_type(*this);
     auto* rhs_value_expr = params.rhs_value_expr(*this, 0);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << FriendlyName(lhs_type1) << " = " << FriendlyName(lhs_type2) << "("
        << FriendlyName(rhs_type) << "(<rhs value expr>))";
     SCOPED_TRACE(ss.str());
@@ -447,7 +448,7 @@
     auto rhs_type = rhs_params.ast(*this);
     auto* rhs_value_expr = rhs_params.expr_from_double(*this, 0);
 
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << FriendlyName(lhs_type1) << " = " << FriendlyName(lhs_type2) << "("
        << FriendlyName(rhs_type) << "(<rhs value expr>))";
     SCOPED_TRACE(ss.str());
@@ -2414,7 +2415,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns - 1; i++) {
         ast::Type vec_type = param.create_column_ast_type(*this);
@@ -2443,7 +2444,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns * param.rows - 1; i++) {
         args.Push(Call(param.create_element_ast_type(*this)));
@@ -2471,7 +2472,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns + 1; i++) {
         ast::Type vec_type = param.create_column_ast_type(*this);
@@ -2500,7 +2501,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns * param.rows + 1; i++) {
         args.Push(Call(param.create_element_ast_type(*this)));
@@ -2527,7 +2528,7 @@
 
     Enable(builtin::Extension::kF16);
 
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         auto vec_type = ty.vec<u32>(param.rows);
@@ -2555,7 +2556,7 @@
 
     Enable(builtin::Extension::kF16);
 
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         args.Push(Expr(1_u));
@@ -2588,7 +2589,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         ast::Type valid_vec_type = param.create_column_ast_type(*this);
@@ -2626,7 +2627,7 @@
     Enable(builtin::Extension::kF16);
 
     const std::string element_type_name = param.get_element_type_name();
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 8> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         ast::Type valid_vec_type = param.create_column_ast_type(*this);
@@ -2715,7 +2716,7 @@
 
     auto* elem_type_alias = Alias("ElemType", param.create_element_ast_type(*this));
 
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 4> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         auto vec_type = ty.vec(ty.u32(), param.rows);
@@ -2797,7 +2798,7 @@
     ast::Type matrix_type = param.create_mat_ast_type(*this);
     auto* u32_type_alias = Alias("UnsignedInt", ty.u32());
 
-    std::stringstream args_tys;
+    utils::StringStream args_tys;
     utils::Vector<const ast::Expression*, 4> args;
     for (uint32_t i = 0; i < param.columns; i++) {
         auto vec_type = ty.vec(ty.Of(u32_type_alias), param.rows);
@@ -2874,7 +2875,7 @@
 
     Enable(builtin::Extension::kF16);
 
-    std::stringstream err;
+    utils::StringStream err;
     err << "12:34 error: no matching constructor for mat" << param.columns << "x" << param.rows
         << "(";
 
@@ -2905,7 +2906,7 @@
 
     Enable(builtin::Extension::kF16);
 
-    std::stringstream err;
+    utils::StringStream err;
     err << "12:34 error: no matching constructor for mat" << param.columns << "x" << param.rows
         << "(";
 
@@ -3073,7 +3074,7 @@
     auto* tc = Call(ty.Of(s), values);
     WrapInFunction(tc);
 
-    std::stringstream err;
+    utils::StringStream err;
     err << "error: type in structure constructor does not match struct member ";
     err << "type: expected '" << str_params.name() << "', found '" << ctor_params.name() << "'";
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/transform/builtin_polyfill.cc b/src/tint/transform/builtin_polyfill.cc
index 606c6ce..259eb11 100644
--- a/src/tint/transform/builtin_polyfill.cc
+++ b/src/tint/transform/builtin_polyfill.cc
@@ -582,6 +582,33 @@
         return name;
     }
 
+    /// Builds the polyfill function for the `reflect` builtin
+    /// @param ty the parameter and return type for the function
+    /// @return the polyfill function name
+    Symbol reflect(const type::Type* ty) {
+        auto name = b.Symbols().New("tint_reflect");
+
+        // WGSL polyfill function:
+        //      fn tint_reflect(e1 : T, e2 : T) -> T {
+        //          let factor = (-2.0 * dot(e1, e2));
+        //          return (e1 + (factor * e2));
+        //      }
+        // Using -2.0 instead of 2.0 in factor to prevent the optimization that cause wrong result.
+        // See https://crbug.com/tint/1798 for more details.
+        auto body = utils::Vector{
+            b.Decl(b.Let("factor", b.Mul(-2.0_a, b.Call("dot", "e1", "e2")))),
+            b.Return(b.Add("e1", b.Mul("factor", "e2"))),
+        };
+        b.Func(name,
+               utils::Vector{
+                   b.Param("e1", T(ty)),
+                   b.Param("e2", T(ty)),
+               },
+               T(ty), body);
+
+        return name;
+    }
+
     /// Builds the polyfill function for the `saturate` builtin
     /// @param ty the parameter and return type for the function
     /// @return the polyfill function name
@@ -1007,6 +1034,18 @@
                                 builtin, [&] { return s.insertBits(builtin->ReturnType()); });
                         }
                         break;
+                    case sem::BuiltinType::kReflect:
+                        // Only polyfill for vec2<f32>. See https://crbug.com/tint/1798 for more
+                        // details.
+                        if (polyfill.reflect_vec2_f32) {
+                            auto& sig = builtin->Signature();
+                            auto* vec = sig.return_type->As<type::Vector>();
+                            if (vec && vec->Width() == 2 && vec->type()->Is<type::F32>()) {
+                                fn = builtin_polyfills.GetOrCreate(
+                                    builtin, [&] { return s.reflect(builtin->ReturnType()); });
+                            }
+                        }
+                        break;
                     case sem::BuiltinType::kSaturate:
                         if (polyfill.saturate) {
                             fn = builtin_polyfills.GetOrCreate(
diff --git a/src/tint/transform/builtin_polyfill.h b/src/tint/transform/builtin_polyfill.h
index 521940c..b070248 100644
--- a/src/tint/transform/builtin_polyfill.h
+++ b/src/tint/transform/builtin_polyfill.h
@@ -70,6 +70,8 @@
         bool int_div_mod = false;
         /// Should float modulos be polyfilled to emit a precise modulo operation as per the spec?
         bool precise_float_mod = false;
+        /// Should `reflect()` be polyfilled for vec2<f32>?
+        bool reflect_vec2_f32 = false;
         /// Should `saturate()` be polyfilled?
         bool saturate = false;
         /// Should `sign()` be polyfilled for integer types?
diff --git a/src/tint/transform/builtin_polyfill_test.cc b/src/tint/transform/builtin_polyfill_test.cc
index 09d17b8..8c0d787 100644
--- a/src/tint/transform/builtin_polyfill_test.cc
+++ b/src/tint/transform/builtin_polyfill_test.cc
@@ -3045,6 +3045,177 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+// reflect for vec2<f32>
+////////////////////////////////////////////////////////////////////////////////
+DataMap polyfillReflectVec2F32() {
+    BuiltinPolyfill::Builtins builtins;
+    builtins.reflect_vec2_f32 = true;
+    DataMap data;
+    data.Add<BuiltinPolyfill::Config>(builtins);
+    return data;
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec2_f32) {
+    auto* src = R"(
+fn f() {
+  let e1 = vec2<f32>(1.0f);
+  let e2 = vec2<f32>(1.0f);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_TRUE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec2_f16) {
+    auto* src = R"(
+enable f16;
+
+fn f() {
+  let e1 = vec2<f16>(1.0h);
+  let e2 = vec2<f16>(1.0h);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec3_f32) {
+    auto* src = R"(
+fn f() {
+  let e1 = vec3<f32>(1.0f);
+  let e2 = vec3<f32>(1.0f);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec3_f16) {
+    auto* src = R"(
+enable f16;
+
+fn f() {
+  let e1 = vec3<f16>(1.0h);
+  let e2 = vec3<f16>(1.0h);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec4_f32) {
+    auto* src = R"(
+fn f() {
+  let e1 = vec3<f32>(1.0f);
+  let e2 = vec3<f32>(1.0f);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, ShouldRunReflect_vec4_f16) {
+    auto* src = R"(
+enable f16;
+
+fn f() {
+  let e1 = vec3<f16>(1.0h);
+  let e2 = vec3<f16>(1.0h);
+  let x = reflect(e1, e2);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src));
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, Reflect_ConstantExpression) {
+    auto* src = R"(
+fn f() {
+  let r : vec2<f32> = reflect(vec2<f32>(1.0), vec2<f32>(1.0));
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<BuiltinPolyfill>(src, polyfillReflectVec2F32()));
+}
+
+TEST_F(BuiltinPolyfillTest, Reflect_vec2_f32) {
+    auto* src = R"(
+fn f() {
+  let v = 0.5f;
+  let r : vec2<f32> = reflect(vec2<f32>(v), vec2<f32>(v));
+}
+)";
+
+    auto* expect = R"(
+fn tint_reflect(e1 : vec2<f32>, e2 : vec2<f32>) -> vec2<f32> {
+  let factor = (-2.0 * dot(e1, e2));
+  return (e1 + (factor * e2));
+}
+
+fn f() {
+  let v = 0.5f;
+  let r : vec2<f32> = tint_reflect(vec2<f32>(v), vec2<f32>(v));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillReflectVec2F32());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(BuiltinPolyfillTest, Reflect_multiple_types) {
+    auto* src = R"(
+enable f16;
+
+fn f() {
+  let in_f32 = 0.5f;
+  let out_f32_vec2 : vec2<f32> = reflect(vec2<f32>(in_f32), vec2<f32>(in_f32));
+  let out_f32_vec3 : vec3<f32> = reflect(vec3<f32>(in_f32), vec3<f32>(in_f32));
+  let out_f32_vec4 : vec4<f32> = reflect(vec4<f32>(in_f32), vec4<f32>(in_f32));
+  let in_f16 = 0.5h;
+  let out_f16_vec2 : vec2<f16> = reflect(vec2<f16>(in_f16), vec2<f16>(in_f16));
+  let out_f16_vec3 : vec3<f16> = reflect(vec3<f16>(in_f16), vec3<f16>(in_f16));
+  let out_f16_vec4 : vec4<f16> = reflect(vec4<f16>(in_f16), vec4<f16>(in_f16));
+}
+)";
+
+    auto* expect = R"(
+enable f16;
+
+fn tint_reflect(e1 : vec2<f32>, e2 : vec2<f32>) -> vec2<f32> {
+  let factor = (-2.0 * dot(e1, e2));
+  return (e1 + (factor * e2));
+}
+
+fn f() {
+  let in_f32 = 0.5f;
+  let out_f32_vec2 : vec2<f32> = tint_reflect(vec2<f32>(in_f32), vec2<f32>(in_f32));
+  let out_f32_vec3 : vec3<f32> = reflect(vec3<f32>(in_f32), vec3<f32>(in_f32));
+  let out_f32_vec4 : vec4<f32> = reflect(vec4<f32>(in_f32), vec4<f32>(in_f32));
+  let in_f16 = 0.5h;
+  let out_f16_vec2 : vec2<f16> = reflect(vec2<f16>(in_f16), vec2<f16>(in_f16));
+  let out_f16_vec3 : vec3<f16> = reflect(vec3<f16>(in_f16), vec3<f16>(in_f16));
+  let out_f16_vec4 : vec4<f16> = reflect(vec4<f16>(in_f16), vec4<f16>(in_f16));
+}
+)";
+
+    auto got = Run<BuiltinPolyfill>(src, polyfillReflectVec2F32());
+
+    EXPECT_EQ(expect, str(got));
+}
+
+////////////////////////////////////////////////////////////////////////////////
 // saturate
 ////////////////////////////////////////////////////////////////////////////////
 DataMap polyfillSaturate() {
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index 28ecebd..0030ca1 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -36,6 +36,7 @@
 #include "src/tint/utils/block_allocator.h"
 #include "src/tint/utils/hash.h"
 #include "src/tint/utils/map.h"
+#include "src/tint/utils/string_stream.h"
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -695,7 +696,7 @@
     : Base(pid, nid), op(o), type(ty), address_space(as), buffer(buf) {}
 DecomposeMemoryAccess::Intrinsic::~Intrinsic() = default;
 std::string DecomposeMemoryAccess::Intrinsic::InternalName() const {
-    std::stringstream ss;
+    utils::StringStream ss;
     switch (op) {
         case Op::kLoad:
             ss << "intrinsic_load_";
diff --git a/src/tint/transform/direct_variable_access.cc b/src/tint/transform/direct_variable_access.cc
index a6ef9d8..00d1e67 100644
--- a/src/tint/transform/direct_variable_access.cc
+++ b/src/tint/transform/direct_variable_access.cc
@@ -32,6 +32,7 @@
 #include "src/tint/type/abstract_int.h"
 #include "src/tint/utils/reverse.h"
 #include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::DirectVariableAccess);
 TINT_INSTANTIATE_TYPEINFO(tint::transform::DirectVariableAccess::Config);
@@ -687,7 +688,7 @@
                 // Build an appropriate variant function name.
                 // This is derived from the original function name and the pointer parameter
                 // chains.
-                std::stringstream ss;
+                utils::StringStream ss;
                 ss << ctx.src->Symbols().NameFor(target->Declaration()->name->symbol);
                 for (auto* param : target->Parameters()) {
                     if (auto indices = target_signature.Find(param)) {
@@ -1080,7 +1081,7 @@
 
     /// @returns a name describing the given shape
     std::string AccessShapeName(const AccessShape& shape) {
-        std::stringstream ss;
+        utils::StringStream ss;
 
         if (IsPrivateOrFunction(shape.root.address_space)) {
             ss << "F";
diff --git a/src/tint/transform/packed_vec3.cc b/src/tint/transform/packed_vec3.cc
index 6337197..956adcb 100644
--- a/src/tint/transform/packed_vec3.cc
+++ b/src/tint/transform/packed_vec3.cc
@@ -18,16 +18,23 @@
 #include <string>
 #include <utility>
 
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/builtin/builtin.h"
 #include "src/tint/program_builder.h"
+#include "src/tint/sem/array_count.h"
 #include "src/tint/sem/index_accessor_expression.h"
-#include "src/tint/sem/member_accessor_expression.h"
+#include "src/tint/sem/load.h"
 #include "src/tint/sem/statement.h"
+#include "src/tint/sem/type_expression.h"
 #include "src/tint/sem/variable.h"
+#include "src/tint/type/array.h"
+#include "src/tint/type/reference.h"
+#include "src/tint/type/vector.h"
 #include "src/tint/utils/hashmap.h"
 #include "src/tint/utils/hashset.h"
+#include "src/tint/utils/vector.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::PackedVec3);
-TINT_INSTANTIATE_TYPEINFO(tint::transform::PackedVec3::Attribute);
 
 using namespace tint::number_suffixes;  // NOLINT
 
@@ -39,105 +46,447 @@
     /// @param program the source program
     explicit State(const Program* program) : src(program) {}
 
+    /// The name of the struct member used when wrapping packed vec3 types.
+    static constexpr const char* kStructMemberName = "elements";
+
+    /// The names of the structures used to wrap packed vec3 types.
+    utils::Hashmap<const type::Type*, Symbol, 4> packed_vec3_wrapper_struct_names;
+
+    /// A cache of host-shareable structures that have been rewritten.
+    utils::Hashmap<const type::Type*, Symbol, 4> rewritten_structs;
+
+    /// A map from type to the name of a helper function used to pack that type.
+    utils::Hashmap<const type::Type*, Symbol, 4> pack_helpers;
+
+    /// A map from type to the name of a helper function used to unpack that type.
+    utils::Hashmap<const type::Type*, Symbol, 4> unpack_helpers;
+
+    /// @param ty the type to test
+    /// @returns true if `ty` is a vec3, false otherwise
+    bool IsVec3(const type::Type* ty) {
+        if (auto* vec = ty->As<type::Vector>()) {
+            if (vec->Width() == 3) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /// @param ty the type to test
+    /// @returns true if `ty` is or contains a vec3, false otherwise
+    bool ContainsVec3(const type::Type* ty) {
+        return Switch(
+            ty,  //
+            [&](const type::Vector* vec) { return IsVec3(vec); },
+            [&](const type::Matrix* mat) { return ContainsVec3(mat->ColumnType()); },
+            [&](const type::Array* arr) { return ContainsVec3(arr->ElemType()); },
+            [&](const type::Struct* str) {
+                for (auto* member : str->Members()) {
+                    if (ContainsVec3(member->Type())) {
+                        return true;
+                    }
+                }
+                return false;
+            });
+    }
+
+    /// Create a `__packed_vec3` type with the same element type as `ty`.
+    /// @param ty a three-element vector type
+    /// @returns the new AST type
+    ast::Type MakePackedVec3(const type::Type* ty) {
+        auto* vec = ty->As<type::Vector>();
+        TINT_ASSERT(Transform, vec != nullptr && vec->Width() == 3);
+        return b.ty(builtin::Builtin::kPackedVec3, CreateASTTypeFor(ctx, vec->type()));
+    }
+
+    /// Recursively rewrite a type using `__packed_vec3`, if needed.
+    /// When used as an array element type, the `__packed_vec3` type will be wrapped in a structure
+    /// and given an `@align()` attribute to give it alignment it needs to yield the correct array
+    /// element stride. For vec3 types used in structures directly, the `@align()` attribute is
+    /// placed on the containing structure instead. Matrices with three rows become arrays of
+    /// columns, and used the aligned wrapper struct for the column type.
+    /// @param ty the type to rewrite
+    /// @param array_element `true` if this is being called for the element of an array
+    /// @returns the new AST type, or nullptr if rewriting was not necessary
+    ast::Type RewriteType(const type::Type* ty, bool array_element = false) {
+        return Switch(
+            ty,
+            [&](const type::Vector* vec) -> ast::Type {
+                if (IsVec3(vec)) {
+                    if (array_element) {
+                        // Create a struct with a single `__packed_vec3` member.
+                        // Give the struct member the same alignment as the original unpacked vec3
+                        // type, to avoid changing the array element stride.
+                        return b.ty(packed_vec3_wrapper_struct_names.GetOrCreate(vec, [&]() {
+                            auto name = b.Symbols().New(
+                                "tint_packed_vec3_" + vec->type()->FriendlyName(src->Symbols()) +
+                                (array_element ? "_array_element" : "_struct_member"));
+                            auto* member =
+                                b.Member(kStructMemberName, MakePackedVec3(vec),
+                                         utils::Vector{b.MemberAlign(AInt(vec->Align()))});
+                            b.Structure(b.Ident(name), utils::Vector{member}, utils::Empty);
+                            return name;
+                        }));
+                    } else {
+                        return MakePackedVec3(vec);
+                    }
+                }
+                return {};
+            },
+            [&](const type::Matrix* mat) -> ast::Type {
+                // Rewrite the matrix as an array of columns that use the aligned wrapper struct.
+                auto new_col_type = RewriteType(mat->ColumnType(), /* array_element */ true);
+                if (new_col_type) {
+                    return b.ty.array(new_col_type, u32(mat->columns()));
+                }
+                return {};
+            },
+            [&](const type::Array* arr) -> ast::Type {
+                // Rewrite the array with the modified element type.
+                auto new_type = RewriteType(arr->ElemType(), /* array_element */ true);
+                if (new_type) {
+                    utils::Vector<const ast::Attribute*, 1> attrs;
+                    if (arr->Count()->Is<type::RuntimeArrayCount>()) {
+                        return b.ty.array(new_type, std::move(attrs));
+                    } else if (auto count = arr->ConstantCount()) {
+                        return b.ty.array(new_type, u32(count.value()), std::move(attrs));
+                    } else {
+                        TINT_ICE(Transform, b.Diagnostics())
+                            << type::Array::kErrExpectedConstantCount;
+                        return {};
+                    }
+                }
+                return {};
+            },
+            [&](const sem::Struct* str) -> ast::Type {
+                if (ContainsVec3(str)) {
+                    auto name = rewritten_structs.GetOrCreate(str, [&]() {
+                        utils::Vector<const ast::StructMember*, 4> members;
+                        for (auto* member : str->Members()) {
+                            // If the member type contains a vec3, rewrite it.
+                            auto new_type = RewriteType(member->Type());
+                            if (new_type) {
+                                // Copy the member attributes.
+                                bool needs_align = true;
+                                utils::Vector<const ast::Attribute*, 4> attributes;
+                                for (auto* attr : member->Declaration()->attributes) {
+                                    if (attr->IsAnyOf<ast::StructMemberAlignAttribute,
+                                                      ast::StructMemberOffsetAttribute>()) {
+                                        needs_align = false;
+                                    }
+                                    attributes.Push(ctx.Clone(attr));
+                                }
+                                // If the alignment wasn't already specified, add an attribute to
+                                // make sure that we don't alter the alignment when using the packed
+                                // vector type.
+                                if (needs_align) {
+                                    attributes.Push(b.MemberAlign(AInt(member->Align())));
+                                }
+                                members.Push(b.Member(ctx.Clone(member->Name()), new_type,
+                                                      std::move(attributes)));
+                            } else {
+                                // No vec3s, just clone the member as is.
+                                members.Push(ctx.Clone(member->Declaration()));
+                            }
+                        }
+                        // Create the new structure.
+                        auto struct_name = b.Symbols().New(
+                            src->Symbols().NameFor(str->Declaration()->name->symbol) +
+                            "_tint_packed_vec3");
+                        b.Structure(struct_name, std::move(members));
+                        return struct_name;
+                    });
+                    return b.ty(name);
+                }
+                return {};
+            });
+    }
+
+    /// Create a helper function to recursively pack or unpack a composite that contains vec3 types.
+    /// @param name_prefix the name of the helper function
+    /// @param ty the composite type to pack or unpack
+    /// @param pack_or_unpack_element a function that packs or unpacks an element with a given type
+    /// @param in_type a function that create an AST type for the input type
+    /// @param out_type a function that create an AST type for the output type
+    /// @returns the name of the helper function
+    Symbol MakePackUnpackHelper(
+        const char* name_prefix,
+        const type::Type* ty,
+        const std::function<const ast::Expression*(const ast::Expression*, const type::Type*)>&
+            pack_or_unpack_element,
+        const std::function<ast::Type()>& in_type,
+        const std::function<ast::Type()>& out_type) {
+        // Allocate a variable to hold the return value of the function.
+        utils::Vector<const ast::Statement*, 4> statements;
+        statements.Push(b.Decl(b.Var("result", out_type())));
+
+        // Helper that generates a loop to copy and pack/unpack elements of an array to the result:
+        //   for (var i = 0u; i < num_elements; i = i + 1) {
+        //     result[i] = pack_or_unpack_element(in[i]);
+        //   }
+        auto copy_array_elements = [&](uint32_t num_elements, const type::Type* element_type) {
+            // Generate an expression for packing or unpacking an element of the array.
+            auto* element = pack_or_unpack_element(b.IndexAccessor("in", "i"), element_type);
+            statements.Push(b.For(                   //
+                b.Decl(b.Var("i", b.ty.u32())),      //
+                b.LessThan("i", u32(num_elements)),  //
+                b.Assign("i", b.Add("i", 1_a)),      //
+                b.Block(utils::Vector{
+                    b.Assign(b.IndexAccessor("result", "i"), element),
+                })));
+        };
+
+        // Copy the elements of the value over to the result.
+        Switch(
+            ty,
+            [&](const type::Array* arr) {
+                TINT_ASSERT(Transform, arr->ConstantCount());
+                copy_array_elements(arr->ConstantCount().value(), arr->ElemType());
+            },
+            [&](const type::Matrix* mat) {
+                copy_array_elements(mat->columns(), mat->ColumnType());
+            },
+            [&](const sem::Struct* str) {
+                // Copy the struct members over one at a time, packing/unpacking as necessary.
+                for (auto* member : str->Members()) {
+                    const ast::Expression* element =
+                        b.MemberAccessor("in", b.Ident(ctx.Clone(member->Name())));
+                    if (ContainsVec3(member->Type())) {
+                        element = pack_or_unpack_element(element, member->Type());
+                    }
+                    statements.Push(b.Assign(
+                        b.MemberAccessor("result", b.Ident(ctx.Clone(member->Name()))), element));
+                }
+            });
+
+        // Return the result.
+        statements.Push(b.Return("result"));
+
+        // Create the function and return its name.
+        auto name = b.Symbols().New(name_prefix);
+        b.Func(name, utils::Vector{b.Param("in", in_type())}, out_type(), std::move(statements));
+        return name;
+    }
+
+    /// Unpack the composite value `expr` to the unpacked type `ty`. If `ty` is a matrix, this will
+    /// produce a regular matNx3 value from an array of packed column vectors.
+    /// @param expr the composite value expression to unpack
+    /// @param ty the unpacked type
+    /// @returns an expression that holds the unpacked value
+    const ast::Expression* UnpackComposite(const ast::Expression* expr, const type::Type* ty) {
+        auto helper = unpack_helpers.GetOrCreate(ty, [&]() {
+            return MakePackUnpackHelper(
+                "tint_unpack_vec3_in_composite", ty,
+                [&](const ast::Expression* element,
+                    const type::Type* element_type) -> const ast::Expression* {
+                    if (element_type->Is<type::Vector>()) {
+                        // Unpack a `__packed_vec3` by casting it to a regular vec3.
+                        // If it is an array element, extract the vector from the wrapper struct.
+                        if (element->Is<ast::IndexAccessorExpression>()) {
+                            element = b.MemberAccessor(element, kStructMemberName);
+                        }
+                        return b.Call(CreateASTTypeFor(ctx, element_type), element);
+                    } else {
+                        return UnpackComposite(element, element_type);
+                    }
+                },
+                [&]() { return RewriteType(ty); },  //
+                [&]() { return CreateASTTypeFor(ctx, ty); });
+        });
+        return b.Call(helper, expr);
+    }
+
+    /// Pack the composite value `expr` from the unpacked type `ty`. If `ty` is a matrix, this will
+    /// produce an array of packed column vectors.
+    /// @param expr the composite value expression to pack
+    /// @param ty the unpacked type
+    /// @returns an expression that holds the packed value
+    const ast::Expression* PackComposite(const ast::Expression* expr, const type::Type* ty) {
+        auto helper = pack_helpers.GetOrCreate(ty, [&]() {
+            return MakePackUnpackHelper(
+                "tint_pack_vec3_in_composite", ty,
+                [&](const ast::Expression* element,
+                    const type::Type* element_type) -> const ast::Expression* {
+                    if (element_type->Is<type::Vector>()) {
+                        // Pack a vector element by casting it to a packed_vec3.
+                        // If it is an array element, construct a wrapper struct.
+                        auto* packed = b.Call(MakePackedVec3(element_type), element);
+                        if (element->Is<ast::IndexAccessorExpression>()) {
+                            packed = b.Call(RewriteType(element_type, true), packed);
+                        }
+                        return packed;
+                    } else {
+                        return PackComposite(element, element_type);
+                    }
+                },
+                [&]() { return CreateASTTypeFor(ctx, ty); },  //
+                [&]() { return RewriteType(ty); });
+        });
+        return b.Call(helper, expr);
+    }
+
+    /// @returns true if there are host-shareable vec3's that need transforming
+    bool ShouldRun() {
+        // Check for vec3s in the types of all uniform and storage buffer variables to determine
+        // if the transform is necessary.
+        for (auto* decl : src->AST().GlobalVariables()) {
+            auto* var = sem.Get<sem::GlobalVariable>(decl);
+            if (var && builtin::IsHostShareable(var->AddressSpace()) &&
+                ContainsVec3(var->Type()->UnwrapRef())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /// Runs the transform
     /// @returns the new program or SkipTransform if the transform is not required
     ApplyResult Run() {
-        // Packed vec3<T> struct members
-        utils::Hashset<const sem::StructMember*, 8> members;
-
-        // Find all the packed vector struct members, and apply the @internal(packed_vector)
-        // attribute.
-        for (auto* decl : ctx.src->AST().GlobalDeclarations()) {
-            if (auto* str = sem.Get<sem::Struct>(decl)) {
-                if (str->IsHostShareable()) {
-                    for (auto* member : str->Members()) {
-                        if (auto* vec = member->Type()->As<type::Vector>()) {
-                            if (vec->Width() == 3) {
-                                members.Add(member);
-
-                                // Apply the PackedVec3::Attribute to the member
-                                ctx.InsertFront(
-                                    member->Declaration()->attributes,
-                                    b.ASTNodes().Create<Attribute>(b.ID(), b.AllocateNodeID()));
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-        if (members.IsEmpty()) {
+        if (!ShouldRun()) {
             return SkipTransform;
         }
 
-        // Walk the nodes, starting with the most deeply nested, finding all the AST expressions
-        // that load a whole packed vector (not a scalar / swizzle of the vector).
-        utils::Hashset<const sem::ValueExpression*, 16> refs;
+        // Changing the types of certain structure members can trigger stricter layout validation
+        // rules for the uniform address space. In particular, replacing 16-bit matrices with arrays
+        // violates the requirement that the array element stride is a multiple of 16 bytes, and
+        // replacing vec3s with a structure violates the requirement that there must be at least 16
+        // bytes from the start of a structure to the start of the next member.
+        // Disable these validation rules using an internal extension, as MSL does not have these
+        // restrictions.
+        b.Enable(builtin::Extension::kChromiumInternalRelaxedUniformLayout);
+
+        // Track expressions that need to be packed or unpacked.
+        utils::Hashset<const sem::ValueExpression*, 8> to_pack;
+        utils::Hashset<const sem::ValueExpression*, 8> to_unpack;
+
+        // Replace vec3 types in host-shareable address spaces with `__packed_vec3` types, and
+        // collect expressions that need to be converted to or from values that use the
+        // `__packed_vec3` type.
         for (auto* node : ctx.src->ASTNodes().Objects()) {
-            auto* sem_node = sem.Get(node);
-            if (sem_node) {
-                if (auto* expr = sem_node->As<sem::ValueExpression>()) {
-                    sem_node = expr->UnwrapLoad();
-                }
-            }
             Switch(
-                sem_node,  //
-                [&](const sem::StructMemberAccess* access) {
-                    if (members.Contains(access->Member())) {
-                        // Access to a packed vector member. Seed the expression tracking.
-                        refs.Add(access);
-                    }
-                },
-                [&](const sem::IndexAccessorExpression* access) {
-                    // Not loading a whole packed vector. Ignore.
-                    refs.Remove(access->Object()->UnwrapLoad());
-                },
-                [&](const sem::Swizzle* access) {
-                    // Not loading a whole packed vector. Ignore.
-                    refs.Remove(access->Object()->UnwrapLoad());
-                },
-                [&](const sem::VariableUser* user) {
-                    auto* v = user->Variable();
-                    if (v->Declaration()->Is<ast::Let>() &&  // if variable is let...
-                        v->Type()->Is<type::Pointer>() &&    // and let is a pointer...
-                        refs.Contains(v->Initializer())) {   // and pointer is to a packed vector...
-                        refs.Add(user);  // then propagate tracking to pointer usage
-                    }
-                },
-                [&](const sem::ValueExpression* expr) {
-                    if (auto* unary = expr->Declaration()->As<ast::UnaryOpExpression>()) {
-                        if (unary->op == ast::UnaryOp::kAddressOf ||
-                            unary->op == ast::UnaryOp::kIndirection) {
-                            // Memory access on the packed vector. Track these.
-                            auto* inner = sem.GetVal(unary->expr);
-                            if (refs.Remove(inner)) {
-                                refs.Add(expr);
-                            }
+                sem.Get(node),
+                [&](const sem::TypeExpression* type) {
+                    // Rewrite pointers to types that contain vec3s.
+                    auto* ptr = type->Type()->As<type::Pointer>();
+                    if (ptr && builtin::IsHostShareable(ptr->AddressSpace())) {
+                        auto new_store_type = RewriteType(ptr->StoreType());
+                        if (new_store_type) {
+                            auto access = ptr->AddressSpace() == builtin::AddressSpace::kStorage
+                                              ? ptr->Access()
+                                              : builtin::Access::kUndefined;
+                            auto new_ptr_type =
+                                b.ty.pointer(new_store_type, ptr->AddressSpace(), access);
+                            ctx.Replace(node, new_ptr_type.expr);
                         }
-                        // Note: non-memory ops (e.g. '-') are ignored, leaving any tracked
-                        // reference at the inner expression, so we'd cast, then apply the unary op.
                     }
                 },
-                [&](const sem::Statement* e) {
-                    if (auto* assign = e->Declaration()->As<ast::AssignmentStatement>()) {
-                        // We don't want to cast packed_vectors if they're being assigned to.
-                        refs.Remove(sem.GetVal(assign->lhs));
+                [&](const sem::Variable* var) {
+                    if (!builtin::IsHostShareable(var->AddressSpace())) {
+                        return;
+                    }
+
+                    // Rewrite the var type, if it contains vec3s.
+                    auto new_store_type = RewriteType(var->Type()->UnwrapRef());
+                    if (new_store_type) {
+                        ctx.Replace(var->Declaration()->type.expr, new_store_type.expr);
+                    }
+                },
+                [&](const sem::Statement* stmt) {
+                    // Pack the RHS of assignment statements that are writing to packed types.
+                    if (auto* assign = stmt->Declaration()->As<ast::AssignmentStatement>()) {
+                        auto* lhs = sem.GetVal(assign->lhs);
+                        auto* rhs = sem.GetVal(assign->rhs);
+                        if (!ContainsVec3(rhs->Type()) ||
+                            !builtin::IsHostShareable(
+                                lhs->Type()->As<type::Reference>()->AddressSpace())) {
+                            // Skip assignments to address spaces that are not host-shareable, or
+                            // that do not contain vec3 types.
+                            return;
+                        }
+
+                        // Pack the RHS expression.
+                        if (to_unpack.Contains(rhs)) {
+                            // The expression will already be packed, so skip the pending unpack.
+                            to_unpack.Remove(rhs);
+
+                            // If the expression produces a vec3 from an array element, extract
+                            // the packed vector from the wrapper struct.
+                            if (IsVec3(rhs->Type()) &&
+                                rhs->UnwrapLoad()->Is<sem::IndexAccessorExpression>()) {
+                                ctx.Replace(rhs->Declaration(),
+                                            b.MemberAccessor(ctx.Clone(rhs->Declaration()),
+                                                             kStructMemberName));
+                            }
+                        } else if (rhs) {
+                            to_pack.Add(rhs);
+                        }
+                    }
+                },
+                [&](const sem::Load* load) {
+                    // Unpack loads of types that contain vec3s in host-shareable address spaces.
+                    if (ContainsVec3(load->Type()) &&
+                        builtin::IsHostShareable(load->ReferenceType()->AddressSpace())) {
+                        to_unpack.Add(load);
+                    }
+                },
+                [&](const sem::IndexAccessorExpression* accessor) {
+                    // If the expression produces a reference to a vec3 in a host-shareable address
+                    // space from an array element, extract the packed vector from the wrapper
+                    // struct.
+                    if (auto* ref = accessor->Type()->As<type::Reference>()) {
+                        if (IsVec3(ref->StoreType()) &&
+                            builtin::IsHostShareable(ref->AddressSpace())) {
+                            ctx.Replace(node, b.MemberAccessor(ctx.Clone(accessor->Declaration()),
+                                                               kStructMemberName));
+                        }
                     }
                 });
         }
 
-        // Wrap the load expressions with a cast to the unpacked type.
-        utils::Hashmap<const type::Vector*, Symbol, 3> unpack_fns;
-        for (auto* ref : refs) {
-            // ref is either a packed vec3 that needs casting, or a pointer to a vec3 which we just
-            // leave alone.
-            if (auto* vec_ty = ref->Type()->UnwrapRef()->As<type::Vector>()) {
-                auto* expr = ref->Declaration();
-                ctx.Replace(expr, [this, vec_ty, expr] {  //
-                    auto* packed = ctx.CloneWithoutTransform(expr);
-                    return b.Call(CreateASTTypeFor(ctx, vec_ty), packed);
-                });
+        // Sort the pending pack/unpack operations by AST node ID to make the order deterministic.
+        auto to_unpack_sorted = to_unpack.Vector();
+        auto to_pack_sorted = to_pack.Vector();
+        auto pred = [&](auto* expr_a, auto* expr_b) {
+            return expr_a->Declaration()->node_id < expr_b->Declaration()->node_id;
+        };
+        to_unpack_sorted.Sort(pred);
+        to_pack_sorted.Sort(pred);
+
+        // Apply all of the pending unpack operations that we have collected.
+        for (auto* expr : to_unpack_sorted) {
+            TINT_ASSERT(Transform, ContainsVec3(expr->Type()));
+            auto* packed = ctx.Clone(expr->Declaration());
+            const ast::Expression* unpacked = nullptr;
+            if (IsVec3(expr->Type())) {
+                if (expr->UnwrapLoad()->Is<sem::IndexAccessorExpression>()) {
+                    // If we are unpacking a vec3 from an array element, extract the vector from the
+                    // wrapper struct.
+                    packed = b.MemberAccessor(packed, kStructMemberName);
+                }
+                // Cast the packed vector to a regular vec3.
+                unpacked = b.Call(CreateASTTypeFor(ctx, expr->Type()), packed);
+            } else {
+                // Use a helper function to unpack an array or matrix.
+                unpacked = UnpackComposite(packed, expr->Type());
             }
+            TINT_ASSERT(Transform, unpacked != nullptr);
+            ctx.Replace(expr->Declaration(), unpacked);
+        }
+
+        // Apply all of the pending pack operations that we have collected.
+        for (auto* expr : to_pack_sorted) {
+            TINT_ASSERT(Transform, ContainsVec3(expr->Type()));
+            auto* unpacked = ctx.Clone(expr->Declaration());
+            const ast::Expression* packed = nullptr;
+            if (IsVec3(expr->Type())) {
+                // Cast the regular vec3 to a packed vector type.
+                packed = b.Call(MakePackedVec3(expr->Type()), unpacked);
+            } else {
+                // Use a helper function to pack an array or matrix.
+                packed = PackComposite(unpacked, expr->Type());
+            }
+            TINT_ASSERT(Transform, packed != nullptr);
+            ctx.Replace(expr->Declaration(), packed);
         }
 
         ctx.Clone();
@@ -153,21 +502,8 @@
     CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
     /// Alias to the semantic info in ctx.src
     const sem::Info& sem = ctx.src->Sem();
-    /// Alias to the symbols in ctx.src
-    const SymbolTable& sym = ctx.src->Symbols();
 };
 
-PackedVec3::Attribute::Attribute(ProgramID pid, ast::NodeID nid) : Base(pid, nid) {}
-PackedVec3::Attribute::~Attribute() = default;
-
-const PackedVec3::Attribute* PackedVec3::Attribute::Clone(CloneContext* ctx) const {
-    return ctx->dst->ASTNodes().Create<Attribute>(ctx->dst->ID(), ctx->dst->AllocateNodeID());
-}
-
-std::string PackedVec3::Attribute::InternalName() const {
-    return "packed_vector";
-}
-
 PackedVec3::PackedVec3() = default;
 PackedVec3::~PackedVec3() = default;
 
diff --git a/src/tint/transform/packed_vec3.h b/src/tint/transform/packed_vec3.h
index 0d304fa..89e8286 100644
--- a/src/tint/transform/packed_vec3.h
+++ b/src/tint/transform/packed_vec3.h
@@ -15,42 +15,31 @@
 #ifndef SRC_TINT_TRANSFORM_PACKED_VEC3_H_
 #define SRC_TINT_TRANSFORM_PACKED_VEC3_H_
 
-#include <string>
-
-#include "src/tint/ast/internal_attribute.h"
 #include "src/tint/transform/transform.h"
 
 namespace tint::transform {
 
 /// A transform to be used by the MSL backend which will:
-/// * Apply the `@internal('packed_vector')` attribute (PackedVec3::Attribute) to all host-sharable
-///   structure members that have a vec3<T> type.
+/// * Replace `vec3<T>` types with an internal `__packed_vec3` type when they are used in
+///   host-shareable address spaces.
+/// * Wrap generated `__packed_vec3` types in a structure when they are used in arrays, so that we
+///   ensure that the array has the correct element stride.
+/// * Multi-version structures that contain `vec3<T>` types when they are used in host-shareable
+///   memory, to avoid modifying uses in other address spaces.
+/// * Rewrite matrix types that have three rows into arrays of column vectors.
+/// * Insert calls to helper functions to convert expressions that use these types to or from the
+///   regular vec3 types when accessing host-shareable memory.
 /// * Cast all direct (not sub-accessed) loads of these packed vectors to the 'unpacked' vec3<T>
 ///   type before usage.
 ///
-/// This transform papers over overload holes in the MSL standard library where an MSL
-/// `packed_vector` type cannot be interchangable used as a regular `vec` type.
+/// This transform is necessary in order to emit vec3 types with the correct size (so that scalars
+/// can follow them in structures), and also to ensure that padding bytes are preserved when writing
+/// to a vec3, an array of vec3 elements, or a matrix with vec3 column type.
+///
+/// @note Depends on the following transforms to have been run first:
+/// * ExpandCompoundAssignment
 class PackedVec3 final : public Castable<PackedVec3, Transform> {
   public:
-    /// Attribute is the attribute applied to padded vector structure members.
-    class Attribute final : public Castable<Attribute, ast::InternalAttribute> {
-      public:
-        /// Constructor
-        /// @param pid the identifier of the program that owns this node
-        /// @param nid the unique node identifier
-        Attribute(ProgramID pid, ast::NodeID nid);
-        /// Destructor
-        ~Attribute() override;
-
-        /// @returns "packed_vector".
-        std::string InternalName() const override;
-
-        /// Performs a deep clone of this object using the CloneContext `ctx`.
-        /// @param ctx the clone context
-        /// @return the newly cloned object
-        const Attribute* Clone(CloneContext* ctx) const override;
-    };
-
     /// Constructor
     PackedVec3();
     /// Destructor
diff --git a/src/tint/transform/packed_vec3_test.cc b/src/tint/transform/packed_vec3_test.cc
index 0f5c92e..670bc05 100644
--- a/src/tint/transform/packed_vec3_test.cc
+++ b/src/tint/transform/packed_vec3_test.cc
@@ -18,7 +18,12 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/ast/module.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/struct.h"
+#include "src/tint/sem/variable.h"
 #include "src/tint/transform/test_helper.h"
+#include "src/tint/type/array.h"
 #include "src/tint/utils/string.h"
 
 namespace tint::transform {
@@ -32,14 +37,29 @@
     EXPECT_FALSE(ShouldRun<PackedVec3>(src));
 }
 
-TEST_F(PackedVec3Test, ShouldRun_NonHostSharableStruct) {
+TEST_F(PackedVec3Test, ShouldRun_NoHostShareableVec3s) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
+  m : mat3x3<f32>,
+  a : array<vec3<f32>, 4>,
 }
 
+var<private> p_s : S;
+var<private> p_v : vec3<f32>;
+var<private> p_m : mat3x3<f32>;
+var<private> p_a : array<vec3<f32>, 4>;
+
+var<workgroup> w_s : S;
+var<workgroup> w_v : vec3<f32>;
+var<workgroup> w_m : mat3x3<f32>;
+var<workgroup> w_a : array<vec3<f32>, 4>;
+
 fn f() {
-  var v : S; // function address-space - not host sharable
+  var f_s : S;
+  var f_v : vec3<f32>;
+  var f_m : mat3x3<f32>;
+  var f_a : array<vec3<f32>, 4>;
 }
 )";
 
@@ -53,13 +73,85 @@
   v2 : vec2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> P : S; // Host sharable
+@group(0) @binding(0) var<uniform> Ps : S; // Host sharable
+@group(0) @binding(1) var<uniform> Pv4 : vec4<f32>; // Host sharable
+@group(0) @binding(2) var<uniform> Pv2 : vec2<f32>; // Host sharable
 )";
 
     EXPECT_FALSE(ShouldRun<PackedVec3>(src));
 }
 
-TEST_F(PackedVec3Test, ShouldRun_HostSharableStruct) {
+TEST_F(PackedVec3Test, ShouldRun_OtherMatrices) {
+    auto* src = R"(
+struct S {
+  m2x2 : mat2x2<f32>,
+  m2x4 : mat2x4<f32>,
+  m3x2 : mat3x2<f32>,
+  m3x4 : mat3x4<f32>,
+  m4x2 : mat4x2<f32>,
+  m4x4 : mat4x4<f32>,
+}
+
+@group(0) @binding(0) var<uniform> Ps : S; // Host sharable
+@group(0) @binding(1) var<uniform> Pm2x2 : mat2x2<f32>; // Host sharable
+@group(0) @binding(2) var<uniform> Pm2x4 : mat2x4<f32>; // Host sharable
+@group(0) @binding(3) var<uniform> Pm3x2 : mat3x2<f32>; // Host sharable
+@group(0) @binding(4) var<uniform> Pm3x4 : mat3x4<f32>; // Host sharable
+@group(0) @binding(5) var<uniform> Pm4x2 : mat4x2<f32>; // Host sharable
+@group(0) @binding(6) var<uniform> Pm4x4 : mat4x4<f32>; // Host sharable
+)";
+
+    EXPECT_FALSE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_ArrayOfNonVec3) {
+    auto* src = R"(
+struct S {
+  arr_v : array<vec2<f32>, 4>,
+  arr_m : array<mat3x2<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> Ps : S; // Host sharable
+@group(0) @binding(1) var<storage> Parr_v : array<vec2<f32>, 4>; // Host sharable
+@group(0) @binding(2) var<storage> Parr_m : array<mat3x2<f32>, 4>; // Host sharable
+)";
+
+    EXPECT_FALSE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharable_Vec3) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> P : vec3<f32>; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharable_Mat3x3) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> P : mat3x3<f32>; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharable_ArrayOfVec3) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> P : array<vec3<f32>>; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharable_ArrayOfMat3x3) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> P : array<mat3x3<f32>>; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharableStruct_Vec3) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
@@ -71,61 +163,3929 @@
     EXPECT_TRUE(ShouldRun<PackedVec3>(src));
 }
 
-TEST_F(PackedVec3Test, UniformAddressSpace) {
+TEST_F(PackedVec3Test, ShouldRun_HostSharableStruct_Mat3x3) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<uniform> P : S; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharableStruct_ArrayOfVec3) {
+    auto* src = R"(
+struct S {
+  a : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<uniform> P : S; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, ShouldRun_HostSharableStruct_ArrayOfMat3x3) {
+    auto* src = R"(
+struct S {
+  a : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<uniform> P : S; // Host sharable
+)";
+
+    EXPECT_TRUE(ShouldRun<PackedVec3>(src));
+}
+
+TEST_F(PackedVec3Test, Vec3_ReadVector) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> v : vec3<f32>;
+
+fn f() {
+  let x = v;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage> v : __packed_vec3<f32>;
+
+fn f() {
+  let x = vec3<f32>(v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_ReadComponent_MemberAccessChain) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> v : vec3<f32>;
+
+fn f() {
+  let x = v.yz.x;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage> v : __packed_vec3<f32>;
+
+fn f() {
+  let x = vec3<f32>(v).yz.x;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> v : vec3<f32>;
+
+fn f() {
+  let x = v[1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage> v : __packed_vec3<f32>;
+
+fn f() {
+  let x = v[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_WriteVector_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+
+fn f() {
+  v = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+fn f() {
+  v = __packed_vec3<f32>(vec3(1.23));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_WriteVector_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+@group(0) @binding(1) var<uniform> in : vec3<f32>;
+
+fn f() {
+  v = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+@group(0) @binding(1) var<uniform> in : __packed_vec3<f32>;
+
+fn f() {
+  v = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+
+fn f() {
+  v.y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+fn f() {
+  v.y = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+
+fn f() {
+  v[1] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+fn f() {
+  v[1] = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_ReadArray) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  let x = arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(arr);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_ReadVector) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  let x = arr[0];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+fn f() {
+  let x = vec3<f32>(arr[0].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_ReadComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  let x = arr[0].y;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+fn f() {
+  let x = arr[0].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  let x = arr[0][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+fn f() {
+  let x = arr[0].elements[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteArray_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 2>;
+
+fn f() {
+  arr = array(vec3(1.5, 2.5, 3.5), vec3(4.5, 5.5, 6.5));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 2u>) -> array<tint_packed_vec3_f32_array_element, 2u> {
+  var result : array<tint_packed_vec3_f32_array_element, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 2u>;
+
+fn f() {
+  arr = tint_pack_vec3_in_composite(array(vec3(1.5, 2.5, 3.5), vec3(4.5, 5.5, 6.5)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteArray_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 2>;
+@group(0) @binding(1) var<uniform> in : array<vec3<f32>, 2>;
+
+fn f() {
+  arr = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 2u>;
+
+@group(0) @binding(1) var<uniform> in : array<tint_packed_vec3_f32_array_element, 2u>;
+
+fn f() {
+  arr = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteVector_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  arr[0] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteVector_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 4>;
+@group(0) @binding(1) var<uniform> in_arr : array<vec3<f32>, 4>;
+@group(0) @binding(2) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  arr[0] = in_arr[0];
+  arr[1] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(1) var<uniform> in_arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(2) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  arr[0].elements = in_arr[0].elements;
+  arr[1].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  arr[0].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<vec3<f32>, 4>;
+
+fn f() {
+  arr[0][1] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_ReadMatrix) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> m : mat3x3<f32>;
+
+fn f() {
+  let x = m;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(m);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_ReadColumn) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> m : mat3x3<f32>;
+
+fn f() {
+  let x = m[1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  let x = vec3<f32>(m[1].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_ReadComponent_MemberAccessChain) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> m : mat3x3<f32>;
+
+fn f() {
+  let x = m[1].yz.x;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  let x = vec3<f32>(m[1].elements).yz.x;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> m : mat3x3<f32>;
+
+fn f() {
+  let x = m[2][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  let x = m[2].elements[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteMatrix_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+fn f() {
+  m = mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  m = tint_pack_vec3_in_composite(mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteMatrix_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(1) var<uniform> in : mat3x3<f32>;
+
+fn f() {
+  m = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(1) var<uniform> in : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  m = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteColumn_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+fn f() {
+  m[1] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteColumn_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(1) var<uniform> in_mat : mat3x3<f32>;
+@group(0) @binding(1) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  m[0] = in_mat[0];
+  m[1] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(1) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(1) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  m[0].elements = in_mat[0].elements;
+  m[1].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+fn f() {
+  m[1].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Matrix_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+fn f() {
+  m[1][2] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_ReadArray) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  let x = arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_1(arr);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_ReadMatrix) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  let x = arr[0];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(arr[0]);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_ReadColumn) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  let x = arr[0][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+fn f() {
+  let x = vec3<f32>(arr[0][1].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_ReadComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  let x = arr[0][1].y;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+fn f() {
+  let x = arr[0][1].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  let x = arr[0][1][2];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+fn f() {
+  let x = arr[0][1].elements[2];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteArray_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 2>;
+
+fn f() {
+  arr = array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 2u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 2u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+
+fn f() {
+  arr = tint_pack_vec3_in_composite_1(array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteArray_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 2>;
+@group(0) @binding(1) var<uniform> in : array<mat3x3<f32>, 2>;
+
+fn f() {
+  arr = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+
+@group(0) @binding(1) var<uniform> in : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+
+fn f() {
+  arr = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteMatrix_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  arr[0] = mat3x3(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteMatrix_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(1) var<uniform> in_arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(2) var<uniform> in_mat : mat3x3<f32>;
+
+fn f() {
+  arr[0] = in_arr[0];
+  arr[1] = in_mat;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(1) var<uniform> in_arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(2) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  arr[0] = in_arr[0];
+  arr[1] = in_mat;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteVector_ValueRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  arr[0][1] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteVector_RefRHS) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(1) var<uniform> in_arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(2) var<uniform> in_mat : mat3x3<f32>;
+@group(0) @binding(3) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  arr[0][0] = arr[0][1];
+  arr[0][1] = in_mat[2];
+  arr[0][2] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(1) var<uniform> in_arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(2) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(3) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  arr[0][0].elements = arr[0][1].elements;
+  arr[0][1].elements = in_mat[2].elements;
+  arr[0][2].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  arr[0][1].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrix_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr : array<mat3x3<f32>, 4>;
+
+fn f() {
+  arr[0][1][2] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+@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;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_ReadStruct) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
 }
 
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(P);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_ReadVector) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.v;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_ReadComponent_MemberAccessChain) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.v.yz.x;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.v).yz.x;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.v[1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.v[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteStruct_ValueRHS) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P = S(vec3(1.23));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@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)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteStruct_RefRHS) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in : S;
+
+fn f() {
+  P = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in : S_tint_packed_vec3;
+
+fn f() {
+  P = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteVector_ValueRHS) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.v = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.v = __packed_vec3<f32>(vec3(1.23));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteVector_RefRHS) {
+    auto* src = R"(
+struct S {
+  v1 : vec3<f32>,
+  v2 : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  P.v1 = in_str.v1;
+  P.v2 = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v1 : __packed_vec3<f32>,
+  @align(16)
+  v2 : __packed_vec3<f32>,
+}
+
+struct S {
+  v1 : vec3<f32>,
+  v2 : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  P.v1 = in_str.v1;
+  P.v2 = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.v.y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.v.y = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Vec3_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.v[1] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.v[1] = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_ReadStruct) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.arr = tint_unpack_vec3_in_composite(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_1(P);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_ReadArray) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(P.arr);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_ReadVector) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.arr[0].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_ReadComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0].y;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.arr[0].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.arr[0].elements[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteStruct_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P = S(array(vec3(1.5, 4.5, 7.5), vec3(9.5, 6.5, 3.5)));
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 2u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 2u>) -> array<tint_packed_vec3_f32_array_element, 2u> {
+  var result : array<tint_packed_vec3_f32_array_element, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P = tint_pack_vec3_in_composite_1(S(array(vec3(1.5, 4.5, 7.5), vec3(9.5, 6.5, 3.5))));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteStruct_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in : S;
+
+fn f() {
+  P = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 2u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in : S_tint_packed_vec3;
+
+fn f() {
+  P = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteArray_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr = array(vec3(1.5, 4.5, 7.5), vec3(9.5, 6.5, 3.5));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 2u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 2u>) -> array<tint_packed_vec3_f32_array_element, 2u> {
+  var result : array<tint_packed_vec3_f32_array_element, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  arr : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr = tint_pack_vec3_in_composite(array(vec3(1.5, 4.5, 7.5), vec3(9.5, 6.5, 3.5)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteArray_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr1 : array<vec3<f32>, 2>,
+  arr2 : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_arr : array<vec3<f32>, 2>;
+
+fn f() {
+  P.arr1 = in_str.arr1;
+  P.arr2 = in_arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr1 : array<tint_packed_vec3_f32_array_element, 2u>,
+  @align(16)
+  arr2 : array<tint_packed_vec3_f32_array_element, 2u>,
+}
+
+struct S {
+  arr1 : array<vec3<f32>, 2>,
+  arr2 : array<vec3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_arr : array<tint_packed_vec3_f32_array_element, 2u>;
+
+fn f() {
+  P.arr1 = in_str.arr1;
+  P.arr2 = in_arr;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteVector_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteVector_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_arr : array<vec3<f32>, 4>;
+@group(0) @binding(3) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  P.arr[0] = in_str.arr[0];
+  P.arr[1] = in_arr[1];
+  P.arr[2] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(3) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  P.arr[0].elements = in_str.arr[0].elements;
+  P.arr[1].elements = in_arr[1].elements;
+  P.arr[2].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr[0].elements.y = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfVec3_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0][1] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr[0].elements[1] = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_ReadStruct) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_1(P);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_ReadMatrix) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.m;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(P.m);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_ReadColumn) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.m[1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.m[1].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_ReadComponent_MemberAccessChain) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.m[1].yz.x;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.m[1].elements).yz.x;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.m[2][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.m[2].elements[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteStruct_ValueRHS) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P = S(mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.m = tint_pack_vec3_in_composite(in.m);
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P = tint_pack_vec3_in_composite_1(S(mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteStruct_RefRHS) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in : S;
+
+fn f() {
+  P = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in : S_tint_packed_vec3;
+
+fn f() {
+  P = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteMatrix_ValueRHS) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.m = mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.m = tint_pack_vec3_in_composite(mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteMatrix_RefRHS) {
+    auto* src = R"(
+struct S {
+  m1 : mat3x3<f32>,
+  m2 : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_mat : mat3x3<f32>;
+
+fn f() {
+  P.m1 = in_str.m1;
+  P.m2 = in_mat;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m1 : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  m2 : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m1 : mat3x3<f32>,
+  m2 : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  P.m1 = in_str.m1;
+  P.m2 = in_mat;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteColumn_ValueRHS) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.m[1] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteColumn_RefRHS) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_mat : mat3x3<f32>;
+@group(0) @binding(3) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  P.m[0] = in_str.m[0];
+  P.m[1] = in_mat[1];
+  P.m[2] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(3) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  P.m[0].elements = in_str.m[0].elements;
+  P.m[1].elements = in_mat[1].elements;
+  P.m[2].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.m[1].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.m[1].elements.y = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_Matrix_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.m[1][2] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.m[1].elements[2] = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadStruct) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.arr = tint_unpack_vec3_in_composite_1(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_2(P);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadArray) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_1(P.arr);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadMatrix) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite(P.arr[0]);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadColumn) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0][1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = vec3<f32>(P.arr[0][1].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0][1].y;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.arr[0][1].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_ReadComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  let x = P.arr[0][1][2];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.arr[0][1].elements[2];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteStruct_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P = S(array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5)));
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 2u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 2u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.arr = tint_pack_vec3_in_composite_1(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P = tint_pack_vec3_in_composite_2(S(array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5))));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteStruct_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in : S;
+
+fn f() {
+  P = in;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in : S_tint_packed_vec3;
+
+fn f() {
+  P = in;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteArray_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr = array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 2u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 2u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+  for(var i : u32; (i < 2u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr = tint_pack_vec3_in_composite_1(array(mat3x3<f32>(), mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5)));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteArray_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr1 : array<mat3x3<f32>, 2>,
+  arr2 : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_arr : array<mat3x3<f32>, 2>;
+
+fn f() {
+  P.arr1 = in_str.arr1;
+  P.arr2 = in_arr;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr1 : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>,
+  @align(16)
+  arr2 : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>,
+}
+
+struct S {
+  arr1 : array<mat3x3<f32>, 2>,
+  arr2 : array<mat3x3<f32>, 2>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 2u>;
+
+fn f() {
+  P.arr1 = in_str.arr1;
+  P.arr2 = in_arr;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteMatrix_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0] = mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr[0] = tint_pack_vec3_in_composite(mat3x3(1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteMatrix_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(3) var<uniform> in_mat : mat3x3<f32>;
+
+fn f() {
+  P.arr[0] = in_str.arr[0];
+  P.arr[1] = in_arr[1];
+  P.arr[2] = in_mat;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(3) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+fn f() {
+  P.arr[0] = in_str.arr[0];
+  P.arr[1] = in_arr[1];
+  P.arr[2] = in_mat;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteVector_ValueRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0][1] = vec3(1.23);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@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));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteVector_RefRHS) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(1) var<uniform> in_str : S;
+@group(0) @binding(2) var<uniform> in_arr : array<mat3x3<f32>, 4>;
+@group(0) @binding(3) var<uniform> in_mat : mat3x3<f32>;
+@group(0) @binding(4) var<uniform> in_vec : vec3<f32>;
+
+fn f() {
+  P.arr[0][0] = in_str.arr[0][1];
+  P.arr[1][1] = in_arr[3][2];
+  P.arr[2][2] = in_mat[1];
+  P.arr[3][0] = in_vec;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<uniform> in_str : S_tint_packed_vec3;
+
+@group(0) @binding(2) var<uniform> in_arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(3) var<uniform> in_mat : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(4) var<uniform> in_vec : __packed_vec3<f32>;
+
+fn f() {
+  P.arr[0][0].elements = in_str.arr[0][1].elements;
+  P.arr[1][1].elements = in_arr[3][2].elements;
+  P.arr[2][2].elements = in_mat[1].elements;
+  P.arr[3][0].elements = in_vec;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteComponent_MemberAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0][1].y = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr[0][1].elements.y = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ArrayOfMatrix_WriteComponent_IndexAccessor) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  P.arr[0][1][2] = 1.23;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4u>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  P.arr[0][1].elements[2] = 1.23;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ExistingMemberAttributes) {
+    auto* src = R"(
+struct S {
+  @align(32) @size(32) v : vec3<f32>,
+  @align(64) @size(64) arr : array<vec3<f32>, 4>,
+  @align(128) @size(128) x : u32,
+}
+
 @group(0) @binding(0) var<uniform> P : S;
 
 fn f() {
-  let x = P.v;
+  let x = P.v[0];
 }
 )";
 
     auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(32) @size(32)
+  v : __packed_vec3<f32>,
+  @align(64) @size(64)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(128) @size(128)
+  x : u32,
+}
+
 struct S {
-  @internal(packed_vector)
+  @align(32) @size(32)
   v : vec3<f32>,
+  @align(64) @size(64)
+  arr : array<vec3<f32>, 4>,
+  @align(128) @size(128)
+  x : u32,
+}
+
+@group(0) @binding(0) var<uniform> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = P.v[0];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructMember_ExistingMemberAttributes_SizeMatchesUnpackedVec3) {
+    // Test that the type we replace a vec3 with is not larger than it should be.
+    auto* src = R"(
+struct S {
+  @size(12) v : vec3<f32>,
+  @size(64) arr : array<vec3<f32>, 4>,
 }
 
 @group(0) @binding(0) var<uniform> P : S;
 
 fn f() {
-  let x = vec3<f32>(P.v);
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, StorageAddressSpace) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = P.v;
+  let x = P.v[0];
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+struct S_tint_packed_vec3 {
+  @size(12) @align(16)
+  v : __packed_vec3<f32>,
+  @size(64) @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  @size(12)
+  v : vec3<f32>,
+  @size(64)
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<uniform> P : S_tint_packed_vec3;
 
 fn f() {
-  let x = vec3<f32>(P.v);
+  let x = P.v[0];
 }
 )";
 
@@ -135,29 +4095,52 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, ExistingMemberAttributes) {
+TEST_F(PackedVec3Test, StructMember_ExistingMemberAttributes_AlignTooSmall) {
+    // Test that we add an @align() attribute when the new alignment of the packed vec3 struct would
+    // be too small.
     auto* src = R"(
 struct S {
-  @align(32) @size(64) v : vec3<f32>,
+  a : u32,
+  v : vec3<f32>,
+  b : u32,
+  arr : array<vec3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+@group(0) @binding(0) var<uniform> P : S;
 
 fn f() {
-  let x = P.v;
+  let x = P.v[0];
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector) @align(32) @size(64)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+struct S_tint_packed_vec3 {
+  a : u32,
+  @align(16)
+  v : __packed_vec3<f32>,
+  b : u32,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  a : u32,
+  v : vec3<f32>,
+  b : u32,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<uniform> P : S_tint_packed_vec3;
 
 fn f() {
-  let x = vec3<f32>(P.v);
+  let x = P.v[0];
 }
 )";
 
@@ -167,7 +4150,441 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, MultipleVectors) {
+TEST_F(PackedVec3Test, StructMember_ExistingMemberAttributes_ExplicitOffset) {
+    // Test that the we do not add an @align attribute if @offset is present.
+
+    // struct S {
+    //   a : u32,
+    //   @offset(32) v : vec3<f32>,
+    //   b : u32,
+    //   @offset(128) arr : array<vec3<f32>, 4>,
+    // }
+    //
+    // @group(0) @binding(0) var<uniform> P : S;
+    ProgramBuilder b;
+    b.Structure("S", utils::Vector{
+                         b.Member("a", b.ty.u32()),
+                         b.Member("v", b.ty.vec3<f32>(), utils::Vector{b.MemberOffset(AInt(32))}),
+                         b.Member("b", b.ty.u32()),
+                         b.Member("arr", b.ty.array(b.ty.vec3<f32>(), b.Expr(AInt(4))),
+                                  utils::Vector{b.MemberOffset(AInt(128))}),
+                     });
+    b.GlobalVar("P", builtin::AddressSpace::kStorage, b.ty("S"),
+                utils::Vector{b.Group(AInt(0)), b.Binding(AInt(0))});
+    Program src(std::move(b));
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  a : u32,
+  @size(28)
+  padding : u32,
+  /* @offset(32) */
+  v : __packed_vec3<f32>,
+  b : u32,
+  @size(80)
+  padding_1 : u32,
+  /* @offset(128) */
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  a : u32,
+  @size(16)
+  padding_2 : u32,
+  /* @offset(32) */
+  v : vec3<f32>,
+  b : u32,
+  @size(80)
+  padding_3 : u32,
+  /* @offset(128) */
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(std::move(src), data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructValueConstructor_ViaIndexAccessor) {
+    auto* src = R"(
+struct S {
+  a : vec3<f32>,
+  b : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> s : S;
+
+fn f() {
+  let value_arr : array<vec3<f32>, 4> = array<vec3<f32>, 4>();
+  let x = S(value_arr[0], s.arr[0], value_arr);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  a : __packed_vec3<f32>,
+  @align(16)
+  b : __packed_vec3<f32>,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct S {
+  a : vec3<f32>,
+  b : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> s : S_tint_packed_vec3;
+
+fn f() {
+  let value_arr : array<vec3<f32>, 4> = array<vec3<f32>, 4>();
+  let x = S(value_arr[0], vec3<f32>(s.arr[0].elements), value_arr);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, WrapperStructLayout_MixedUsage) {
+    // Test the layout of the generated wrapper struct(s) when vec3s are used in both structures and
+    // arrays.
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  a : u32,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S;
+@group(0) @binding(1) var<storage, read_write> arr : array<vec3<f32>, 4>;
+
+fn main() {
+  str.v = arr[0];
+  arr[1] = str.v;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  a : u32,
+}
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+  a : u32,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<storage, read_write> arr : array<tint_packed_vec3_f32_array_element, 4u>;
+
+fn main() {
+  str.v = arr[0].elements;
+  arr[1].elements = str.v;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    auto& vars = got.program.AST().GlobalVariables();
+    ASSERT_EQ(vars.Length(), 2u);
+
+    {
+        // Check the layout of the struct type of "str".
+        // The first member should have an alignment of 16 bytes, a size of 12 bytes, and the second
+        // member should have an offset of 12 bytes.
+        auto* sem_str = got.program.Sem().Get(vars[0]);
+        auto* str_ty = sem_str->Type()->UnwrapRef()->As<sem::Struct>();
+        ASSERT_NE(str_ty, nullptr);
+        ASSERT_EQ(str_ty->Members().Length(), 2u);
+        EXPECT_EQ(str_ty->Members()[0]->Align(), 16u);
+        EXPECT_EQ(str_ty->Members()[0]->Size(), 12u);
+        EXPECT_EQ(str_ty->Members()[1]->Offset(), 12u);
+    }
+
+    {
+        // Check the layout of the array type of "arr".
+        // The element stride should be 16 bytes.
+        auto* sem_arr = got.program.Sem().Get(vars[1]);
+        auto* arr_ty = sem_arr->Type()->UnwrapRef()->As<type::Array>();
+        ASSERT_NE(arr_ty, nullptr);
+        EXPECT_EQ(arr_ty->Stride(), 16u);
+    }
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, PackUnpackStructWithNonVec3Members) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+  a : u32,
+  b : vec4<f32>,
+  c : array<vec4<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
+
+fn f() {
+  let x = P;
+  P = x;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+  a : u32,
+  b : vec4<f32>,
+  c : array<vec4<f32>, 4>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.arr = tint_unpack_vec3_in_composite(in.arr);
+  result.a = in.a;
+  result.b = in.b;
+  result.c = in.c;
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  result.a = in.a;
+  result.b = in.b;
+  result.c = in.c;
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+  a : u32,
+  b : vec4<f32>,
+  c : array<vec4<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn f() {
+  let x = tint_unpack_vec3_in_composite_1(P);
+  P = tint_pack_vec3_in_composite_1(x);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Struct_ShaderIO) {
+    // Test that we do not modify structures that are used for shader IO.
+    auto* src = R"(
+struct S1 {
+  @location(0) v : vec3<f32>,
+}
+
+struct S2 {
+  @location(0) v : vec3<f32>,
+  @builtin(position) pos : vec4<f32>,
+}
+
+@vertex
+fn main(s1 : S1) -> S2 {
+  let v : vec3<f32> = s1.v;
+  var s2 : S2;
+  s2.v = v;
+  return s2;
+}
+)";
+
+    auto* expect = R"(
+struct S1 {
+  @location(0)
+  v : vec3<f32>,
+}
+
+struct S2 {
+  @location(0)
+  v : vec3<f32>,
+  @builtin(position)
+  pos : vec4<f32>,
+}
+
+@vertex
+fn main(s1 : S1) -> S2 {
+  let v : vec3<f32> = s1.v;
+  var s2 : S2;
+  s2.v = v;
+  return s2;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ModfReturnStruct) {
+    // Test that we do not try to modify accessors on the anonymous structure returned by modf.
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> output : vec3<f32>;
+
+const values = array(modf(vec3(1.0, 2.0, 3.0)).fract);
+
+@compute @workgroup_size(1)
+fn main() {
+  output = values[0];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> output : __packed_vec3<f32>;
+
+const values = array(modf(vec3(1.0, 2.0, 3.0)).fract);
+
+@compute @workgroup_size(1)
+fn main() {
+  output = __packed_vec3<f32>(values[0]);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ModfReturnStruct_PointerToMember) {
+    // Test that we can pass a pointer to the vec3 member of the modf return struct to a function
+    // parameter to which we also pass a pointer to a vec3 member on a host-shareable struct.
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  v : vec3<f32>
+}
+
+@group(0) @binding(0) var<storage, read_write> output : S;
+
+fn foo(p : ptr<function, vec3<f32>>) {
+  (*p) = vec3(1, 2, 3);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var f : S;
+  var modf_ret = modf(vec3(1.0, 2.0, 3.0));
+  foo(&f.v);
+  foo(&modf_ret.fract);
+  output.v = modf_ret.fract;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> output : S_tint_packed_vec3;
+
+fn foo(p : ptr<function, vec3<f32>>) {
+  *(p) = vec3(1, 2, 3);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var f : S;
+  var modf_ret = modf(vec3(1.0, 2.0, 3.0));
+  foo(&(f.v));
+  foo(&(modf_ret.fract));
+  output.v = __packed_vec3<f32>(modf_ret.fract);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MultipleStructMembers) {
     auto* src = R"(
 struct S {
   v2_a : vec2<f32>,
@@ -176,6 +4593,9 @@
   v2_b : vec2<f32>,
   v3_b : vec3<f32>,
   v4_b : vec4<f32>,
+  v2_arr : array<vec2<f32>, 4>,
+  v3_arr : array<vec3<f32>, 4>,
+  v4_arr : array<vec4<f32>, 4>,
 }
 
 @group(0) @binding(0) var<storage> P : S;
@@ -187,22 +4607,56 @@
   let v2_b = P.v2_b;
   let v3_b = P.v3_b;
   let v4_b = P.v4_b;
+  let v2_arr : array<vec2<f32>, 4> = P.v2_arr;
+  let v3_arr : array<vec3<f32>, 4> = P.v3_arr;
+  let v4_arr : array<vec4<f32>, 4> = P.v4_arr;
 }
 )";
 
     auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  v2_a : vec2<f32>,
+  @align(16)
+  v3_a : __packed_vec3<f32>,
+  v4_a : vec4<f32>,
+  v2_b : vec2<f32>,
+  @align(16)
+  v3_b : __packed_vec3<f32>,
+  v4_b : vec4<f32>,
+  v2_arr : array<vec2<f32>, 4>,
+  @align(16)
+  v3_arr : array<tint_packed_vec3_f32_array_element, 4u>,
+  v4_arr : array<vec4<f32>, 4>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
 struct S {
   v2_a : vec2<f32>,
-  @internal(packed_vector)
   v3_a : vec3<f32>,
   v4_a : vec4<f32>,
   v2_b : vec2<f32>,
-  @internal(packed_vector)
   v3_b : vec3<f32>,
   v4_b : vec4<f32>,
+  v2_arr : array<vec2<f32>, 4>,
+  v3_arr : array<vec3<f32>, 4>,
+  v4_arr : array<vec4<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
 
 fn f() {
   let v2_a = P.v2_a;
@@ -211,6 +4665,9 @@
   let v2_b = P.v2_b;
   let v3_b = vec3<f32>(P.v3_b);
   let v4_b = P.v4_b;
+  let v2_arr : array<vec2<f32>, 4> = P.v2_arr;
+  let v3_arr : array<vec3<f32>, 4> = tint_unpack_vec3_in_composite(P.v3_arr);
+  let v4_arr : array<vec4<f32>, 4> = P.v4_arr;
 }
 )";
 
@@ -220,31 +4677,1116 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, MixedAddressSpace) {
+TEST_F(PackedVec3Test, Vec3Pointers) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+@group(0) @binding(1) var<storage, read_write> arr_v : array<vec3<f32>, 4>;
+@group(0) @binding(2) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(3) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(4) var<storage, read_write> str : S;
+
+fn f() {
+  let p_v = &v;
+  let v = *p_v;
+  *p_v = v;
+
+  let p_arr_v = &arr_v[0];
+  let arr_v = *p_arr_v;
+  *p_arr_v = arr_v;
+
+  let p_m = &m[0];
+  let m = *p_m;
+  *p_m = m;
+
+  let p_arr_m = &arr_m[0][1];
+  let arr_m = *p_arr_m;
+  *p_arr_m = arr_m;
+
+  let p_str_v = &str.v;
+  let str_v = *p_str_v;
+  *p_str_v = str_v;
+
+  let p_str_arr_v = &str.arr_v[0];
+  let str_arr_v = *p_str_arr_v;
+  *p_str_arr_v = str_arr_v;
+
+  let p_str_m = &str.m[0];
+  let str_m = *p_str_m;
+  *p_str_m = str_m;
+
+  let p_str_arr_m = &str.arr_m[0][1];
+  let str_arr_m = *p_str_arr_m;
+  *p_str_arr_m = str_arr_m;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+@group(0) @binding(1) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(2) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(3) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(4) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn f() {
+  let p_v = &(v);
+  let v = vec3<f32>(*(p_v));
+  *(p_v) = __packed_vec3<f32>(v);
+  let p_arr_v = &(arr_v[0].elements);
+  let arr_v = vec3<f32>(*(p_arr_v));
+  *(p_arr_v) = __packed_vec3<f32>(arr_v);
+  let p_m = &(m[0].elements);
+  let m = vec3<f32>(*(p_m));
+  *(p_m) = __packed_vec3<f32>(m);
+  let p_arr_m = &(arr_m[0][1].elements);
+  let arr_m = vec3<f32>(*(p_arr_m));
+  *(p_arr_m) = __packed_vec3<f32>(arr_m);
+  let p_str_v = &(str.v);
+  let str_v = vec3<f32>(*(p_str_v));
+  *(p_str_v) = __packed_vec3<f32>(str_v);
+  let p_str_arr_v = &(str.arr_v[0].elements);
+  let str_arr_v = vec3<f32>(*(p_str_arr_v));
+  *(p_str_arr_v) = __packed_vec3<f32>(str_arr_v);
+  let p_str_m = &(str.m[0].elements);
+  let str_m = vec3<f32>(*(p_str_m));
+  *(p_str_m) = __packed_vec3<f32>(str_m);
+  let p_str_arr_m = &(str.arr_m[0][1].elements);
+  let str_arr_m = vec3<f32>(*(p_str_arr_m));
+  *(p_str_arr_m) = __packed_vec3<f32>(str_arr_m);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MatrixPointers) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(1) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(2) var<storage, read_write> str : S;
+
+fn f() {
+  let p_m = &m;
+  let m = *p_m;
+  *p_m = m;
+
+  let p_arr_m = &arr_m[0];
+  let arr_m = *p_arr_m;
+  *p_arr_m = arr_m;
+
+  let p_str_m = &str.m;
+  let str_m = *p_str_m;
+  *p_str_m = str_m;
+
+  let p_str_arr_m = &str.arr_m[0];
+  let str_arr_m = *p_str_arr_m;
+  *p_str_arr_m = str_arr_m;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(1) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(2) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn f() {
+  let p_m = &(m);
+  let m = tint_unpack_vec3_in_composite(*(p_m));
+  *(p_m) = tint_pack_vec3_in_composite(m);
+  let p_arr_m = &(arr_m[0]);
+  let arr_m = tint_unpack_vec3_in_composite(*(p_arr_m));
+  *(p_arr_m) = tint_pack_vec3_in_composite(arr_m);
+  let p_str_m = &(str.m);
+  let str_m = tint_unpack_vec3_in_composite(*(p_str_m));
+  *(p_str_m) = tint_pack_vec3_in_composite(str_m);
+  let p_str_arr_m = &(str.arr_m[0]);
+  let str_arr_m = tint_unpack_vec3_in_composite(*(p_str_arr_m));
+  *(p_str_arr_m) = tint_pack_vec3_in_composite(str_arr_m);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVec3Pointers) {
+    auto* src = R"(
+struct S {
+  arr_v : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<vec3<f32>, 4>;
+@group(0) @binding(1) var<storage, read_write> str : S;
+
+fn f() {
+  let p_arr_v = &arr_v;
+  let arr_v = *p_arr_v;
+  *p_arr_v = arr_v;
+
+  let p_str_arr_v = &str.arr_v;
+  let str_arr_v = *p_str_arr_v;
+  *p_str_arr_v = str_arr_v;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  arr_v : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn f() {
+  let p_arr_v = &(arr_v);
+  let arr_v = tint_unpack_vec3_in_composite(*(p_arr_v));
+  *(p_arr_v) = tint_pack_vec3_in_composite(arr_v);
+  let p_str_arr_v = &(str.arr_v);
+  let str_arr_v = tint_unpack_vec3_in_composite(*(p_str_arr_v));
+  *(p_str_arr_v) = tint_pack_vec3_in_composite(str_arr_v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrixPointers) {
+    auto* src = R"(
+struct S {
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(1) var<storage, read_write> str : S;
+
+fn f() {
+  let p_arr_m = &arr_m;
+  let arr_m = *p_arr_m;
+  *p_arr_m = arr_m;
+
+  let p_str_arr_m = &str.arr_m;
+  let str_arr_m = *p_str_arr_m;
+  *p_str_arr_m = str_arr_m;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn f() {
+  let p_arr_m = &(arr_m);
+  let arr_m = tint_unpack_vec3_in_composite_1(*(p_arr_m));
+  *(p_arr_m) = tint_pack_vec3_in_composite_1(arr_m);
+  let p_str_arr_m = &(str.arr_m);
+  let str_arr_m = tint_unpack_vec3_in_composite_1(*(p_str_arr_m));
+  *(p_str_arr_m) = tint_pack_vec3_in_composite_1(str_arr_m);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructPointers) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S;
+
+fn f() {
+  let p_str = &str;
+  let str = *p_str;
+  *p_str = str;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : array<mat3x3<f32>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_3(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.m = tint_pack_vec3_in_composite(in.m);
+  result.arr_v = tint_pack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_pack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn f() {
+  let p_str = &(str);
+  let str = tint_unpack_vec3_in_composite_3(*(p_str));
+  *(p_str) = tint_pack_vec3_in_composite_3(str);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, VectorPointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+@group(0) @binding(1) var<storage, read_write> arr_v : array<vec3<f32>, 4>;
+@group(0) @binding(2) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(3) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(4) var<storage, read_write> str : S;
+
+fn load(p : ptr<storage, vec3<f32>, read_write>) -> vec3<f32> {
+  return *p;
+}
+
+fn store(p : ptr<storage, vec3<f32>, read_write>) {
+  *p = vec3(1, 2, 3);
+}
+
+fn f() {
+  load(&v);
+  store(&v);
+  load(&arr_v[0]);
+  store(&arr_v[0]);
+  load(&m[0]);
+  store(&m[0]);
+  load(&arr_m[0][1]);
+  store(&arr_m[0][1]);
+  load(&str.v);
+  store(&str.v);
+  load(&str.arr_v[0]);
+  store(&str.arr_v[0]);
+  load(&str.m[0]);
+  store(&str.m[0]);
+  load(&str.arr_m[0][1]);
+  store(&str.arr_m[0][1]);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+@group(0) @binding(1) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(2) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(3) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(4) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn load(p : ptr<storage, __packed_vec3<f32>, read_write>) -> vec3<f32> {
+  return vec3<f32>(*(p));
+}
+
+fn store(p : ptr<storage, __packed_vec3<f32>, read_write>) {
+  *(p) = __packed_vec3<f32>(vec3(1, 2, 3));
+}
+
+fn f() {
+  load(&(v));
+  store(&(v));
+  load(&(arr_v[0].elements));
+  store(&(arr_v[0].elements));
+  load(&(m[0].elements));
+  store(&(m[0].elements));
+  load(&(arr_m[0][1].elements));
+  store(&(arr_m[0][1].elements));
+  load(&(str.v));
+  store(&(str.v));
+  load(&(str.arr_v[0].elements));
+  store(&(str.arr_v[0].elements));
+  load(&(str.m[0].elements));
+  store(&(str.m[0].elements));
+  load(&(str.arr_m[0][1].elements));
+  store(&(str.arr_m[0][1].elements));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MatrixPointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  m : mat3x3<f32>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+@group(0) @binding(1) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(2) var<storage, read_write> str : S;
+
+fn load(p : ptr<storage, mat3x3<f32>, read_write>) -> mat3x3<f32> {
+  return *p;
+}
+
+fn store(p : ptr<storage, mat3x3<f32>, read_write>) {
+  *p = mat3x3(1, 2, 3, 4, 5, 6, 7, 8, 9);
+}
+
+fn f() {
+  load(&m);
+  store(&m);
+  load(&arr_m[0]);
+  store(&arr_m[0]);
+  load(&str.m);
+  store(&str.m);
+  load(&str.arr_m[0]);
+  store(&str.arr_m[0]);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(1) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(2) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn load(p : ptr<storage, array<tint_packed_vec3_f32_array_element, 3u>, read_write>) -> mat3x3<f32> {
+  return tint_unpack_vec3_in_composite(*(p));
+}
+
+fn store(p : ptr<storage, array<tint_packed_vec3_f32_array_element, 3u>, read_write>) {
+  *(p) = tint_pack_vec3_in_composite(mat3x3(1, 2, 3, 4, 5, 6, 7, 8, 9));
+}
+
+fn f() {
+  load(&(m));
+  store(&(m));
+  load(&(arr_m[0]));
+  store(&(arr_m[0]));
+  load(&(str.m));
+  store(&(str.m));
+  load(&(str.arr_m[0]));
+  store(&(str.arr_m[0]));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfVectorPointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  arr_v : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<vec3<f32>, 4>;
+@group(0) @binding(1) var<storage, read_write> str : S;
+
+fn load(p : ptr<storage, array<vec3<f32>, 4>, read_write>) -> array<vec3<f32>, 4> {
+  return *p;
+}
+
+fn store(p : ptr<storage, array<vec3<f32>, 4>, read_write>) {
+  *p = array(vec3(1.0), vec3(2.0), vec3(3.0), vec3(4.0));
+}
+
+fn f() {
+  load(&arr_v);
+  store(&arr_v);
+  load(&str.arr_v);
+  store(&str.arr_v);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+struct S {
+  arr_v : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn load(p : ptr<storage, array<tint_packed_vec3_f32_array_element, 4u>, read_write>) -> array<vec3<f32>, 4> {
+  return tint_unpack_vec3_in_composite(*(p));
+}
+
+fn store(p : ptr<storage, array<tint_packed_vec3_f32_array_element, 4u>, read_write>) {
+  *(p) = tint_pack_vec3_in_composite(array(vec3(1.0), vec3(2.0), vec3(3.0), vec3(4.0)));
+}
+
+fn f() {
+  load(&(arr_v));
+  store(&(arr_v));
+  load(&(str.arr_v));
+  store(&(str.arr_v));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ArrayOfMatrixPointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(1) var<storage, read_write> str : S;
+
+fn load(p : ptr<storage, array<mat3x3<f32>, 4>, read_write>) -> array<mat3x3<f32>, 4> {
+  return *p;
+}
+
+fn store(p : ptr<storage, array<mat3x3<f32>, 4>, read_write>) {
+  *p = array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>());
+}
+
+fn f() {
+  load(&arr_m);
+  store(&arr_m);
+  load(&str.arr_m);
+  store(&str.arr_m);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn load(p : ptr<storage, array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, read_write>) -> array<mat3x3<f32>, 4> {
+  return tint_unpack_vec3_in_composite_1(*(p));
+}
+
+fn store(p : ptr<storage, array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, read_write>) {
+  *(p) = tint_pack_vec3_in_composite_1(array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>()));
+}
+
+fn f() {
+  load(&(arr_m));
+  store(&(arr_m));
+  load(&(str.arr_m));
+  store(&(str.arr_m));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, StructPointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S;
+
+fn load(p : ptr<storage, S, read_write>) -> S {
+  return *p;
+}
+
+fn store(p : ptr<storage, S, read_write>) {
+  *p = S();
+}
+
+fn f() {
+  load(&str);
+  store(&str);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : array<mat3x3<f32>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_3(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.m = tint_pack_vec3_in_composite(in.m);
+  result.arr_v = tint_pack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_pack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> str : S_tint_packed_vec3;
+
+fn load(p : ptr<storage, S_tint_packed_vec3, read_write>) -> S {
+  return tint_unpack_vec3_in_composite_3(*(p));
+}
+
+fn store(p : ptr<storage, S_tint_packed_vec3, read_write>) {
+  *(p) = tint_pack_vec3_in_composite_3(S());
+}
+
+fn f() {
+  load(&(str));
+  store(&(str));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_Struct) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S;
 
 fn f() {
   var f : S;
-  let x = f.v;
+  let v = f.v;
+  let arr = f.arr;
+  P = f;
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
 
 fn f() {
   var f : S;
-  let x = vec3<f32>(f.v);
+  let v = f.v;
+  let arr = f.arr;
+  P = tint_pack_vec3_in_composite_1(f);
 }
 )";
 
@@ -254,29 +5796,88 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, ReadMemberAccessChain) {
+TEST_F(PackedVec3Test, MixedAddressSpace_NestedStruct) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+struct Outer {
+  inner : S,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : Outer;
 
 fn f() {
-  let x = P.v.yz.x;
+  var f : Outer;
+  let v = f.inner.v;
+  let arr = f.inner.arr;
+  P = f;
+  P.inner = f.inner;
+  P.inner.v = f.inner.v;
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+struct Outer_tint_packed_vec3 {
+  @align(16)
+  inner : S_tint_packed_vec3,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : Outer) -> Outer_tint_packed_vec3 {
+  var result : Outer_tint_packed_vec3;
+  result.inner = tint_pack_vec3_in_composite_1(in.inner);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+struct Outer {
+  inner : S,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : Outer_tint_packed_vec3;
 
 fn f() {
-  let x = P.v.yz.x;
+  var f : Outer;
+  let v = f.inner.v;
+  let arr = f.inner.arr;
+  P = tint_pack_vec3_in_composite_2(f);
+  P.inner = tint_pack_vec3_in_composite_1(f.inner);
+  P.inner.v = __packed_vec3<f32>(f.inner.v);
 }
 )";
 
@@ -286,305 +5887,94 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, ReadVector) {
+TEST_F(PackedVec3Test, MixedAddressSpace_AnotherStructNotShared) {
+    // Test that we can pass a pointers to a members of both shared and non-shared structs to the
+    // same function.
     auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
 struct S {
   v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = P.v;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
+struct NotShared {
   v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = vec3<f32>(P.v);
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadIndexAccessor) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = P.v[1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = P.v[1];
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadViaStructPtrDirect) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = (*(&(*(&P)))).v;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = vec3<f32>((*(&(*(&(P))))).v);
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadViaVectorPtrDirect) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = *(&(*(&(P.v))));
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = vec3<f32>(*(&(*(&(P.v)))));
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadViaStructPtrViaLet) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let p0 = &P;
-  let p1 = &(*(p0));
-  let a = (*p1).v;
-  let p2 = &(*(p1));
-  let b = (*p2).v;
-  let c = (*p2).v;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let p0 = &(P);
-  let p1 = &(*(p0));
-  let a = vec3<f32>((*(p1)).v);
-  let p2 = &(*(p1));
-  let b = vec3<f32>((*(p2)).v);
-  let c = vec3<f32>((*(p2)).v);
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadViaVectorPtrViaLet) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let p0 = &(P.v);
-  let p1 = &(*(p0));
-  let a = *p1;
-  let p2 = &(*(p1));
-  let b = *p2;
-  let c = *p2;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let p0 = &(P.v);
-  let p1 = &(*(p0));
-  let a = vec3<f32>(*(p1));
-  let p2 = &(*(p1));
-  let b = vec3<f32>(*(p2));
-  let c = vec3<f32>(*(p2));
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadUnaryOp) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = -P.v;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = -(vec3<f32>(P.v));
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, ReadBinaryOp) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = P.v + P.v;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
-}
-
-@group(0) @binding(0) var<storage> P : S;
-
-fn f() {
-  let x = (vec3<f32>(P.v) + vec3<f32>(P.v));
-}
-)";
-
-    DataMap data;
-    auto got = Run<PackedVec3>(src, data);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(PackedVec3Test, WriteVector) {
-    auto* src = R"(
-struct S {
-  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
 }
 
 @group(0) @binding(0) var<storage, read_write> P : S;
 
+fn g(p : ptr<function, vec3<f32>>) -> vec3<f32> {
+  return *p;
+}
+
 fn f() {
-  P.v = vec3(1.23);
+  var f1 : S;
+  var f2 : NotShared;
+  g(&f1.v);
+  g(&f1.arr[0]);
+  g(&f2.v);
+  g(&f2.arr[0]);
+  P = f1;
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage, read_write> P : S;
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+struct NotShared {
+  v : vec3<f32>,
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> P : S_tint_packed_vec3;
+
+fn g(p : ptr<function, vec3<f32>>) -> vec3<f32> {
+  return *(p);
+}
 
 fn f() {
-  P.v = vec3(1.23);
+  var f1 : S;
+  var f2 : NotShared;
+  g(&(f1.v));
+  g(&(f1.arr[0]));
+  g(&(f2.v));
+  g(&(f2.arr[0]));
+  P = tint_pack_vec3_in_composite_1(f1);
 }
 )";
 
@@ -594,29 +5984,103 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, WriteMemberAccess) {
+TEST_F(PackedVec3Test, MixedAddressSpace_InitFromLoad_ExplicitVarType) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(0) var<storage> P : S;
 
 fn f() {
-  P.v.y = 1.23;
+  var f1 : S = P;
+  var f2 : vec3<f32> = P.v;
+  var f3 : mat3x3<f32> = P.m;
+  var f4 : array<vec3<f32>, 4> = P.arr_v;
+  var f5 : array<mat3x3<f32>, 4> = P.arr_m;
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
 }
 )";
 
     auto* expect = R"(
-struct S {
-  @internal(packed_vector)
-  v : vec3<f32>,
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage, read_write> P : S;
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
 
 fn f() {
-  P.v.y = 1.23;
+  var f1 : S = tint_unpack_vec3_in_composite_3(P);
+  var f2 : vec3<f32> = vec3<f32>(P.v);
+  var f3 : mat3x3<f32> = tint_unpack_vec3_in_composite(P.m);
+  var f4 : array<vec3<f32>, 4> = tint_unpack_vec3_in_composite_1(P.arr_v);
+  var f5 : array<mat3x3<f32>, 4> = tint_unpack_vec3_in_composite_2(P.arr_m);
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
 }
 )";
 
@@ -626,29 +6090,1998 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PackedVec3Test, WriteIndexAccessor) {
+TEST_F(PackedVec3Test, MixedAddressSpace_InitFromLoad_InferredVarType) {
     auto* src = R"(
 struct S {
   v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
 }
 
-@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(0) var<storage> P : S;
 
 fn f() {
-  P.v[1] = 1.23;
+  var f1 = P;
+  var f2 = P.v;
+  var f3 = P.m;
+  var f4 = P.arr_v;
+  var f5 = P.arr_m;
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
 }
 )";
 
     auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_2(in.arr_m);
+  return result;
+}
+
 struct S {
-  @internal(packed_vector)
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  var f1 = tint_unpack_vec3_in_composite_3(P);
+  var f2 = vec3<f32>(P.v);
+  var f3 = tint_unpack_vec3_in_composite(P.m);
+  var f4 = tint_unpack_vec3_in_composite_1(P.arr_v);
+  var f5 = tint_unpack_vec3_in_composite_2(P.arr_m);
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_InitFromValue_ExplicitVarType) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  var f1 : S = S();
+  var f2 : vec3<f32> = vec3<f32>();
+  var f3 : mat3x3<f32> = mat3x3<f32>();
+  var f4 : array<vec3<f32>, 4> = array(vec3<f32>(), vec3<f32>(), vec3<f32>(), vec3<f32>());
+  var f5 : array<mat3x3<f32>, 4> = array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>());
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  var f1 : S = S();
+  var f2 : vec3<f32> = vec3<f32>();
+  var f3 : mat3x3<f32> = mat3x3<f32>();
+  var f4 : array<vec3<f32>, 4> = array(vec3<f32>(), vec3<f32>(), vec3<f32>(), vec3<f32>());
+  var f5 : array<mat3x3<f32>, 4> = array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>());
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_InitFromValue_InferredVarType) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  var f1 = S();
+  var f2 = vec3<f32>();
+  var f3 = mat3x3<f32>();
+  var f4 = array(vec3<f32>(), vec3<f32>(), vec3<f32>(), vec3<f32>());
+  var f5 = array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>());
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  var f1 = S();
+  var f2 = vec3<f32>();
+  var f3 = mat3x3<f32>();
+  var f4 = array(vec3<f32>(), vec3<f32>(), vec3<f32>(), vec3<f32>());
+  var f5 = array(mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>(), mat3x3<f32>());
+  let v_1 = f1.v;
+  let v_2 = f2;
+  let v_3 = f3[0];
+  let v_4 = f4[1];
+  let v_5 = f5[2][2];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_Pointers_Function) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn f() {
+  var f1 : S = P;
+  var f2 : vec3<f32> = P.v;
+  var f3 : array<vec3<f32>, 4>;
+  var f4 : mat3x3<f32> = P.m;
+  let pv_1 : ptr<function, vec3<f32>> = &f1.v;
+  let pv_2 : ptr<function, vec3<f32>> = &f2;
+  let pv_3 : ptr<function, vec3<f32>> = &f3[0];
+  let pv_4 : ptr<function, mat3x3<f32>> = &f1.m;
+  let pv_5 : ptr<function, mat3x3<f32>> = &f4;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn f() {
+  var f1 : S = tint_unpack_vec3_in_composite_1(P);
+  var f2 : vec3<f32> = vec3<f32>(P.v);
+  var f3 : array<vec3<f32>, 4>;
+  var f4 : mat3x3<f32> = tint_unpack_vec3_in_composite(P.m);
+  let pv_1 : ptr<function, vec3<f32>> = &(f1.v);
+  let pv_2 : ptr<function, vec3<f32>> = &(f2);
+  let pv_3 : ptr<function, vec3<f32>> = &(f3[0]);
+  let pv_4 : ptr<function, mat3x3<f32>> = &(f1.m);
+  let pv_5 : ptr<function, mat3x3<f32>> = &(f4);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_Pointers_Private) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+var<private> p1 : S;
+var<private> p2 : vec3<f32>;
+var<private> p3 : array<vec3<f32>, 4>;
+var<private> p4 : mat3x3<f32>;
+
+fn f() {
+  let pv_1 : ptr<private, vec3<f32>> = &p1.v;
+  let pv_2 : ptr<private, vec3<f32>> = &p2;
+  let pv_3 : ptr<private, vec3<f32>> = &p3[0];
+  let pv_4 : ptr<private, mat3x3<f32>> = &p1.m;
+  let pv_5 : ptr<private, mat3x3<f32>> = &p4;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+var<private> p1 : S;
+
+var<private> p2 : vec3<f32>;
+
+var<private> p3 : array<vec3<f32>, 4>;
+
+var<private> p4 : mat3x3<f32>;
+
+fn f() {
+  let pv_1 : ptr<private, vec3<f32>> = &(p1.v);
+  let pv_2 : ptr<private, vec3<f32>> = &(p2);
+  let pv_3 : ptr<private, vec3<f32>> = &(p3[0]);
+  let pv_4 : ptr<private, mat3x3<f32>> = &(p1.m);
+  let pv_5 : ptr<private, mat3x3<f32>> = &(p4);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_Pointers_Workgroup) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+var<workgroup> w1 : S;
+var<workgroup> w2 : vec3<f32>;
+var<workgroup> w3 : array<vec3<f32>, 4>;
+var<workgroup> w4 : mat3x3<f32>;
+
+fn f() {
+  let pv_1 : ptr<workgroup, vec3<f32>> = &w1.v;
+  let pv_2 : ptr<workgroup, vec3<f32>> = &w2;
+  let pv_3 : ptr<workgroup, vec3<f32>> = &w3[0];
+  let pv_4 : ptr<workgroup, mat3x3<f32>> = &w1.m;
+  let pv_5 : ptr<workgroup, mat3x3<f32>> = &w4;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+var<workgroup> w1 : S;
+
+var<workgroup> w2 : vec3<f32>;
+
+var<workgroup> w3 : array<vec3<f32>, 4>;
+
+var<workgroup> w4 : mat3x3<f32>;
+
+fn f() {
+  let pv_1 : ptr<workgroup, vec3<f32>> = &(w1.v);
+  let pv_2 : ptr<workgroup, vec3<f32>> = &(w2);
+  let pv_3 : ptr<workgroup, vec3<f32>> = &(w3[0]);
+  let pv_4 : ptr<workgroup, mat3x3<f32>> = &(w1.m);
+  let pv_5 : ptr<workgroup, mat3x3<f32>> = &(w4);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MixedAddressSpace_PointerParameters) {
+    auto* src = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S;
+
+fn g_v(p : ptr<function, vec3<f32>>) -> vec3<f32> {
+  return *p;
+}
+
+fn g_m(p : ptr<function, mat3x3<f32>>) -> mat3x3<f32> {
+  return *p;
+}
+
+fn f() {
+  var f1 : S = P;
+  var f2 : vec3<f32> = P.v;
+  var f3 : array<vec3<f32>, 4>;
+  var f4 : mat3x3<f32> = P.m;
+  g_v(&f1.v);
+  g_v(&f2);
+  g_v(&f3[0]);
+  g_m(&f1.m);
+  g_m(&f4);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.v = vec3<f32>(in.v);
+  result.m = tint_unpack_vec3_in_composite(in.m);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage> P : S_tint_packed_vec3;
+
+fn g_v(p : ptr<function, vec3<f32>>) -> vec3<f32> {
+  return *(p);
+}
+
+fn g_m(p : ptr<function, mat3x3<f32>>) -> mat3x3<f32> {
+  return *(p);
+}
+
+fn f() {
+  var f1 : S = tint_unpack_vec3_in_composite_1(P);
+  var f2 : vec3<f32> = vec3<f32>(P.v);
+  var f3 : array<vec3<f32>, 4>;
+  var f4 : mat3x3<f32> = tint_unpack_vec3_in_composite(P.m);
+  g_v(&(f1.v));
+  g_v(&(f2));
+  g_v(&(f3[0]));
+  g_m(&(f1.m));
+  g_m(&(f4));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, WriteVec3Swizzle_FromRef) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+
+fn f() {
+  v = v.zyx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+fn f() {
+  v = __packed_vec3<f32>(vec3<f32>(v).zyx);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, WriteVec3Swizzle_FromValue) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> v : vec3<f32>;
+
+fn f() {
+  v = vec3f(1, 2, 3).zyx;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> v : __packed_vec3<f32>;
+
+fn f() {
+  v = __packed_vec3<f32>(vec3f(1, 2, 3).zyx);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, WriteVec3Component_FromPackedValueIndexAccessor) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>
+}
+
+@group(0) @binding(0) var<storage, read_write> s : S;
+
+fn g() -> S {
+  return S();
+}
+
+fn f() {
+  s.v[0] = g().v[1];
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+struct S {
   v : vec3<f32>,
 }
 
-@group(0) @binding(0) var<storage, read_write> P : S;
+@group(0) @binding(0) var<storage, read_write> s : S_tint_packed_vec3;
+
+fn g() -> S {
+  return S();
+}
 
 fn f() {
-  P.v[1] = 1.23;
+  s.v[0] = g().v[1];
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ExtractVec3FromStructValueExpression) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var v_var : vec3<f32> = S().v;
+  let v_let : vec3<f32> = S().v;
+  v_var = S().v;
+  v_var = S().v * 2.0;
+  buffer = S(S().v);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+}
+
+fn tint_pack_vec3_in_composite(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.v = __packed_vec3<f32>(in.v);
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var v_var : vec3<f32> = S().v;
+  let v_let : vec3<f32> = S().v;
+  v_var = S().v;
+  v_var = (S().v * 2.0);
+  buffer = tint_pack_vec3_in_composite(S(S().v));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ExtractArrayOfVec3FromStructValueExpression) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var arr_var : array<vec3<f32>, 4> = S().arr;
+  let arr_let : array<vec3<f32>, 4> = S().arr;
+  arr_var = S().arr;
+  arr_var[0] = S().arr[0] * 2.0;
+  buffer = S(S().arr);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.arr = tint_pack_vec3_in_composite(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<vec3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var arr_var : array<vec3<f32>, 4> = S().arr;
+  let arr_let : array<vec3<f32>, 4> = S().arr;
+  arr_var = S().arr;
+  arr_var[0] = (S().arr[0] * 2.0);
+  buffer = tint_pack_vec3_in_composite_1(S(S().arr));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ExtractNestedArrayFromStructValueExpression) {
+    auto* src = R"(
+struct S {
+  arr : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var arr_var : array<array<vec3<f32>, 4>, 4> = S().arr;
+  var inner_var : array<vec3<f32>, 4> = S().arr[0];
+  let arr_let : array<array<vec3<f32>, 4>, 4> = S().arr;
+  arr_var = S().arr;
+  inner_var = S().arr[0];
+  arr_var[0][0] = S().arr[0][0] * 2.0;
+  buffer = S(S().arr);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : array<vec3<f32>, 4u>) -> array<tint_packed_vec3_f32_array_element, 4u> {
+  var result : array<tint_packed_vec3_f32_array_element, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<array<vec3<f32>, 4u>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 4u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.arr = tint_pack_vec3_in_composite_1(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var arr_var : array<array<vec3<f32>, 4>, 4> = S().arr;
+  var inner_var : array<vec3<f32>, 4> = S().arr[0];
+  let arr_let : array<array<vec3<f32>, 4>, 4> = S().arr;
+  arr_var = S().arr;
+  inner_var = S().arr[0];
+  arr_var[0][0] = (S().arr[0][0] * 2.0);
+  buffer = tint_pack_vec3_in_composite_2(S(S().arr));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ExtractMatrixFromStructValueExpression) {
+    auto* src = R"(
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var m_var : mat3x3<f32> = S().m;
+  let m_let : mat3x3<f32> = S().m;
+  m_var = S().m;
+  m_var = S().m * 2.0;
+  buffer = S(S().m);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.m = tint_pack_vec3_in_composite(in.m);
+  return result;
+}
+
+struct S {
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var m_var : mat3x3<f32> = S().m;
+  let m_let : mat3x3<f32> = S().m;
+  m_var = S().m;
+  m_var = (S().m * 2.0);
+  buffer = tint_pack_vec3_in_composite_1(S(S().m));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, ExtractArrayOfMatrixFromStructValueExpression) {
+    auto* src = R"(
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var arr_var : array<mat3x3<f32>, 4> = S().arr;
+  let arr_let : array<mat3x3<f32>, 4> = S().arr;
+  arr_var = S().arr;
+  arr_var[0] = S().arr[0] * 2.0;
+  buffer = S(S().arr);
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+}
+
+fn tint_pack_vec3_in_composite(in : mat3x3<f32>) -> array<tint_packed_vec3_f32_array_element, 3u> {
+  var result : array<tint_packed_vec3_f32_array_element, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = tint_packed_vec3_f32_array_element(__packed_vec3<f32>(in[i]));
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_1(in : array<mat3x3<f32>, 4u>) -> array<array<tint_packed_vec3_f32_array_element, 3u>, 4u> {
+  var result : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_pack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_pack_vec3_in_composite_2(in : S) -> S_tint_packed_vec3 {
+  var result : S_tint_packed_vec3;
+  result.arr = tint_pack_vec3_in_composite_1(in.arr);
+  return result;
+}
+
+struct S {
+  arr : array<mat3x3<f32>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var arr_var : array<mat3x3<f32>, 4> = S().arr;
+  let arr_let : array<mat3x3<f32>, 4> = S().arr;
+  arr_var = S().arr;
+  arr_var[0] = (S().arr[0] * 2.0);
+  buffer = tint_pack_vec3_in_composite_2(S(S().arr));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, NestedArrays_Let) {
+    auto* src = R"(
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S, 4>;
+
+fn f() {
+  let full_let : array<S, 4> = arr_s;
+  let struct_let : S = arr_s[0];
+  let outer_arr_v_let : array<array<vec3<f32>, 4>, 4> = arr_s[0].arr_v;
+  let inner_arr_v_let : array<vec3<f32>, 4> = arr_s[0].arr_v[1];
+  let v_let : vec3<f32> = arr_s[0].arr_v[1][2];
+  let v_element_let : f32 = arr_s[0].arr_v[1][2].y;
+  let outer_arr_m_let : array<array<mat3x3<f32>, 4>, 4> = arr_s[0].arr_m;
+  let inner_arr_m_let : array<mat3x3<f32>, 4> = arr_s[0].arr_m[1];
+  let m_let : mat3x3<f32> = arr_s[0].arr_m[1][2];
+  let m_col_let : vec3<f32> = arr_s[0].arr_m[1][2][0];
+  let m_element_let : f32 = arr_s[0].arr_m[1][2][0].y;
+}
+)";
+
+    auto* expect = R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+  @align(16)
+  arr_m : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>) -> array<array<vec3<f32>, 4u>, 4u> {
+  var result : array<array<vec3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_2(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_4(in : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>) -> array<array<mat3x3<f32>, 4u>, 4u> {
+  var result : array<array<mat3x3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_3(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_5(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_4(in.arr_m);
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_6(in : array<S_tint_packed_vec3, 4u>) -> array<S, 4u> {
+  var result : array<S, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_5(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S_tint_packed_vec3, 4u>;
+
+fn f() {
+  let full_let : array<S, 4> = tint_unpack_vec3_in_composite_6(arr_s);
+  let struct_let : S = tint_unpack_vec3_in_composite_5(arr_s[0]);
+  let outer_arr_v_let : array<array<vec3<f32>, 4>, 4> = tint_unpack_vec3_in_composite_1(arr_s[0].arr_v);
+  let inner_arr_v_let : array<vec3<f32>, 4> = tint_unpack_vec3_in_composite(arr_s[0].arr_v[1]);
+  let v_let : vec3<f32> = vec3<f32>(arr_s[0].arr_v[1][2].elements);
+  let v_element_let : f32 = arr_s[0].arr_v[1][2].elements.y;
+  let outer_arr_m_let : array<array<mat3x3<f32>, 4>, 4> = tint_unpack_vec3_in_composite_4(arr_s[0].arr_m);
+  let inner_arr_m_let : array<mat3x3<f32>, 4> = tint_unpack_vec3_in_composite_3(arr_s[0].arr_m[1]);
+  let m_let : mat3x3<f32> = tint_unpack_vec3_in_composite_2(arr_s[0].arr_m[1][2]);
+  let m_col_let : vec3<f32> = vec3<f32>(arr_s[0].arr_m[1][2][0].elements);
+  let m_element_let : f32 = arr_s[0].arr_m[1][2][0].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, NestedArrays_VarInit) {
+    auto* src = R"(
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S, 4>;
+
+fn f() {
+  var full_var : array<S, 4> = arr_s;
+  var struct_var : S = arr_s[0];
+  var outer_arr_v_var : array<array<vec3<f32>, 4>, 4> = arr_s[0].arr_v;
+  var inner_arr_v_var : array<vec3<f32>, 4> = arr_s[0].arr_v[1];
+  var v_var : vec3<f32> = arr_s[0].arr_v[1][2];
+  var v_element_var : f32 = arr_s[0].arr_v[1][2].y;
+  var outer_arr_m_var : array<array<mat3x3<f32>, 4>, 4> = arr_s[0].arr_m;
+  var inner_arr_m_var : array<mat3x3<f32>, 4> = arr_s[0].arr_m[1];
+  var m_var : mat3x3<f32> = arr_s[0].arr_m[1][2];
+  var m_col_var : vec3<f32> = arr_s[0].arr_m[1][2][0];
+  var m_element_var : f32 = arr_s[0].arr_m[1][2][0].y;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+  @align(16)
+  arr_m : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>) -> array<array<vec3<f32>, 4u>, 4u> {
+  var result : array<array<vec3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_2(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_4(in : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>) -> array<array<mat3x3<f32>, 4u>, 4u> {
+  var result : array<array<mat3x3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_3(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_5(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_4(in.arr_m);
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_6(in : array<S_tint_packed_vec3, 4u>) -> array<S, 4u> {
+  var result : array<S, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_5(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S_tint_packed_vec3, 4u>;
+
+fn f() {
+  var full_var : array<S, 4> = tint_unpack_vec3_in_composite_6(arr_s);
+  var struct_var : S = tint_unpack_vec3_in_composite_5(arr_s[0]);
+  var outer_arr_v_var : array<array<vec3<f32>, 4>, 4> = tint_unpack_vec3_in_composite_1(arr_s[0].arr_v);
+  var inner_arr_v_var : array<vec3<f32>, 4> = tint_unpack_vec3_in_composite(arr_s[0].arr_v[1]);
+  var v_var : vec3<f32> = vec3<f32>(arr_s[0].arr_v[1][2].elements);
+  var v_element_var : f32 = arr_s[0].arr_v[1][2].elements.y;
+  var outer_arr_m_var : array<array<mat3x3<f32>, 4>, 4> = tint_unpack_vec3_in_composite_4(arr_s[0].arr_m);
+  var inner_arr_m_var : array<mat3x3<f32>, 4> = tint_unpack_vec3_in_composite_3(arr_s[0].arr_m[1]);
+  var m_var : mat3x3<f32> = tint_unpack_vec3_in_composite_2(arr_s[0].arr_m[1][2]);
+  var m_col_var : vec3<f32> = vec3<f32>(arr_s[0].arr_m[1][2][0].elements);
+  var m_element_var : f32 = arr_s[0].arr_m[1][2][0].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, NestedArrays_VarAssignment) {
+    auto* src = R"(
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S, 4>;
+
+fn f() {
+  var full_var : array<S, 4>;
+  var struct_var : S;
+  var outer_arr_v_var : array<array<vec3<f32>, 4>, 4>;
+  var inner_arr_v_var : array<vec3<f32>, 4>;
+  var v_var : vec3<f32>;
+  var v_element_var : f32;
+  var outer_arr_m_var : array<array<mat3x3<f32>, 4>, 4>;
+  var inner_arr_m_var : array<mat3x3<f32>, 4>;
+  var m_var : mat3x3<f32>;
+  var m_col_var : vec3<f32>;
+  var m_element_var : f32;
+
+  full_var = arr_s;
+  struct_var = arr_s[0];
+  outer_arr_v_var = arr_s[0].arr_v;
+  inner_arr_v_var = arr_s[0].arr_v[1];
+  v_var = arr_s[0].arr_v[1][2];
+  v_element_var = arr_s[0].arr_v[1][2].y;
+  outer_arr_m_var = arr_s[0].arr_m;
+  inner_arr_m_var = arr_s[0].arr_m[1];
+  m_var = arr_s[0].arr_m[1][2];
+  m_col_var = arr_s[0].arr_m[1][2][0];
+  m_element_var = arr_s[0].arr_m[1][2][0].y;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+  @align(16)
+  arr_m : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_1(in : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>) -> array<array<vec3<f32>, 4u>, 4u> {
+  var result : array<array<vec3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_2(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_3(in : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>) -> array<mat3x3<f32>, 4u> {
+  var result : array<mat3x3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_2(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_4(in : array<array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>, 4u>) -> array<array<mat3x3<f32>, 4u>, 4u> {
+  var result : array<array<mat3x3<f32>, 4u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_3(in[i]);
+  }
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_5(in : S_tint_packed_vec3) -> S {
+  var result : S;
+  result.arr_v = tint_unpack_vec3_in_composite_1(in.arr_v);
+  result.arr_m = tint_unpack_vec3_in_composite_4(in.arr_m);
+  return result;
+}
+
+fn tint_unpack_vec3_in_composite_6(in : array<S_tint_packed_vec3, 4u>) -> array<S, 4u> {
+  var result : array<S, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = tint_unpack_vec3_in_composite_5(in[i]);
+  }
+  return result;
+}
+
+struct S {
+  arr_v : array<array<vec3<f32>, 4>, 4>,
+  arr_m : array<array<mat3x3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_s : array<S_tint_packed_vec3, 4u>;
+
+fn f() {
+  var full_var : array<S, 4>;
+  var struct_var : S;
+  var outer_arr_v_var : array<array<vec3<f32>, 4>, 4>;
+  var inner_arr_v_var : array<vec3<f32>, 4>;
+  var v_var : vec3<f32>;
+  var v_element_var : f32;
+  var outer_arr_m_var : array<array<mat3x3<f32>, 4>, 4>;
+  var inner_arr_m_var : array<mat3x3<f32>, 4>;
+  var m_var : mat3x3<f32>;
+  var m_col_var : vec3<f32>;
+  var m_element_var : f32;
+  full_var = tint_unpack_vec3_in_composite_6(arr_s);
+  struct_var = tint_unpack_vec3_in_composite_5(arr_s[0]);
+  outer_arr_v_var = tint_unpack_vec3_in_composite_1(arr_s[0].arr_v);
+  inner_arr_v_var = tint_unpack_vec3_in_composite(arr_s[0].arr_v[1]);
+  v_var = vec3<f32>(arr_s[0].arr_v[1][2].elements);
+  v_element_var = arr_s[0].arr_v[1][2].elements.y;
+  outer_arr_m_var = tint_unpack_vec3_in_composite_4(arr_s[0].arr_m);
+  inner_arr_m_var = tint_unpack_vec3_in_composite_3(arr_s[0].arr_m[1]);
+  m_var = tint_unpack_vec3_in_composite_2(arr_s[0].arr_m[1][2]);
+  m_col_var = vec3<f32>(arr_s[0].arr_m[1][2][0].elements);
+  m_element_var = arr_s[0].arr_m[1][2][0].elements.y;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, RuntimeSizedArray) {
+    auto* src = R"(
+struct S {
+  arr : array<vec3<f32>>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<vec3<f32>>;
+@group(0) @binding(1) var<storage, read_write> s : S;
+
+fn main() {
+  s.arr[0] = arr_v[0];
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  arr : array<tint_packed_vec3_f32_array_element>,
+}
+
+struct S {
+  arr : array<vec3<f32>>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element>;
+
+@group(0) @binding(1) var<storage, read_write> s : S_tint_packed_vec3;
+
+fn main() {
+  s.arr[0].elements = arr_v[0].elements;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Mat3x3_F16_Uniform) {
+    // Test that array element alignment validation rules do not trigger when we rewrite an f16
+    // matrix into an array of vec3s in uniform storage.
+    auto* src = R"(
+enable f16;
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<uniform> m : mat3x3<f16>;
+
+fn g(p : ptr<uniform, mat3x3<f16>>) -> vec3<f16> {
+  return (*p)[0] + vec3<f16>(1);
+}
+
+fn f() {
+  let v = g(&m);
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable f16;
+enable chromium_experimental_full_ptr_parameters;
+
+struct tint_packed_vec3_f16_array_element {
+  @align(8)
+  elements : __packed_vec3<f16>,
+}
+
+@group(0) @binding(0) var<uniform> m : array<tint_packed_vec3_f16_array_element, 3u>;
+
+fn g(p : ptr<uniform, array<tint_packed_vec3_f16_array_element, 3u>>) -> vec3<f16> {
+  return (vec3<f16>((*(p))[0].elements) + vec3<f16>(1));
+}
+
+fn f() {
+  let v = g(&(m));
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MultipleComponentTypes_StructMembers) {
+    auto* src = R"(
+enable f16;
+
+struct S {
+  f : vec3f,
+  h : vec3h,
+  i : vec3i,
+  u : vec3u,
+}
+
+@group(0) @binding(0) var<storage, read_write> s : S;
+
+fn f() {
+  let f = s.f;
+  let h = s.h;
+  let i = s.i;
+  let u = s.u;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable f16;
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  f : __packed_vec3<f32>,
+  @align(8)
+  h : __packed_vec3<f16>,
+  @align(16)
+  i : __packed_vec3<i32>,
+  @align(16)
+  u : __packed_vec3<u32>,
+}
+
+struct S {
+  f : vec3f,
+  h : vec3h,
+  i : vec3i,
+  u : vec3u,
+}
+
+@group(0) @binding(0) var<storage, read_write> s : S_tint_packed_vec3;
+
+fn f() {
+  let f = vec3<f32>(s.f);
+  let h = vec3<f16>(s.h);
+  let i = vec3<i32>(s.i);
+  let u = vec3<u32>(s.u);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, MultipleComponentTypes_ArrayElement) {
+    auto* src = R"(
+enable f16;
+
+@group(0) @binding(0) var<storage, read_write> arr_f : array<vec3f>;
+@group(0) @binding(1) var<storage, read_write> arr_h : array<vec3h>;
+@group(0) @binding(2) var<storage, read_write> arr_i : array<vec3i>;
+@group(0) @binding(3) var<storage, read_write> arr_u : array<vec3u>;
+
+fn main() {
+  let f = arr_f[0];
+  let h = arr_h[0];
+  let i = arr_i[0];
+  let u = arr_u[0];
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+enable f16;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct tint_packed_vec3_f16_array_element {
+  @align(8)
+  elements : __packed_vec3<f16>,
+}
+
+struct tint_packed_vec3_i32_array_element {
+  @align(16)
+  elements : __packed_vec3<i32>,
+}
+
+struct tint_packed_vec3_u32_array_element {
+  @align(16)
+  elements : __packed_vec3<u32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> arr_f : array<tint_packed_vec3_f32_array_element>;
+
+@group(0) @binding(1) var<storage, read_write> arr_h : array<tint_packed_vec3_f16_array_element>;
+
+@group(0) @binding(2) var<storage, read_write> arr_i : array<tint_packed_vec3_i32_array_element>;
+
+@group(0) @binding(3) var<storage, read_write> arr_u : array<tint_packed_vec3_u32_array_element>;
+
+fn main() {
+  let f = vec3<f32>(arr_f[0].elements);
+  let h = vec3<f16>(arr_h[0].elements);
+  let i = vec3<i32>(arr_i[0].elements);
+  let u = vec3<u32>(arr_u[0].elements);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Arithmetic_FromRef) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> buffer_v : vec3<f32>;
+@group(0) @binding(1) var<storage, read_write> buffer_m : mat3x3<f32>;
+@group(0) @binding(2) var<storage, read_write> buffer_arr_v : array<vec3<f32>, 4>;
+@group(0) @binding(3) var<storage, read_write> buffer_arr_m : array<mat3x3<f32>, 4>;
+@group(0) @binding(4) var<storage, read_write> buffer_nested_arr_v : array<array<vec3<f32>, 4>, 4>;
+
+fn f() {
+  var v : vec3<f32> = buffer_v * 2;
+  v = -v;
+  v = buffer_m * v;
+  v = buffer_m[0] + v;
+  v = buffer_arr_v[0] + v;
+  v = buffer_arr_m[0] * v;
+  v = buffer_arr_m[0][1] + v;
+  v = buffer_nested_arr_v[0][0] + v;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer_v : __packed_vec3<f32>;
+
+@group(0) @binding(1) var<storage, read_write> buffer_m : array<tint_packed_vec3_f32_array_element, 3u>;
+
+@group(0) @binding(2) var<storage, read_write> buffer_arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(3) var<storage, read_write> buffer_arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(4) var<storage, read_write> buffer_nested_arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>;
+
+fn f() {
+  var v : vec3<f32> = (vec3<f32>(buffer_v) * 2);
+  v = -(v);
+  v = (tint_unpack_vec3_in_composite(buffer_m) * v);
+  v = (vec3<f32>(buffer_m[0].elements) + v);
+  v = (vec3<f32>(buffer_arr_v[0].elements) + v);
+  v = (tint_unpack_vec3_in_composite(buffer_arr_m[0]) * v);
+  v = (vec3<f32>(buffer_arr_m[0][1].elements) + v);
+  v = (vec3<f32>(buffer_nested_arr_v[0][0].elements) + v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Arithmetic_FromValue) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> buffer_v : vec3f;
+
+fn f() {
+  var v : vec3f = buffer_v;
+  v = -vec3f(1, 2, 3);
+  v = mat3x3f() * v;
+  v = mat3x3f()[0] + v;
+  v = array<vec3f, 4>()[0] + v;
+  v = array<mat3x3f, 4>()[0] * v;
+  v = array<mat3x3f, 4>()[0][1] + v;
+  v = array<array<vec3f, 4>, 4>()[0][0] + v;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+@group(0) @binding(0) var<storage, read_write> buffer_v : __packed_vec3<f32>;
+
+fn f() {
+  var v : vec3f = vec3<f32>(buffer_v);
+  v = -(vec3f(1, 2, 3));
+  v = (mat3x3f() * v);
+  v = (mat3x3f()[0] + v);
+  v = (array<vec3f, 4>()[0] + v);
+  v = (array<mat3x3f, 4>()[0] * v);
+  v = (array<mat3x3f, 4>()[0][1] + v);
+  v = (array<array<vec3f, 4>, 4>()[0][0] + v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Arithmetic_FromRefStruct) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+  nested_arr_v : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var v : vec3<f32> = buffer.v * 2;
+  v = -v;
+  v = buffer.m * v;
+  v = buffer.m[0] + v;
+  v = buffer.arr_v[0] + v;
+  v = buffer.arr_m[0] * v;
+  v = buffer.arr_m[0][1] + v;
+  v = buffer.nested_arr_v[0][0] + v;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+  @align(16)
+  nested_arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 3u>) -> mat3x3<f32> {
+  var result : mat3x3<f32>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+  nested_arr_v : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var v : vec3<f32> = (vec3<f32>(buffer.v) * 2);
+  v = -(v);
+  v = (tint_unpack_vec3_in_composite(buffer.m) * v);
+  v = (vec3<f32>(buffer.m[0].elements) + v);
+  v = (vec3<f32>(buffer.arr_v[0].elements) + v);
+  v = (tint_unpack_vec3_in_composite(buffer.arr_m[0]) * v);
+  v = (vec3<f32>(buffer.arr_m[0][1].elements) + v);
+  v = (vec3<f32>(buffer.nested_arr_v[0][0].elements) + v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Arithmetic_FromValueStruct) {
+    auto* src = R"(
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+  nested_arr_v : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn f() {
+  var v : vec3<f32> = S().v;
+  v = -S().v;
+  v = S().m * v;
+  v = S().m[0] + v;
+  v = S().arr_v[0] + v;
+  v = S().arr_m[0] * v;
+  v = S().arr_m[0][1] + v;
+  v = S().nested_arr_v[0][0] + v;
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : __packed_vec3<f32>,
+  @align(16)
+  m : array<tint_packed_vec3_f32_array_element, 3u>,
+  @align(16)
+  arr_v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+  @align(16)
+  nested_arr_v : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+}
+
+struct S {
+  v : vec3<f32>,
+  m : mat3x3<f32>,
+  arr_v : array<vec3<f32>, 4>,
+  arr_m : array<mat3x3<f32>, 4>,
+  nested_arr_v : array<array<vec3<f32>, 4>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S_tint_packed_vec3;
+
+fn f() {
+  var v : vec3<f32> = S().v;
+  v = -(S().v);
+  v = (S().m * v);
+  v = (S().m[0] + v);
+  v = (S().arr_v[0] + v);
+  v = (S().arr_m[0] * v);
+  v = (S().arr_m[0][1] + v);
+  v = (S().nested_arr_v[0][0] + v);
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Aliases) {
+    auto* src = R"(
+alias VecArray = array<vec3<f32>, 4>;
+alias MatArray = array<mat3x3<f32>, 4>;
+alias NestedArray = array<VecArray, 4>;
+
+struct S {
+  v : VecArray,
+  m : MatArray,
+  n : NestedArray,
+}
+
+@group(0) @binding(0) var<storage, read_write> s : S;
+@group(0) @binding(1) var<storage, read_write> arr_v : VecArray;
+@group(0) @binding(2) var<storage, read_write> arr_m : MatArray;
+@group(0) @binding(3) var<storage, read_write> arr_n : NestedArray;
+
+fn g(p : ptr<function, VecArray>) {
+}
+
+fn f() {
+  var f_arr_v : VecArray = s.v;
+  g(&f_arr_v);
+
+  arr_v = s.v;
+  arr_m[0] = s.m[0];
+  arr_n[1][2] = s.n[1][2];
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct tint_packed_vec3_f32_array_element {
+  @align(16)
+  elements : __packed_vec3<f32>,
+}
+
+struct S_tint_packed_vec3 {
+  @align(16)
+  v : array<tint_packed_vec3_f32_array_element, 4u>,
+  @align(16)
+  m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>,
+  @align(16)
+  n : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>,
+}
+
+fn tint_unpack_vec3_in_composite(in : array<tint_packed_vec3_f32_array_element, 4u>) -> array<vec3<f32>, 4u> {
+  var result : array<vec3<f32>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    result[i] = vec3<f32>(in[i].elements);
+  }
+  return result;
+}
+
+alias VecArray = array<vec3<f32>, 4>;
+
+alias MatArray = array<mat3x3<f32>, 4>;
+
+alias NestedArray = array<VecArray, 4>;
+
+struct S {
+  v : VecArray,
+  m : MatArray,
+  n : NestedArray,
+}
+
+@group(0) @binding(0) var<storage, read_write> s : S_tint_packed_vec3;
+
+@group(0) @binding(1) var<storage, read_write> arr_v : array<tint_packed_vec3_f32_array_element, 4u>;
+
+@group(0) @binding(2) var<storage, read_write> arr_m : array<array<tint_packed_vec3_f32_array_element, 3u>, 4u>;
+
+@group(0) @binding(3) var<storage, read_write> arr_n : array<array<tint_packed_vec3_f32_array_element, 4u>, 4u>;
+
+fn g(p : ptr<function, VecArray>) {
+}
+
+fn f() {
+  var f_arr_v : VecArray = tint_unpack_vec3_in_composite(s.v);
+  g(&(f_arr_v));
+  arr_v = s.v;
+  arr_m[0] = s.m[0];
+  arr_n[1][2].elements = s.n[1][2].elements;
+}
+)";
+
+    DataMap data;
+    auto got = Run<PackedVec3>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PackedVec3Test, Vec3Bool) {
+    // Make sure that we don't rewrite vec3<bool> types, as the `packed_bool<n>` types are reserved
+    // in MSL and might not be supported everywhere.
+    auto* src = R"(
+struct S {
+  vf : vec3<f32>,
+  af : array<vec3<f32>, 4>,
+  vb : vec3<bool>,
+  ab : array<vec3<bool>, 4>,
+}
+
+// Create a vec3 storage buffer so that the transform is not skipped.
+@group(0) @binding(0) var<storage, read_write> buffer : vec3<f32>;
+
+fn f() {
+  var f : S;
+  f.vf = buffer;
+  buffer = f.af[0];
+}
+)";
+
+    auto* expect =
+        R"(
+enable chromium_internal_relaxed_uniform_layout;
+
+struct S {
+  vf : vec3<f32>,
+  af : array<vec3<f32>, 4>,
+  vb : vec3<bool>,
+  ab : array<vec3<bool>, 4>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : __packed_vec3<f32>;
+
+fn f() {
+  var f : S;
+  f.vf = vec3<f32>(buffer);
+  buffer = __packed_vec3<f32>(f.af[0]);
 }
 )";
 
diff --git a/src/tint/transform/preserve_padding.cc b/src/tint/transform/preserve_padding.cc
index cb5a8ee..fbe785c 100644
--- a/src/tint/transform/preserve_padding.cc
+++ b/src/tint/transform/preserve_padding.cc
@@ -147,6 +147,18 @@
                     return body;
                 });
             },
+            [&](const type::Matrix* mat) {
+                // Call a helper function that assigns each column separately.
+                return call_helper([&]() {
+                    utils::Vector<const ast::Statement*, 4> body;
+                    for (uint32_t i = 0; i < mat->columns(); i++) {
+                        body.Push(MakeAssignment(mat->ColumnType(),
+                                                 b.IndexAccessor(b.Deref(kDestParamName), u32(i)),
+                                                 b.IndexAccessor(kValueParamName, u32(i))));
+                    }
+                    return body;
+                });
+            },
             [&](const sem::Struct* str) {
                 // Call a helper function that assigns each member separately.
                 return call_helper([&]() {
@@ -179,6 +191,13 @@
                 }
                 return HasPadding(elem_ty);
             },
+            [&](const type::Matrix* mat) {
+                auto* col_ty = mat->ColumnType();
+                if (mat->ColumnStride() > col_ty->Size()) {
+                    return true;
+                }
+                return HasPadding(col_ty);
+            },
             [&](const sem::Struct* str) {
                 uint32_t current_offset = 0;
                 for (auto* member : str->Members()) {
diff --git a/src/tint/transform/preserve_padding_test.cc b/src/tint/transform/preserve_padding_test.cc
index 9405210..2b4869a 100644
--- a/src/tint/transform/preserve_padding_test.cc
+++ b/src/tint/transform/preserve_padding_test.cc
@@ -466,6 +466,125 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(PreservePaddingTest, Mat3x3) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+@compute @workgroup_size(1)
+fn foo() {
+  m = mat3x3<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage, read_write> m : mat3x3<f32>;
+
+fn assign_and_preserve_padding(dest : ptr<storage, mat3x3<f32>, read_write>, value : mat3x3<f32>) {
+  (*(dest))[0u] = value[0u];
+  (*(dest))[1u] = value[1u];
+  (*(dest))[2u] = value[2u];
+}
+
+@compute @workgroup_size(1)
+fn foo() {
+  assign_and_preserve_padding(&(m), mat3x3<f32>());
+}
+)";
+
+    auto got = Run<PreservePadding>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PreservePaddingTest, Mat3x3_InStruct) {
+    auto* src = R"(
+struct S {
+  a : u32,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+@compute @workgroup_size(1)
+fn foo() {
+  buffer = S();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+struct S {
+  a : u32,
+  m : mat3x3<f32>,
+}
+
+@group(0) @binding(0) var<storage, read_write> buffer : S;
+
+fn assign_and_preserve_padding_1(dest : ptr<storage, mat3x3<f32>, read_write>, value : mat3x3<f32>) {
+  (*(dest))[0u] = value[0u];
+  (*(dest))[1u] = value[1u];
+  (*(dest))[2u] = value[2u];
+}
+
+fn assign_and_preserve_padding(dest : ptr<storage, S, read_write>, value : S) {
+  (*(dest)).a = value.a;
+  assign_and_preserve_padding_1(&((*(dest)).m), value.m);
+}
+
+@compute @workgroup_size(1)
+fn foo() {
+  assign_and_preserve_padding(&(buffer), S());
+}
+)";
+
+    auto got = Run<PreservePadding>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(PreservePaddingTest, ArrayOfMat3x3) {
+    auto* src = R"(
+@group(0) @binding(0) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+
+@compute @workgroup_size(1)
+fn foo() {
+  arr_m = array<mat3x3<f32>, 4>();
+  arr_m[0] = mat3x3<f32>();
+}
+)";
+
+    auto* expect = R"(
+enable chromium_experimental_full_ptr_parameters;
+
+@group(0) @binding(0) var<storage, read_write> arr_m : array<mat3x3<f32>, 4>;
+
+fn assign_and_preserve_padding_1(dest : ptr<storage, mat3x3<f32>, read_write>, value : mat3x3<f32>) {
+  (*(dest))[0u] = value[0u];
+  (*(dest))[1u] = value[1u];
+  (*(dest))[2u] = value[2u];
+}
+
+fn assign_and_preserve_padding(dest : ptr<storage, array<mat3x3<f32>, 4u>, read_write>, value : array<mat3x3<f32>, 4u>) {
+  for(var i = 0u; (i < 4u); i = (i + 1u)) {
+    assign_and_preserve_padding_1(&((*(dest))[i]), value[i]);
+  }
+}
+
+@compute @workgroup_size(1)
+fn foo() {
+  assign_and_preserve_padding(&(arr_m), array<mat3x3<f32>, 4>());
+  assign_and_preserve_padding_1(&(arr_m[0]), mat3x3<f32>());
+}
+)";
+
+    auto got = Run<PreservePadding>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 TEST_F(PreservePaddingTest, NoModify_Vec3) {
     auto* src = R"(
 @group(0) @binding(0) var<storage, read_write> v : vec3<u32>;
@@ -524,23 +643,6 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(PreservePaddingTest, NoModify_Mat3x3) {
-    auto* src = R"(
-@group(0) @binding(0) var<storage, read_write> v : mat3x3<f32>;
-
-@compute @workgroup_size(1)
-fn foo() {
-  v = mat3x3<f32>();
-}
-)";
-
-    auto* expect = src;
-
-    auto got = Run<PreservePadding>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
 TEST_F(PreservePaddingTest, NoModify_StructNoPadding) {
     auto* src = R"(
 struct S {
diff --git a/src/tint/transform/promote_initializers_to_let.cc b/src/tint/transform/promote_initializers_to_let.cc
index e201f11..cb18e04 100644
--- a/src/tint/transform/promote_initializers_to_let.cc
+++ b/src/tint/transform/promote_initializers_to_let.cc
@@ -52,6 +52,11 @@
             // Follow const-chains
             auto* root_expr = expr;
             if (expr->Stage() == sem::EvaluationStage::kConstant) {
+                if (expr->Type()->HoldsAbstract()) {
+                    // Do not hoist expressions that are not materialized, as doing so would cause
+                    // premature materialization.
+                    return false;
+                }
                 while (auto* user = root_expr->UnwrapMaterialize()->As<sem::VariableUser>()) {
                     root_expr = user->Variable()->Initializer();
                 }
diff --git a/src/tint/transform/promote_initializers_to_let_test.cc b/src/tint/transform/promote_initializers_to_let_test.cc
index 1198067..539f513 100644
--- a/src/tint/transform/promote_initializers_to_let_test.cc
+++ b/src/tint/transform/promote_initializers_to_let_test.cc
@@ -1335,5 +1335,17 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(PromoteInitializersToLetTest, AssignAbstractArray_ToPhony) {
+    // Test that we do not try to hoist an abstract array expression that is the RHS of a phony
+    // assignment, as its type will not be materialized.
+    auto* src = R"(
+fn f() {
+  _ = array(1, 2, 3, 4);
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<PromoteInitializersToLet>(src));
+}
+
 }  // namespace
 }  // namespace tint::transform
diff --git a/src/tint/transform/renamer_test.cc b/src/tint/transform/renamer_test.cc
index 8ff6898..fc34251 100644
--- a/src/tint/transform/renamer_test.cc
+++ b/src/tint/transform/renamer_test.cc
@@ -1714,7 +1714,7 @@
     for (auto* ty : builtin::kBuiltinStrings) {
         std::string_view type(ty);
         if (type != "ptr" && type != "atomic" && !utils::HasPrefix(type, "sampler") &&
-            !utils::HasPrefix(type, "texture")) {
+            !utils::HasPrefix(type, "texture") && !utils::HasPrefix(type, "__")) {
             out.push_back(ty);
         }
     }
@@ -1924,7 +1924,9 @@
 std::vector<const char*> Identifiers() {
     std::vector<const char*> out;
     for (auto* ident : builtin::kBuiltinStrings) {
-        out.push_back(ident);
+        if (!utils::HasPrefix(ident, "__")) {
+            out.push_back(ident);
+        }
     }
     for (auto* ident : builtin::kAddressSpaceStrings) {
         if (!utils::HasPrefix(ident, "_")) {
diff --git a/src/tint/transform/transform.cc b/src/tint/transform/transform.cc
index cc09e6c..ad954ee 100644
--- a/src/tint/transform/transform.cc
+++ b/src/tint/transform/transform.cc
@@ -17,6 +17,7 @@
 #include <algorithm>
 #include <string>
 
+#include "src/tint/builtin/builtin.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/sem/block_statement.h"
 #include "src/tint/sem/for_loop_statement.h"
@@ -98,7 +99,12 @@
     }
     if (auto* v = ty->As<type::Vector>()) {
         auto el = CreateASTTypeFor(ctx, v->type());
-        return ctx.dst->ty.vec(el, v->Width());
+        if (v->Packed()) {
+            TINT_ASSERT(Transform, v->Width() == 3u);
+            return ctx.dst->ty(builtin::Builtin::kPackedVec3, el);
+        } else {
+            return ctx.dst->ty.vec(el, v->Width());
+        }
     }
     if (auto* a = ty->As<type::Array>()) {
         auto el = CreateASTTypeFor(ctx, a->ElemType());
diff --git a/src/tint/transform/utils/hoist_to_decl_before.cc b/src/tint/transform/utils/hoist_to_decl_before.cc
index 28f6b36..0c2c6eb 100644
--- a/src/tint/transform/utils/hoist_to_decl_before.cc
+++ b/src/tint/transform/utils/hoist_to_decl_before.cc
@@ -44,10 +44,11 @@
 
         switch (kind) {
             case VariableKind::kLet: {
-                auto builder = [this, expr, name] {
-                    return b.Decl(b.Let(
-                        name, Transform::CreateASTTypeFor(ctx, ctx.src->Sem().GetVal(expr)->Type()),
-                        ctx.CloneWithoutTransform(expr)));
+                auto* ty = ctx.src->Sem().GetVal(expr)->Type();
+                TINT_ASSERT(Transform, !ty->HoldsAbstract());
+                auto builder = [this, expr, name, ty] {
+                    return b.Decl(b.Let(name, Transform::CreateASTTypeFor(ctx, ty),
+                                        ctx.CloneWithoutTransform(expr)));
                 };
                 if (!InsertBeforeImpl(before_expr->Stmt(), std::move(builder))) {
                     return false;
@@ -56,10 +57,11 @@
             }
 
             case VariableKind::kVar: {
-                auto builder = [this, expr, name] {
-                    return b.Decl(b.Var(
-                        name, Transform::CreateASTTypeFor(ctx, ctx.src->Sem().GetVal(expr)->Type()),
-                        ctx.CloneWithoutTransform(expr)));
+                auto* ty = ctx.src->Sem().GetVal(expr)->Type();
+                TINT_ASSERT(Transform, !ty->HoldsAbstract());
+                auto builder = [this, expr, name, ty] {
+                    return b.Decl(b.Var(name, Transform::CreateASTTypeFor(ctx, ty),
+                                        ctx.CloneWithoutTransform(expr)));
                 };
                 if (!InsertBeforeImpl(before_expr->Stmt(), std::move(builder))) {
                     return false;
@@ -78,7 +80,7 @@
             }
         }
 
-        // Replace the initializer expression with a reference to the let
+        // Replace the source expression with a reference to the hoisted declaration.
         ctx.Replace(expr, b.Expr(name));
         return true;
     }
diff --git a/src/tint/transform/vertex_pulling.cc b/src/tint/transform/vertex_pulling.cc
index ecc7576..09a68bb 100644
--- a/src/tint/transform/vertex_pulling.cc
+++ b/src/tint/transform/vertex_pulling.cc
@@ -26,6 +26,7 @@
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/math.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::VertexPulling);
 TINT_INSTANTIATE_TYPEINFO(tint::transform::VertexPulling::Config);
@@ -59,7 +60,7 @@
 /// @param out the std::ostream to write to
 /// @param format the VertexFormat to write
 /// @returns out so calls can be chained
-std::ostream& operator<<(std::ostream& out, VertexFormat format) {
+utils::StringStream& operator<<(utils::StringStream& out, VertexFormat format) {
     switch (format) {
         case VertexFormat::kUint8x2:
             return out << "uint8x2";
@@ -379,7 +380,7 @@
 
                 // Base types must match between the vertex stream and the WGSL variable
                 if (!IsTypeCompatible(var_dt, fmt_dt)) {
-                    std::stringstream err;
+                    utils::StringStream err;
                     err << "VertexAttributeDescriptor for location "
                         << std::to_string(attribute_desc.shader_location) << " has format "
                         << attribute_desc.format << " but shader expects "
diff --git a/src/tint/type/array.cc b/src/tint/type/array.cc
index dedf986..96c745c 100644
--- a/src/tint/type/array.cc
+++ b/src/tint/type/array.cc
@@ -21,6 +21,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Array);
 
@@ -81,7 +82,7 @@
 }
 
 std::string Array::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     if (!IsStrideImplicit()) {
         out << "@stride(" << stride_ << ") ";
     }
diff --git a/src/tint/type/atomic.cc b/src/tint/type/atomic.cc
index 0459394..39e2aae 100644
--- a/src/tint/type/atomic.cc
+++ b/src/tint/type/atomic.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/reference.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Atomic);
 
@@ -42,7 +43,7 @@
 }
 
 std::string Atomic::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "atomic<" << subtype_->FriendlyName(symbols) << ">";
     return out.str();
 }
diff --git a/src/tint/type/depth_multisampled_texture.cc b/src/tint/type/depth_multisampled_texture.cc
index 84d7c32..fc3b753 100644
--- a/src/tint/type/depth_multisampled_texture.cc
+++ b/src/tint/type/depth_multisampled_texture.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::DepthMultisampledTexture);
 
@@ -46,7 +47,7 @@
 }
 
 std::string DepthMultisampledTexture::FriendlyName(const SymbolTable&) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "texture_depth_multisampled_" << dim();
     return out.str();
 }
diff --git a/src/tint/type/depth_texture.cc b/src/tint/type/depth_texture.cc
index ca216e7..90a6127 100644
--- a/src/tint/type/depth_texture.cc
+++ b/src/tint/type/depth_texture.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::DepthTexture);
 
@@ -47,7 +48,7 @@
 }
 
 std::string DepthTexture::FriendlyName(const SymbolTable&) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "texture_depth_" << dim();
     return out.str();
 }
diff --git a/src/tint/type/matrix.cc b/src/tint/type/matrix.cc
index 76970e6..195a9e9 100644
--- a/src/tint/type/matrix.cc
+++ b/src/tint/type/matrix.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/vector.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Matrix);
 
@@ -51,7 +52,7 @@
 }
 
 std::string Matrix::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "mat" << columns_ << "x" << rows_ << "<" << subtype_->FriendlyName(symbols) << ">";
     return out.str();
 }
diff --git a/src/tint/type/multisampled_texture.cc b/src/tint/type/multisampled_texture.cc
index 182ae88..0b2dfd0 100644
--- a/src/tint/type/multisampled_texture.cc
+++ b/src/tint/type/multisampled_texture.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::MultisampledTexture);
 
@@ -40,7 +41,7 @@
 }
 
 std::string MultisampledTexture::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "texture_multisampled_" << dim() << "<" << type_->FriendlyName(symbols) << ">";
     return out.str();
 }
diff --git a/src/tint/type/pointer.cc b/src/tint/type/pointer.cc
index 17bbe1a..aa77816 100644
--- a/src/tint/type/pointer.cc
+++ b/src/tint/type/pointer.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/reference.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Pointer);
 
@@ -43,7 +44,7 @@
 }
 
 std::string Pointer::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "ptr<";
     if (address_space_ != builtin::AddressSpace::kUndefined) {
         out << address_space_ << ", ";
diff --git a/src/tint/type/reference.cc b/src/tint/type/reference.cc
index 5db6300..03ed224 100644
--- a/src/tint/type/reference.cc
+++ b/src/tint/type/reference.cc
@@ -18,6 +18,7 @@
 #include "src/tint/diagnostic/diagnostic.h"
 #include "src/tint/type/manager.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Reference);
 
@@ -44,7 +45,7 @@
 }
 
 std::string Reference::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "ref<";
     if (address_space_ != builtin::AddressSpace::kUndefined) {
         out << address_space_ << ", ";
diff --git a/src/tint/type/sampled_texture.cc b/src/tint/type/sampled_texture.cc
index b3e7375..b3e6e10 100644
--- a/src/tint/type/sampled_texture.cc
+++ b/src/tint/type/sampled_texture.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/texture_dimension.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::SampledTexture);
 
@@ -39,7 +40,7 @@
 }
 
 std::string SampledTexture::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "texture_" << dim() << "<" << type_->FriendlyName(symbols) << ">";
     return out.str();
 }
diff --git a/src/tint/type/storage_texture.cc b/src/tint/type/storage_texture.cc
index beb5da6..180db4f 100644
--- a/src/tint/type/storage_texture.cc
+++ b/src/tint/type/storage_texture.cc
@@ -19,6 +19,7 @@
 #include "src/tint/type/manager.h"
 #include "src/tint/type/u32.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::StorageTexture);
 
@@ -43,7 +44,7 @@
 }
 
 std::string StorageTexture::FriendlyName(const SymbolTable&) const {
-    std::ostringstream out;
+    utils::StringStream out;
     out << "texture_storage_" << dim() << "<" << texel_format_ << ", " << access_ << ">";
     return out.str();
 }
diff --git a/src/tint/type/struct.cc b/src/tint/type/struct.cc
index c39916b..7649691 100644
--- a/src/tint/type/struct.cc
+++ b/src/tint/type/struct.cc
@@ -22,6 +22,7 @@
 #include "src/tint/symbol_table.h"
 #include "src/tint/type/manager.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Struct);
 TINT_INSTANTIATE_TYPEINFO(tint::type::StructMember);
@@ -96,7 +97,7 @@
 }
 
 std::string Struct::Layout(const tint::SymbolTable& symbols) const {
-    std::stringstream ss;
+    utils::StringStream ss;
 
     auto member_name_of = [&](const StructMember* sm) { return symbols.NameFor(sm->Name()); };
 
diff --git a/src/tint/type/vector.cc b/src/tint/type/vector.cc
index 146c1f4..9f1f415 100644
--- a/src/tint/type/vector.cc
+++ b/src/tint/type/vector.cc
@@ -18,20 +18,22 @@
 #include "src/tint/diagnostic/diagnostic.h"
 #include "src/tint/type/manager.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::type::Vector);
 
 namespace tint::type {
 
-Vector::Vector(Type const* subtype, uint32_t width)
-    : Base(utils::Hash(TypeInfo::Of<Vector>().full_hashcode, width, subtype),
+Vector::Vector(Type const* subtype, uint32_t width, bool packed /* = false */)
+    : Base(utils::Hash(TypeInfo::Of<Vector>().full_hashcode, width, subtype, packed),
            type::Flags{
                Flag::kConstructable,
                Flag::kCreationFixedFootprint,
                Flag::kFixedFootprint,
            }),
       subtype_(subtype),
-      width_(width) {
+      width_(width),
+      packed_(packed) {
     TINT_ASSERT(Type, width_ > 1);
     TINT_ASSERT(Type, width_ < 5);
 }
@@ -40,13 +42,16 @@
 
 bool Vector::Equals(const UniqueNode& other) const {
     if (auto* v = other.As<Vector>()) {
-        return v->width_ == width_ && v->subtype_ == subtype_;
+        return v->width_ == width_ && v->subtype_ == subtype_ && v->packed_ == packed_;
     }
     return false;
 }
 
 std::string Vector::FriendlyName(const SymbolTable& symbols) const {
-    std::ostringstream out;
+    utils::StringStream out;
+    if (packed_) {
+        out << "__packed_";
+    }
     out << "vec" << width_ << "<" << subtype_->FriendlyName(symbols) << ">";
     return out.str();
 }
@@ -60,7 +65,7 @@
         case 2:
             return subtype_->Size() * 2;
         case 3:
-            return subtype_->Size() * 4;
+            return subtype_->Size() * (packed_ ? 1 : 4);
         case 4:
             return subtype_->Size() * 4;
     }
@@ -69,7 +74,7 @@
 
 Vector* Vector::Clone(CloneContext& ctx) const {
     auto* subtype = subtype_->Clone(ctx);
-    return ctx.dst.mgr->Get<Vector>(subtype, width_);
+    return ctx.dst.mgr->Get<Vector>(subtype, width_, packed_);
 }
 
 }  // namespace tint::type
diff --git a/src/tint/type/vector.h b/src/tint/type/vector.h
index acffb8c..e7fd881 100644
--- a/src/tint/type/vector.h
+++ b/src/tint/type/vector.h
@@ -22,12 +22,13 @@
 namespace tint::type {
 
 /// A vector type.
-class Vector final : public Castable<Vector, Type> {
+class Vector : public Castable<Vector, Type> {
   public:
     /// Constructor
     /// @param subtype the vector element type
     /// @param size the number of elements in the vector
-    Vector(Type const* subtype, uint32_t size);
+    /// @param packed the optional 'packed' modifier
+    Vector(Type const* subtype, uint32_t size, bool packed = false);
 
     /// Destructor
     ~Vector() override;
@@ -50,10 +51,12 @@
     /// @returns the size in bytes of the type. This may include tail padding.
     uint32_t Size() const override;
 
-    /// @returns the alignment in bytes of the type. This may include tail
-    /// padding.
+    /// @returns the alignment in bytes of the type. This may include tail padding.
     uint32_t Align() const override;
 
+    /// @returns `true` if this vector is packed, false otherwise
+    bool Packed() const { return packed_; }
+
     /// @param width the width of the vector
     /// @returns the size in bytes of a vector of the given width.
     static uint32_t SizeOf(uint32_t width);
@@ -69,6 +72,7 @@
   private:
     Type const* const subtype_;
     const uint32_t width_;
+    const bool packed_;
 };
 
 }  // namespace tint::type
diff --git a/src/tint/type/vector_test.cc b/src/tint/type/vector_test.cc
index 7dd5c9d..acc62ed 100644
--- a/src/tint/type/vector_test.cc
+++ b/src/tint/type/vector_test.cc
@@ -34,6 +34,21 @@
     EXPECT_NE(a, d);
 }
 
+TEST_F(VectorTest, Creation_Packed) {
+    auto* v = create<Vector>(create<F32>(), 3u);
+    auto* p1 = create<Vector>(create<F32>(), 3u, true);
+    auto* p2 = create<Vector>(create<F32>(), 3u, true);
+
+    EXPECT_FALSE(v->Packed());
+
+    EXPECT_EQ(p1->type(), create<F32>());
+    EXPECT_EQ(p1->Width(), 3u);
+    EXPECT_TRUE(p1->Packed());
+
+    EXPECT_NE(v, p1);
+    EXPECT_EQ(p1, p2);
+}
+
 TEST_F(VectorTest, Hash) {
     auto* a = create<Vector>(create<I32>(), 2u);
     auto* b = create<Vector>(create<I32>(), 2u);
@@ -59,6 +74,12 @@
     EXPECT_EQ(v->FriendlyName(Symbols()), "vec3<f32>");
 }
 
+TEST_F(VectorTest, FriendlyName_Packed) {
+    auto* f32 = create<F32>();
+    auto* v = create<Vector>(f32, 3u, true);
+    EXPECT_EQ(v->FriendlyName(Symbols()), "__packed_vec3<f32>");
+}
+
 TEST_F(VectorTest, Clone) {
     auto* a = create<Vector>(create<I32>(), 2u);
 
@@ -68,6 +89,19 @@
     auto* vec = a->Clone(ctx);
     EXPECT_TRUE(vec->type()->Is<I32>());
     EXPECT_EQ(vec->Width(), 2u);
+    EXPECT_FALSE(vec->Packed());
+}
+
+TEST_F(VectorTest, Clone_Packed) {
+    auto* a = create<Vector>(create<I32>(), 3u, true);
+
+    type::Manager mgr;
+    type::CloneContext ctx{{nullptr}, {nullptr, &mgr}};
+
+    auto* vec = a->Clone(ctx);
+    EXPECT_TRUE(vec->type()->Is<I32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TRUE(vec->Packed());
 }
 
 }  // namespace
diff --git a/src/tint/utils/enum_set_test.cc b/src/tint/utils/enum_set_test.cc
index 4cdeb63..a6bb169 100644
--- a/src/tint/utils/enum_set_test.cc
+++ b/src/tint/utils/enum_set_test.cc
@@ -18,6 +18,7 @@
 #include <vector>
 
 #include "gmock/gmock.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 namespace {
@@ -232,7 +233,7 @@
 }
 
 TEST(EnumSetTest, Ostream) {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << EnumSet<E>(E::A, E::C);
     EXPECT_EQ(ss.str(), "{A, C}");
 }
diff --git a/src/tint/utils/io/command_windows.cc b/src/tint/utils/io/command_windows.cc
index abe7242..31d0308 100644
--- a/src/tint/utils/io/command_windows.cc
+++ b/src/tint/utils/io/command_windows.cc
@@ -21,6 +21,7 @@
 #include <string>
 
 #include "src/tint/utils/defer.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 
@@ -197,7 +198,7 @@
     si.hStdError = stderr_pipe.write;
     si.hStdInput = stdin_pipe.read;
 
-    std::stringstream args;
+    utils::StringStream args;
     args << path_;
     for (auto& arg : arguments) {
         if (!arg.empty()) {
diff --git a/src/tint/utils/io/tmpfile.h b/src/tint/utils/io/tmpfile.h
index 24e7208..7949a37 100644
--- a/src/tint/utils/io/tmpfile.h
+++ b/src/tint/utils/io/tmpfile.h
@@ -18,6 +18,8 @@
 #include <sstream>
 #include <string>
 
+#include "src/tint/utils/string_stream.h"
+
 namespace tint::utils {
 
 /// TmpFile constructs a temporary file that can be written to, and is
@@ -55,7 +57,7 @@
     /// @return a reference to this TmpFile
     template <typename T>
     inline TmpFile& operator<<(T&& data) {
-        std::stringstream ss;
+        utils::StringStream ss;
         ss << data;
         std::string str = ss.str();
         Append(str.data(), str.size());
diff --git a/src/tint/utils/string.cc b/src/tint/utils/string.cc
index bccd52c..2f28a3e 100644
--- a/src/tint/utils/string.cc
+++ b/src/tint/utils/string.cc
@@ -50,7 +50,7 @@
 
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
-                         std::ostringstream& ss) {
+                         utils::StringStream& ss) {
     // If the string typed was within kSuggestionDistance of one of the possible enum values,
     // suggest that. Don't bother with suggestions if the string was extremely long.
     constexpr size_t kSuggestionDistance = 5;
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index 08e0be3..e77e637 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -20,6 +20,7 @@
 #include <variant>
 
 #include "src/tint/utils/slice.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 
@@ -42,7 +43,7 @@
 /// @returns value printed as a string via the std::ostream `<<` operator
 template <typename T>
 std::string ToString(const T& value) {
-    std::stringstream s;
+    utils::StringStream s;
     s << value;
     return s.str();
 }
@@ -51,7 +52,7 @@
 /// @returns value printed as a string via the std::ostream `<<` operator
 template <typename... TYs>
 std::string ToString(const std::variant<TYs...>& value) {
-    std::stringstream s;
+    utils::StringStream s;
     s << std::visit([&](auto& v) { return ToString(v); }, value);
     return s.str();
 }
@@ -74,7 +75,7 @@
 /// @param ss the stream to write the suggest and list of possible values to
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
-                         std::ostringstream& ss);
+                         utils::StringStream& ss);
 
 }  // namespace tint::utils
 
diff --git a/src/tint/utils/string_stream.cc b/src/tint/utils/string_stream.cc
new file mode 100644
index 0000000..2d1ed44
--- /dev/null
+++ b/src/tint/utils/string_stream.cc
@@ -0,0 +1,27 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::utils {
+
+StringStream::StringStream() {
+    sstream_.flags(sstream_.flags() | std::ios_base::showpoint | std::ios_base::fixed);
+    sstream_.imbue(std::locale::classic());
+    sstream_.precision(9);
+}
+
+StringStream::~StringStream() = default;
+
+}  // namespace tint::utils
diff --git a/src/tint/utils/string_stream.h b/src/tint/utils/string_stream.h
new file mode 100644
index 0000000..893d2b9
--- /dev/null
+++ b/src/tint/utils/string_stream.h
@@ -0,0 +1,123 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_UTILS_STRING_STREAM_H_
+#define SRC_TINT_UTILS_STRING_STREAM_H_
+
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+
+namespace tint::utils {
+
+/// Stringstream wrapper which automatically resets the locale and sets floating point emission
+/// settings needed for Tint.
+class StringStream {
+  public:
+    /// Constructor
+    StringStream();
+    /// Destructor
+    ~StringStream();
+
+    /// @returns the format flags for the stream
+    std::ios_base::fmtflags flags() const { return sstream_.flags(); }
+
+    /// @param flags the flags to set
+    /// @returns the original format flags
+    std::ios_base::fmtflags flags(std::ios_base::fmtflags flags) { return sstream_.flags(flags); }
+
+    /// 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) {
+        sstream_ << 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) {
+        // 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 << value;
+
+        std::string str = fixed.str();
+
+        // If this string can be parsed without loss of information, use it.
+        // (Use double here to dodge a bug in older libc++ versions which would incorrectly read
+        // back FLT_MAX as INF.)
+        double roundtripped;
+        fixed >> roundtripped;
+
+        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] != '.') {
+                str.pop_back();
+            }
+
+            sstream_ << str;
+            return *this;
+        }
+
+        // Resort to scientific, with the minimum precision needed to preserve the whole float
+        std::stringstream sci;
+        sci.imbue(std::locale::classic());
+        sci.precision(std::numeric_limits<T>::max_digits10);
+        sci << value;
+        sstream_ << sci.str();
+
+        return *this;
+    }
+
+    /// Swaps streams
+    /// @param other stream to swap too
+    void swap(StringStream& other) { sstream_.swap(other.sstream_); }
+
+    /// repeat queues the character c to be written to the printer n times.
+    /// @param c the character to print `n` times
+    /// @param n the number of times to print character `c`
+    void repeat(char c, size_t n) { std::fill_n(std::ostream_iterator<char>(sstream_), n, c); }
+
+    /// The callback to emit a `endl` to the stream
+    using StdEndl = std::ostream& (*)(std::ostream&);
+
+    /// @param manipulator the callback to emit too
+    /// @returns a reference to this
+    StringStream& operator<<(StdEndl manipulator) {
+        // call the function, and return it's value
+        manipulator(sstream_);
+        return *this;
+    }
+
+    /// @returns the string contents of the stream
+    std::string str() const { return sstream_.str(); }
+
+  private:
+    std::stringstream sstream_;
+};
+
+}  // namespace tint::utils
+
+#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
new file mode 100644
index 0000000..eebefd6
--- /dev/null
+++ b/src/tint/utils/string_stream_test.cc
@@ -0,0 +1,111 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/utils/string_stream.h"
+
+#include <math.h>
+#include <cstring>
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace tint::utils {
+namespace {
+
+using StringStreamTest = testing::Test;
+
+TEST_F(StringStreamTest, Zero) {
+    StringStream s;
+    s << 0.0f;
+    EXPECT_EQ(s.str(), "0.0");
+}
+
+TEST_F(StringStreamTest, One) {
+    StringStream s;
+    s << 1.0f;
+    EXPECT_EQ(s.str(), "1.0");
+}
+
+TEST_F(StringStreamTest, MinusOne) {
+    StringStream s;
+    s << -1.0f;
+    EXPECT_EQ(s.str(), "-1.0");
+}
+
+TEST_F(StringStreamTest, Billion) {
+    StringStream s;
+    s << 1e9f;
+    EXPECT_EQ(s.str(), "1000000000.0");
+}
+
+TEST_F(StringStreamTest, Small) {
+    StringStream s;
+    s << std::numeric_limits<float>::epsilon();
+    EXPECT_NE(s.str(), "0.0");
+}
+
+TEST_F(StringStreamTest, Highest) {
+    const auto highest = std::numeric_limits<float>::max();
+    const auto expected_highest = 340282346638528859811704183484516925440.0f;
+
+    if (highest < expected_highest || highest > expected_highest) {
+        GTEST_SKIP() << "std::numeric_limits<float>::max() is not as expected for "
+                        "this target";
+    }
+
+    StringStream s;
+    s << std::numeric_limits<float>::max();
+    EXPECT_EQ(s.str(), "340282346638528859811704183484516925440.0");
+}
+
+TEST_F(StringStreamTest, Lowest) {
+    // Some compilers complain if you test floating point numbers for equality.
+    // So say it via two inequalities.
+    const auto lowest = std::numeric_limits<float>::lowest();
+    const auto expected_lowest = -340282346638528859811704183484516925440.0f;
+    if (lowest < expected_lowest || lowest > expected_lowest) {
+        GTEST_SKIP() << "std::numeric_limits<float>::lowest() is not as expected for "
+                        "this target";
+    }
+
+    StringStream s;
+    s << std::numeric_limits<float>::lowest();
+    EXPECT_EQ(s.str(), "-340282346638528859811704183484516925440.0");
+}
+
+TEST_F(StringStreamTest, Precision) {
+    {
+        StringStream s;
+        s << 1e-8f;
+        EXPECT_EQ(s.str(), "0.00000001");
+    }
+    {
+        StringStream s;
+        s << 1e-9f;
+        EXPECT_EQ(s.str(), "0.000000001");
+    }
+    {
+        StringStream s;
+        s << 1e-10f;
+        EXPECT_EQ(s.str(), "1.00000001e-10");
+    }
+    {
+        StringStream s;
+        s << 1e-20f;
+        EXPECT_EQ(s.str(), "9.99999968e-21");
+    }
+}
+
+}  // namespace
+}  // namespace tint::utils
diff --git a/src/tint/utils/string_test.cc b/src/tint/utils/string_test.cc
index b9e1ebb..0f351cf 100644
--- a/src/tint/utils/string_test.cc
+++ b/src/tint/utils/string_test.cc
@@ -15,6 +15,7 @@
 #include "src/tint/utils/string.h"
 
 #include "gtest/gtest.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 namespace {
@@ -61,14 +62,14 @@
 TEST(StringTest, SuggestAlternatives) {
     {
         const char* alternatives[] = {"hello world", "Hello World"};
-        std::ostringstream ss;
+        utils::StringStream ss;
         SuggestAlternatives("hello wordl", alternatives, ss);
         EXPECT_EQ(ss.str(), R"(Did you mean 'hello world'?
 Possible values: 'hello world', 'Hello World')");
     }
     {
         const char* alternatives[] = {"foobar", "something else"};
-        std::ostringstream ss;
+        utils::StringStream ss;
         SuggestAlternatives("hello world", alternatives, ss);
         EXPECT_EQ(ss.str(), R"(Possible values: 'foobar', 'something else')");
     }
diff --git a/src/tint/utils/vector_test.cc b/src/tint/utils/vector_test.cc
index 6245476..0604732 100644
--- a/src/tint/utils/vector_test.cc
+++ b/src/tint/utils/vector_test.cc
@@ -20,6 +20,7 @@
 #include "gmock/gmock.h"
 
 #include "src/tint/utils/bitcast.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::utils {
 namespace {
@@ -1788,7 +1789,7 @@
 }
 
 TEST(TintVectorTest, ostream) {
-    std::stringstream ss;
+    utils::StringStream ss;
     ss << Vector{1, 2, 3};
     EXPECT_EQ(ss.str(), "[1, 2, 3]");
 }
@@ -2065,7 +2066,7 @@
 }
 
 TEST(TintVectorRefTest, ostream) {
-    std::stringstream ss;
+    utils::StringStream ss;
     Vector vec{1, 2, 3};
     const VectorRef<int> vec_ref(vec);
     ss << vec_ref;
diff --git a/src/tint/writer/float_to_string.cc b/src/tint/writer/float_to_string.cc
index 3b4260e..0ce4954 100644
--- a/src/tint/writer/float_to_string.cc
+++ b/src/tint/writer/float_to_string.cc
@@ -22,6 +22,7 @@
 #include <sstream>
 
 #include "src/tint/debug.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::writer {
 
@@ -52,35 +53,9 @@
 
 template <typename F>
 std::string ToString(F f) {
-    // 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 << f;
-    std::string str = fixed.str();
-
-    // If this string can be parsed without loss of information, use it.
-    // (Use double here to dodge a bug in older libc++ versions which would incorrectly read back
-    // FLT_MAX as INF.)
-    double roundtripped;
-    fixed >> roundtripped;
-
-    auto float_equal_no_warning = std::equal_to<F>();
-    if (float_equal_no_warning(f, static_cast<F>(roundtripped))) {
-        while (str.length() >= 2 && str[str.size() - 1] == '0' && str[str.size() - 2] != '.') {
-            str.pop_back();
-        }
-
-        return str;
-    }
-
-    // Resort to scientific, with the minimum precision needed to preserve the whole float
-    std::stringstream sci;
-    sci.imbue(std::locale::classic());
-    sci.precision(std::numeric_limits<F>::max_digits10);
-    sci << f;
-    return sci.str();
+    utils::StringStream s;
+    s << f;
+    return s.str();
 }
 
 template <typename F>
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index e8b5b9b..46428b0 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -75,6 +75,7 @@
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/append_vector.h"
 #include "src/tint/writer/float_to_string.h"
 #include "src/tint/writer/generate_external_texture_bindings.h"
@@ -111,7 +112,7 @@
     return IsAnyOf<ast::BreakStatement>(stmts->Last());
 }
 
-void PrintF32(std::ostream& out, float value) {
+void PrintF32(utils::StringStream& out, float value) {
     if (std::isinf(value)) {
         out << "0.0f " << (value >= 0 ? "/* inf */" : "/* -inf */");
     } else if (std::isnan(value)) {
@@ -121,7 +122,7 @@
     }
 }
 
-void PrintF16(std::ostream& out, float value) {
+void PrintF16(utils::StringStream& out, float value) {
     if (std::isinf(value)) {
         out << "0.0hf " << (value >= 0 ? "/* inf */" : "/* -inf */");
     } else if (std::isnan(value)) {
@@ -333,7 +334,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr) {
+bool GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
+                                      const ast::IndexAccessorExpression* expr) {
     if (!EmitExpression(out, expr->object)) {
         return false;
     }
@@ -347,7 +349,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr) {
+bool GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
     auto* src_type = TypeOf(expr->expr)->UnwrapRef();
     auto* dst_type = TypeOf(expr)->UnwrapRef();
 
@@ -399,7 +401,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitVectorRelational(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitVectorRelational(utils::StringStream& out,
+                                         const ast::BinaryExpression* expr) {
     switch (expr->op) {
         case ast::BinaryOp::kEqual:
             out << "equal";
@@ -433,7 +436,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBitwiseBoolOp(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr) {
     auto* bool_type = TypeOf(expr->lhs)->UnwrapRef();
     auto* uint_type = BoolTypeToUint(bool_type);
 
@@ -479,7 +482,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitFloatModulo(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr) {
     std::string fn;
     auto* ret_ty = TypeOf(expr)->UnwrapRef();
     auto* lhs_ty = TypeOf(expr->lhs)->UnwrapRef();
@@ -541,7 +544,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
     if (IsRelational(expr->op) && !TypeOf(expr->lhs)->UnwrapRef()->is_scalar()) {
         return EmitVectorRelational(out, expr);
     }
@@ -706,7 +709,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* call = builder_.Sem().Get<sem::Call>(expr);
     return Switch(
         call->Target(),  //
@@ -721,7 +724,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFunctionCall(std::ostream& out,
+bool GeneratorImpl::EmitFunctionCall(utils::StringStream& out,
                                      const sem::Call* call,
                                      const sem::Function* fn) {
     const auto& args = call->Arguments();
@@ -745,7 +748,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBuiltinCall(std::ostream& out,
+bool GeneratorImpl::EmitBuiltinCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     auto* expr = call->Declaration();
@@ -827,7 +830,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitValueConversion(std::ostream& out,
+bool GeneratorImpl::EmitValueConversion(utils::StringStream& out,
                                         const sem::Call* call,
                                         const sem::ValueConversion* conv) {
     if (!EmitType(out, conv->Target(), builtin::AddressSpace::kUndefined,
@@ -843,7 +846,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitValueConstructor(std::ostream& out,
+bool GeneratorImpl::EmitValueConstructor(utils::StringStream& out,
                                          const sem::Call* call,
                                          const sem::ValueConstructor* ctor) {
     auto* type = ctor->ReturnType();
@@ -874,7 +877,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitWorkgroupAtomicCall(std::ostream& out,
+bool GeneratorImpl::EmitWorkgroupAtomicCall(utils::StringStream& out,
                                             const ast::CallExpression* expr,
                                             const sem::Builtin* builtin) {
     auto call = [&](const char* name) {
@@ -995,7 +998,7 @@
     return false;
 }
 
-bool GeneratorImpl::EmitArrayLength(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "uint(";
     if (!EmitExpression(out, expr->args[0])) {
         return false;
@@ -1004,7 +1007,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitExtractBits(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "bitfieldExtract(";
     if (!EmitExpression(out, expr->args[0])) {
         return false;
@@ -1021,7 +1024,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitInsertBits(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "bitfieldInsert(";
     if (!EmitExpression(out, expr->args[0])) {
         return false;
@@ -1042,7 +1045,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitEmulatedFMA(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr) {
     out << "((";
     if (!EmitExpression(out, expr->args[0])) {
         return false;
@@ -1059,7 +1062,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitCountOneBitsCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCountOneBitsCall(utils::StringStream& out,
+                                         const ast::CallExpression* expr) {
     // GLSL's bitCount returns an integer type, so cast it to the appropriate
     // unsigned type.
     if (!EmitType(out, TypeOf(expr)->UnwrapRef(), builtin::AddressSpace::kUndefined,
@@ -1075,7 +1079,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitSelectCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitSelectCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* expr_false = expr->args[0];
     auto* expr_true = expr->args[1];
     auto* expr_cond = expr->args[2];
@@ -1117,7 +1121,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitDotCall(std::ostream& out,
+bool GeneratorImpl::EmitDotCall(utils::StringStream& out,
                                 const ast::CallExpression* expr,
                                 const sem::Builtin* builtin) {
     auto* vec_ty = builtin->Parameters()[0]->Type()->As<type::Vector>();
@@ -1133,7 +1137,7 @@
 
             std::string v;
             {
-                std::stringstream s;
+                utils::StringStream s;
                 if (!EmitType(s, vec_ty->type(), builtin::AddressSpace::kUndefined,
                               builtin::Access::kRead, "")) {
                     return "";
@@ -1190,7 +1194,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitModfCall(std::ostream& out,
+bool GeneratorImpl::EmitModfCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin) {
     TINT_ASSERT(Writer, expr->args.Length() == 1);
@@ -1216,7 +1220,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
+bool GeneratorImpl::EmitFrexpCall(utils::StringStream& out,
                                   const ast::CallExpression* expr,
                                   const sem::Builtin* builtin) {
     TINT_ASSERT(Writer, expr->args.Length() == 1);
@@ -1242,7 +1246,7 @@
         });
 }
 
-bool GeneratorImpl::EmitDegreesCall(std::ostream& out,
+bool GeneratorImpl::EmitDegreesCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     auto* return_elem_type = type::Type::DeepestElementOf(builtin->ReturnType());
@@ -1255,7 +1259,7 @@
                              });
 }
 
-bool GeneratorImpl::EmitRadiansCall(std::ostream& out,
+bool GeneratorImpl::EmitRadiansCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     auto* return_elem_type = type::Type::DeepestElementOf(builtin->ReturnType());
@@ -1268,7 +1272,7 @@
                              });
 }
 
-bool GeneratorImpl::EmitQuantizeToF16Call(std::ostream& out,
+bool GeneratorImpl::EmitQuantizeToF16Call(utils::StringStream& out,
                                           const ast::CallExpression* expr,
                                           const sem::Builtin* builtin) {
     // Emulate by casting to f16 and back again.
@@ -1300,7 +1304,7 @@
         });
 }
 
-bool GeneratorImpl::EmitBarrierCall(std::ostream& out, const sem::Builtin* builtin) {
+bool GeneratorImpl::EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin) {
     // TODO(crbug.com/tint/661): Combine sequential barriers to a single
     // instruction.
     if (builtin->Type() == sem::BuiltinType::kWorkgroupBarrier) {
@@ -1325,7 +1329,7 @@
     return zero;
 }
 
-bool GeneratorImpl::EmitTextureCall(std::ostream& out,
+bool GeneratorImpl::EmitTextureCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     using Usage = sem::ParameterUsage;
@@ -1818,7 +1822,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, const ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
     if (auto* sem = builder_.Sem().GetVal(expr)) {
         if (auto* constant = sem->ConstantValue()) {
             return EmitConstant(out, constant);
@@ -1841,7 +1845,8 @@
         });
 }
 
-bool GeneratorImpl::EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr) {
+bool GeneratorImpl::EmitIdentifier(utils::StringStream& out,
+                                   const ast::IdentifierExpression* expr) {
     out << builder_.Symbols().NameFor(expr->identifier->symbol);
     return true;
 }
@@ -2187,7 +2192,7 @@
 }
 
 void GeneratorImpl::EmitInterpolationQualifiers(
-    std::ostream& out,
+    utils::StringStream& out,
     utils::VectorRef<const ast::Attribute*> attributes) {
     for (auto* attr : attributes) {
         if (auto* interpolate = attr->As<ast::InterpolateAttribute>()) {
@@ -2223,7 +2228,7 @@
     }
 }
 
-bool GeneratorImpl::EmitAttributes(std::ostream& out,
+bool GeneratorImpl::EmitAttributes(utils::StringStream& out,
                                    const sem::GlobalVariable* var,
                                    utils::VectorRef<const ast::Attribute*> attributes) {
     if (attributes.IsEmpty()) {
@@ -2333,7 +2338,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitConstant(std::ostream& out, const constant::Value* constant) {
+bool GeneratorImpl::EmitConstant(utils::StringStream& out, const constant::Value* constant) {
     return Switch(
         constant->Type(),  //
         [&](const type::Bool*) {
@@ -2450,7 +2455,7 @@
         });
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit) {
+bool GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
     return Switch(
         lit,
         [&](const ast::BoolLiteralExpression* l) {
@@ -2478,7 +2483,7 @@
         });
 }
 
-bool GeneratorImpl::EmitZeroValue(std::ostream& out, const type::Type* type) {
+bool GeneratorImpl::EmitZeroValue(utils::StringStream& out, const type::Type* type) {
     if (type->Is<type::Bool>()) {
         out << "false";
     } else if (type->Is<type::F32>()) {
@@ -2604,7 +2609,7 @@
     }
 
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
     if (auto* cond = stmt->condition) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
         if (!EmitExpression(cond_buf, cond)) {
@@ -2696,7 +2701,7 @@
 
 bool GeneratorImpl::EmitWhile(const ast::WhileStatement* stmt) {
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
     {
         auto* cond = stmt->condition;
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
@@ -2745,7 +2750,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
+bool GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
                                        const ast::MemberAccessorExpression* expr) {
     if (!EmitExpression(out, expr->object)) {
         return false;
@@ -2857,7 +2862,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitType(std::ostream& out,
+bool GeneratorImpl::EmitType(utils::StringStream& out,
                              const type::Type* type,
                              builtin::AddressSpace address_space,
                              builtin::Access access,
@@ -3038,7 +3043,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitTypeAndName(std::ostream& out,
+bool GeneratorImpl::EmitTypeAndName(utils::StringStream& out,
                                     const type::Type* type,
                                     builtin::AddressSpace address_space,
                                     builtin::Access access,
@@ -3086,7 +3091,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr) {
+bool GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
     switch (expr->op) {
         case ast::UnaryOp::kIndirection:
         case ast::UnaryOp::kAddressOf:
@@ -3182,7 +3187,7 @@
 }
 
 template <typename F>
-bool GeneratorImpl::CallBuiltinHelper(std::ostream& out,
+bool GeneratorImpl::CallBuiltinHelper(utils::StringStream& out,
                                       const ast::CallExpression* call,
                                       const sem::Builtin* builtin,
                                       F&& build) {
diff --git a/src/tint/writer/glsl/generator_impl.h b/src/tint/writer/glsl/generator_impl.h
index d220877..d954921 100644
--- a/src/tint/writer/glsl/generator_impl.h
+++ b/src/tint/writer/glsl/generator_impl.h
@@ -37,6 +37,7 @@
 #include "src/tint/scope_stack.h"
 #include "src/tint/transform/decompose_memory_access.h"
 #include "src/tint/utils/hash.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/generator.h"
 #include "src/tint/writer/glsl/version.h"
 #include "src/tint/writer/text_generator.h"
@@ -93,7 +94,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the index accessor was emitted
-    bool EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr);
+    bool EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
     /// Handles an assignment statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -102,27 +103,27 @@
     /// @param out the output of the expression stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitBitwiseBoolOp(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitBitwiseBoolOp(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a binary expression
     /// @param out the output of the expression stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitFloatModulo(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitFloatModulo(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating the modulo operator on float vector operands
     /// @param out the output of the expression stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a bitcast expression
     /// @param out the output of the expression stream
     /// @param expr the expression
     /// @returns true if the binary expression was emitted
-    bool EmitVectorRelational(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitVectorRelational(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a vector relational expression
     /// @param out the output of the expression stream
     /// @param expr the expression
     /// @returns true if the vector relational expression was emitted
-    bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
+    bool EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
     /// Emits a list of statements
     /// @param stmts the statement list
     /// @returns true if the statements were emitted successfully
@@ -147,25 +148,27 @@
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a function call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param fn the function being called
     /// @returns true if the expression is emitted
-    bool EmitFunctionCall(std::ostream& out, const sem::Call* call, const sem::Function* fn);
+    bool EmitFunctionCall(utils::StringStream& out, const sem::Call* call, const sem::Function* fn);
     /// Handles generating a builtin call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param builtin the builtin being called
     /// @returns true if the expression is emitted
-    bool EmitBuiltinCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitBuiltinCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a value conversion expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param conv the value conversion
     /// @returns true if the expression is emitted
-    bool EmitValueConversion(std::ostream& out,
+    bool EmitValueConversion(utils::StringStream& out,
                              const sem::Call* call,
                              const sem::ValueConversion* conv);
     /// Handles generating a value constructor expression
@@ -173,42 +176,42 @@
     /// @param call the call expression
     /// @param ctor the value constructor
     /// @returns true if the expression is emitted
-    bool EmitValueConstructor(std::ostream& out,
+    bool EmitValueConstructor(utils::StringStream& out,
                               const sem::Call* call,
                               const sem::ValueConstructor* ctor);
     /// Handles generating a barrier builtin call
     /// @param out the output of the expression stream
     /// @param builtin the semantic information for the barrier builtin
     /// @returns true if the call expression is emitted
-    bool EmitBarrierCall(std::ostream& out, const sem::Builtin* builtin);
+    bool EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin);
     /// Handles generating an atomic builtin call for a workgroup variable
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the atomic builtin
     /// @returns true if the call expression is emitted
-    bool EmitWorkgroupAtomicCall(std::ostream& out,
+    bool EmitWorkgroupAtomicCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin);
     /// Handles generating an array.length() call
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the array length expression is emitted
-    bool EmitArrayLength(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitArrayLength(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to `bitfieldExtract`
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the expression is emitted
-    bool EmitExtractBits(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitExtractBits(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to `bitfieldInsert`
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the expression is emitted
-    bool EmitInsertBits(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitInsertBits(utils::StringStream& out, const ast::CallExpression* expr);
     /// Emulates 'fma' on GLSL ES, where it is unsupported.
     /// @param out the output of the expression stream
     /// @param expr the fma() expression
     /// @returns true if the expression is emitted
-    bool EmitEmulatedFMA(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitEmulatedFMA(utils::StringStream& out, const ast::CallExpression* expr);
     /// Create a float literal zero AST node, and associated semantic nodes.
     /// @param stmt the statement which will own the semantic expression node
     /// @returns an AST expression representing 0.0f
@@ -220,23 +223,25 @@
     /// @param call the call expression
     /// @param builtin the semantic information for the texture builtin
     /// @returns true if the call expression is emitted
-    bool EmitTextureCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitTextureCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a call to the `select()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitCountOneBitsCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitCountOneBitsCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to the `countOneBits()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitSelectCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitSelectCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to the `dot()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDotCall(std::ostream& out,
+    bool EmitDotCall(utils::StringStream& out,
                      const ast::CallExpression* expr,
                      const sem::Builtin* builtin);
     /// Handles generating a call to the `modf()` builtin
@@ -244,7 +249,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitModfCall(std::ostream& out,
+    bool EmitModfCall(utils::StringStream& out,
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to the `frexp()` builtin
@@ -252,7 +257,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitFrexpCall(std::ostream& out,
+    bool EmitFrexpCall(utils::StringStream& out,
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to the `degrees()` builtin
@@ -260,7 +265,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDegreesCall(std::ostream& out,
+    bool EmitDegreesCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `radians()` builtin
@@ -268,7 +273,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitRadiansCall(std::ostream& out,
+    bool EmitRadiansCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `quantizeToF16()` intrinsic
@@ -276,7 +281,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitQuantizeToF16Call(std::ostream& out,
+    bool EmitQuantizeToF16Call(utils::StringStream& out,
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles a case statement
@@ -295,7 +300,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression
     /// @returns true if the expression was emitted
-    bool EmitExpression(std::ostream& out, const ast::Expression* expr);
+    bool EmitExpression(utils::StringStream& out, const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
     /// @returns true if the function was emitted
@@ -342,14 +347,14 @@
     /// Handles emitting interpolation qualifiers
     /// @param out the output of the expression stream
     /// @param attrs the attributes
-    void EmitInterpolationQualifiers(std::ostream& out,
+    void EmitInterpolationQualifiers(utils::StringStream& out,
                                      utils::VectorRef<const ast::Attribute*> attrs);
     /// Handles emitting attributes
     /// @param out the output of the expression stream
     /// @param var the global variable semantics
     /// @param attrs the attributes
     /// @returns true if the attributes were emitted
-    bool EmitAttributes(std::ostream& out,
+    bool EmitAttributes(utils::StringStream& out,
                         const sem::GlobalVariable* var,
                         utils::VectorRef<const ast::Attribute*> attrs);
     /// Handles emitting the entry point function
@@ -364,12 +369,12 @@
     /// @param out the output stream
     /// @param constant the constant value to emit
     /// @returns true if the constant value was successfully emitted
-    bool EmitConstant(std::ostream& out, const constant::Value* constant);
+    bool EmitConstant(utils::StringStream& out, const constant::Value* constant);
     /// Handles a literal
     /// @param out the output stream
     /// @param lit the literal to emit
     /// @returns true if the literal was successfully emitted
-    bool EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit);
+    bool EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit);
     /// Handles a loop statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted
@@ -386,12 +391,12 @@
     /// @param out the output of the expression stream
     /// @param expr the identifier expression
     /// @returns true if the identifier was emitted
-    bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
+    bool EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles a member accessor expression
     /// @param out the output of the expression stream
     /// @param expr the member accessor expression
     /// @returns true if the member accessor was emitted
-    bool EmitMemberAccessor(std::ostream& out, const ast::MemberAccessorExpression* expr);
+    bool EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
     /// Handles return statements
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -413,7 +418,7 @@
     /// @param name_printed (optional) if not nullptr and an array was printed
     /// then the boolean is set to true.
     /// @returns true if the type is emitted
-    bool EmitType(std::ostream& out,
+    bool EmitType(utils::StringStream& out,
                   const type::Type* type,
                   builtin::AddressSpace address_space,
                   builtin::Access access,
@@ -426,7 +431,7 @@
     /// @param access the access control type of the variable
     /// @param name the name to emit
     /// @returns true if the type is emitted
-    bool EmitTypeAndName(std::ostream& out,
+    bool EmitTypeAndName(utils::StringStream& out,
                          const type::Type* type,
                          builtin::AddressSpace address_space,
                          builtin::Access access,
@@ -446,12 +451,12 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the expression was emitted
-    bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
+    bool EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
     /// Emits the zero value for the given type
     /// @param out the output stream
     /// @param type the type to emit the value for
     /// @returns true if the zero value was successfully emitted.
-    bool EmitZeroValue(std::ostream& out, const type::Type* type);
+    bool EmitZeroValue(utils::StringStream& out, const type::Type* type);
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
@@ -504,7 +509,7 @@
     ///          `params` is the name of all the generated function parameters
     /// @returns true if the call expression is emitted
     template <typename F>
-    bool CallBuiltinHelper(std::ostream& out,
+    bool CallBuiltinHelper(utils::StringStream& out,
                            const ast::CallExpression* call,
                            const sem::Builtin* builtin,
                            F&& build);
diff --git a/src/tint/writer/glsl/generator_impl_array_accessor_test.cc b/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
index bd405d3..453af22 100644
--- a/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_array_accessor_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -28,7 +30,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "ary[5]");
 }
diff --git a/src/tint/writer/glsl/generator_impl_binary_test.cc b/src/tint/writer/glsl/generator_impl_binary_test.cc
index f90a428..475ae46 100644
--- a/src/tint/writer/glsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/glsl/generator_impl_binary_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -55,7 +56,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -83,7 +84,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -102,7 +103,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -126,7 +127,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -161,7 +162,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(a * 1.0f)");
 }
@@ -179,7 +180,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(a * 1.0hf)");
 }
@@ -195,7 +196,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0f * a)");
 }
@@ -213,7 +214,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0hf * a)");
 }
@@ -228,7 +229,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * 1.0f)");
 }
@@ -245,7 +246,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * 1.0hf)");
 }
@@ -260,7 +261,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0f * mat)");
 }
@@ -277,7 +278,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0hf * mat)");
 }
@@ -292,7 +293,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * vec3(1.0f))");
 }
@@ -309,7 +310,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * f16vec3(1.0hf))");
 }
@@ -324,7 +325,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(vec3(1.0f) * mat)");
 }
@@ -341,7 +342,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(f16vec3(1.0hf) * mat)");
 }
@@ -355,7 +356,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(lhs * rhs)");
 }
@@ -371,7 +372,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(lhs * rhs)");
 }
@@ -385,7 +386,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -401,7 +402,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -415,7 +416,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -431,7 +432,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -445,7 +446,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -461,7 +462,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -475,7 +476,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -491,7 +492,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "tint_float_modulo(a, b)");
 }
@@ -592,7 +593,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
@@ -617,7 +618,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = a;
@@ -644,7 +645,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
diff --git a/src/tint/writer/glsl/generator_impl_bitcast_test.cc b/src/tint/writer/glsl/generator_impl_bitcast_test.cc
index a7d2f66..b6e4f92 100644
--- a/src/tint/writer/glsl/generator_impl_bitcast_test.cc
+++ b/src/tint/writer/glsl/generator_impl_bitcast_test.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/writer/glsl/test_helper.h"
 
+#include "src/tint/utils/string_stream.h"
+
 using namespace tint::number_suffixes;  // NOLINT
 
 namespace tint::writer::glsl {
@@ -28,7 +30,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "intBitsToFloat(a)");
 }
@@ -40,7 +42,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "int(a)");
 }
@@ -52,7 +54,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "uint(a)");
 }
diff --git a/src/tint/writer/glsl/generator_impl_builtin_test.cc b/src/tint/writer/glsl/generator_impl_builtin_test.cc
index fa0781b..3d14a68 100644
--- a/src/tint/writer/glsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/glsl/generator_impl_builtin_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/sem/call.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using ::testing::HasSubstr;
@@ -65,8 +66,8 @@
                                         CallParamType type,
                                         ProgramBuilder* builder) {
     std::string name;
-    std::ostringstream str(name);
-    str << builtin;
+    utils::StringStream str;
+    str << name << builtin;
     switch (builtin) {
         case BuiltinType::kAcos:
         case BuiltinType::kAsin:
@@ -350,7 +351,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "dot(param1, param2)");
 }
@@ -363,7 +364,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "(true ? b : a)");
 }
@@ -376,7 +377,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "mix(a, b, bvec2(true, false))");
 }
@@ -393,7 +394,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "((a) * (b) + (c))");
 }
@@ -411,7 +412,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "((a) * (b) + (c))");
 }
@@ -931,7 +932,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082322865f;
+  return param_0 * 57.295779513082323f;
 }
 
 
@@ -959,7 +960,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_degrees(vec3 param_0) {
-  return param_0 * 57.295779513082322865f;
+  return param_0 * 57.295779513082323f;
 }
 
 
@@ -990,7 +991,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 float16_t tint_degrees(float16_t param_0) {
-  return param_0 * 57.295779513082322865hf;
+  return param_0 * 57.295779513082323hf;
 }
 
 
@@ -1021,7 +1022,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 f16vec3 tint_degrees(f16vec3 param_0) {
-  return param_0 * 57.295779513082322865hf;
+  return param_0 * 57.295779513082323hf;
 }
 
 
@@ -1049,7 +1050,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295474f;
+  return param_0 * 0.017453292519943295f;
 }
 
 
@@ -1077,7 +1078,7 @@
     EXPECT_EQ(gen.result(), R"(#version 310 es
 
 vec3 tint_radians(vec3 param_0) {
-  return param_0 * 0.017453292519943295474f;
+  return param_0 * 0.017453292519943295f;
 }
 
 
@@ -1108,7 +1109,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 float16_t tint_radians(float16_t param_0) {
-  return param_0 * 0.017453292519943295474hf;
+  return param_0 * 0.017453292519943295hf;
 }
 
 
@@ -1139,7 +1140,7 @@
 #extension GL_AMD_gpu_shader_half_float : require
 
 f16vec3 tint_radians(f16vec3 param_0) {
-  return param_0 * 0.017453292519943295474hf;
+  return param_0 * 0.017453292519943295hf;
 }
 
 
diff --git a/src/tint/writer/glsl/generator_impl_call_test.cc b/src/tint/writer/glsl/generator_impl_call_test.cc
index 2f81463..69a78f4 100644
--- a/src/tint/writer/glsl/generator_impl_call_test.cc
+++ b/src/tint/writer/glsl/generator_impl_call_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -30,7 +31,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func()");
 }
@@ -50,7 +51,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func(param1, param2)");
 }
diff --git a/src/tint/writer/glsl/generator_impl_cast_test.cc b/src/tint/writer/glsl/generator_impl_cast_test.cc
index 4954dee..41ee6f0 100644
--- a/src/tint/writer/glsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/glsl/generator_impl_cast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "1.0f");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "vec3(1.0f, 2.0f, 3.0f)");
 }
diff --git a/src/tint/writer/glsl/generator_impl_identifier_test.cc b/src/tint/writer/glsl/generator_impl_identifier_test.cc
index cd0b109..64b9fb3 100644
--- a/src/tint/writer/glsl/generator_impl_identifier_test.cc
+++ b/src/tint/writer/glsl/generator_impl_identifier_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 namespace tint::writer::glsl {
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, i)) << gen.error();
     EXPECT_EQ(out.str(), "foo");
 }
diff --git a/src/tint/writer/glsl/generator_impl_import_test.cc b/src/tint/writer/glsl/generator_impl_import_test.cc
index 10da8aa..3201712 100644
--- a/src/tint/writer/glsl/generator_impl_import_test.cc
+++ b/src/tint/writer/glsl/generator_impl_import_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -39,7 +40,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f)");
 }
@@ -78,7 +79,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1)");
 }
@@ -95,7 +96,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    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))");
@@ -136,7 +137,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f, 2.0f)");
 }
@@ -158,7 +159,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(),
               std::string(param.glsl_name) + "(vec3(1.0f, 2.0f, 3.0f), vec3(4.0f, 5.0f, 6.0f))");
@@ -183,7 +184,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1, 2)");
 }
@@ -201,7 +202,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1.0f, 2.0f, 3.0f)");
 }
@@ -221,7 +222,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(),
               std::string(param.glsl_name) +
@@ -242,7 +243,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.glsl_name) + "(1, 2, 3)");
 }
@@ -258,7 +259,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string("determinant(var)"));
 }
diff --git a/src/tint/writer/glsl/generator_impl_member_accessor_test.cc b/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
index e551c75..2eb1b8e 100644
--- a/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/glsl/generator_impl_member_accessor_test.cc
@@ -241,13 +241,22 @@
                                          TypeCase{ty_vec4<f32>, "data.inner.b = value"},
                                          TypeCase{ty_vec4<i32>, "data.inner.b = value"},
                                          TypeCase{ty_mat2x2<f32>, "data.inner.b = value"},
-                                         TypeCase{ty_mat2x3<f32>, "data.inner.b = value"},
+                                         TypeCase{ty_mat2x3<f32>, R"(
+  data.inner.b[0] = value[0u];
+  data.inner.b[1] = value[1u];)"},
                                          TypeCase{ty_mat2x4<f32>, "data.inner.b = value"},
                                          TypeCase{ty_mat3x2<f32>, "data.inner.b = value"},
-                                         TypeCase{ty_mat3x3<f32>, "data.inner.b = value"},
+                                         TypeCase{ty_mat3x3<f32>, R"(
+  data.inner.b[0] = value[0u];
+  data.inner.b[1] = value[1u];
+  data.inner.b[2] = value[2u];)"},
                                          TypeCase{ty_mat3x4<f32>, "data.inner.b = value"},
                                          TypeCase{ty_mat4x2<f32>, "data.inner.b = value"},
-                                         TypeCase{ty_mat4x3<f32>, "data.inner.b = value"},
+                                         TypeCase{ty_mat4x3<f32>, R"(
+  data.inner.b[0] = value[0u];
+  data.inner.b[1] = value[1u];
+  data.inner.b[2] = value[2u];
+  data.inner.b[3] = value[3u];)"},
                                          TypeCase{ty_mat4x4<f32>, "data.inner.b = value"}));
 
 TEST_F(GlslGeneratorImplTest_MemberAccessor, StorageBuffer_Store_Matrix_Empty) {
@@ -286,8 +295,13 @@
   Data inner;
 } data;
 
+void assign_and_preserve_padding_data_b(mat2x3 value) {
+  data.inner.b[0] = value[0u];
+  data.inner.b[1] = value[1u];
+}
+
 void tint_symbol() {
-  data.inner.b = mat2x3(vec3(0.0f), vec3(0.0f));
+  assign_and_preserve_padding_data_b(mat2x3(vec3(0.0f), vec3(0.0f)));
 }
 
 void main() {
diff --git a/src/tint/writer/glsl/generator_impl_type_test.cc b/src/tint/writer/glsl/generator_impl_type_test.cc
index 492910d..2cc13f6 100644
--- a/src/tint/writer/glsl/generator_impl_type_test.cc
+++ b/src/tint/writer/glsl/generator_impl_type_test.cc
@@ -21,6 +21,7 @@
 #include "src/tint/type/sampler.h"
 #include "src/tint/type/storage_texture.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 using ::testing::HasSubstr;
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -64,7 +65,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -77,7 +78,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -89,7 +90,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, bool_, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -101,7 +102,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, f32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -115,7 +116,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, f16, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -127,7 +128,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, i32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -141,7 +142,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -157,7 +158,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -194,7 +195,7 @@
     GeneratorImpl& gen = Build();
 
     auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -243,7 +244,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, u32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -256,7 +257,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -271,7 +272,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -283,7 +284,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, void_, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -295,7 +296,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_FALSE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
                               builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -306,7 +307,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_FALSE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
                               builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -513,7 +514,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
diff --git a/src/tint/writer/glsl/generator_impl_unary_op_test.cc b/src/tint/writer/glsl/generator_impl_unary_op_test.cc
index 18f180e..b7c5fd1 100644
--- a/src/tint/writer/glsl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/glsl/generator_impl_unary_op_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/glsl/test_helper.h"
 
 namespace tint::writer::glsl {
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "expr");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "~(expr)");
 }
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "expr");
 }
@@ -63,7 +64,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "!(expr)");
 }
@@ -75,7 +76,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "-(expr)");
 }
diff --git a/src/tint/writer/hlsl/generator.h b/src/tint/writer/hlsl/generator.h
index c624943..80dd518 100644
--- a/src/tint/writer/hlsl/generator.h
+++ b/src/tint/writer/hlsl/generator.h
@@ -61,6 +61,8 @@
     /// 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,
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 787962b..2bb4a9f 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -75,6 +75,7 @@
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/scoped_assignment.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/append_vector.h"
 #include "src/tint/writer/check_supported_extensions.h"
 #include "src/tint/writer/float_to_string.h"
@@ -114,7 +115,7 @@
     }
 }
 
-void PrintF32(std::ostream& out, float value) {
+void PrintF32(utils::StringStream& out, float value) {
     if (std::isinf(value)) {
         out << "0.0f " << (value >= 0 ? "/* inf */" : "/* -inf */");
     } else if (std::isnan(value)) {
@@ -124,7 +125,7 @@
     }
 }
 
-void PrintF16(std::ostream& out, float value) {
+void PrintF16(utils::StringStream& out, float value) {
     if (std::isinf(value)) {
         out << "0.0h " << (value >= 0 ? "/* inf */" : "/* -inf */");
     } else if (std::isnan(value)) {
@@ -143,7 +144,7 @@
     sem::BindingPoint const binding_point;
 };
 
-std::ostream& operator<<(std::ostream& s, const RegisterAndSpace& rs) {
+utils::StringStream& operator<<(utils::StringStream& s, const RegisterAndSpace& rs) {
     s << " : register(" << rs.reg << rs.binding_point.binding << ", space" << rs.binding_point.group
       << ")";
     return s;
@@ -181,6 +182,7 @@
         polyfills.insert_bits = transform::BuiltinPolyfill::Level::kFull;
         polyfills.int_div_mod = true;
         polyfills.precise_float_mod = true;
+        polyfills.reflect_vec2_f32 = options.polyfill_reflect_vec2_f32;
         polyfills.texture_sample_base_clamp_to_edge_2d_f32 = true;
         polyfills.workgroup_uniform_load = true;
         data.Add<transform::BuiltinPolyfill::Config>(polyfills);
@@ -376,7 +378,7 @@
     auto name = utils::GetOrCreate(dynamic_vector_write_, vec, [&]() -> std::string {
         std::string fn;
         {
-            std::ostringstream ss;
+            utils::StringStream ss;
             if (!EmitType(ss, vec, tint::builtin::AddressSpace::kUndefined,
                           builtin::Access::kUndefined, "")) {
                 return "";
@@ -450,7 +452,7 @@
     auto name = utils::GetOrCreate(dynamic_matrix_vector_write_, mat, [&]() -> std::string {
         std::string fn;
         {
-            std::ostringstream ss;
+            utils::StringStream ss;
             if (!EmitType(ss, mat, tint::builtin::AddressSpace::kUndefined,
                           builtin::Access::kUndefined, "")) {
                 return "";
@@ -519,7 +521,7 @@
     auto name = utils::GetOrCreate(dynamic_matrix_scalar_write_, mat, [&]() -> std::string {
         std::string fn;
         {
-            std::ostringstream ss;
+            utils::StringStream ss;
             if (!EmitType(ss, mat, tint::builtin::AddressSpace::kUndefined,
                           builtin::Access::kUndefined, "")) {
                 return "";
@@ -615,7 +617,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr) {
+bool GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
+                                      const ast::IndexAccessorExpression* expr) {
     if (!EmitExpression(out, expr->object)) {
         return false;
     }
@@ -629,7 +632,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr) {
+bool GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
     auto* type = TypeOf(expr);
     if (auto* vec = type->UnwrapRef()->As<type::Vector>()) {
         type = vec->type();
@@ -697,7 +700,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
     if (expr->op == ast::BinaryOp::kLogicalAnd || expr->op == ast::BinaryOp::kLogicalOr) {
         auto name = UniqueIdentifier(kTempNamePrefix);
 
@@ -872,7 +875,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* call = builder_.Sem().Get<sem::Call>(expr);
     auto* target = call->Target();
     return Switch(
@@ -887,7 +890,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFunctionCall(std::ostream& out,
+bool GeneratorImpl::EmitFunctionCall(utils::StringStream& out,
                                      const sem::Call* call,
                                      const sem::Function* func) {
     auto* expr = call->Declaration();
@@ -943,7 +946,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBuiltinCall(std::ostream& out,
+bool GeneratorImpl::EmitBuiltinCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     const auto type = builtin->Type();
@@ -1028,7 +1031,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitValueConversion(std::ostream& out,
+bool GeneratorImpl::EmitValueConversion(utils::StringStream& out,
                                         const sem::Call* call,
                                         const sem::ValueConversion* conv) {
     if (!EmitType(out, conv->Target(), builtin::AddressSpace::kUndefined,
@@ -1045,7 +1048,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitValueConstructor(std::ostream& out,
+bool GeneratorImpl::EmitValueConstructor(utils::StringStream& out,
                                          const sem::Call* call,
                                          const sem::ValueConstructor* ctor) {
     auto* type = call->Type();
@@ -1109,7 +1112,7 @@
 }
 
 bool GeneratorImpl::EmitUniformBufferAccess(
-    std::ostream& out,
+    utils::StringStream& out,
     const ast::CallExpression* expr,
     const transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
     auto const buffer = program_->Symbols().NameFor(intrinsic->buffer);
@@ -1181,7 +1184,7 @@
                 out << ")";
                 return result;
             };
-            auto load_u32_to = [&](std::ostream& target) {
+            auto load_u32_to = [&](utils::StringStream& target) {
                 target << buffer;
                 if (scalar_offset_constant) {
                     target << "[" << (scalar_offset_index / 4) << "]."
@@ -1194,7 +1197,7 @@
             };
             auto load_u32 = [&] { return load_u32_to(out); };
             // Has a minimum alignment of 8 bytes, so is either .xy or .zw
-            auto load_vec2_u32_to = [&](std::ostream& target) {
+            auto load_vec2_u32_to = [&](utils::StringStream& target) {
                 if (scalar_offset_constant) {
                     target << buffer << "[" << (scalar_offset_index / 4) << "]"
                            << ((scalar_offset_index & 2) == 0 ? ".xy" : ".zw");
@@ -1397,7 +1400,7 @@
 }
 
 bool GeneratorImpl::EmitStorageBufferAccess(
-    std::ostream& out,
+    utils::StringStream& out,
     const ast::CallExpression* expr,
     const transform::DecomposeMemoryAccess::Intrinsic* intrinsic) {
     auto const buffer = program_->Symbols().NameFor(intrinsic->buffer);
@@ -1762,7 +1765,7 @@
     return false;
 }
 
-bool GeneratorImpl::EmitWorkgroupAtomicCall(std::ostream& out,
+bool GeneratorImpl::EmitWorkgroupAtomicCall(utils::StringStream& out,
                                             const ast::CallExpression* expr,
                                             const sem::Builtin* builtin) {
     std::string result = UniqueIdentifier("atomic_result");
@@ -1937,7 +1940,7 @@
     return false;
 }
 
-bool GeneratorImpl::EmitSelectCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitSelectCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* expr_false = expr->args[0];
     auto* expr_true = expr->args[1];
     auto* expr_cond = expr->args[2];
@@ -1961,7 +1964,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitModfCall(std::ostream& out,
+bool GeneratorImpl::EmitModfCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -1994,7 +1997,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
+bool GeneratorImpl::EmitFrexpCall(utils::StringStream& out,
                                   const ast::CallExpression* expr,
                                   const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -2035,7 +2038,7 @@
         });
 }
 
-bool GeneratorImpl::EmitDegreesCall(std::ostream& out,
+bool GeneratorImpl::EmitDegreesCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     return CallBuiltinHelper(out, expr, builtin,
@@ -2046,7 +2049,7 @@
                              });
 }
 
-bool GeneratorImpl::EmitRadiansCall(std::ostream& out,
+bool GeneratorImpl::EmitRadiansCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     return CallBuiltinHelper(out, expr, builtin,
@@ -2060,7 +2063,9 @@
 // The HLSL `sign` method always returns an `int` result (scalar or vector). In WGSL the result is
 // expected to be the same type as the argument. This injects a cast to the expected WGSL result
 // type after the call to `sign`.
-bool GeneratorImpl::EmitSignCall(std::ostream& out, const sem::Call* call, const sem::Builtin*) {
+bool GeneratorImpl::EmitSignCall(utils::StringStream& out,
+                                 const sem::Call* call,
+                                 const sem::Builtin*) {
     auto* arg = call->Arguments()[0];
     if (!EmitType(out, arg->Type(), builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite,
                   "")) {
@@ -2074,7 +2079,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitQuantizeToF16Call(std::ostream& out,
+bool GeneratorImpl::EmitQuantizeToF16Call(utils::StringStream& out,
                                           const ast::CallExpression* expr,
                                           const sem::Builtin* builtin) {
     // Emulate by casting to min16float and back again.
@@ -2090,7 +2095,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitDataPackingCall(std::ostream& out,
+bool GeneratorImpl::EmitDataPackingCall(utils::StringStream& out,
                                         const ast::CallExpression* expr,
                                         const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -2153,7 +2158,7 @@
         });
 }
 
-bool GeneratorImpl::EmitDataUnpackingCall(std::ostream& out,
+bool GeneratorImpl::EmitDataUnpackingCall(utils::StringStream& out,
                                           const ast::CallExpression* expr,
                                           const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -2220,7 +2225,7 @@
         });
 }
 
-bool GeneratorImpl::EmitDP4aCall(std::ostream& out,
+bool GeneratorImpl::EmitDP4aCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin) {
     // TODO(crbug.com/tint/1497): support the polyfill version of DP4a functions.
@@ -2248,7 +2253,7 @@
         });
 }
 
-bool GeneratorImpl::EmitBarrierCall(std::ostream& out, const sem::Builtin* builtin) {
+bool GeneratorImpl::EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin) {
     // TODO(crbug.com/tint/661): Combine sequential barriers to a single
     // instruction.
     if (builtin->Type() == sem::BuiltinType::kWorkgroupBarrier) {
@@ -2263,7 +2268,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitTextureCall(std::ostream& out,
+bool GeneratorImpl::EmitTextureCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     using Usage = sem::ParameterUsage;
@@ -2772,7 +2777,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, const ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
     if (auto* sem = builder_.Sem().GetVal(expr)) {
         if (auto* constant = sem->ConstantValue()) {
             bool is_variable_initializer = false;
@@ -2801,7 +2806,8 @@
         });
 }
 
-bool GeneratorImpl::EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr) {
+bool GeneratorImpl::EmitIdentifier(utils::StringStream& out,
+                                   const ast::IdentifierExpression* expr) {
     out << builder_.Symbols().NameFor(expr->identifier->symbol);
     return true;
 }
@@ -3275,7 +3281,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitConstant(std::ostream& out,
+bool GeneratorImpl::EmitConstant(utils::StringStream& out,
                                  const constant::Value* constant,
                                  bool is_variable_initializer) {
     return Switch(
@@ -3395,7 +3401,7 @@
                 return true;
             }
 
-            auto emit_member_values = [&](std::ostream& o) {
+            auto emit_member_values = [&](utils::StringStream& o) {
                 o << "{";
                 for (size_t i = 0; i < s->Members().Length(); i++) {
                     if (i > 0) {
@@ -3437,7 +3443,7 @@
         });
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit) {
+bool GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
     return Switch(
         lit,
         [&](const ast::BoolLiteralExpression* l) {
@@ -3473,7 +3479,7 @@
         });
 }
 
-bool GeneratorImpl::EmitValue(std::ostream& out, const type::Type* type, int value) {
+bool GeneratorImpl::EmitValue(utils::StringStream& out, const type::Type* type, int value) {
     return Switch(
         type,
         [&](const type::Bool*) {
@@ -3548,7 +3554,7 @@
         });
 }
 
-bool GeneratorImpl::EmitZeroValue(std::ostream& out, const type::Type* type) {
+bool GeneratorImpl::EmitZeroValue(utils::StringStream& out, const type::Type* type) {
     return EmitValue(out, type, 0);
 }
 
@@ -3597,7 +3603,7 @@
     }
 
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
     if (auto* cond = stmt->condition) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
         if (!EmitExpression(cond_buf, cond)) {
@@ -3689,7 +3695,7 @@
 
 bool GeneratorImpl::EmitWhile(const ast::WhileStatement* stmt) {
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
     {
         auto* cond = stmt->condition;
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
@@ -3737,7 +3743,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
+bool GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
                                        const ast::MemberAccessorExpression* expr) {
     if (!EmitExpression(out, expr->object)) {
         return false;
@@ -3910,7 +3916,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitType(std::ostream& out,
+bool GeneratorImpl::EmitType(utils::StringStream& out,
                              const type::Type* type,
                              builtin::AddressSpace address_space,
                              builtin::Access access,
@@ -4137,7 +4143,7 @@
         });
 }
 
-bool GeneratorImpl::EmitTypeAndName(std::ostream& out,
+bool GeneratorImpl::EmitTypeAndName(utils::StringStream& out,
                                     const type::Type* type,
                                     builtin::AddressSpace address_space,
                                     builtin::Access access,
@@ -4248,7 +4254,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr) {
+bool GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
     switch (expr->op) {
         case ast::UnaryOp::kIndirection:
         case ast::UnaryOp::kAddressOf:
@@ -4320,7 +4326,7 @@
 }
 
 template <typename F>
-bool GeneratorImpl::CallBuiltinHelper(std::ostream& out,
+bool GeneratorImpl::CallBuiltinHelper(utils::StringStream& out,
                                       const ast::CallExpression* call,
                                       const sem::Builtin* builtin,
                                       F&& build) {
diff --git a/src/tint/writer/hlsl/generator_impl.h b/src/tint/writer/hlsl/generator_impl.h
index 6db120c..f7adc9f 100644
--- a/src/tint/writer/hlsl/generator_impl.h
+++ b/src/tint/writer/hlsl/generator_impl.h
@@ -88,7 +88,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the index accessor was emitted
-    bool EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr);
+    bool EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
     /// Handles an assignment statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -97,12 +97,12 @@
     /// @param out the output of the expression stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a bitcast expression
     /// @param out the output of the expression stream
     /// @param expr the as expression
     /// @returns true if the bitcast was emitted
-    bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
+    bool EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
     /// Emits a list of statements
     /// @param stmts the statement list
     /// @returns true if the statements were emitted successfully
@@ -127,25 +127,29 @@
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a function call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param function the function being called
     /// @returns true if the expression is emitted
-    bool EmitFunctionCall(std::ostream& out, const sem::Call* call, const sem::Function* function);
+    bool EmitFunctionCall(utils::StringStream& out,
+                          const sem::Call* call,
+                          const sem::Function* function);
     /// Handles generating a builtin call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param builtin the builtin being called
     /// @returns true if the expression is emitted
-    bool EmitBuiltinCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitBuiltinCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a value conversion expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param conv the value conversion
     /// @returns true if the expression is emitted
-    bool EmitValueConversion(std::ostream& out,
+    bool EmitValueConversion(utils::StringStream& out,
                              const sem::Call* call,
                              const sem::ValueConversion* conv);
     /// Handles generating a value constructor expression
@@ -153,7 +157,7 @@
     /// @param call the call expression
     /// @param ctor the value constructor
     /// @returns true if the expression is emitted
-    bool EmitValueConstructor(std::ostream& out,
+    bool EmitValueConstructor(utils::StringStream& out,
                               const sem::Call* call,
                               const sem::ValueConstructor* ctor);
     /// Handles generating a call expression to a
@@ -162,7 +166,7 @@
     /// @param expr the call expression
     /// @param intrinsic the transform::DecomposeMemoryAccess::Intrinsic
     /// @returns true if the call expression is emitted
-    bool EmitUniformBufferAccess(std::ostream& out,
+    bool EmitUniformBufferAccess(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating a call expression to a
@@ -171,20 +175,20 @@
     /// @param expr the call expression
     /// @param intrinsic the transform::DecomposeMemoryAccess::Intrinsic
     /// @returns true if the call expression is emitted
-    bool EmitStorageBufferAccess(std::ostream& out,
+    bool EmitStorageBufferAccess(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating a barrier intrinsic call
     /// @param out the output of the expression stream
     /// @param builtin the semantic information for the barrier builtin
     /// @returns true if the call expression is emitted
-    bool EmitBarrierCall(std::ostream& out, const sem::Builtin* builtin);
+    bool EmitBarrierCall(utils::StringStream& out, const sem::Builtin* builtin);
     /// Handles generating an atomic intrinsic call for a storage buffer variable
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param intrinsic the atomic intrinsic
     /// @returns true if the call expression is emitted
-    bool EmitStorageAtomicCall(std::ostream& out,
+    bool EmitStorageAtomicCall(utils::StringStream& out,
                                const ast::CallExpression* expr,
                                const transform::DecomposeMemoryAccess::Intrinsic* intrinsic);
     /// Handles generating the helper function for the atomic intrinsic function
@@ -198,7 +202,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the atomic builtin
     /// @returns true if the call expression is emitted
-    bool EmitWorkgroupAtomicCall(std::ostream& out,
+    bool EmitWorkgroupAtomicCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin);
     /// Handles generating a call to a texture function (`textureSample`,
@@ -207,18 +211,20 @@
     /// @param call the call expression
     /// @param builtin the semantic information for the texture builtin
     /// @returns true if the call expression is emitted
-    bool EmitTextureCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitTextureCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a call to the `select()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitSelectCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitSelectCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a call to the `modf()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitModfCall(std::ostream& out,
+    bool EmitModfCall(utils::StringStream& out,
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to the `frexp()` builtin
@@ -226,7 +232,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitFrexpCall(std::ostream& out,
+    bool EmitFrexpCall(utils::StringStream& out,
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to the `degrees()` builtin
@@ -234,7 +240,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDegreesCall(std::ostream& out,
+    bool EmitDegreesCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `radians()` builtin
@@ -242,7 +248,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitRadiansCall(std::ostream& out,
+    bool EmitRadiansCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `sign()` builtin
@@ -250,13 +256,13 @@
     /// @param call the call semantic node
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitSignCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitSignCall(utils::StringStream& out, const sem::Call* call, const sem::Builtin* builtin);
     /// Handles generating a call to data packing builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDataPackingCall(std::ostream& out,
+    bool EmitDataPackingCall(utils::StringStream& out,
                              const ast::CallExpression* expr,
                              const sem::Builtin* builtin);
     /// Handles generating a call to data unpacking builtin
@@ -264,7 +270,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDataUnpackingCall(std::ostream& out,
+    bool EmitDataUnpackingCall(utils::StringStream& out,
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles generating a call to the `quantizeToF16()` intrinsic
@@ -272,7 +278,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitQuantizeToF16Call(std::ostream& out,
+    bool EmitQuantizeToF16Call(utils::StringStream& out,
                                const ast::CallExpression* expr,
                                const sem::Builtin* builtin);
     /// Handles generating a call to DP4a builtins (dot4I8Packed and dot4U8Packed)
@@ -280,7 +286,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDP4aCall(std::ostream& out,
+    bool EmitDP4aCall(utils::StringStream& out,
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles a case statement
@@ -300,7 +306,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression
     /// @returns true if the expression was emitted
-    bool EmitExpression(std::ostream& out, const ast::Expression* expr);
+    bool EmitExpression(utils::StringStream& out, const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
     /// @returns true if the function was emitted
@@ -357,14 +363,14 @@
     /// @param is_variable_initializer true if the constant is used as the RHS of a variable
     /// initializer
     /// @returns true if the constant value was successfully emitted
-    bool EmitConstant(std::ostream& out,
+    bool EmitConstant(utils::StringStream& out,
                       const constant::Value* constant,
                       bool is_variable_initializer);
     /// Handles a literal
     /// @param out the output stream
     /// @param lit the literal to emit
     /// @returns true if the literal was successfully emitted
-    bool EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit);
+    bool EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit);
     /// Handles a loop statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted
@@ -381,12 +387,12 @@
     /// @param out the output of the expression stream
     /// @param expr the identifier expression
     /// @returns true if the identifeir was emitted
-    bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
+    bool EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles a member accessor expression
     /// @param out the output of the expression stream
     /// @param expr the member accessor expression
     /// @returns true if the member accessor was emitted
-    bool EmitMemberAccessor(std::ostream& out, const ast::MemberAccessorExpression* expr);
+    bool EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
     /// Handles return statements
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -412,7 +418,7 @@
     /// @param name_printed (optional) if not nullptr and an array was printed
     /// then the boolean is set to true.
     /// @returns true if the type is emitted
-    bool EmitType(std::ostream& out,
+    bool EmitType(utils::StringStream& out,
                   const type::Type* type,
                   builtin::AddressSpace address_space,
                   builtin::Access access,
@@ -425,7 +431,7 @@
     /// @param access the access control type of the variable
     /// @param name the name to emit
     /// @returns true if the type is emitted
-    bool EmitTypeAndName(std::ostream& out,
+    bool EmitTypeAndName(utils::StringStream& out,
                          const type::Type* type,
                          builtin::AddressSpace address_space,
                          builtin::Access access,
@@ -440,18 +446,18 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the expression was emitted
-    bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
+    bool EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
     /// Emits `value` for the given type
     /// @param out the output stream
     /// @param type the type to emit the value for
     /// @param value the value to emit
     /// @returns true if the value was successfully emitted.
-    bool EmitValue(std::ostream& out, const type::Type* type, int value);
+    bool EmitValue(utils::StringStream& out, const type::Type* type, int value);
     /// Emits the zero value for the given type
     /// @param out the output stream
     /// @param type the type to emit the value for
     /// @returns true if the zero value was successfully emitted.
-    bool EmitZeroValue(std::ostream& out, const type::Type* type);
+    bool EmitZeroValue(utils::StringStream& out, const type::Type* type);
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
@@ -541,7 +547,7 @@
     ///          `params` is the name of all the generated function parameters
     /// @returns true if the call expression is emitted
     template <typename F>
-    bool CallBuiltinHelper(std::ostream& out,
+    bool CallBuiltinHelper(utils::StringStream& out,
                            const ast::CallExpression* call,
                            const sem::Builtin* builtin,
                            F&& build);
diff --git a/src/tint/writer/hlsl/generator_impl_array_accessor_test.cc b/src/tint/writer/hlsl/generator_impl_array_accessor_test.cc
index 73eea96..df68694 100644
--- a/src/tint/writer/hlsl/generator_impl_array_accessor_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_array_accessor_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "ary[5]");
 }
diff --git a/src/tint/writer/hlsl/generator_impl_binary_test.cc b/src/tint/writer/hlsl/generator_impl_binary_test.cc
index c99ae54..630fc09 100644
--- a/src/tint/writer/hlsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_binary_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -62,7 +63,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -94,7 +95,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -117,7 +118,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -145,7 +146,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -182,7 +183,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0f).xxx");
 }
@@ -199,7 +200,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(float16_t(1.0h)).xxx");
 }
@@ -214,7 +215,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0f).xxx");
 }
@@ -231,7 +232,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(float16_t(1.0h)).xxx");
 }
@@ -246,7 +247,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * 1.0f)");
 }
@@ -263,7 +264,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(mat * float16_t(1.0h))");
 }
@@ -278,7 +279,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(1.0f * mat)");
 }
@@ -295,7 +296,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(float16_t(1.0h) * mat)");
 }
@@ -310,7 +311,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul((1.0f).xxx, mat)");
 }
@@ -327,7 +328,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul((float16_t(1.0h)).xxx, mat)");
 }
@@ -342,7 +343,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul(mat, (1.0f).xxx)");
 }
@@ -359,7 +360,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul(mat, (float16_t(1.0h)).xxx)");
 }
@@ -373,7 +374,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul(rhs, lhs)");
 }
@@ -389,7 +390,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     EXPECT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "mul(rhs, lhs)");
 }
@@ -403,7 +404,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
@@ -428,7 +429,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp_1 = a;
@@ -455,7 +456,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(tint_tmp)");
     EXPECT_EQ(gen.result(), R"(bool tint_tmp = a;
diff --git a/src/tint/writer/hlsl/generator_impl_bitcast_test.cc b/src/tint/writer/hlsl/generator_impl_bitcast_test.cc
index 5c4b9cc..3061fbe 100644
--- a/src/tint/writer/hlsl/generator_impl_bitcast_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_bitcast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "asfloat(a)");
 }
@@ -40,7 +41,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "asint(a)");
 }
@@ -52,7 +53,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "asuint(a)");
 }
diff --git a/src/tint/writer/hlsl/generator_impl_builtin_test.cc b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
index 7cd3de3..229213b 100644
--- a/src/tint/writer/hlsl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_builtin_test.cc
@@ -16,6 +16,7 @@
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/stage_attribute.h"
 #include "src/tint/sem/call.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using ::testing::HasSubstr;
@@ -64,8 +65,8 @@
                                         CallParamType type,
                                         ProgramBuilder* builder) {
     std::string name;
-    std::ostringstream str(name);
-    str << builtin;
+    utils::StringStream str;
+    str << name << builtin;
     switch (builtin) {
         case BuiltinType::kAcos:
         case BuiltinType::kAsin:
@@ -347,7 +348,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "dot(param1, param2)");
 }
@@ -360,7 +361,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "(true ? b : a)");
 }
@@ -373,7 +374,7 @@
     GeneratorImpl& gen = Build();
 
     gen.increment_indent();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "(bool2(true, false) ? b : a)");
 }
@@ -811,7 +812,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 [numthreads(1, 1, 1)]
@@ -832,7 +833,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float3 tint_degrees(float3 param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 [numthreads(1, 1, 1)]
@@ -855,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.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 [numthreads(1, 1, 1)]
@@ -878,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.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 [numthreads(1, 1, 1)]
@@ -899,7 +900,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 [numthreads(1, 1, 1)]
@@ -920,7 +921,7 @@
 
     ASSERT_TRUE(gen.Generate()) << gen.error();
     EXPECT_EQ(gen.result(), R"(float3 tint_radians(float3 param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 [numthreads(1, 1, 1)]
@@ -943,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.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 [numthreads(1, 1, 1)]
@@ -966,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.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 [numthreads(1, 1, 1)]
diff --git a/src/tint/writer/hlsl/generator_impl_call_test.cc b/src/tint/writer/hlsl/generator_impl_call_test.cc
index 9947a08..50909bb 100644
--- a/src/tint/writer/hlsl/generator_impl_call_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_call_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -30,7 +31,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func()");
 }
@@ -50,7 +51,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func(param1, param2)");
 }
diff --git a/src/tint/writer/hlsl/generator_impl_cast_test.cc b/src/tint/writer/hlsl/generator_impl_cast_test.cc
index 4c522a3..7bb77da 100644
--- a/src/tint/writer/hlsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_cast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "1.0f");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "float3(1.0f, 2.0f, 3.0f)");
 }
diff --git a/src/tint/writer/hlsl/generator_impl_identifier_test.cc b/src/tint/writer/hlsl/generator_impl_identifier_test.cc
index 1e2b47b..db09c45 100644
--- a/src/tint/writer/hlsl/generator_impl_identifier_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_identifier_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 namespace tint::writer::hlsl {
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, i)) << gen.error();
     EXPECT_EQ(out.str(), "foo");
 }
diff --git a/src/tint/writer/hlsl/generator_impl_import_test.cc b/src/tint/writer/hlsl/generator_impl_import_test.cc
index 425c663..28b72dc 100644
--- a/src/tint/writer/hlsl/generator_impl_import_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_import_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -39,7 +40,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1.0f)");
 }
@@ -77,7 +78,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1)");
 }
@@ -94,7 +95,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    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))");
@@ -134,7 +135,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1.0f, 2.0f)");
 }
@@ -156,7 +157,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) +
                              "(float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f))");
@@ -181,7 +182,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1, 2)");
 }
@@ -199,7 +200,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1.0f, 2.0f, 3.0f)");
 }
@@ -220,7 +221,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(
         out.str(),
@@ -243,7 +244,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.hlsl_name) + "(1, 2, 3)");
 }
@@ -259,7 +260,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string("determinant(var)"));
 }
@@ -272,7 +273,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string("float(min16float(v))"));
 }
@@ -285,7 +286,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string("float3(min16float3(v))"));
 }
diff --git a/src/tint/writer/hlsl/generator_impl_type_test.cc b/src/tint/writer/hlsl/generator_impl_type_test.cc
index 71fe338..4ee8a22 100644
--- a/src/tint/writer/hlsl/generator_impl_type_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_type_test.cc
@@ -21,6 +21,7 @@
 #include "src/tint/type/sampler.h"
 #include "src/tint/type/storage_texture.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 using ::testing::HasSubstr;
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -64,7 +65,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, "ary"))
         << gen.error();
@@ -77,7 +78,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(ty), builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -89,7 +90,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, bool_, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -101,7 +102,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, f16, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -113,7 +114,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, f32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -125,7 +126,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, i32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -139,7 +140,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -153,7 +154,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -203,7 +204,7 @@
     GeneratorImpl& gen = Build();
 
     auto* sem_s = program->TypeOf(s)->As<sem::Struct>();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sem_s, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -251,7 +252,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, u32, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -264,7 +265,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, vec3, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -276,7 +277,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, void_, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -288,7 +289,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -300,7 +301,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sampler, builtin::AddressSpace::kUndefined,
                              builtin::Access::kReadWrite, ""))
         << gen.error();
@@ -511,7 +512,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(
         gen.EmitType(out, s, builtin::AddressSpace::kUndefined, builtin::Access::kReadWrite, ""))
         << gen.error();
diff --git a/src/tint/writer/hlsl/generator_impl_unary_op_test.cc b/src/tint/writer/hlsl/generator_impl_unary_op_test.cc
index 4bf4329..1ac6851 100644
--- a/src/tint/writer/hlsl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_unary_op_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/hlsl/test_helper.h"
 
 namespace tint::writer::hlsl {
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "expr");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "~(expr)");
 }
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "expr");
 }
@@ -63,7 +64,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "!(expr)");
 }
@@ -75,7 +76,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "-(expr)");
 }
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index c433207..81b32c0 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -78,6 +78,7 @@
 #include "src/tint/utils/defer.h"
 #include "src/tint/utils/map.h"
 #include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/check_supported_extensions.h"
 #include "src/tint/writer/float_to_string.h"
 #include "src/tint/writer/generate_external_texture_bindings.h"
@@ -89,7 +90,7 @@
     return IsAnyOf<ast::BreakStatement>(stmts->Last());
 }
 
-void PrintF32(std::ostream& out, float value) {
+void PrintF32(utils::StringStream& out, float value) {
     // Note: Currently inf and nan should not be constructable, but this is implemented for the day
     // we support them.
     if (std::isinf(value)) {
@@ -101,7 +102,7 @@
     }
 }
 
-void PrintF16(std::ostream& out, float value) {
+void PrintF16(utils::StringStream& out, float value) {
     // Note: Currently inf and nan should not be constructable, but this is implemented for the day
     // we support them.
     if (std::isinf(value)) {
@@ -115,7 +116,7 @@
     }
 }
 
-void PrintI32(std::ostream& out, int32_t value) {
+void PrintI32(utils::StringStream& out, int32_t value) {
     // MSL (and C++) parse `-2147483648` as a `long` because it parses unary minus and `2147483648`
     // as separate tokens, and the latter doesn't fit into an (32-bit) `int`.
     // WGSL, on the other hand, parses this as an `i32`.
@@ -131,7 +132,7 @@
 class ScopedBitCast {
   public:
     ScopedBitCast(GeneratorImpl* generator,
-                  std::ostream& stream,
+                  utils::StringStream& stream,
                   const type::Type* curr_type,
                   const type::Type* target_type)
         : s(stream) {
@@ -152,7 +153,7 @@
     ~ScopedBitCast() { s << ")"; }
 
   private:
-    std::ostream& s;
+    utils::StringStream& s;
 };
 
 }  // namespace
@@ -245,8 +246,9 @@
     // ArrayLengthFromUniform must come after SimplifyPointers, as
     // it assumes that the form of the array length argument is &var.array.
     manager.Add<transform::ArrayLengthFromUniform>();
-    manager.Add<transform::ModuleScopeVarToEntryPointParam>();
+    // PackedVec3 must come after ExpandCompoundAssignment.
     manager.Add<transform::PackedVec3>();
+    manager.Add<transform::ModuleScopeVarToEntryPointParam>();
     data.Add<transform::ArrayLengthFromUniform::Config>(std::move(array_length_from_uniform_cfg));
     data.Add<transform::CanonicalizeEntryPointIO::Config>(std::move(entry_point_io_cfg));
     auto out = manager.Run(in, data);
@@ -273,6 +275,7 @@
                                       builtin::Extension::kChromiumDisableUniformityAnalysis,
                                       builtin::Extension::kChromiumExperimentalFullPtrParameters,
                                       builtin::Extension::kChromiumExperimentalPushConstant,
+                                      builtin::Extension::kChromiumInternalRelaxedUniformLayout,
                                       builtin::Extension::kF16,
                                   })) {
         return false;
@@ -367,7 +370,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr) {
+bool GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
+                                      const ast::IndexAccessorExpression* expr) {
     bool paren_lhs =
         !expr->object
              ->IsAnyOf<ast::AccessorExpression, ast::CallExpression, ast::IdentifierExpression>();
@@ -392,7 +396,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr) {
+bool GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
     out << "as_type<";
     if (!EmitType(out, TypeOf(expr)->UnwrapRef(), "")) {
         return false;
@@ -425,7 +429,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
     auto emit_op = [&] {
         out << " ";
 
@@ -634,7 +638,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
     auto* call = program_->Sem().Get<sem::Call>(expr);
     auto* target = call->Target();
     return Switch(
@@ -648,7 +652,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFunctionCall(std::ostream& out,
+bool GeneratorImpl::EmitFunctionCall(utils::StringStream& out,
                                      const sem::Call* call,
                                      const sem::Function* fn) {
     out << program_->Symbols().NameFor(fn->Declaration()->name->symbol) << "(";
@@ -669,7 +673,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBuiltinCall(std::ostream& out,
+bool GeneratorImpl::EmitBuiltinCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     auto* expr = call->Declaration();
@@ -783,7 +787,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitTypeConversion(std::ostream& out,
+bool GeneratorImpl::EmitTypeConversion(utils::StringStream& out,
                                        const sem::Call* call,
                                        const sem::ValueConversion* conv) {
     if (!EmitType(out, conv->Target(), "")) {
@@ -799,7 +803,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitTypeInitializer(std::ostream& out,
+bool GeneratorImpl::EmitTypeInitializer(utils::StringStream& out,
                                         const sem::Call* call,
                                         const sem::ValueConstructor* ctor) {
     auto* type = ctor->ReturnType();
@@ -856,7 +860,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitAtomicCall(std::ostream& out,
+bool GeneratorImpl::EmitAtomicCall(utils::StringStream& out,
                                    const ast::CallExpression* expr,
                                    const sem::Builtin* builtin) {
     auto call = [&](const std::string& name, bool append_memory_order_relaxed) {
@@ -980,7 +984,7 @@
     return false;
 }
 
-bool GeneratorImpl::EmitTextureCall(std::ostream& out,
+bool GeneratorImpl::EmitTextureCall(utils::StringStream& out,
                                     const sem::Call* call,
                                     const sem::Builtin* builtin) {
     using Usage = sem::ParameterUsage;
@@ -1229,7 +1233,7 @@
                 out << "gradientcube(";
                 break;
             default: {
-                std::stringstream err;
+                utils::StringStream err;
                 err << "MSL does not support gradients for " << dim << " textures";
                 diagnostics_.add_error(diag::System::Writer, err.str());
                 return false;
@@ -1292,7 +1296,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitDotCall(std::ostream& out,
+bool GeneratorImpl::EmitDotCall(utils::StringStream& out,
                                 const ast::CallExpression* expr,
                                 const sem::Builtin* builtin) {
     auto* vec_ty = builtin->Parameters()[0]->Type()->As<type::Vector>();
@@ -1337,7 +1341,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitModfCall(std::ostream& out,
+bool GeneratorImpl::EmitModfCall(utils::StringStream& out,
                                  const ast::CallExpression* expr,
                                  const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -1363,7 +1367,7 @@
         });
 }
 
-bool GeneratorImpl::EmitFrexpCall(std::ostream& out,
+bool GeneratorImpl::EmitFrexpCall(utils::StringStream& out,
                                   const ast::CallExpression* expr,
                                   const sem::Builtin* builtin) {
     return CallBuiltinHelper(
@@ -1389,7 +1393,7 @@
         });
 }
 
-bool GeneratorImpl::EmitDegreesCall(std::ostream& out,
+bool GeneratorImpl::EmitDegreesCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     return CallBuiltinHelper(out, expr, builtin,
@@ -1400,7 +1404,7 @@
                              });
 }
 
-bool GeneratorImpl::EmitRadiansCall(std::ostream& out,
+bool GeneratorImpl::EmitRadiansCall(utils::StringStream& out,
                                     const ast::CallExpression* expr,
                                     const sem::Builtin* builtin) {
     return CallBuiltinHelper(out, expr, builtin,
@@ -1612,7 +1616,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitZeroValue(std::ostream& out, const type::Type* type) {
+bool GeneratorImpl::EmitZeroValue(utils::StringStream& out, const type::Type* type) {
     return Switch(
         type,
         [&](const type::Bool*) {
@@ -1661,7 +1665,7 @@
         });
 }
 
-bool GeneratorImpl::EmitConstant(std::ostream& out, const constant::Value* constant) {
+bool GeneratorImpl::EmitConstant(utils::StringStream& out, const constant::Value* constant) {
     return Switch(
         constant->Type(),  //
         [&](const type::Bool*) {
@@ -1788,7 +1792,7 @@
         });
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit) {
+bool GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
     return Switch(
         lit,
         [&](const ast::BoolLiteralExpression* l) {
@@ -1824,7 +1828,7 @@
         });
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, const ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
     if (auto* sem = builder_.Sem().GetVal(expr)) {
         if (auto* constant = sem->ConstantValue()) {
             return EmitConstant(out, constant);
@@ -1847,7 +1851,7 @@
         });
 }
 
-void GeneratorImpl::EmitStage(std::ostream& out, ast::PipelineStage stage) {
+void GeneratorImpl::EmitStage(utils::StringStream& out, ast::PipelineStage stage) {
     switch (stage) {
         case ast::PipelineStage::kFragment:
             out << "fragment";
@@ -2124,7 +2128,8 @@
     return true;
 }
 
-bool GeneratorImpl::EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr) {
+bool GeneratorImpl::EmitIdentifier(utils::StringStream& out,
+                                   const ast::IdentifierExpression* expr) {
     out << program_->Symbols().NameFor(expr->identifier->symbol);
     return true;
 }
@@ -2165,7 +2170,7 @@
     }
 
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
     if (auto* cond = stmt->condition) {
         TINT_SCOPED_ASSIGNMENT(current_buffer_, &cond_pre);
         if (!EmitExpression(cond_buf, cond)) {
@@ -2266,7 +2271,7 @@
 
 bool GeneratorImpl::EmitWhile(const ast::WhileStatement* stmt) {
     TextBuffer cond_pre;
-    std::stringstream cond_buf;
+    utils::StringStream cond_buf;
 
     {
         auto* cond = stmt->condition;
@@ -2352,7 +2357,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
+bool GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
                                        const ast::MemberAccessorExpression* expr) {
     auto write_lhs = [&] {
         bool paren_lhs = !expr->object->IsAnyOf<ast::AccessorExpression, ast::CallExpression,
@@ -2374,25 +2379,20 @@
     return Switch(
         sem,
         [&](const sem::Swizzle* swizzle) {
-            // Metal 1.x does not support swizzling of packed vector types.
-            // For single element swizzles, we can use the index operator.
-            // For multi-element swizzles, we need to cast to a regular vector type
-            // first. Note that we do not currently allow assignments to swizzles, so
-            // the casting which will convert the l-value to r-value is fine.
+            // Metal did not add support for swizzle syntax with packed vector types until
+            // Metal 2.1, so we need to use the index operator for single-element selection instead.
+            // For multi-component swizzles, the PackedVec3 transform will have inserted casts to
+            // the non-packed types, so we can safely use swizzle syntax here.
             if (swizzle->Indices().Length() == 1) {
                 if (!write_lhs()) {
                     return false;
                 }
                 out << "[" << swizzle->Indices()[0] << "]";
             } else {
-                if (!EmitType(out, swizzle->Object()->Type()->UnwrapRef(), "")) {
-                    return false;
-                }
-                out << "(";
                 if (!write_lhs()) {
                     return false;
                 }
-                out << ")." << program_->Symbols().NameFor(expr->member->symbol);
+                out << "." << program_->Symbols().NameFor(expr->member->symbol);
             }
             return true;
         },
@@ -2544,7 +2544,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitType(std::ostream& out,
+bool GeneratorImpl::EmitType(utils::StringStream& out,
                              const type::Type* type,
                              const std::string& name,
                              bool* name_printed /* = nullptr */) {
@@ -2731,6 +2731,9 @@
             return true;
         },
         [&](const type::Vector* vec) {
+            if (vec->Packed()) {
+                out << "packed_";
+            }
             if (!EmitType(out, vec->type(), "")) {
                 return false;
             }
@@ -2749,7 +2752,7 @@
         });
 }
 
-bool GeneratorImpl::EmitTypeAndName(std::ostream& out,
+bool GeneratorImpl::EmitTypeAndName(utils::StringStream& out,
                                     const type::Type* type,
                                     const std::string& name) {
     bool name_printed = false;
@@ -2762,7 +2765,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitAddressSpace(std::ostream& out, builtin::AddressSpace sc) {
+bool GeneratorImpl::EmitAddressSpace(utils::StringStream& out, builtin::AddressSpace sc) {
     switch (sc) {
         case builtin::AddressSpace::kFunction:
         case builtin::AddressSpace::kPrivate:
@@ -2796,7 +2799,7 @@
     bool is_host_shareable = str->IsHostShareable();
 
     // Emits a `/* 0xnnnn */` byte offset comment for a struct member.
-    auto add_byte_offset_comment = [&](std::ostream& out, uint32_t offset) {
+    auto add_byte_offset_comment = [&](utils::StringStream& out, uint32_t offset) {
         std::ios_base::fmtflags saved_flag_state(out.flags());
         out << "/* 0x" << std::hex << std::setfill('0') << std::setw(4) << offset << " */ ";
         out.flags(saved_flag_state);
@@ -2838,11 +2841,6 @@
             add_byte_offset_comment(out, msl_offset);
         }
 
-        if (auto* decl = mem->Declaration()) {
-            if (ast::HasAttribute<transform::PackedVec3::Attribute>(decl->attributes)) {
-                out << "packed_";
-            }
-        }
         if (!EmitType(out, mem->Type(), mem_name)) {
             return false;
         }
@@ -2924,7 +2922,6 @@
                     [&](const ast::StructMemberOffsetAttribute*) { return true; },
                     [&](const ast::StructMemberAlignAttribute*) { return true; },
                     [&](const ast::StructMemberSizeAttribute*) { return true; },
-                    [&](const transform::PackedVec3::Attribute*) { return true; },
                     [&](Default) {
                         TINT_ICE(Writer, diagnostics_)
                             << "unhandled struct member attribute: " << attr->Name();
@@ -2961,7 +2958,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr) {
+bool GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
     // Handle `-e` when `e` is signed, so that we ensure that if `e` is the
     // largest negative value, it returns `e`.
     auto* expr_type = TypeOf(expr->expr)->UnwrapRef();
@@ -3240,7 +3237,7 @@
 }
 
 template <typename F>
-bool GeneratorImpl::CallBuiltinHelper(std::ostream& out,
+bool GeneratorImpl::CallBuiltinHelper(utils::StringStream& out,
                                       const ast::CallExpression* call,
                                       const sem::Builtin* builtin,
                                       F&& build) {
diff --git a/src/tint/writer/msl/generator_impl.h b/src/tint/writer/msl/generator_impl.h
index 9c7c0d4..5699242 100644
--- a/src/tint/writer/msl/generator_impl.h
+++ b/src/tint/writer/msl/generator_impl.h
@@ -40,6 +40,7 @@
 #include "src/tint/program.h"
 #include "src/tint/scope_stack.h"
 #include "src/tint/sem/struct.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/array_length_from_uniform_options.h"
 #include "src/tint/writer/msl/generator.h"
 #include "src/tint/writer/text_generator.h"
@@ -106,7 +107,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the index accessor was emitted
-    bool EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr);
+    bool EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
     /// Handles an assignment statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -115,12 +116,12 @@
     /// @param out the output of the expression stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a bitcast expression
     /// @param out the output of the expression stream
     /// @param expr the bitcast expression
     /// @returns true if the bitcast was emitted
-    bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
+    bool EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
     /// Handles a block statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -137,19 +138,21 @@
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles generating a builtin call expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param builtin the builtin being called
     /// @returns true if the call expression is emitted
-    bool EmitBuiltinCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitBuiltinCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a value conversion expression
     /// @param out the output of the expression stream
     /// @param call the call expression
     /// @param conv the value conversion
     /// @returns true if the expression is emitted
-    bool EmitTypeConversion(std::ostream& out,
+    bool EmitTypeConversion(utils::StringStream& out,
                             const sem::Call* call,
                             const sem::ValueConversion* conv);
     /// Handles generating a value constructor
@@ -157,7 +160,7 @@
     /// @param call the call expression
     /// @param ctor the value constructor
     /// @returns true if the initializer is emitted
-    bool EmitTypeInitializer(std::ostream& out,
+    bool EmitTypeInitializer(utils::StringStream& out,
                              const sem::Call* call,
                              const sem::ValueConstructor* ctor);
     /// Handles generating a function call
@@ -165,14 +168,16 @@
     /// @param call the call expression
     /// @param func the target function
     /// @returns true if the call is emitted
-    bool EmitFunctionCall(std::ostream& out, const sem::Call* call, const sem::Function* func);
+    bool EmitFunctionCall(utils::StringStream& out,
+                          const sem::Call* call,
+                          const sem::Function* func);
     /// Handles generating a call to an atomic function (`atomicAdd`,
     /// `atomicMax`, etc)
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the atomic builtin
     /// @returns true if the call expression is emitted
-    bool EmitAtomicCall(std::ostream& out,
+    bool EmitAtomicCall(utils::StringStream& out,
                         const ast::CallExpression* expr,
                         const sem::Builtin* builtin);
     /// Handles generating a call to a texture function (`textureSample`,
@@ -181,13 +186,15 @@
     /// @param call the call expression
     /// @param builtin the semantic information for the texture builtin
     /// @returns true if the call expression is emitted
-    bool EmitTextureCall(std::ostream& out, const sem::Call* call, const sem::Builtin* builtin);
+    bool EmitTextureCall(utils::StringStream& out,
+                         const sem::Call* call,
+                         const sem::Builtin* builtin);
     /// Handles generating a call to the `dot()` builtin
     /// @param out the output of the expression stream
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDotCall(std::ostream& out,
+    bool EmitDotCall(utils::StringStream& out,
                      const ast::CallExpression* expr,
                      const sem::Builtin* builtin);
     /// Handles generating a call to the `modf()` builtin
@@ -195,7 +202,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitModfCall(std::ostream& out,
+    bool EmitModfCall(utils::StringStream& out,
                       const ast::CallExpression* expr,
                       const sem::Builtin* builtin);
     /// Handles generating a call to the `frexp()` builtin
@@ -203,7 +210,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitFrexpCall(std::ostream& out,
+    bool EmitFrexpCall(utils::StringStream& out,
                        const ast::CallExpression* expr,
                        const sem::Builtin* builtin);
     /// Handles generating a call to the `degrees()` builtin
@@ -211,7 +218,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitDegreesCall(std::ostream& out,
+    bool EmitDegreesCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles generating a call to the `radians()` builtin
@@ -219,7 +226,7 @@
     /// @param expr the call expression
     /// @param builtin the semantic information for the builtin
     /// @returns true if the call expression is emitted
-    bool EmitRadiansCall(std::ostream& out,
+    bool EmitRadiansCall(utils::StringStream& out,
                          const ast::CallExpression* expr,
                          const sem::Builtin* builtin);
     /// Handles a case statement
@@ -242,7 +249,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression
     /// @returns true if the expression was emitted
-    bool EmitExpression(std::ostream& out, const ast::Expression* expr);
+    bool EmitExpression(utils::StringStream& out, const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
     /// @returns true if the function was emitted
@@ -251,7 +258,7 @@
     /// @param out the output of the expression stream
     /// @param expr the identifier expression
     /// @returns true if the identifier was emitted
-    bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
+    bool EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles an if statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -260,12 +267,12 @@
     /// @param out the output stream
     /// @param constant the constant value to emit
     /// @returns true if the constant value was successfully emitted
-    bool EmitConstant(std::ostream& out, const constant::Value* constant);
+    bool EmitConstant(utils::StringStream& out, const constant::Value* constant);
     /// Handles a literal
     /// @param out the output of the expression stream
     /// @param lit the literal to emit
     /// @returns true if the literal was successfully emitted
-    bool EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit);
+    bool EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit);
     /// Handles a loop statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted
@@ -282,7 +289,7 @@
     /// @param out the output of the expression stream
     /// @param expr the member accessor expression
     /// @returns true if the member accessor was emitted
-    bool EmitMemberAccessor(std::ostream& out, const ast::MemberAccessorExpression* expr);
+    bool EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
     /// Handles return statements
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -290,7 +297,7 @@
     /// Handles emitting a pipeline stage name
     /// @param out the output of the expression stream
     /// @param stage the stage to emit
-    void EmitStage(std::ostream& out, ast::PipelineStage stage);
+    void EmitStage(utils::StringStream& out, ast::PipelineStage stage);
     /// Handles statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted
@@ -313,7 +320,7 @@
     /// @param name the name of the variable, only used for array emission
     /// @param name_printed (optional) if not nullptr and an array was printed
     /// @returns true if the type is emitted
-    bool EmitType(std::ostream& out,
+    bool EmitType(utils::StringStream& out,
                   const type::Type* type,
                   const std::string& name,
                   bool* name_printed = nullptr);
@@ -322,12 +329,12 @@
     /// @param type the type to generate
     /// @param name the name to emit
     /// @returns true if the type is emitted
-    bool EmitTypeAndName(std::ostream& out, const type::Type* type, const std::string& name);
+    bool EmitTypeAndName(utils::StringStream& out, const type::Type* type, const std::string& name);
     /// Handles generating a address space
     /// @param out the output of the type stream
     /// @param sc the address space to generate
     /// @returns true if the address space is emitted
-    bool EmitAddressSpace(std::ostream& out, builtin::AddressSpace sc);
+    bool EmitAddressSpace(utils::StringStream& out, builtin::AddressSpace sc);
     /// Handles generating a struct declaration. If the structure has already been emitted, then
     /// this function will simply return `true` without emitting anything.
     /// @param buffer the text buffer that the type declaration will be written to
@@ -338,7 +345,7 @@
     /// @param out the output of the expression stream
     /// @param expr the expression to emit
     /// @returns true if the expression was emitted
-    bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
+    bool EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
     /// Handles generating a 'var' declaration
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
@@ -351,7 +358,7 @@
     /// @param out the output of the expression stream
     /// @param type the type to emit the value for
     /// @returns true if the zero value was successfully emitted.
-    bool EmitZeroValue(std::ostream& out, const type::Type* type);
+    bool EmitZeroValue(utils::StringStream& out, const type::Type* type);
 
     /// Handles generating a builtin name
     /// @param builtin the semantic info for the builtin
@@ -391,7 +398,7 @@
     ///          `params` is the name of all the generated function parameters
     /// @returns true if the call expression is emitted
     template <typename F>
-    bool CallBuiltinHelper(std::ostream& out,
+    bool CallBuiltinHelper(utils::StringStream& out,
                            const ast::CallExpression* call,
                            const sem::Builtin* builtin,
                            F&& build);
diff --git a/src/tint/writer/msl/generator_impl_array_accessor_test.cc b/src/tint/writer/msl/generator_impl_array_accessor_test.cc
index 47145d1..f721416 100644
--- a/src/tint/writer/msl/generator_impl_array_accessor_test.cc
+++ b/src/tint/writer/msl/generator_impl_array_accessor_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "ary[5]");
 }
@@ -42,7 +43,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(*(p))[5]");
 }
diff --git a/src/tint/writer/msl/generator_impl_binary_test.cc b/src/tint/writer/msl/generator_impl_binary_test.cc
index 7519b61..8b7e4b5 100644
--- a/src/tint/writer/msl/generator_impl_binary_test.cc
+++ b/src/tint/writer/msl/generator_impl_binary_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 namespace tint::writer::msl {
@@ -44,7 +45,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -88,7 +89,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -122,7 +123,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr2)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -149,7 +150,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
@@ -164,7 +165,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
@@ -177,7 +178,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
@@ -192,7 +193,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "fmod(left, right)");
 }
@@ -205,7 +206,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "bool(left & right)");
 }
@@ -218,7 +219,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "bool(left | right)");
 }
diff --git a/src/tint/writer/msl/generator_impl_bitcast_test.cc b/src/tint/writer/msl/generator_impl_bitcast_test.cc
index 31aa79a..a4aa870 100644
--- a/src/tint/writer/msl/generator_impl_bitcast_test.cc
+++ b/src/tint/writer/msl/generator_impl_bitcast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "as_type<float>(a)");
 }
diff --git a/src/tint/writer/msl/generator_impl_builtin_test.cc b/src/tint/writer/msl/generator_impl_builtin_test.cc
index 84fb7d5..0c6ba28 100644
--- a/src/tint/writer/msl/generator_impl_builtin_test.cc
+++ b/src/tint/writer/msl/generator_impl_builtin_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ast/call_statement.h"
 #include "src/tint/sem/call.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -61,8 +62,8 @@
                                         CallParamType type,
                                         ProgramBuilder* builder) {
     std::string name;
-    std::ostringstream str(name);
-    str << builtin;
+    utils::StringStream str;
+    str << name << builtin;
     switch (builtin) {
         case BuiltinType::kAcos:
         case BuiltinType::kAsin:
@@ -378,7 +379,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "dot(param1, param2)");
 }
@@ -389,7 +390,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "threadgroup_barrier(mem_flags::mem_device)");
 }
@@ -400,7 +401,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "threadgroup_barrier(mem_flags::mem_threadgroup)");
 }
@@ -850,7 +851,7 @@
 using namespace metal;
 
 float tint_degrees(float param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 kernel void test_function() {
@@ -875,7 +876,7 @@
 using namespace metal;
 
 float3 tint_degrees(float3 param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 kernel void test_function() {
@@ -902,7 +903,7 @@
 using namespace metal;
 
 half tint_degrees(half param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 kernel void test_function() {
@@ -929,7 +930,7 @@
 using namespace metal;
 
 half3 tint_degrees(half3 param_0) {
-  return param_0 * 57.295779513082322865;
+  return param_0 * 57.295779513082323;
 }
 
 kernel void test_function() {
@@ -954,7 +955,7 @@
 using namespace metal;
 
 float tint_radians(float param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 kernel void test_function() {
@@ -979,7 +980,7 @@
 using namespace metal;
 
 float3 tint_radians(float3 param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 kernel void test_function() {
@@ -1006,7 +1007,7 @@
 using namespace metal;
 
 half tint_radians(half param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 kernel void test_function() {
@@ -1033,7 +1034,7 @@
 using namespace metal;
 
 half3 tint_radians(half3 param_0) {
-  return param_0 * 0.017453292519943295474;
+  return param_0 * 0.017453292519943295;
 }
 
 kernel void test_function() {
@@ -1052,7 +1053,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "as_type<uint>(half2(p1))");
 }
@@ -1064,7 +1065,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "float2(as_type<half2>(p1))");
 }
diff --git a/src/tint/writer/msl/generator_impl_builtin_texture_test.cc b/src/tint/writer/msl/generator_impl_builtin_texture_test.cc
index f3d65b1..730fe99 100644
--- a/src/tint/writer/msl/generator_impl_builtin_texture_test.cc
+++ b/src/tint/writer/msl/generator_impl_builtin_texture_test.cc
@@ -14,6 +14,7 @@
 
 #include "src/tint/ast/builtin_texture_helper_test.h"
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 namespace tint::writer::msl {
@@ -285,7 +286,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
 
     auto expected = expected_texture_overload(param.overload);
diff --git a/src/tint/writer/msl/generator_impl_call_test.cc b/src/tint/writer/msl/generator_impl_call_test.cc
index 26da4e4..95fbe91 100644
--- a/src/tint/writer/msl/generator_impl_call_test.cc
+++ b/src/tint/writer/msl/generator_impl_call_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -30,7 +31,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func()");
 }
@@ -53,7 +54,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func(param1, param2)");
 }
diff --git a/src/tint/writer/msl/generator_impl_cast_test.cc b/src/tint/writer/msl/generator_impl_cast_test.cc
index ec1fe7c..51fc0ab 100644
--- a/src/tint/writer/msl/generator_impl_cast_test.cc
+++ b/src/tint/writer/msl/generator_impl_cast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "1.0f");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "float3(1.0f, 2.0f, 3.0f)");
 }
@@ -49,7 +50,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "2147483648u");
 }
diff --git a/src/tint/writer/msl/generator_impl_identifier_test.cc b/src/tint/writer/msl/generator_impl_identifier_test.cc
index a625776..a1e6260 100644
--- a/src/tint/writer/msl/generator_impl_identifier_test.cc
+++ b/src/tint/writer/msl/generator_impl_identifier_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 namespace tint::writer::msl {
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, i)) << gen.error();
     EXPECT_EQ(out.str(), "foo");
 }
diff --git a/src/tint/writer/msl/generator_impl_import_test.cc b/src/tint/writer/msl/generator_impl_import_test.cc
index d00acd5..841e7dc 100644
--- a/src/tint/writer/msl/generator_impl_import_test.cc
+++ b/src/tint/writer/msl/generator_impl_import_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/sem/call.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -81,7 +82,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), R"(abs(1))");
 }
@@ -92,7 +93,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), R"(fabs(2.0f))");
 }
@@ -106,7 +107,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.msl_name) + "(1.0f, 2.0f)");
 }
@@ -124,7 +125,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), R"(fabs(2.0f - 3.0f))");
 }
@@ -138,7 +139,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.msl_name) +
                              R"((float3(1.0f, 2.0f, 3.0f), float3(4.0f, 5.0f, 6.0f)))");
@@ -163,7 +164,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.msl_name) + "(1, 2)");
 }
@@ -180,7 +181,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.msl_name) + "(1.0f, 2.0f, 3.0f)");
 }
@@ -201,7 +202,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(
         out.str(),
@@ -224,7 +225,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.msl_name) + "(1, 2, 3)");
 }
@@ -242,7 +243,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), std::string("determinant(var)"));
 }
@@ -255,7 +256,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "float(half(v))");
 }
@@ -268,7 +269,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitCall(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "float3(half3(v))");
 }
diff --git a/src/tint/writer/msl/generator_impl_member_accessor_test.cc b/src/tint/writer/msl/generator_impl_member_accessor_test.cc
index 1e1136e..38a222c 100644
--- a/src/tint/writer/msl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/msl/generator_impl_member_accessor_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 namespace tint::writer::msl {
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "str.mem");
 }
@@ -39,9 +40,9 @@
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
-    EXPECT_EQ(out.str(), "float4(my_vec).xyz");
+    EXPECT_EQ(out.str(), "my_vec.xyz");
 }
 
 TEST_F(MslGeneratorImplTest, EmitExpression_MemberAccessor_Swizzle_gbr) {
@@ -51,9 +52,9 @@
     WrapInFunction(expr);
 
     GeneratorImpl& gen = Build();
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
-    EXPECT_EQ(out.str(), "float4(my_vec).gbr");
+    EXPECT_EQ(out.str(), "my_vec.gbr");
 }
 
 }  // namespace
diff --git a/src/tint/writer/msl/generator_impl_type_test.cc b/src/tint/writer/msl/generator_impl_type_test.cc
index 9775cf9..e1e18a3 100644
--- a/src/tint/writer/msl/generator_impl_type_test.cc
+++ b/src/tint/writer/msl/generator_impl_type_test.cc
@@ -23,6 +23,7 @@
 #include "src/tint/type/sampler.h"
 #include "src/tint/type/storage_texture.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 using ::testing::HasSubstr;
@@ -32,7 +33,7 @@
 namespace tint::writer::msl {
 namespace {
 
-void FormatMSLField(std::stringstream& out,
+void FormatMSLField(utils::StringStream& out,
                     const char* addr,
                     const char* type,
                     size_t array_count,
@@ -94,7 +95,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "ary")) << gen.error();
     EXPECT_EQ(out.str(), "tint_array<bool, 4>");
 }
@@ -106,7 +107,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "ary")) << gen.error();
     EXPECT_EQ(out.str(), "tint_array<tint_array<bool, 4>, 5>");
 }
@@ -119,7 +120,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "ary")) << gen.error();
     EXPECT_EQ(out.str(), "tint_array<tint_array<tint_array<bool, 4>, 5>, 6>");
 }
@@ -130,7 +131,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "")) << gen.error();
     EXPECT_EQ(out.str(), "tint_array<bool, 4>");
 }
@@ -141,7 +142,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "ary")) << gen.error();
     EXPECT_EQ(out.str(), "tint_array<bool, 1>");
 }
@@ -151,7 +152,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, bool_, "")) << gen.error();
     EXPECT_EQ(out.str(), "bool");
 }
@@ -161,7 +162,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, f32, "")) << gen.error();
     EXPECT_EQ(out.str(), "float");
 }
@@ -171,7 +172,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, f16, "")) << gen.error();
     EXPECT_EQ(out.str(), "half");
 }
@@ -181,7 +182,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, i32, "")) << gen.error();
     EXPECT_EQ(out.str(), "int");
 }
@@ -193,7 +194,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, "")) << gen.error();
     EXPECT_EQ(out.str(), "float2x3");
 }
@@ -205,7 +206,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, mat2x3, "")) << gen.error();
     EXPECT_EQ(out.str(), "half2x3");
 }
@@ -217,7 +218,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, p, "")) << gen.error();
     EXPECT_EQ(out.str(), "threadgroup float* ");
 }
@@ -230,7 +231,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(s), "")) << gen.error();
     EXPECT_EQ(out.str(), "S");
 }
@@ -338,7 +339,7 @@
     FIELD(0x0304, int8_t, 124, tint_pad_12)
 
     // Check that the generated string is as expected.
-    std::stringstream expect;
+    utils::StringStream expect;
     expect << "struct S {\n";
 #define FIELD(ADDR, TYPE, ARRAY_COUNT, NAME) \
     FormatMSLField(expect, #ADDR, #TYPE, ARRAY_COUNT, #NAME);
@@ -415,7 +416,7 @@
     FIELD(0x080c, int8_t, 500, tint_pad_1)
 
     // Check that the generated string is as expected.
-    std::stringstream expect;
+    utils::StringStream expect;
     expect << "struct S {\n";
 #define FIELD(ADDR, TYPE, ARRAY_COUNT, NAME) \
     FormatMSLField(expect, #ADDR, #TYPE, ARRAY_COUNT, #NAME);
@@ -508,7 +509,7 @@
     FIELD(0x1208, int8_t, 504, tint_pad_1)
 
     // Check that the generated string is as expected.
-    std::stringstream expect;
+    utils::StringStream expect;
     expect << "struct S {\n";
 #define FIELD(ADDR, TYPE, ARRAY_COUNT, NAME) \
     FormatMSLField(expect, #ADDR, #TYPE, ARRAY_COUNT, #NAME);
@@ -589,7 +590,7 @@
     FIELD(0x0054, int8_t, 12, tint_pad_1)
 
     // Check that the generated string is as expected.
-    std::stringstream expect;
+    utils::StringStream expect;
     expect << "struct S {\n";
 #define FIELD(ADDR, TYPE, ARRAY_COUNT, NAME) \
     FormatMSLField(expect, #ADDR, #TYPE, ARRAY_COUNT, #NAME);
@@ -711,7 +712,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, u32, "")) << gen.error();
     EXPECT_EQ(out.str(), "uint");
 }
@@ -722,7 +723,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, vec3, "")) << gen.error();
     EXPECT_EQ(out.str(), "float3");
 }
@@ -732,7 +733,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, void_, "")) << gen.error();
     EXPECT_EQ(out.str(), "void");
 }
@@ -742,7 +743,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sampler, "")) << gen.error();
     EXPECT_EQ(out.str(), "sampler");
 }
@@ -752,7 +753,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, sampler, "")) << gen.error();
     EXPECT_EQ(out.str(), "sampler");
 }
@@ -773,7 +774,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, &s, "")) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -794,7 +795,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, &s, "")) << gen.error();
     EXPECT_EQ(out.str(), "depth2d_ms<float, access::read>");
 }
@@ -816,7 +817,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, s, "")) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
@@ -838,7 +839,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, ms, "")) << gen.error();
     EXPECT_EQ(out.str(), "texture2d_ms<uint, access::read>");
 }
@@ -860,7 +861,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitType(out, program->TypeOf(type), "")) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
diff --git a/src/tint/writer/msl/generator_impl_unary_op_test.cc b/src/tint/writer/msl/generator_impl_unary_op_test.cc
index 95801a8..d92a9cb 100644
--- a/src/tint/writer/msl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/msl/generator_impl_unary_op_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/msl/test_helper.h"
 
 namespace tint::writer::msl {
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "&(expr)");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "~(expr)");
 }
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "*(expr)");
 }
@@ -63,7 +64,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "!(expr)");
 }
@@ -75,7 +76,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "tint_unary_minus(expr)");
 }
@@ -86,7 +87,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "(-2147483647 - 1)");
 }
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index ad0d746..40f98d8 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -49,6 +49,7 @@
 #include "src/tint/utils/compiler_macros.h"
 #include "src/tint/utils/defer.h"
 #include "src/tint/utils/map.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/append_vector.h"
 #include "src/tint/writer/check_supported_extensions.h"
 
@@ -4138,7 +4139,7 @@
 
 bool Builder::push_function_inst(spv::Op op, const OperandList& operands) {
     if (functions_.empty()) {
-        std::ostringstream ss;
+        utils::StringStream ss;
         ss << "Internal error: trying to add SPIR-V instruction " << int(op)
            << " outside a function";
         error_ = ss.str();
diff --git a/src/tint/writer/text_generator.cc b/src/tint/writer/text_generator.cc
index 6d29844..20c5fe7 100644
--- a/src/tint/writer/text_generator.cc
+++ b/src/tint/writer/text_generator.cc
@@ -114,7 +114,7 @@
 }
 
 std::string TextGenerator::TextBuffer::String(uint32_t indent /* = 0 */) const {
-    std::stringstream ss;
+    utils::StringStream ss;
     for (auto& line : lines) {
         if (!line.content.empty()) {
             for (uint32_t i = 0; i < indent + line.indent; i++) {
@@ -127,9 +127,10 @@
     return ss.str();
 }
 
-TextGenerator::ScopedParen::ScopedParen(std::ostream& stream) : s(stream) {
+TextGenerator::ScopedParen::ScopedParen(utils::StringStream& stream) : s(stream) {
     s << "(";
 }
+
 TextGenerator::ScopedParen::~ScopedParen() {
     s << ")";
 }
diff --git a/src/tint/writer/text_generator.h b/src/tint/writer/text_generator.h
index 1c22522..74e3ecb 100644
--- a/src/tint/writer/text_generator.h
+++ b/src/tint/writer/text_generator.h
@@ -23,6 +23,7 @@
 
 #include "src/tint/diagnostic/diagnostic.h"
 #include "src/tint/program_builder.h"
+#include "src/tint/utils/string_stream.h"
 
 namespace tint::writer {
 
@@ -139,13 +140,13 @@
         /// Destructor
         ~LineWriter();
 
-        /// @returns the ostringstream
-        operator std::ostream&() { return os; }
+        /// @returns the utils::StringStream
+        operator utils::StringStream&() { return os; }
 
         /// @param rhs the value to write to the line
-        /// @returns the ostream so calls can be chained
+        /// @returns the utils::StringStream so calls can be chained
         template <typename T>
-        std::ostream& operator<<(T&& rhs) {
+        utils::StringStream& operator<<(T&& rhs) {
             return os << std::forward<T>(rhs);
         }
 
@@ -153,15 +154,15 @@
         LineWriter(const LineWriter&) = delete;
         LineWriter& operator=(const LineWriter&) = delete;
 
-        std::ostringstream os;
+        utils::StringStream os;
         TextBuffer* buffer;
     };
 
     /// Helper for writing a '(' on construction and a ')' destruction.
     struct ScopedParen {
         /// Constructor
-        /// @param stream the std::ostream that will be written to
-        explicit ScopedParen(std::ostream& stream);
+        /// @param stream the utils::StringStream that will be written to
+        explicit ScopedParen(utils::StringStream& stream);
         /// Destructor
         ~ScopedParen();
 
@@ -169,7 +170,7 @@
         ScopedParen(ScopedParen&& rhs) = delete;
         ScopedParen(const ScopedParen&) = delete;
         ScopedParen& operator=(const ScopedParen&) = delete;
-        std::ostream& s;
+        utils::StringStream& s;
     };
 
     /// Helper for incrementing indentation on construction and decrementing
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 4363b62..c052075 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -89,7 +89,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitDiagnosticControl(std::ostream& out,
+bool GeneratorImpl::EmitDiagnosticControl(utils::StringStream& out,
                                           const ast::DiagnosticControl& diagnostic) {
     out << "diagnostic(" << diagnostic.severity << ", "
         << program_->Symbols().NameFor(diagnostic.rule_name->symbol) << ")";
@@ -124,7 +124,7 @@
         });
 }
 
-bool GeneratorImpl::EmitExpression(std::ostream& out, const ast::Expression* expr) {
+bool GeneratorImpl::EmitExpression(utils::StringStream& out, const ast::Expression* expr) {
     return Switch(
         expr,
         [&](const ast::IndexAccessorExpression* a) {  //
@@ -161,7 +161,8 @@
         });
 }
 
-bool GeneratorImpl::EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr) {
+bool GeneratorImpl::EmitIndexAccessor(utils::StringStream& out,
+                                      const ast::IndexAccessorExpression* expr) {
     bool paren_lhs =
         !expr->object
              ->IsAnyOf<ast::AccessorExpression, ast::CallExpression, ast::IdentifierExpression>();
@@ -184,7 +185,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitMemberAccessor(std::ostream& out,
+bool GeneratorImpl::EmitMemberAccessor(utils::StringStream& out,
                                        const ast::MemberAccessorExpression* expr) {
     bool paren_lhs =
         !expr->object
@@ -203,7 +204,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr) {
+bool GeneratorImpl::EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr) {
     out << "bitcast<";
     if (!EmitExpression(out, expr->type)) {
         return false;
@@ -218,7 +219,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitCall(std::ostream& out, const ast::CallExpression* expr) {
+bool GeneratorImpl::EmitCall(utils::StringStream& out, const ast::CallExpression* expr) {
     if (!EmitExpression(out, expr->target)) {
         return false;
     }
@@ -242,7 +243,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitLiteral(std::ostream& out, const ast::LiteralExpression* lit) {
+bool GeneratorImpl::EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* lit) {
     return Switch(
         lit,
         [&](const ast::BoolLiteralExpression* l) {  //
@@ -271,11 +272,12 @@
         });
 }
 
-bool GeneratorImpl::EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr) {
+bool GeneratorImpl::EmitIdentifier(utils::StringStream& out,
+                                   const ast::IdentifierExpression* expr) {
     return EmitIdentifier(out, expr->identifier);
 }
 
-bool GeneratorImpl::EmitIdentifier(std::ostream& out, const ast::Identifier* ident) {
+bool GeneratorImpl::EmitIdentifier(utils::StringStream& out, const ast::Identifier* ident) {
     if (auto* tmpl_ident = ident->As<ast::TemplatedIdentifier>()) {
         if (!tmpl_ident->attributes.IsEmpty()) {
             EmitAttributes(out, tmpl_ident->attributes);
@@ -363,7 +365,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitImageFormat(std::ostream& out, const builtin::TexelFormat fmt) {
+bool GeneratorImpl::EmitImageFormat(utils::StringStream& out, const builtin::TexelFormat fmt) {
     switch (fmt) {
         case builtin::TexelFormat::kUndefined:
             diagnostics_.add_error(diag::System::Writer, "unknown image format");
@@ -441,7 +443,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitVariable(std::ostream& out, const ast::Variable* v) {
+bool GeneratorImpl::EmitVariable(utils::StringStream& out, const ast::Variable* v) {
     if (!v->attributes.IsEmpty()) {
         if (!EmitAttributes(out, v->attributes)) {
             return false;
@@ -508,7 +510,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitAttributes(std::ostream& out,
+bool GeneratorImpl::EmitAttributes(utils::StringStream& out,
                                    utils::VectorRef<const ast::Attribute*> attrs) {
     bool first = true;
     for (auto* attr : attrs) {
@@ -650,7 +652,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBinary(std::ostream& out, const ast::BinaryExpression* expr) {
+bool GeneratorImpl::EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr) {
     out << "(";
 
     if (!EmitExpression(out, expr->lhs)) {
@@ -670,7 +672,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBinaryOp(std::ostream& out, const ast::BinaryOp op) {
+bool GeneratorImpl::EmitBinaryOp(utils::StringStream& out, const ast::BinaryOp op) {
     switch (op) {
         case ast::BinaryOp::kAnd:
             out << "&";
@@ -733,7 +735,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr) {
+bool GeneratorImpl::EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr) {
     switch (expr->op) {
         case ast::UnaryOp::kAddressOf:
             out << "&";
@@ -777,7 +779,7 @@
     return true;
 }
 
-bool GeneratorImpl::EmitBlockHeader(std::ostream& out, const ast::BlockStatement* stmt) {
+bool GeneratorImpl::EmitBlockHeader(utils::StringStream& out, const ast::BlockStatement* stmt) {
     if (!stmt->attributes.IsEmpty()) {
         if (!EmitAttributes(out, stmt->attributes)) {
             return false;
diff --git a/src/tint/writer/wgsl/generator_impl.h b/src/tint/writer/wgsl/generator_impl.h
index abfee1d..c72d95e 100644
--- a/src/tint/writer/wgsl/generator_impl.h
+++ b/src/tint/writer/wgsl/generator_impl.h
@@ -35,6 +35,7 @@
 #include "src/tint/ast/unary_op_expression.h"
 #include "src/tint/program.h"
 #include "src/tint/sem/struct.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/text_generator.h"
 
 namespace tint::writer::wgsl {
@@ -55,7 +56,7 @@
     /// @param out the output stream
     /// @param diagnostic the diagnostic control node
     /// @returns true if the diagnostic control was emitted
-    bool EmitDiagnosticControl(std::ostream& out, const ast::DiagnosticControl& diagnostic);
+    bool EmitDiagnosticControl(utils::StringStream& out, const ast::DiagnosticControl& diagnostic);
     /// Handles generating an enable directive
     /// @param enable the enable node
     /// @returns true if the enable directive was emitted
@@ -68,7 +69,7 @@
     /// @param out the output stream
     /// @param expr the expression to emit
     /// @returns true if the index accessor was emitted
-    bool EmitIndexAccessor(std::ostream& out, const ast::IndexAccessorExpression* expr);
+    bool EmitIndexAccessor(utils::StringStream& out, const ast::IndexAccessorExpression* expr);
     /// Handles an assignment statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -77,17 +78,17 @@
     /// @param out the output stream
     /// @param expr the binary expression
     /// @returns true if the expression was emitted, false otherwise
-    bool EmitBinary(std::ostream& out, const ast::BinaryExpression* expr);
+    bool EmitBinary(utils::StringStream& out, const ast::BinaryExpression* expr);
     /// Handles generating a binary operator
     /// @param out the output stream
     /// @param op the binary operator
     /// @returns true if the operator was emitted, false otherwise
-    bool EmitBinaryOp(std::ostream& out, const ast::BinaryOp op);
+    bool EmitBinaryOp(utils::StringStream& out, const ast::BinaryOp op);
     /// Handles generating a bitcast expression
     /// @param out the output stream
     /// @param expr the bitcast expression
     /// @returns true if the bitcast was emitted
-    bool EmitBitcast(std::ostream& out, const ast::BitcastExpression* expr);
+    bool EmitBitcast(utils::StringStream& out, const ast::BitcastExpression* expr);
     /// Handles a block statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -96,7 +97,7 @@
     /// @param out the output stream to write the header to
     /// @param stmt the block statement to emit the header for
     /// @returns true if the statement was emitted successfully
-    bool EmitBlockHeader(std::ostream& out, const ast::BlockStatement* stmt);
+    bool EmitBlockHeader(utils::StringStream& out, const ast::BlockStatement* stmt);
     /// Handles a break statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -109,7 +110,7 @@
     /// @param out the output stream
     /// @param expr the call expression
     /// @returns true if the call expression is emitted
-    bool EmitCall(std::ostream& out, const ast::CallExpression* expr);
+    bool EmitCall(utils::StringStream& out, const ast::CallExpression* expr);
     /// Handles a case statement
     /// @param stmt the statement
     /// @returns true if the statment was emitted successfully
@@ -122,7 +123,7 @@
     /// @param out the output stream
     /// @param expr the literal expression expression
     /// @returns true if the literal expression is emitted
-    bool EmitLiteral(std::ostream& out, const ast::LiteralExpression* expr);
+    bool EmitLiteral(utils::StringStream& out, const ast::LiteralExpression* expr);
     /// Handles a continue statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was emitted successfully
@@ -131,7 +132,7 @@
     /// @param out the output stream
     /// @param expr the expression
     /// @returns true if the expression was emitted
-    bool EmitExpression(std::ostream& out, const ast::Expression* expr);
+    bool EmitExpression(utils::StringStream& out, const ast::Expression* expr);
     /// Handles generating a function
     /// @param func the function to generate
     /// @returns true if the function was emitted
@@ -140,12 +141,12 @@
     /// @param out the output stream
     /// @param expr the identifier expression
     /// @returns true if the identifier was emitted
-    bool EmitIdentifier(std::ostream& out, const ast::IdentifierExpression* expr);
+    bool EmitIdentifier(utils::StringStream& out, const ast::IdentifierExpression* expr);
     /// Handles generating an identifier
     /// @param out the output of the expression stream
     /// @param ident the identifier
     /// @returns true if the identifier was emitted
-    bool EmitIdentifier(std::ostream& out, const ast::Identifier* ident);
+    bool EmitIdentifier(utils::StringStream& out, const ast::Identifier* ident);
     /// Handles an if statement
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -174,7 +175,7 @@
     /// @param out the output stream
     /// @param expr the member accessor expression
     /// @returns true if the member accessor was emitted
-    bool EmitMemberAccessor(std::ostream& out, const ast::MemberAccessorExpression* expr);
+    bool EmitMemberAccessor(utils::StringStream& out, const ast::MemberAccessorExpression* expr);
     /// Handles return statements
     /// @param stmt the statement to emit
     /// @returns true if the statement was successfully emitted
@@ -207,22 +208,22 @@
     /// @param out the output stream
     /// @param fmt the format to generate
     /// @returns true if the format is emitted
-    bool EmitImageFormat(std::ostream& out, const builtin::TexelFormat fmt);
+    bool EmitImageFormat(utils::StringStream& out, const builtin::TexelFormat fmt);
     /// Handles a unary op expression
     /// @param out the output stream
     /// @param expr the expression to emit
     /// @returns true if the expression was emitted
-    bool EmitUnaryOp(std::ostream& out, const ast::UnaryOpExpression* expr);
+    bool EmitUnaryOp(utils::StringStream& out, const ast::UnaryOpExpression* expr);
     /// Handles generating a variable
     /// @param out the output stream
     /// @param var the variable to generate
     /// @returns true if the variable was emitted
-    bool EmitVariable(std::ostream& out, const ast::Variable* var);
+    bool EmitVariable(utils::StringStream& out, const ast::Variable* var);
     /// Handles generating a attribute list
     /// @param out the output stream
     /// @param attrs the attribute list
     /// @returns true if the attributes were emitted
-    bool EmitAttributes(std::ostream& out, utils::VectorRef<const ast::Attribute*> attrs);
+    bool EmitAttributes(utils::StringStream& out, utils::VectorRef<const ast::Attribute*> attrs);
 };
 
 }  // namespace tint::writer::wgsl
diff --git a/src/tint/writer/wgsl/generator_impl_array_accessor_test.cc b/src/tint/writer/wgsl/generator_impl_array_accessor_test.cc
index 31e7e6b..0cf7cf4 100644
--- a/src/tint/writer/wgsl/generator_impl_array_accessor_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_array_accessor_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "ary[5i]");
 }
@@ -42,7 +43,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(*(p))[5i]");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_binary_test.cc b/src/tint/writer/wgsl/generator_impl_binary_test.cc
index d86b1c4..88bf9f5 100644
--- a/src/tint/writer/wgsl/generator_impl_binary_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_binary_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 namespace tint::writer::wgsl {
@@ -47,7 +48,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), params.result);
 }
diff --git a/src/tint/writer/wgsl/generator_impl_bitcast_test.cc b/src/tint/writer/wgsl/generator_impl_bitcast_test.cc
index 6d2de3a..df2a9aa 100644
--- a/src/tint/writer/wgsl/generator_impl_bitcast_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_bitcast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, bitcast)) << gen.error();
     EXPECT_EQ(out.str(), "bitcast<f32>(1i)");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_call_test.cc b/src/tint/writer/wgsl/generator_impl_call_test.cc
index 4492628..f484cb6 100644
--- a/src/tint/writer/wgsl/generator_impl_call_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_call_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "src/tint/ast/call_statement.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -33,7 +34,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func()");
 }
@@ -56,7 +57,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, call)) << gen.error();
     EXPECT_EQ(out.str(), "my_func(param1, param2)");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_cast_test.cc b/src/tint/writer/wgsl/generator_impl_cast_test.cc
index 66b7a2e..31e3ad0 100644
--- a/src/tint/writer/wgsl/generator_impl_cast_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_cast_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -27,7 +28,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "f32(1i)");
 }
@@ -40,7 +41,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "f16(1i)");
 }
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "vec3<f32>(vec3<i32>(1i, 2i, 3i))");
 }
@@ -64,7 +65,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
     EXPECT_EQ(out.str(), "vec3<f16>(vec3<i32>(1i, 2i, 3i))");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_identifier_test.cc b/src/tint/writer/wgsl/generator_impl_identifier_test.cc
index 7b60b63..f270ede 100644
--- a/src/tint/writer/wgsl/generator_impl_identifier_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_identifier_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 namespace tint::writer::wgsl {
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, i)) << gen.error();
     EXPECT_EQ(out.str(), "glsl");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_literal_test.cc b/src/tint/writer/wgsl/generator_impl_literal_test.cc
index c660198..e947778 100644
--- a/src/tint/writer/wgsl/generator_impl_literal_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_literal_test.cc
@@ -14,6 +14,7 @@
 
 #include <cstring>
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -114,7 +115,7 @@
     SetResolveOnBuild(false);
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitLiteral(out, v)) << gen.error();
     EXPECT_EQ(out.str(), GetParam().expected);
 }
@@ -162,7 +163,7 @@
     SetResolveOnBuild(false);
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitLiteral(out, v)) << gen.error();
     EXPECT_EQ(out.str(), GetParam().expected);
 }
diff --git a/src/tint/writer/wgsl/generator_impl_member_accessor_test.cc b/src/tint/writer/wgsl/generator_impl_member_accessor_test.cc
index 208be35..8317305 100644
--- a/src/tint/writer/wgsl/generator_impl_member_accessor_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_member_accessor_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 namespace tint::writer::wgsl {
@@ -28,7 +29,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "str.mem");
 }
@@ -43,7 +44,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, expr)) << gen.error();
     EXPECT_EQ(out.str(), "(*(p)).mem");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_type_test.cc b/src/tint/writer/wgsl/generator_impl_type_test.cc
index 422b711..aa22d73 100644
--- a/src/tint/writer/wgsl/generator_impl_type_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_type_test.cc
@@ -17,6 +17,7 @@
 #include "src/tint/type/multisampled_texture.h"
 #include "src/tint/type/sampled_texture.h"
 #include "src/tint/type/texture_dimension.h"
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -32,7 +33,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "alias");
 }
@@ -42,7 +43,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "array<bool, 4u>");
 }
@@ -53,7 +54,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "@stride(16) array<bool, 4u>");
 }
@@ -63,7 +64,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "array<bool>");
 }
@@ -73,7 +74,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "bool");
 }
@@ -83,7 +84,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "f32");
 }
@@ -95,7 +96,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "f16");
 }
@@ -105,7 +106,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "i32");
 }
@@ -115,7 +116,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "mat2x3<f32>");
 }
@@ -127,7 +128,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "mat2x3<f16>");
 }
@@ -138,7 +139,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "ptr<workgroup, f32>");
 }
@@ -150,7 +151,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "ptr<storage, f32, read_write>");
 }
@@ -164,7 +165,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "S");
 }
@@ -291,7 +292,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "u32");
 }
@@ -301,7 +302,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "vec3<f32>");
 }
@@ -313,7 +314,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "vec3<f16>");
 }
@@ -335,7 +336,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), param.name);
 }
@@ -356,7 +357,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<f32>");
 }
@@ -369,7 +370,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<i32>");
 }
@@ -382,7 +383,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<u32>");
 }
@@ -405,7 +406,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<f32>");
 }
@@ -418,7 +419,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<i32>");
 }
@@ -431,7 +432,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), std::string(param.name) + "<u32>");
 }
@@ -459,7 +460,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), param.name);
 }
@@ -490,7 +491,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitImageFormat(out, param.fmt)) << gen.error();
     EXPECT_EQ(out.str(), param.name);
 }
@@ -521,7 +522,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "sampler");
 }
@@ -532,7 +533,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, type)) << gen.error();
     EXPECT_EQ(out.str(), "sampler_comparison");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_unary_op_test.cc b/src/tint/writer/wgsl/generator_impl_unary_op_test.cc
index a9cd10c..537f5d4 100644
--- a/src/tint/writer/wgsl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_unary_op_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 namespace tint::writer::wgsl {
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "&(expr)");
 }
@@ -38,7 +39,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "~(expr)");
 }
@@ -51,7 +52,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "*(expr)");
 }
@@ -63,7 +64,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "!(expr)");
 }
@@ -75,7 +76,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
     EXPECT_EQ(out.str(), "-(expr)");
 }
diff --git a/src/tint/writer/wgsl/generator_impl_variable_test.cc b/src/tint/writer/wgsl/generator_impl_variable_test.cc
index 5c9506f..2706c30 100644
--- a/src/tint/writer/wgsl/generator_impl_variable_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_variable_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/utils/string_stream.h"
 #include "src/tint/writer/wgsl/test_helper.h"
 
 using namespace tint::number_suffixes;  // NOLINT
@@ -26,7 +27,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(var<private> a : f32;)");
 }
@@ -36,7 +37,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(var<private> a : f32;)");
 }
@@ -48,7 +49,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(@binding(0) @group(0) var<storage, read> a : S;)");
 }
@@ -60,7 +61,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(@binding(0) @group(0) var<storage, read_write> a : S;)");
 }
@@ -70,7 +71,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(@group(1) @binding(2) var a : sampler;)");
 }
@@ -80,7 +81,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(var<private> a : f32 = 1.0f;)");
 }
@@ -91,7 +92,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(let a : f32 = 1.0f;)");
 }
@@ -102,7 +103,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(let a = 1.0f;)");
 }
@@ -113,7 +114,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(const a : f32 = 1.0f;)");
 }
@@ -124,7 +125,7 @@
 
     GeneratorImpl& gen = Build();
 
-    std::stringstream out;
+    utils::StringStream out;
     ASSERT_TRUE(gen.EmitVariable(out, v)) << gen.error();
     EXPECT_EQ(out.str(), R"(const a = 1.0f;)");
 }