Import Tint changes from Dawn

Changes:
  - fafeb9a3279b602f59cc7493bb15fe1931fefaee tint/reader/wgsl: Improve errors when parsing access cont... by Ben Clayton <bclayton@google.com>
  - bccd87c37a90dca50e82f31a2eb1f055398db656 tint/ast: Generate access.[h|cc] by Ben Clayton <bclayton@google.com>
  - a44e3d8bea3f4e4430c04b20695ff0812ac32a52 tint: include T type in converter overload diagnostics by Ben Clayton <bclayton@google.com>
  - c889500f6e3e8fa5c23c8235848a2a06a2f5fa31 tint/reader/wgsl: Improve errors when parsing interpolati... by Ben Clayton <bclayton@google.com>
  - 542d27d874fb558614f229886e083f9a3119dea2 spirv-reader: support GLSL.std.450 instruction FindILsb by David Neto <dneto@google.com>
  - 2ad747dedd99d4f88742295f2760ae35759b2dd9 tint: Fix const eval unary tests by Antonio Maiorano <amaiorano@google.com>
  - 75bc93c0df83671adc3a62c172bd33a8be939dad tint: Fix const eval of type conversions by Ben Clayton <bclayton@google.com>
  - feb447d9dc271b94017a0ee41ff6ef7f97276cc3 tint/resolver: Formatting by Ben Clayton <bclayton@google.com>
  - f9ed9d3a6349f781957fccae7af9bf8d6bd268b4 tint/ast: Generate interpolate_attribute.[h|cc] by Ben Clayton <bclayton@google.com>
  - c1af0f5005d04d2db500acba0f093cafe9a95de5 tint/reader/wgsl: Improve errors when parsing address spa... by Ben Clayton <bclayton@google.com>
  - 9af5b406bab262d713338f350d3a8544cf762c8d tint/reader/wgsl: Improve errors when parsing texel formats by Ben Clayton <bclayton@google.com>
  - ecd7f7ee7fbbf438ff05a0b0ced26983e13fa6f6 tint/reader/wgsl: Improve errors when parsing extensions by Ben Clayton <bclayton@google.com>
  - 760eee9e925141186b0e76357dcb08d3d010ebfc tint/reader/wgsl: Improve errors when parsing builtin by Ben Clayton <bclayton@google.com>
  - b7aef033fa73937cbc837478d9244999df26257d tint/templates: Generate enum string arrays by Ben Clayton <bclayton@google.com>
  - 3e0984afe036c85238cd61e647ccf892894b88d0 tint/program_builder.h: Remove stale comment by Ben Clayton <bclayton@google.com>
  - db368f1b9c310873f1194a18deac890f44de81b3 tools/tint/intrinsic: Sort enum values lexicographically by Ben Clayton <bclayton@google.com>
  - ae832a2c6ae769593321ae5f8368c948f4727376 tint/reader/wgsl: Minor cleanups by Ben Clayton <bclayton@google.com>
  - d04c54e1784c6cafa5c8b3a936c1c682f99de4e4 tint/utils: Add Levenshtein Distance() function by Ben Clayton <bclayton@google.com>
  - 3c34d83088f6e90294f63d1191b04d2f1d70429a tint/ast: Add missing 'const' on pointer member by Ben Clayton <bclayton@google.com>
  - ab2fa8be87f3cc7a636230b07d61b96f2d9c7d30 tint: Fix vector equality operator by Ben Clayton <bclayton@google.com>
  - 8d3d4f6fd11fb179b4c56b782e9e5ef0da1fd4a9 tint/inspector: Reflect whether @builtin(frag_depth) is u... by Corentin Wallez <cwallez@chromium.org>
  - 98a7cd11aff1850f953f26fb6db6dc06632b8d63 Add a ClampFragDepth transform. by Corentin Wallez <cwallez@chromium.org>
  - 5cbb32d764fb93c7586fb01b48a20fca32b6682b Tint/tranfsorm/test: Refactor and add exhaustive test for... by Zhaoming Jiang <zhaoming.jiang@intel.com>
  - 112d7614483a266869cb74891a8bdb9c5b5bb1ee spirv-reader: Track storage class for pointer/ref values by David Neto <dneto@google.com>
GitOrigin-RevId: fafeb9a3279b602f59cc7493bb15fe1931fefaee
Change-Id: I00adc0bca37895f36e239a67b32b04270f606e4a
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/105380
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Copybara Prod <copybara-worker-blackhole@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 07c5350..9e39630 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -482,8 +482,11 @@
     "transform/builtin_polyfill.h",
     "transform/calculate_array_length.cc",
     "transform/calculate_array_length.h",
+    "transform/calculate_array_length.h",
     "transform/canonicalize_entry_point_io.cc",
     "transform/canonicalize_entry_point_io.h",
+    "transform/clamp_frag_depth.cc",
+    "transform/clamp_frag_depth.h",
     "transform/combine_samplers.cc",
     "transform/combine_samplers.h",
     "transform/decompose_memory_access.cc",
@@ -574,6 +577,7 @@
     "utils/map.h",
     "utils/math.h",
     "utils/scoped_assignment.h",
+    "utils/string.cc",
     "utils/string.h",
     "utils/unique_allocator.h",
     "utils/unique_vector.h",
@@ -1191,6 +1195,7 @@
       "transform/builtin_polyfill_test.cc",
       "transform/calculate_array_length_test.cc",
       "transform/canonicalize_entry_point_io_test.cc",
+      "transform/clamp_frag_depth_test.cc",
       "transform/combine_samplers_test.cc",
       "transform/decompose_memory_access_test.cc",
       "transform/decompose_strided_array_test.cc",
@@ -1214,6 +1219,7 @@
       "transform/simplify_pointers_test.cc",
       "transform/single_entry_point_test.cc",
       "transform/spirv_atomic_test.cc",
+      "transform/std140_exhaustive_test.cc",
       "transform/std140_test.cc",
       "transform/substitute_override_test.cc",
       "transform/test_helper.h",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 10b0686..c331ccf 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -394,6 +394,8 @@
   transform/builtin_polyfill.h
   transform/calculate_array_length.cc
   transform/calculate_array_length.h
+  transform/clamp_frag_depth.cc
+  transform/clamp_frag_depth.h
   transform/canonicalize_entry_point_io.cc
   transform/canonicalize_entry_point_io.h
   transform/combine_samplers.cc
@@ -484,6 +486,7 @@
   utils/map.h
   utils/math.h
   utils/scoped_assignment.h
+  utils/string.cc
   utils/string.h
   utils/unique_allocator.h
   utils/unique_vector.h
@@ -1105,6 +1108,7 @@
       transform/binding_remapper_test.cc
       transform/builtin_polyfill_test.cc
       transform/calculate_array_length_test.cc
+      transform/clamp_frag_depth_test.cc
       transform/canonicalize_entry_point_io_test.cc
       transform/combine_samplers_test.cc
       transform/decompose_memory_access_test.cc
@@ -1130,6 +1134,7 @@
       transform/simplify_pointers_test.cc
       transform/single_entry_point_test.cc
       transform/spirv_atomic_test.cc
+      transform/std140_exhaustive_test.cc
       transform/std140_test.cc
       transform/substitute_override_test.cc
       transform/test_helper.h
diff --git a/src/tint/ast/access.cc b/src/tint/ast/access.cc
index 642d852..3997715 100644
--- a/src/tint/ast/access.cc
+++ b/src/tint/ast/access.cc
@@ -12,30 +12,46 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/ast/access.cc.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
 #include "src/tint/ast/access.h"
 
 namespace tint::ast {
 
-std::ostream& operator<<(std::ostream& out, Access access) {
-    switch (access) {
-        case ast::Access::kUndefined: {
-            out << "undefined";
-            break;
-        }
-        case ast::Access::kRead: {
-            out << "read";
-            break;
-        }
-        case ast::Access::kReadWrite: {
-            out << "read_write";
-            break;
-        }
-        case ast::Access::kWrite: {
-            out << "write";
-            break;
-        }
+/// ParseAccess parses a Access from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Access::kInvalid if the string could not be parsed.
+Access ParseAccess(std::string_view str) {
+    if (str == "read") {
+        return Access::kRead;
     }
-    return out;
+    if (str == "read_write") {
+        return Access::kReadWrite;
+    }
+    if (str == "write") {
+        return Access::kWrite;
+    }
+    return Access::kInvalid;
+}
+
+std::ostream& operator<<(std::ostream& out, Access value) {
+    switch (value) {
+        case Access::kInvalid:
+            return out << "invalid";
+        case Access::kRead:
+            return out << "read";
+        case Access::kReadWrite:
+            return out << "read_write";
+        case Access::kWrite:
+            return out << "write";
+    }
+    return out << "<unknown>";
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/access.cc.tmpl b/src/tint/ast/access.cc.tmpl
new file mode 100644
index 0000000..bd52a3b
--- /dev/null
+++ b/src/tint/ast/access.cc.tmpl
@@ -0,0 +1,25 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate access.cc
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "access") -}}
+
+#include "src/tint/ast/access.h"
+
+namespace tint::ast {
+
+{{ Eval "ParseEnum" $enum}}
+
+{{ Eval "EnumOStream" $enum}}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/access.h b/src/tint/ast/access.h
index 3714b70..12e1a20 100644
--- a/src/tint/ast/access.h
+++ b/src/tint/ast/access.h
@@ -12,32 +12,44 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/ast/access.h.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
 #ifndef SRC_TINT_AST_ACCESS_H_
 #define SRC_TINT_AST_ACCESS_H_
 
 #include <ostream>
-#include <string>
 
 namespace tint::ast {
 
-/// The access control settings
-enum Access {
-    /// Not declared in the source
-    kUndefined = 0,
-    /// Read only
+/// Address space of a given pointer.
+enum class Access {
+    kInvalid,
     kRead,
-    /// Write only
-    kWrite,
-    /// Read write
     kReadWrite,
-    // Last valid access mode
-    kLastValid = kReadWrite,
+    kWrite,
 };
 
 /// @param out the std::ostream to write to
-/// @param access the Access
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, Access access);
+/// @param value the Access
+/// @returns `out` so calls can be chained
+std::ostream& operator<<(std::ostream& out, Access value);
+
+/// ParseAccess parses a Access from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or Access::kInvalid if the string could not be parsed.
+Access ParseAccess(std::string_view str);
+
+constexpr const char* kAccessStrings[] = {
+    "read",
+    "read_write",
+    "write",
+};
 
 }  // namespace tint::ast
 
diff --git a/src/tint/ast/access.h.tmpl b/src/tint/ast/access.h.tmpl
new file mode 100644
index 0000000..ab14fcf
--- /dev/null
+++ b/src/tint/ast/access.h.tmpl
@@ -0,0 +1,29 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate access.h
+
+To update the generated file, run:
+    ./tools/run gen
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+{{- $enum := (Sem.Enum "access") -}}
+
+#ifndef SRC_TINT_AST_ACCESS_H_
+#define SRC_TINT_AST_ACCESS_H_
+
+#include <ostream>
+
+namespace tint::ast {
+
+/// Address space of a given pointer.
+{{ Eval "DeclareEnum" $enum}}
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_ACCESS_H_
diff --git a/src/tint/ast/address_space.cc b/src/tint/ast/address_space.cc
index a55fb87..d8174a2 100644
--- a/src/tint/ast/address_space.cc
+++ b/src/tint/ast/address_space.cc
@@ -34,17 +34,17 @@
     if (str == "private") {
         return AddressSpace::kPrivate;
     }
-    if (str == "workgroup") {
-        return AddressSpace::kWorkgroup;
-    }
-    if (str == "uniform") {
-        return AddressSpace::kUniform;
+    if (str == "push_constant") {
+        return AddressSpace::kPushConstant;
     }
     if (str == "storage") {
         return AddressSpace::kStorage;
     }
-    if (str == "push_constant") {
-        return AddressSpace::kPushConstant;
+    if (str == "uniform") {
+        return AddressSpace::kUniform;
+    }
+    if (str == "workgroup") {
+        return AddressSpace::kWorkgroup;
     }
     return AddressSpace::kInvalid;
 }
@@ -53,26 +53,26 @@
     switch (value) {
         case AddressSpace::kInvalid:
             return out << "invalid";
-        case AddressSpace::kNone:
-            return out << "none";
         case AddressSpace::kFunction:
             return out << "function";
-        case AddressSpace::kPrivate:
-            return out << "private";
-        case AddressSpace::kWorkgroup:
-            return out << "workgroup";
-        case AddressSpace::kUniform:
-            return out << "uniform";
-        case AddressSpace::kStorage:
-            return out << "storage";
-        case AddressSpace::kPushConstant:
-            return out << "push_constant";
         case AddressSpace::kHandle:
             return out << "handle";
         case AddressSpace::kIn:
             return out << "in";
+        case AddressSpace::kNone:
+            return out << "none";
         case AddressSpace::kOut:
             return out << "out";
+        case AddressSpace::kPrivate:
+            return out << "private";
+        case AddressSpace::kPushConstant:
+            return out << "push_constant";
+        case AddressSpace::kStorage:
+            return out << "storage";
+        case AddressSpace::kUniform:
+            return out << "uniform";
+        case AddressSpace::kWorkgroup:
+            return out << "workgroup";
     }
     return out << "<unknown>";
 }
diff --git a/src/tint/ast/address_space.h b/src/tint/ast/address_space.h
index 210dc7f..8b96f0f 100644
--- a/src/tint/ast/address_space.h
+++ b/src/tint/ast/address_space.h
@@ -30,16 +30,16 @@
 /// Address space of a given pointer.
 enum class AddressSpace {
     kInvalid,
-    kNone,  // Tint-internal enum entry - not parsed
     kFunction,
-    kPrivate,
-    kWorkgroup,
-    kUniform,
-    kStorage,
-    kPushConstant,
     kHandle,  // Tint-internal enum entry - not parsed
     kIn,      // Tint-internal enum entry - not parsed
+    kNone,    // Tint-internal enum entry - not parsed
     kOut,     // Tint-internal enum entry - not parsed
+    kPrivate,
+    kPushConstant,
+    kStorage,
+    kUniform,
+    kWorkgroup,
 };
 
 /// @param out the std::ostream to write to
@@ -52,6 +52,10 @@
 /// @returns the parsed enum, or AddressSpace::kInvalid if the string could not be parsed.
 AddressSpace ParseAddressSpace(std::string_view str);
 
+constexpr const char* kAddressSpaceStrings[] = {
+    "function", "private", "push_constant", "storage", "uniform", "workgroup",
+};
+
 /// @returns true if the AddressSpace is host-shareable
 /// @param address_space the AddressSpace
 /// @see https://gpuweb.github.io/gpuweb/wgsl.html#host-shareable
diff --git a/src/tint/ast/address_space_bench.cc b/src/tint/ast/address_space_bench.cc
index 364d015..fb3b0a5 100644
--- a/src/tint/ast/address_space_bench.cc
+++ b/src/tint/ast/address_space_bench.cc
@@ -31,48 +31,15 @@
 
 void AddressSpaceParser(::benchmark::State& state) {
     std::array kStrings{
-        "fccnctin",
-        "ucti3",
-        "functVon",
-        "function",
-        "1unction",
-        "unJtqqon",
-        "llun77tion",
-        "ppqqivtHH",
-        "prcv",
-        "bivaGe",
-        "private",
-        "priviive",
-        "8WWivate",
-        "pxxvate",
-        "wXkgrggup",
-        "worXVup",
-        "3orkgroup",
-        "workgroup",
-        "workgroEp",
-        "woTTPkroup",
-        "ddorkroxxp",
-        "u44iform",
-        "unSSfoVVm",
-        "RniR22m",
-        "uniform",
-        "uFfo9m",
-        "uniorm",
-        "VOORRHrm",
-        "straye",
-        "llntrrr77ge",
-        "stor4g00",
-        "storage",
-        "trooe",
-        "zzrage",
-        "siioppa1",
-        "puXXh_constant",
-        "pusII9_nn55nstant",
-        "YusHH_coaastSSrnt",
-        "push_constant",
-        "pushonkkHan",
-        "jush_consgRt",
-        "puh_cobsant",
+        "fccnctin",       "ucti3",         "functVon",      "function",      "1unction",
+        "unJtqqon",       "llun77tion",    "ppqqivtHH",     "prcv",          "bivaGe",
+        "private",        "priviive",      "8WWivate",      "pxxvate",       "pXh_cggnstant",
+        "pX_Vonstanu",    "push_consta3t", "push_constant", "push_constanE", "push_TTPnstant",
+        "puxxdh_constan", "s44orage",      "stSSraVVe",     "RtoR22e",       "storage",
+        "sFra9e",         "stoage",        "VOORRHge",      "unfoym",        "llnnrrf77rm",
+        "unif4r00",       "uniform",       "nfoom",         "zzform",        "uiiippo1",
+        "workgrouXX",     "wor55gro99nII", "wrrrkgroSSaHH", "workgroup",     "kkrHoup",
+        "jgkrouRR",       "wokroub",
     };
     for (auto _ : state) {
         for (auto& str : kStrings) {
diff --git a/src/tint/ast/address_space_test.cc b/src/tint/ast/address_space_test.cc
index 86a8e40..271cf33 100644
--- a/src/tint/ast/address_space_test.cc
+++ b/src/tint/ast/address_space_test.cc
@@ -42,21 +42,24 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"function", AddressSpace::kFunction},   {"private", AddressSpace::kPrivate},
-    {"workgroup", AddressSpace::kWorkgroup}, {"uniform", AddressSpace::kUniform},
-    {"storage", AddressSpace::kStorage},     {"push_constant", AddressSpace::kPushConstant},
+    {"function", AddressSpace::kFunction},
+    {"private", AddressSpace::kPrivate},
+    {"push_constant", AddressSpace::kPushConstant},
+    {"storage", AddressSpace::kStorage},
+    {"uniform", AddressSpace::kUniform},
+    {"workgroup", AddressSpace::kWorkgroup},
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"fccnctin", AddressSpace::kInvalid},       {"ucti3", AddressSpace::kInvalid},
-    {"functVon", AddressSpace::kInvalid},       {"priv1te", AddressSpace::kInvalid},
-    {"pqiJate", AddressSpace::kInvalid},        {"privat7ll", AddressSpace::kInvalid},
-    {"workroppqHH", AddressSpace::kInvalid},    {"workru", AddressSpace::kInvalid},
-    {"wbkgGoup", AddressSpace::kInvalid},       {"unifiivm", AddressSpace::kInvalid},
-    {"8WWiform", AddressSpace::kInvalid},       {"uxxform", AddressSpace::kInvalid},
-    {"sXraggg", AddressSpace::kInvalid},        {"traXe", AddressSpace::kInvalid},
-    {"stor3ge", AddressSpace::kInvalid},        {"push_constanE", AddressSpace::kInvalid},
-    {"push_TTPnstant", AddressSpace::kInvalid}, {"puxxdh_constan", AddressSpace::kInvalid},
+    {"fccnctin", AddressSpace::kInvalid},        {"ucti3", AddressSpace::kInvalid},
+    {"functVon", AddressSpace::kInvalid},        {"priv1te", AddressSpace::kInvalid},
+    {"pqiJate", AddressSpace::kInvalid},         {"privat7ll", AddressSpace::kInvalid},
+    {"pqqsh_pponstHnt", AddressSpace::kInvalid}, {"pus_cnstat", AddressSpace::kInvalid},
+    {"bus_Gonstant", AddressSpace::kInvalid},    {"storiive", AddressSpace::kInvalid},
+    {"8WWorage", AddressSpace::kInvalid},        {"sxxrage", AddressSpace::kInvalid},
+    {"uXforgg", AddressSpace::kInvalid},         {"nfoXm", AddressSpace::kInvalid},
+    {"unif3rm", AddressSpace::kInvalid},         {"workgroEp", AddressSpace::kInvalid},
+    {"woTTPkroup", AddressSpace::kInvalid},      {"ddorkroxxp", AddressSpace::kInvalid},
 };
 
 using AddressSpaceParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/builtin_value.cc b/src/tint/ast/builtin_value.cc
index e8d6451..d4eaec0 100644
--- a/src/tint/ast/builtin_value.cc
+++ b/src/tint/ast/builtin_value.cc
@@ -28,20 +28,17 @@
 /// @param str the string to parse
 /// @returns the parsed enum, or BuiltinValue::kInvalid if the string could not be parsed.
 BuiltinValue ParseBuiltinValue(std::string_view str) {
-    if (str == "position") {
-        return BuiltinValue::kPosition;
-    }
-    if (str == "vertex_index") {
-        return BuiltinValue::kVertexIndex;
-    }
-    if (str == "instance_index") {
-        return BuiltinValue::kInstanceIndex;
+    if (str == "frag_depth") {
+        return BuiltinValue::kFragDepth;
     }
     if (str == "front_facing") {
         return BuiltinValue::kFrontFacing;
     }
-    if (str == "frag_depth") {
-        return BuiltinValue::kFragDepth;
+    if (str == "global_invocation_id") {
+        return BuiltinValue::kGlobalInvocationId;
+    }
+    if (str == "instance_index") {
+        return BuiltinValue::kInstanceIndex;
     }
     if (str == "local_invocation_id") {
         return BuiltinValue::kLocalInvocationId;
@@ -49,21 +46,24 @@
     if (str == "local_invocation_index") {
         return BuiltinValue::kLocalInvocationIndex;
     }
-    if (str == "global_invocation_id") {
-        return BuiltinValue::kGlobalInvocationId;
-    }
-    if (str == "workgroup_id") {
-        return BuiltinValue::kWorkgroupId;
-    }
     if (str == "num_workgroups") {
         return BuiltinValue::kNumWorkgroups;
     }
+    if (str == "position") {
+        return BuiltinValue::kPosition;
+    }
     if (str == "sample_index") {
         return BuiltinValue::kSampleIndex;
     }
     if (str == "sample_mask") {
         return BuiltinValue::kSampleMask;
     }
+    if (str == "vertex_index") {
+        return BuiltinValue::kVertexIndex;
+    }
+    if (str == "workgroup_id") {
+        return BuiltinValue::kWorkgroupId;
+    }
     return BuiltinValue::kInvalid;
 }
 
@@ -71,32 +71,32 @@
     switch (value) {
         case BuiltinValue::kInvalid:
             return out << "invalid";
-        case BuiltinValue::kPosition:
-            return out << "position";
-        case BuiltinValue::kVertexIndex:
-            return out << "vertex_index";
-        case BuiltinValue::kInstanceIndex:
-            return out << "instance_index";
-        case BuiltinValue::kFrontFacing:
-            return out << "front_facing";
         case BuiltinValue::kFragDepth:
             return out << "frag_depth";
+        case BuiltinValue::kFrontFacing:
+            return out << "front_facing";
+        case BuiltinValue::kGlobalInvocationId:
+            return out << "global_invocation_id";
+        case BuiltinValue::kInstanceIndex:
+            return out << "instance_index";
         case BuiltinValue::kLocalInvocationId:
             return out << "local_invocation_id";
         case BuiltinValue::kLocalInvocationIndex:
             return out << "local_invocation_index";
-        case BuiltinValue::kGlobalInvocationId:
-            return out << "global_invocation_id";
-        case BuiltinValue::kWorkgroupId:
-            return out << "workgroup_id";
         case BuiltinValue::kNumWorkgroups:
             return out << "num_workgroups";
+        case BuiltinValue::kPointSize:
+            return out << "point_size";
+        case BuiltinValue::kPosition:
+            return out << "position";
         case BuiltinValue::kSampleIndex:
             return out << "sample_index";
         case BuiltinValue::kSampleMask:
             return out << "sample_mask";
-        case BuiltinValue::kPointSize:
-            return out << "point_size";
+        case BuiltinValue::kVertexIndex:
+            return out << "vertex_index";
+        case BuiltinValue::kWorkgroupId:
+            return out << "workgroup_id";
     }
     return out << "<unknown>";
 }
diff --git a/src/tint/ast/builtin_value.h b/src/tint/ast/builtin_value.h
index 0a2c7f0..f7dd660 100644
--- a/src/tint/ast/builtin_value.h
+++ b/src/tint/ast/builtin_value.h
@@ -27,22 +27,22 @@
 
 namespace tint::ast {
 
-/// Storage class of a given pointer.
+/// Builtin value defined with `@builtin(<name>)`.
 enum class BuiltinValue {
     kInvalid,
-    kPosition,
-    kVertexIndex,
-    kInstanceIndex,
-    kFrontFacing,
     kFragDepth,
+    kFrontFacing,
+    kGlobalInvocationId,
+    kInstanceIndex,
     kLocalInvocationId,
     kLocalInvocationIndex,
-    kGlobalInvocationId,
-    kWorkgroupId,
     kNumWorkgroups,
+    kPointSize,  // Tint-internal enum entry - not parsed
+    kPosition,
     kSampleIndex,
     kSampleMask,
-    kPointSize,  // Tint-internal enum entry - not parsed
+    kVertexIndex,
+    kWorkgroupId,
 };
 
 /// @param out the std::ostream to write to
@@ -55,6 +55,15 @@
 /// @returns the parsed enum, or BuiltinValue::kInvalid if the string could not be parsed.
 BuiltinValue ParseBuiltinValue(std::string_view str);
 
+constexpr const char* kBuiltinValueStrings[] = {
+    "frag_depth",           "front_facing",
+    "global_invocation_id", "instance_index",
+    "local_invocation_id",  "local_invocation_index",
+    "num_workgroups",       "position",
+    "sample_index",         "sample_mask",
+    "vertex_index",         "workgroup_id",
+};
+
 }  // namespace tint::ast
 
 #endif  // SRC_TINT_AST_BUILTIN_VALUE_H_
diff --git a/src/tint/ast/builtin_value.h.tmpl b/src/tint/ast/builtin_value.h.tmpl
index 1985305..ea63d56 100644
--- a/src/tint/ast/builtin_value.h.tmpl
+++ b/src/tint/ast/builtin_value.h.tmpl
@@ -18,7 +18,7 @@
 
 namespace tint::ast {
 
-/// Storage class of a given pointer.
+/// Builtin value defined with `@builtin(<name>)`.
 {{ Eval "DeclareEnum" $enum}}
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/builtin_value_bench.cc b/src/tint/ast/builtin_value_bench.cc
index 0a4048c..b323b6b 100644
--- a/src/tint/ast/builtin_value_bench.cc
+++ b/src/tint/ast/builtin_value_bench.cc
@@ -31,90 +31,90 @@
 
 void BuiltinValueParser(::benchmark::State& state) {
     std::array kStrings{
-        "pccsitin",
-        "oiti3",
-        "positVon",
-        "position",
-        "1osition",
-        "osJtqqon",
-        "llos77tion",
-        "vrtHHppx_index",
-        "vertx_icx",
-        "veGtex_bnde",
-        "vertex_index",
-        "vertex_inveii",
-        "veWWtex_ind8x",
-        "vxxrtMx_indx",
-        "isXance_indegg",
-        "insanc_iXVex",
-        "instance_in3ex",
-        "instance_index",
-        "instancE_index",
-        "nsTTance_PPndex",
-        "nstancxx_indddx",
-        "44ront_facing",
-        "fSSont_facinVV",
-        "fronR_Racing",
-        "front_facing",
-        "ron9_faciFg",
-        "front_facin",
-        "fVonRR_HaOing",
-        "fyag_epth",
-        "f77ag_nnellrrh",
-        "fra400depth",
+        "fragdeccth",
+        "flaget3",
+        "fVag_depth",
         "frag_depth",
-        "fa_epooh",
-        "frg_ezzth",
-        "f11a_eppiih",
-        "local_invXXcation_id",
-        "lIIcal_i5599ocation_inn",
-        "HHrrcal_inSSocation_Yaa",
-        "local_invocation_id",
-        "lokkal_invocatini",
-        "jocal_invocRRongid",
-        "local_inocatbon_i",
-        "local_injocation_index",
-        "local_invocatio_index",
-        "locl_invocqtion_ndex",
-        "local_invocation_index",
-        "localNNinvocaton_index",
-        "local_invocatin_ivvdx",
-        "locl_invocatioQQ_index",
-        "globalrnvocaton_iff",
-        "global_invocation_jd",
-        "NNlbal_wwnvocation82d",
+        "frag1depth",
+        "fraJqqepth",
+        "fra7ll_depth",
+        "fonHHpp_facing",
+        "fron_facg",
+        "frGnt_fbcin",
+        "front_facing",
+        "front_facvnii",
+        "frWWnt_faci8g",
+        "fxxonM_facig",
+        "gXobalgginvocationid",
+        "goVal_uvocatioX_id",
+        "global_in3ocation_id",
         "global_invocation_id",
-        "global_invocationid",
-        "globalrrinvocation_id",
-        "globaG_invocation_id",
-        "workgroupFFid",
-        "worgrupid",
-        "workgroup_rr",
-        "workgroup_id",
-        "workgrouid",
-        "DokgXoJJp_id",
-        "8orgrup_i",
-        "num_wkkr11up",
-        "numworkgroups",
-        "Ju_workgroups",
+        "global_invocation_iE",
+        "TTobal_invocationPPid",
+        "globdd_invocatioxx_id",
+        "instance44index",
+        "instaVVce_SSndex",
+        "Rnstane_ind2Rx",
+        "instance_index",
+        "inFtanceind9x",
+        "insance_index",
+        "inRRancV_OOHdex",
+        "local_nvocytion_id",
+        "llGcnnl_inv77catirrn_id",
+        "local_invoca4i00n_id",
+        "local_invocation_id",
+        "loool_nvocaton_id",
+        "local_inozztion_id",
+        "p11cal_invocatiiin_i",
+        "local_invocation_iXXdex",
+        "local_invnnIIati99n55index",
+        "localYirrHHocaationSSindex",
+        "local_invocation_index",
+        "lkkal_invHcation_idx",
+        "gRcal_invocatioj_inex",
+        "lcal_invcbtion_index",
+        "num_workgroujs",
+        "num_worgroups",
+        "nuq_orkgoups",
         "num_workgroups",
-        "num_corkgroups",
-        "num_woOkgroups",
-        "num_workKK__vvttps",
-        "smple5inxxe8",
-        "s__mle_qFdex",
-        "saqqple_idex",
+        "nm_workgroNNps",
+        "um_workgrovps",
+        "nQQm_orkgroups",
+        "posftrn",
+        "pojition",
+        "poswNN82n",
+        "position",
+        "positon",
+        "porrition",
+        "pGsition",
+        "sample_inFFex",
+        "samleinex",
+        "sample_indrr",
         "sample_index",
-        "saOpe_33nde66",
-        "s6oople_indttQx",
-        "sam66le_inex",
-        "samxe66masOz",
-        "yyample_mask",
-        "amplZZHask",
+        "sample_iex",
+        "DaplX_JJndex",
+        "8amleinde",
+        "saplekmak",
+        "samle_mask",
+        "saJple_mak",
         "sample_mask",
-        "WWaple_maq4k",
-        "samplOO_ask",
-        "sYohpe_msk",
+        "sample_cask",
+        "sample_maOk",
+        "__attpvve_KKask",
+        "vrtex5inxxe8",
+        "v__rex_qFdex",
+        "veqqtex_idex",
+        "vertex_index",
+        "veOtx_33nde66",
+        "v6ootex_indttQx",
+        "ver66ex_inex",
+        "worzzroup6Oxd",
+        "workgroyyp_id",
+        "wokrHHZpZid",
+        "workgroup_id",
+        "wWWkgqoup44id",
+        "wrkgroOOp_id",
+        "hrkgYooup_d",
     };
     for (auto _ : state) {
         for (auto& str : kStrings) {
diff --git a/src/tint/ast/builtin_value_test.cc b/src/tint/ast/builtin_value_test.cc
index a29810a..55f8e4d 100644
--- a/src/tint/ast/builtin_value_test.cc
+++ b/src/tint/ast/builtin_value_test.cc
@@ -42,57 +42,57 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"position", BuiltinValue::kPosition},
-    {"vertex_index", BuiltinValue::kVertexIndex},
-    {"instance_index", BuiltinValue::kInstanceIndex},
-    {"front_facing", BuiltinValue::kFrontFacing},
     {"frag_depth", BuiltinValue::kFragDepth},
+    {"front_facing", BuiltinValue::kFrontFacing},
+    {"global_invocation_id", BuiltinValue::kGlobalInvocationId},
+    {"instance_index", BuiltinValue::kInstanceIndex},
     {"local_invocation_id", BuiltinValue::kLocalInvocationId},
     {"local_invocation_index", BuiltinValue::kLocalInvocationIndex},
-    {"global_invocation_id", BuiltinValue::kGlobalInvocationId},
-    {"workgroup_id", BuiltinValue::kWorkgroupId},
     {"num_workgroups", BuiltinValue::kNumWorkgroups},
+    {"position", BuiltinValue::kPosition},
     {"sample_index", BuiltinValue::kSampleIndex},
     {"sample_mask", BuiltinValue::kSampleMask},
+    {"vertex_index", BuiltinValue::kVertexIndex},
+    {"workgroup_id", BuiltinValue::kWorkgroupId},
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"pccsitin", BuiltinValue::kInvalid},
-    {"oiti3", BuiltinValue::kInvalid},
-    {"positVon", BuiltinValue::kInvalid},
-    {"1ertex_index", BuiltinValue::kInvalid},
-    {"vertex_Jnqex", BuiltinValue::kInvalid},
-    {"velltex_inde77", BuiltinValue::kInvalid},
-    {"inpptanceqHHindx", BuiltinValue::kInvalid},
-    {"cnsanvendex", BuiltinValue::kInvalid},
-    {"istancG_index", BuiltinValue::kInvalid},
-    {"front_facvnii", BuiltinValue::kInvalid},
-    {"frWWnt_faci8g", BuiltinValue::kInvalid},
-    {"fxxonM_facig", BuiltinValue::kInvalid},
-    {"fXag_detgg", BuiltinValue::kInvalid},
-    {"fag_XuVh", BuiltinValue::kInvalid},
-    {"frag_dept3", BuiltinValue::kInvalid},
-    {"local_Envocation_id", BuiltinValue::kInvalid},
-    {"localiPPvocatioTT_id", BuiltinValue::kInvalid},
-    {"localxxnvocationddid", BuiltinValue::kInvalid},
-    {"loca44_invocation_index", BuiltinValue::kInvalid},
-    {"local_invocSStionVVindex", BuiltinValue::kInvalid},
-    {"locRR_invocat22n_index", BuiltinValue::kInvalid},
-    {"globalFinvoction_id", BuiltinValue::kInvalid},
-    {"gloal_invocation_id", BuiltinValue::kInvalid},
-    {"RRlHOOaV_invoction_id", BuiltinValue::kInvalid},
-    {"workgyoup_i", BuiltinValue::kInvalid},
-    {"wnrrrkg77loup_Gd", BuiltinValue::kInvalid},
-    {"00orkgr4up_id", BuiltinValue::kInvalid},
-    {"numwroogrops", BuiltinValue::kInvalid},
-    {"nzm_wokgroups", BuiltinValue::kInvalid},
-    {"uippworkgro11ps", BuiltinValue::kInvalid},
-    {"sample_iXXdex", BuiltinValue::kInvalid},
-    {"5nnample_99IIdex", BuiltinValue::kInvalid},
-    {"samYlaaHHrrndeSS", BuiltinValue::kInvalid},
-    {"aHkk_mask", BuiltinValue::kInvalid},
-    {"jRRmpl_gsk", BuiltinValue::kInvalid},
-    {"smple_mbk", BuiltinValue::kInvalid},
+    {"fragdeccth", BuiltinValue::kInvalid},
+    {"flaget3", BuiltinValue::kInvalid},
+    {"fVag_depth", BuiltinValue::kInvalid},
+    {"1ront_facing", BuiltinValue::kInvalid},
+    {"front_fJcqng", BuiltinValue::kInvalid},
+    {"frllnt_facin77", BuiltinValue::kInvalid},
+    {"global_invoqqtionppHid", BuiltinValue::kInvalid},
+    {"clvbal_inocaionid", BuiltinValue::kInvalid},
+    {"global_Gvocation_id", BuiltinValue::kInvalid},
+    {"invtance_iniiex", BuiltinValue::kInvalid},
+    {"8nstanceWWindex", BuiltinValue::kInvalid},
+    {"insxxanceindex", BuiltinValue::kInvalid},
+    {"lXcal_invoation_igg", BuiltinValue::kInvalid},
+    {"Xocal_nvocatin_Vd", BuiltinValue::kInvalid},
+    {"local_invoca3ion_id", BuiltinValue::kInvalid},
+    {"local_invocation_indeE", BuiltinValue::kInvalid},
+    {"loTTal_invPPcatin_index", BuiltinValue::kInvalid},
+    {"loal_invocadxxion_index", BuiltinValue::kInvalid},
+    {"num_work44roups", BuiltinValue::kInvalid},
+    {"num_wVVrkgSSoups", BuiltinValue::kInvalid},
+    {"Rum_wokgrou2Rs", BuiltinValue::kInvalid},
+    {"oFi9ion", BuiltinValue::kInvalid},
+    {"postion", BuiltinValue::kInvalid},
+    {"ROOoHiiVn", BuiltinValue::kInvalid},
+    {"samply_inde", BuiltinValue::kInvalid},
+    {"snrrmpl77l_indGx", BuiltinValue::kInvalid},
+    {"00ample4index", BuiltinValue::kInvalid},
+    {"smoo_mask", BuiltinValue::kInvalid},
+    {"sampzemask", BuiltinValue::kInvalid},
+    {"ppaplii1_mas", BuiltinValue::kInvalid},
+    {"vertex_iXXdex", BuiltinValue::kInvalid},
+    {"5nnertex_99IIdex", BuiltinValue::kInvalid},
+    {"verYeaaHHrrndeSS", BuiltinValue::kInvalid},
+    {"workkgHo_i", BuiltinValue::kInvalid},
+    {"worRgoupjid", BuiltinValue::kInvalid},
+    {"wrkgrupbid", BuiltinValue::kInvalid},
 };
 
 using BuiltinValueParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/extension.cc b/src/tint/ast/extension.cc
index e57b248..a4d823f 100644
--- a/src/tint/ast/extension.cc
+++ b/src/tint/ast/extension.cc
@@ -28,18 +28,18 @@
 /// @param str the string to parse
 /// @returns the parsed enum, or Extension::kInvalid if the string could not be parsed.
 Extension ParseExtension(std::string_view str) {
-    if (str == "f16") {
-        return Extension::kF16;
+    if (str == "chromium_disable_uniformity_analysis") {
+        return Extension::kChromiumDisableUniformityAnalysis;
     }
     if (str == "chromium_experimental_dp4a") {
         return Extension::kChromiumExperimentalDp4A;
     }
-    if (str == "chromium_disable_uniformity_analysis") {
-        return Extension::kChromiumDisableUniformityAnalysis;
-    }
     if (str == "chromium_experimental_push_constant") {
         return Extension::kChromiumExperimentalPushConstant;
     }
+    if (str == "f16") {
+        return Extension::kF16;
+    }
     return Extension::kInvalid;
 }
 
@@ -47,14 +47,14 @@
     switch (value) {
         case Extension::kInvalid:
             return out << "invalid";
-        case Extension::kF16:
-            return out << "f16";
-        case Extension::kChromiumExperimentalDp4A:
-            return out << "chromium_experimental_dp4a";
         case Extension::kChromiumDisableUniformityAnalysis:
             return out << "chromium_disable_uniformity_analysis";
+        case Extension::kChromiumExperimentalDp4A:
+            return out << "chromium_experimental_dp4a";
         case Extension::kChromiumExperimentalPushConstant:
             return out << "chromium_experimental_push_constant";
+        case Extension::kF16:
+            return out << "f16";
     }
     return out << "<unknown>";
 }
diff --git a/src/tint/ast/extension.h b/src/tint/ast/extension.h
index eea9bec..4d1a937 100644
--- a/src/tint/ast/extension.h
+++ b/src/tint/ast/extension.h
@@ -33,10 +33,10 @@
 /// @see src/tint/intrinsics.def for extension descriptions
 enum class Extension {
     kInvalid,
-    kF16,
-    kChromiumExperimentalDp4A,
     kChromiumDisableUniformityAnalysis,
+    kChromiumExperimentalDp4A,
     kChromiumExperimentalPushConstant,
+    kF16,
 };
 
 /// @param out the std::ostream to write to
@@ -49,6 +49,13 @@
 /// @returns the parsed enum, or Extension::kInvalid if the string could not be parsed.
 Extension ParseExtension(std::string_view str);
 
+constexpr const char* kExtensionStrings[] = {
+    "chromium_disable_uniformity_analysis",
+    "chromium_experimental_dp4a",
+    "chromium_experimental_push_constant",
+    "f16",
+};
+
 // A unique vector of extensions
 using Extensions = utils::UniqueVector<Extension, 4>;
 
diff --git a/src/tint/ast/extension_bench.cc b/src/tint/ast/extension_bench.cc
index 8fc9d1c..94b5216 100644
--- a/src/tint/ast/extension_bench.cc
+++ b/src/tint/ast/extension_bench.cc
@@ -31,13 +31,13 @@
 
 void ExtensionParser(::benchmark::State& state) {
     std::array kStrings{
-        "cc6",
-        "s",
-        "HH6",
-        "f16",
-        "116",
-        "qJ6",
-        "f17ll",
+        "chromium_disableuniformiccy_analysis",
+        "chromil3_disable_unifority_analss",
+        "chromium_disable_Vniformity_analysis",
+        "chromium_disable_uniformity_analysis",
+        "chromium_dis1ble_uniformity_analysis",
+        "chromium_qqisable_unifomity_anaJysis",
+        "chrollium_disable_uniformity_analysi77",
         "chromippHm_experqqmetal_dp4a",
         "chrmium_expecimntal_dp4",
         "chrmiumGexpebimental_dp4a",
@@ -45,20 +45,20 @@
         "chromium_exverimentiil_dp4a",
         "chro8ium_experimenWWal_dp4a",
         "chromiMm_eperimxxntal_dp4a",
-        "chXggmium_disable_uniformity_aalysis",
-        "Xhomiuu_disale_uniformity_analysis",
-        "chromium_3isable_uniformity_analysis",
-        "chromium_disable_uniformity_analysis",
-        "chromiuE_disable_uniformity_analysis",
-        "chromium_disable_uniTTormity_aPPalsis",
-        "ddhromium_disabexxuniformity_analysis",
-        "c44romium_experimental_push_constant",
-        "chromium_experimental_pSSsVV_constant",
-        "chrom22Rm_experimental_pushRonstant",
+        "chrXmium_experimeggtal_ush_constant",
+        "chromiu_experVmentalpusX_constant",
+        "chro3ium_experimental_push_constant",
         "chromium_experimental_push_constant",
-        "chromium_exp9rimFntal_ush_constant",
-        "chrmium_experimental_push_constant",
-        "cOOromium_experiVeHtal_puh_conRRtant",
+        "chromium_experEmental_push_constant",
+        "chPPomiumexperimental_push_conTTtant",
+        "chromixxm_experimentddl_push_constnt",
+        "4416",
+        "fSVV6",
+        "RR2",
+        "f16",
+        "96",
+        "f1",
+        "VOR6",
     };
     for (auto _ : state) {
         for (auto& str : kStrings) {
diff --git a/src/tint/ast/extension_test.cc b/src/tint/ast/extension_test.cc
index 283088d..e43266c 100644
--- a/src/tint/ast/extension_test.cc
+++ b/src/tint/ast/extension_test.cc
@@ -42,25 +42,25 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"f16", Extension::kF16},
-    {"chromium_experimental_dp4a", Extension::kChromiumExperimentalDp4A},
     {"chromium_disable_uniformity_analysis", Extension::kChromiumDisableUniformityAnalysis},
+    {"chromium_experimental_dp4a", Extension::kChromiumExperimentalDp4A},
     {"chromium_experimental_push_constant", Extension::kChromiumExperimentalPushConstant},
+    {"f16", Extension::kF16},
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"cc6", Extension::kInvalid},
-    {"s", Extension::kInvalid},
-    {"HH6", Extension::kInvalid},
+    {"chromium_disableuniformiccy_analysis", Extension::kInvalid},
+    {"chromil3_disable_unifority_analss", Extension::kInvalid},
+    {"chromium_disable_Vniformity_analysis", Extension::kInvalid},
     {"chro1ium_experimental_dp4a", Extension::kInvalid},
     {"chrJmium_experiqqetal_dp4a", Extension::kInvalid},
     {"chromium_experimenll77l_dp4a", Extension::kInvalid},
-    {"chromiumppdisableqquniformity_aalysHHs", Extension::kInvalid},
-    {"chromiu_disable_unifovmitc_analyi", Extension::kInvalid},
-    {"chromium_diable_uGbformity_analysis", Extension::kInvalid},
-    {"chvomium_experimental_push_constiint", Extension::kInvalid},
-    {"chromiu8WWexperimental_push_constant", Extension::kInvalid},
-    {"chromium_experiMental_push_costanxx", Extension::kInvalid},
+    {"cppromium_experiHHenal_qqush_constant", Extension::kInvalid},
+    {"chromium_xpericental_sh_vonstant", Extension::kInvalid},
+    {"chromium_experimental_Gsh_cbnstant", Extension::kInvalid},
+    {"f1vi", Extension::kInvalid},
+    {"f8WW", Extension::kInvalid},
+    {"fxx", Extension::kInvalid},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/if_statement.h b/src/tint/ast/if_statement.h
index 81d9756..1c1abc3 100644
--- a/src/tint/ast/if_statement.h
+++ b/src/tint/ast/if_statement.h
@@ -55,7 +55,7 @@
     const BlockStatement* const body;
 
     /// The optional else statement, or nullptr
-    const Statement* else_statement;
+    const Statement* const else_statement;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/interpolate_attribute.cc b/src/tint/ast/interpolate_attribute.cc
index 29e3bfe..7c89ee1 100644
--- a/src/tint/ast/interpolate_attribute.cc
+++ b/src/tint/ast/interpolate_attribute.cc
@@ -12,6 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/ast/interpolate_attribute.cc.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
 #include "src/tint/ast/interpolate_attribute.h"
 
 #include <string>
@@ -41,44 +49,64 @@
     return ctx->dst->create<InterpolateAttribute>(src, type, sampling);
 }
 
-std::ostream& operator<<(std::ostream& out, InterpolationType type) {
-    switch (type) {
-        case InterpolationType::kPerspective: {
-            out << "perspective";
-            break;
-        }
-        case InterpolationType::kLinear: {
-            out << "linear";
-            break;
-        }
-        case InterpolationType::kFlat: {
-            out << "flat";
-            break;
-        }
+/// ParseInterpolationType parses a InterpolationType from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or InterpolationType::kInvalid if the string could not be parsed.
+InterpolationType ParseInterpolationType(std::string_view str) {
+    if (str == "flat") {
+        return InterpolationType::kFlat;
     }
-    return out;
+    if (str == "linear") {
+        return InterpolationType::kLinear;
+    }
+    if (str == "perspective") {
+        return InterpolationType::kPerspective;
+    }
+    return InterpolationType::kInvalid;
 }
 
-std::ostream& operator<<(std::ostream& out, InterpolationSampling sampling) {
-    switch (sampling) {
-        case InterpolationSampling::kNone: {
-            out << "none";
-            break;
-        }
-        case InterpolationSampling::kCenter: {
-            out << "center";
-            break;
-        }
-        case InterpolationSampling::kCentroid: {
-            out << "centroid";
-            break;
-        }
-        case InterpolationSampling::kSample: {
-            out << "sample";
-            break;
-        }
+std::ostream& operator<<(std::ostream& out, InterpolationType value) {
+    switch (value) {
+        case InterpolationType::kInvalid:
+            return out << "invalid";
+        case InterpolationType::kFlat:
+            return out << "flat";
+        case InterpolationType::kLinear:
+            return out << "linear";
+        case InterpolationType::kPerspective:
+            return out << "perspective";
     }
-    return out;
+    return out << "<unknown>";
+}
+
+/// ParseInterpolationSampling parses a InterpolationSampling from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or InterpolationSampling::kInvalid if the string could not be parsed.
+InterpolationSampling ParseInterpolationSampling(std::string_view str) {
+    if (str == "center") {
+        return InterpolationSampling::kCenter;
+    }
+    if (str == "centroid") {
+        return InterpolationSampling::kCentroid;
+    }
+    if (str == "sample") {
+        return InterpolationSampling::kSample;
+    }
+    return InterpolationSampling::kInvalid;
+}
+
+std::ostream& operator<<(std::ostream& out, InterpolationSampling value) {
+    switch (value) {
+        case InterpolationSampling::kInvalid:
+            return out << "invalid";
+        case InterpolationSampling::kCenter:
+            return out << "center";
+        case InterpolationSampling::kCentroid:
+            return out << "centroid";
+        case InterpolationSampling::kSample:
+            return out << "sample";
+    }
+    return out << "<unknown>";
 }
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/interpolate_attribute.cc.tmpl b/src/tint/ast/interpolate_attribute.cc.tmpl
new file mode 100644
index 0000000..b6f0b35
--- /dev/null
+++ b/src/tint/ast/interpolate_attribute.cc.tmpl
@@ -0,0 +1,50 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate builtin_value.cc
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+
+#include "src/tint/ast/interpolate_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::InterpolateAttribute);
+
+namespace tint::ast {
+
+InterpolateAttribute::InterpolateAttribute(ProgramID pid,
+                                           NodeID nid,
+                                           const Source& src,
+                                           InterpolationType ty,
+                                           InterpolationSampling smpl)
+    : Base(pid, nid, src), type(ty), sampling(smpl) {}
+
+InterpolateAttribute::~InterpolateAttribute() = default;
+
+std::string InterpolateAttribute::Name() const {
+    return "interpolate";
+}
+
+const InterpolateAttribute* InterpolateAttribute::Clone(CloneContext* ctx) const {
+    // Clone arguments outside of create() call to have deterministic ordering
+    auto src = ctx->Clone(source);
+    return ctx->dst->create<InterpolateAttribute>(src, type, sampling);
+}
+
+{{ Eval "ParseEnum" (Sem.Enum "interpolation_type")}}
+
+{{ Eval "EnumOStream" (Sem.Enum "interpolation_type")}}
+
+{{ Eval "ParseEnum" (Sem.Enum "interpolation_sampling")}}
+
+{{ Eval "EnumOStream" (Sem.Enum "interpolation_sampling")}}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
index 4f9ea9d..c6b4af0 100644
--- a/src/tint/ast/interpolate_attribute.h
+++ b/src/tint/ast/interpolate_attribute.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Tint Authors.
+// Copyright 2022 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.
@@ -12,6 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/src/cmd/gen
+// using the template:
+//   src/tint/ast/interpolate_attribute.h.tmpl
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
 #ifndef SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
 #define SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
 
@@ -23,10 +31,52 @@
 namespace tint::ast {
 
 /// The interpolation type.
-enum class InterpolationType { kPerspective, kLinear, kFlat };
+enum class InterpolationType {
+    kInvalid,
+    kFlat,
+    kLinear,
+    kPerspective,
+};
+
+/// @param out the std::ostream to write to
+/// @param value the InterpolationType
+/// @returns `out` so calls can be chained
+std::ostream& operator<<(std::ostream& out, InterpolationType value);
+
+/// ParseInterpolationType parses a InterpolationType from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or InterpolationType::kInvalid if the string could not be parsed.
+InterpolationType ParseInterpolationType(std::string_view str);
+
+constexpr const char* kInterpolationTypeStrings[] = {
+    "flat",
+    "linear",
+    "perspective",
+};
 
 /// The interpolation sampling.
-enum class InterpolationSampling { kNone = -1, kCenter, kCentroid, kSample };
+enum class InterpolationSampling {
+    kInvalid,
+    kCenter,
+    kCentroid,
+    kSample,
+};
+
+/// @param out the std::ostream to write to
+/// @param value the InterpolationSampling
+/// @returns `out` so calls can be chained
+std::ostream& operator<<(std::ostream& out, InterpolationSampling value);
+
+/// ParseInterpolationSampling parses a InterpolationSampling from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or InterpolationSampling::kInvalid if the string could not be parsed.
+InterpolationSampling ParseInterpolationSampling(std::string_view str);
+
+constexpr const char* kInterpolationSamplingStrings[] = {
+    "center",
+    "centroid",
+    "sample",
+};
 
 /// An interpolate attribute
 class InterpolateAttribute final : public Castable<InterpolateAttribute, Attribute> {
@@ -60,16 +110,6 @@
     const InterpolationSampling sampling;
 };
 
-/// @param out the std::ostream to write to
-/// @param type the interpolation type
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, InterpolationType type);
-
-/// @param out the std::ostream to write to
-/// @param sampling the interpolation sampling
-/// @return the std::ostream so calls can be chained
-std::ostream& operator<<(std::ostream& out, InterpolationSampling sampling);
-
 }  // namespace tint::ast
 
 #endif  // SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
diff --git a/src/tint/ast/interpolate_attribute.h.tmpl b/src/tint/ast/interpolate_attribute.h.tmpl
new file mode 100644
index 0000000..c225cb6
--- /dev/null
+++ b/src/tint/ast/interpolate_attribute.h.tmpl
@@ -0,0 +1,63 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/src/cmd/gen to generate interpolate_attribute.h
+
+See:
+* tools/src/cmd/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+{{- Import "src/tint/templates/enums.tmpl.inc" -}}
+
+#ifndef SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
+#define SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
+
+#include <ostream>
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint::ast {
+
+/// The interpolation type.
+{{ Eval "DeclareEnum" (Sem.Enum "interpolation_type") }}
+
+/// The interpolation sampling.
+{{ Eval "DeclareEnum" (Sem.Enum "interpolation_sampling") }}
+
+/// An interpolate attribute
+class InterpolateAttribute final : public Castable<InterpolateAttribute, Attribute> {
+  public:
+    /// Create an interpolate attribute.
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param type the interpolation type
+    /// @param sampling the interpolation sampling
+    InterpolateAttribute(ProgramID pid,
+                         NodeID nid,
+                         const Source& src,
+                         InterpolationType type,
+                         InterpolationSampling sampling);
+    ~InterpolateAttribute() override;
+
+    /// @returns the WGSL name for the attribute
+    std::string Name() const override;
+
+    /// Clones this node and all transitive child nodes using the `CloneContext`
+    /// `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const InterpolateAttribute* Clone(CloneContext* ctx) const override;
+
+    /// The interpolation type
+    const InterpolationType type;
+
+    /// The interpolation sampling
+    const InterpolationSampling sampling;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
diff --git a/src/tint/ast/pointer.cc b/src/tint/ast/pointer.cc
index 71b7004..96bbdaf 100644
--- a/src/tint/ast/pointer.cc
+++ b/src/tint/ast/pointer.cc
@@ -35,7 +35,7 @@
         out << address_space << ", ";
     }
     out << type->FriendlyName(symbols);
-    if (access != ast::Access::kUndefined) {
+    if (access != ast::Access::kInvalid) {
         out << ", " << access;
     }
     out << ">";
diff --git a/src/tint/ast/pointer_test.cc b/src/tint/ast/pointer_test.cc
index aea260a..56a04b0 100644
--- a/src/tint/ast/pointer_test.cc
+++ b/src/tint/ast/pointer_test.cc
@@ -32,7 +32,7 @@
 
 TEST_F(AstPointerTest, FriendlyName) {
     auto* i32 = create<I32>();
-    auto* p = create<Pointer>(i32, ast::AddressSpace::kWorkgroup, Access::kUndefined);
+    auto* p = create<Pointer>(i32, ast::AddressSpace::kWorkgroup, Access::kInvalid);
     EXPECT_EQ(p->FriendlyName(Symbols()), "ptr<workgroup, i32>");
 }
 
diff --git a/src/tint/ast/texel_format.cc b/src/tint/ast/texel_format.cc
index cfac9f1..201cad3 100644
--- a/src/tint/ast/texel_format.cc
+++ b/src/tint/ast/texel_format.cc
@@ -28,8 +28,44 @@
 /// @param str the string to parse
 /// @returns the parsed enum, or TexelFormat::kInvalid if the string could not be parsed.
 TexelFormat ParseTexelFormat(std::string_view str) {
-    if (str == "rgba8unorm") {
-        return TexelFormat::kRgba8Unorm;
+    if (str == "r32float") {
+        return TexelFormat::kR32Float;
+    }
+    if (str == "r32sint") {
+        return TexelFormat::kR32Sint;
+    }
+    if (str == "r32uint") {
+        return TexelFormat::kR32Uint;
+    }
+    if (str == "rg32float") {
+        return TexelFormat::kRg32Float;
+    }
+    if (str == "rg32sint") {
+        return TexelFormat::kRg32Sint;
+    }
+    if (str == "rg32uint") {
+        return TexelFormat::kRg32Uint;
+    }
+    if (str == "rgba16float") {
+        return TexelFormat::kRgba16Float;
+    }
+    if (str == "rgba16sint") {
+        return TexelFormat::kRgba16Sint;
+    }
+    if (str == "rgba16uint") {
+        return TexelFormat::kRgba16Uint;
+    }
+    if (str == "rgba32float") {
+        return TexelFormat::kRgba32Float;
+    }
+    if (str == "rgba32sint") {
+        return TexelFormat::kRgba32Sint;
+    }
+    if (str == "rgba32uint") {
+        return TexelFormat::kRgba32Uint;
+    }
+    if (str == "rgba8sint") {
+        return TexelFormat::kRgba8Sint;
     }
     if (str == "rgba8snorm") {
         return TexelFormat::kRgba8Snorm;
@@ -37,44 +73,8 @@
     if (str == "rgba8uint") {
         return TexelFormat::kRgba8Uint;
     }
-    if (str == "rgba8sint") {
-        return TexelFormat::kRgba8Sint;
-    }
-    if (str == "rgba16uint") {
-        return TexelFormat::kRgba16Uint;
-    }
-    if (str == "rgba16sint") {
-        return TexelFormat::kRgba16Sint;
-    }
-    if (str == "rgba16float") {
-        return TexelFormat::kRgba16Float;
-    }
-    if (str == "r32uint") {
-        return TexelFormat::kR32Uint;
-    }
-    if (str == "r32sint") {
-        return TexelFormat::kR32Sint;
-    }
-    if (str == "r32float") {
-        return TexelFormat::kR32Float;
-    }
-    if (str == "rg32uint") {
-        return TexelFormat::kRg32Uint;
-    }
-    if (str == "rg32sint") {
-        return TexelFormat::kRg32Sint;
-    }
-    if (str == "rg32float") {
-        return TexelFormat::kRg32Float;
-    }
-    if (str == "rgba32uint") {
-        return TexelFormat::kRgba32Uint;
-    }
-    if (str == "rgba32sint") {
-        return TexelFormat::kRgba32Sint;
-    }
-    if (str == "rgba32float") {
-        return TexelFormat::kRgba32Float;
+    if (str == "rgba8unorm") {
+        return TexelFormat::kRgba8Unorm;
     }
     return TexelFormat::kInvalid;
 }
@@ -83,38 +83,38 @@
     switch (value) {
         case TexelFormat::kInvalid:
             return out << "invalid";
-        case TexelFormat::kRgba8Unorm:
-            return out << "rgba8unorm";
+        case TexelFormat::kR32Float:
+            return out << "r32float";
+        case TexelFormat::kR32Sint:
+            return out << "r32sint";
+        case TexelFormat::kR32Uint:
+            return out << "r32uint";
+        case TexelFormat::kRg32Float:
+            return out << "rg32float";
+        case TexelFormat::kRg32Sint:
+            return out << "rg32sint";
+        case TexelFormat::kRg32Uint:
+            return out << "rg32uint";
+        case TexelFormat::kRgba16Float:
+            return out << "rgba16float";
+        case TexelFormat::kRgba16Sint:
+            return out << "rgba16sint";
+        case TexelFormat::kRgba16Uint:
+            return out << "rgba16uint";
+        case TexelFormat::kRgba32Float:
+            return out << "rgba32float";
+        case TexelFormat::kRgba32Sint:
+            return out << "rgba32sint";
+        case TexelFormat::kRgba32Uint:
+            return out << "rgba32uint";
+        case TexelFormat::kRgba8Sint:
+            return out << "rgba8sint";
         case TexelFormat::kRgba8Snorm:
             return out << "rgba8snorm";
         case TexelFormat::kRgba8Uint:
             return out << "rgba8uint";
-        case TexelFormat::kRgba8Sint:
-            return out << "rgba8sint";
-        case TexelFormat::kRgba16Uint:
-            return out << "rgba16uint";
-        case TexelFormat::kRgba16Sint:
-            return out << "rgba16sint";
-        case TexelFormat::kRgba16Float:
-            return out << "rgba16float";
-        case TexelFormat::kR32Uint:
-            return out << "r32uint";
-        case TexelFormat::kR32Sint:
-            return out << "r32sint";
-        case TexelFormat::kR32Float:
-            return out << "r32float";
-        case TexelFormat::kRg32Uint:
-            return out << "rg32uint";
-        case TexelFormat::kRg32Sint:
-            return out << "rg32sint";
-        case TexelFormat::kRg32Float:
-            return out << "rg32float";
-        case TexelFormat::kRgba32Uint:
-            return out << "rgba32uint";
-        case TexelFormat::kRgba32Sint:
-            return out << "rgba32sint";
-        case TexelFormat::kRgba32Float:
-            return out << "rgba32float";
+        case TexelFormat::kRgba8Unorm:
+            return out << "rgba8unorm";
     }
     return out << "<unknown>";
 }
diff --git a/src/tint/ast/texel_format.h b/src/tint/ast/texel_format.h
index 28119b3..9c65fb6 100644
--- a/src/tint/ast/texel_format.h
+++ b/src/tint/ast/texel_format.h
@@ -30,22 +30,22 @@
 /// Enumerator of texel formats
 enum class TexelFormat {
     kInvalid,
-    kRgba8Unorm,
+    kR32Float,
+    kR32Sint,
+    kR32Uint,
+    kRg32Float,
+    kRg32Sint,
+    kRg32Uint,
+    kRgba16Float,
+    kRgba16Sint,
+    kRgba16Uint,
+    kRgba32Float,
+    kRgba32Sint,
+    kRgba32Uint,
+    kRgba8Sint,
     kRgba8Snorm,
     kRgba8Uint,
-    kRgba8Sint,
-    kRgba16Uint,
-    kRgba16Sint,
-    kRgba16Float,
-    kR32Uint,
-    kR32Sint,
-    kR32Float,
-    kRg32Uint,
-    kRg32Sint,
-    kRg32Float,
-    kRgba32Uint,
-    kRgba32Sint,
-    kRgba32Float,
+    kRgba8Unorm,
 };
 
 /// @param out the std::ostream to write to
@@ -58,6 +58,12 @@
 /// @returns the parsed enum, or TexelFormat::kInvalid if the string could not be parsed.
 TexelFormat ParseTexelFormat(std::string_view str);
 
+constexpr const char* kTexelFormatStrings[] = {
+    "r32float",    "r32sint",    "r32uint",    "rg32float",   "rg32sint",   "rg32uint",
+    "rgba16float", "rgba16sint", "rgba16uint", "rgba32float", "rgba32sint", "rgba32uint",
+    "rgba8sint",   "rgba8snorm", "rgba8uint",  "rgba8unorm",
+};
+
 }  // namespace tint::ast
 
 #endif  // SRC_TINT_AST_TEXEL_FORMAT_H_
diff --git a/src/tint/ast/texel_format_bench.cc b/src/tint/ast/texel_format_bench.cc
index a17906a..c48e0f6 100644
--- a/src/tint/ast/texel_format_bench.cc
+++ b/src/tint/ast/texel_format_bench.cc
@@ -31,29 +31,29 @@
 
 void TexelFormatParser(::benchmark::State& state) {
     std::array kStrings{
-        "rgbaunccrm",    "rlbanr3",        "rVba8unorm",     "rgba8unorm",   "rgba1unorm",
-        "rgbJqqnorm",    "rgb7ll8unorm",   "rgqqappnoHHm",   "rv8scor",      "rgbbGsnrm",
-        "rgba8snorm",    "rgba8vniirm",    "rg8a8snoWWm",    "Mgbaxxnorm",   "rXa8uggnt",
-        "rgbXVut",       "3gba8uint",      "rgba8uint",      "rgba8uiEt",    "rgTTPauint",
-        "ddgbauixxt",    "44gba8sint",     "VVgbaSSsint",    "rba8si2Rt",    "rgba8sint",
-        "r9bFsint",      "rgba8int",       "rgVROOsHnt",     "ryba1uint",    "r77ba1nnullrrt",
-        "rgb4006uint",   "rgba16uint",     "rb1uioot",       "rga1uzznt",    "r11b1uppiit",
-        "XXgba16sint",   "IIgb9916nni55t", "rYbaSSrrsiHHat", "rgba16sint",   "rbkk6Hit",
-        "jgba1sgRR",     "rgbab6si",       "rgba16fljat",    "rgba6float",   "rbq6float",
-        "rgba16float",   "rgba1NNloat",    "rgbvv6flot",     "rgbaQQ6foat",  "r3ffir",
-        "r32uijt",       "rNNwuin8",       "r32uint",        "r32int",       "rrr2uint",
-        "G32uint",       "r32sinFF",       "32st",           "r3rrint",      "r32sint",
-        "2sint",         "D3siJJt",        "r38n",           "r211lk",       "r32floa",
-        "r3flJat",       "r32float",       "r32fcoat",       "r32floOt",     "r32floKK_vtt",
-        "rxx32ui8",      "Fg3qq__n",       "rg32iqqt",       "rg32uint",     "rg333uin6",
-        "rtto62u9QQt",   "rg366uin",       "rOx2si6zz",      "rg3yysint",    "rHHsint",
-        "rg32sint",      "qWW432snt",      "rg3OOsnt",       "g32siYt",      "g32flo",
-        "rg32foaF",      "rg32fwat",       "rg32float",      "G3fKoaff",     "KKgq2float",
-        "rg32mmlo3t",    "rgba32uit",      "rqba3uint",      "rgbabb2uin",   "rgba32uint",
-        "rba32iint",     "qgba32uiOt",     "rgba32uiTTvv",   "rgFFa32sint",  "rg00Q2sPnt",
-        "rgbaP2sint",    "rgba32sint",     "rgb77s2sint",    "rgba32sbbRRC", "rgbXX32sint",
-        "rOOOba3CCqoat", "rgbu32fsLt",     "rgba3Xfloat",    "rgba32float",  "rba32float",
-        "qqb3float",     "rgba32fl22at",
+        "rcc2flot",      "3flo3",        "r32flVat",     "r32float",    "132float",
+        "32Jlqqat",      "ll3277loat",   "ppqq2snHH",    "r3cv",        "b2siGt",
+        "r32sint",       "r32siivt",     "8WW2sint",     "rxxsint",     "rXuingg",
+        "3uiXt",         "r32u3nt",      "r32uint",      "E32uint",     "rPTTuint",
+        "r32uidxx",      "44g32float",   "VVg32SSloat",  "r32flo2Rt",   "rg32float",
+        "r93Float",      "rg32foat",     "rgVROOlHat",   "rg3ysin",     "77grr2sllnnt",
+        "04g32sint",     "rg32sint",     "g3oont",       "rg32zzt",     "rgiipps1n",
+        "XXg32uint",     "rII39955nnnt", "aagHH2uinYSS", "rg32uint",    "rkk3it",
+        "gj3uRRn",       "r3bunt",       "rgba16fljat",  "rgba6float",  "rbq6float",
+        "rgba16float",   "rgba1NNloat",  "rgbvv6flot",   "rgbaQQ6foat", "rgb6srnff",
+        "rgba16sijt",    "NNgba16ww2t",  "rgba16sint",   "rgba16snt",   "rgba16rrint",
+        "rgba1Gsint",    "rgba16uFFnt",  "g16uEnt",      "rgb16rrint",  "rgba16uint",
+        "gba16uit",      "rXa1DuiJJt",   "rgauint",      "rga32klot",   "rgb32float",
+        "rgJa32flot",    "rgba32float",  "rgba32fcoat",  "rgba32floOt", "__gttavv2fKKoat",
+        "rg5a32xxnt",    "__ba3sqqFt",   "rgbqq2sint",   "rgba32sint",  "33ba32s66nt",
+        "rtt6a3QQooint", "r66ba3sint",   "xba32zzinO6",  "ryyba32uint", "rbZ32HinZ",
+        "rgba32uint",    "rgba3u4WWnq",  "rgba32uOOt",   "oogba2Yin",   "gba8si",
+        "rgba8inF",      "rgba8wnt",     "rgba8sint",    "Gb8Kinff",    "KKgqa8sint",
+        "rgbammsi3t",    "rgba8snom",    "rqba8norm",    "rgbabbsnor",  "rgba8snorm",
+        "rba8siorm",     "qgba8snoOm",   "rgba8snoTTvv", "rgbaFFuint",  "rgQa00uiP",
+        "rgPa8uint",     "rgba8uint",    "rgssa77unt",   "Cgbbb8uiRRt", "rgba8uinXX",
+        "CqgbaOOunorm",  "rgbu8usrL",    "rgba8Xnorm",   "rgba8unorm",  "rgba8unrm",
+        "ba8uqqor",      "rgba8unor22",
     };
     for (auto _ : state) {
         for (auto& str : kStrings) {
diff --git a/src/tint/ast/texel_format_test.cc b/src/tint/ast/texel_format_test.cc
index 606d78f..d431918 100644
--- a/src/tint/ast/texel_format_test.cc
+++ b/src/tint/ast/texel_format_test.cc
@@ -42,41 +42,41 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"rgba8unorm", TexelFormat::kRgba8Unorm},   {"rgba8snorm", TexelFormat::kRgba8Snorm},
-    {"rgba8uint", TexelFormat::kRgba8Uint},     {"rgba8sint", TexelFormat::kRgba8Sint},
-    {"rgba16uint", TexelFormat::kRgba16Uint},   {"rgba16sint", TexelFormat::kRgba16Sint},
-    {"rgba16float", TexelFormat::kRgba16Float}, {"r32uint", TexelFormat::kR32Uint},
-    {"r32sint", TexelFormat::kR32Sint},         {"r32float", TexelFormat::kR32Float},
-    {"rg32uint", TexelFormat::kRg32Uint},       {"rg32sint", TexelFormat::kRg32Sint},
-    {"rg32float", TexelFormat::kRg32Float},     {"rgba32uint", TexelFormat::kRgba32Uint},
-    {"rgba32sint", TexelFormat::kRgba32Sint},   {"rgba32float", TexelFormat::kRgba32Float},
+    {"r32float", TexelFormat::kR32Float},       {"r32sint", TexelFormat::kR32Sint},
+    {"r32uint", TexelFormat::kR32Uint},         {"rg32float", TexelFormat::kRg32Float},
+    {"rg32sint", TexelFormat::kRg32Sint},       {"rg32uint", TexelFormat::kRg32Uint},
+    {"rgba16float", TexelFormat::kRgba16Float}, {"rgba16sint", TexelFormat::kRgba16Sint},
+    {"rgba16uint", TexelFormat::kRgba16Uint},   {"rgba32float", TexelFormat::kRgba32Float},
+    {"rgba32sint", TexelFormat::kRgba32Sint},   {"rgba32uint", TexelFormat::kRgba32Uint},
+    {"rgba8sint", TexelFormat::kRgba8Sint},     {"rgba8snorm", TexelFormat::kRgba8Snorm},
+    {"rgba8uint", TexelFormat::kRgba8Uint},     {"rgba8unorm", TexelFormat::kRgba8Unorm},
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"rgbaunccrm", TexelFormat::kInvalid},   {"rlbanr3", TexelFormat::kInvalid},
-    {"rVba8unorm", TexelFormat::kInvalid},   {"rgba1snorm", TexelFormat::kInvalid},
-    {"rgbJqqnorm", TexelFormat::kInvalid},   {"rgb7ll8snorm", TexelFormat::kInvalid},
-    {"rgbauippqHH", TexelFormat::kInvalid},  {"rgbaun", TexelFormat::kInvalid},
-    {"rba8Gint", TexelFormat::kInvalid},     {"rgvia8sint", TexelFormat::kInvalid},
-    {"rgba8WWint", TexelFormat::kInvalid},   {"rgbasxxMt", TexelFormat::kInvalid},
-    {"rXba16ungg", TexelFormat::kInvalid},   {"rba1XuVt", TexelFormat::kInvalid},
-    {"rgba16uin3", TexelFormat::kInvalid},   {"rgba16sinE", TexelFormat::kInvalid},
-    {"TTgba16sPPn", TexelFormat::kInvalid},  {"rgbad6xxint", TexelFormat::kInvalid},
-    {"rgba446float", TexelFormat::kInvalid}, {"SSVVba16float", TexelFormat::kInvalid},
-    {"rgbRR6float", TexelFormat::kInvalid},  {"rFui9t", TexelFormat::kInvalid},
-    {"r32int", TexelFormat::kInvalid},       {"VOORRHnt", TexelFormat::kInvalid},
-    {"r3siyt", TexelFormat::kInvalid},       {"lln3rrs77nt", TexelFormat::kInvalid},
-    {"r32s4n00", TexelFormat::kInvalid},     {"32ooat", TexelFormat::kInvalid},
-    {"r32fzzt", TexelFormat::kInvalid},      {"r3iippl1a", TexelFormat::kInvalid},
-    {"XXg32uint", TexelFormat::kInvalid},    {"rII39955nnnt", TexelFormat::kInvalid},
-    {"aagHH2uinYSS", TexelFormat::kInvalid}, {"rkk3it", TexelFormat::kInvalid},
-    {"gj3sRRn", TexelFormat::kInvalid},      {"r3bsnt", TexelFormat::kInvalid},
-    {"rg32flojt", TexelFormat::kInvalid},    {"r32floa", TexelFormat::kInvalid},
-    {"rg32lot", TexelFormat::kInvalid},      {"rgb3uit", TexelFormat::kInvalid},
-    {"rgjj3uint", TexelFormat::kInvalid},    {"rgb2urnff", TexelFormat::kInvalid},
-    {"rgba32sijt", TexelFormat::kInvalid},   {"NNgba32ww2t", TexelFormat::kInvalid},
-    {"rgba32snt", TexelFormat::kInvalid},    {"rgba32rrloat", TexelFormat::kInvalid},
-    {"rgGa32float", TexelFormat::kInvalid},  {"FFgba32float", TexelFormat::kInvalid},
+    {"rcc2flot", TexelFormat::kInvalid},       {"3flo3", TexelFormat::kInvalid},
+    {"r32flVat", TexelFormat::kInvalid},       {"r32s1nt", TexelFormat::kInvalid},
+    {"rq2Jint", TexelFormat::kInvalid},        {"r32sin7ll", TexelFormat::kInvalid},
+    {"ppqq2unHH", TexelFormat::kInvalid},      {"r3cv", TexelFormat::kInvalid},
+    {"b2uiGt", TexelFormat::kInvalid},         {"rgvi2float", TexelFormat::kInvalid},
+    {"rg328WWoat", TexelFormat::kInvalid},     {"rg32lxxMt", TexelFormat::kInvalid},
+    {"rgXggsnt", TexelFormat::kInvalid},       {"rgXsnu", TexelFormat::kInvalid},
+    {"rg32s3nt", TexelFormat::kInvalid},       {"rg3Euint", TexelFormat::kInvalid},
+    {"PP32TTint", TexelFormat::kInvalid},      {"xxg32ddnt", TexelFormat::kInvalid},
+    {"rgba446float", TexelFormat::kInvalid},   {"SSVVba16float", TexelFormat::kInvalid},
+    {"rgbRR6float", TexelFormat::kInvalid},    {"rga16Fint", TexelFormat::kInvalid},
+    {"rgb16sint", TexelFormat::kInvalid},      {"ORVHa16sint", TexelFormat::kInvalid},
+    {"ryba1uint", TexelFormat::kInvalid},      {"r77ba1nnullrrt", TexelFormat::kInvalid},
+    {"rgb4006uint", TexelFormat::kInvalid},    {"rboofloat", TexelFormat::kInvalid},
+    {"rgbaz2loat", TexelFormat::kInvalid},     {"ppga3ii1floa", TexelFormat::kInvalid},
+    {"XXgba32sint", TexelFormat::kInvalid},    {"IIgb9932nni55t", TexelFormat::kInvalid},
+    {"rYbaSSrrsiHHat", TexelFormat::kInvalid}, {"rbkk2Hit", TexelFormat::kInvalid},
+    {"jgba3ugRR", TexelFormat::kInvalid},      {"rgbab2ui", TexelFormat::kInvalid},
+    {"rgba8sijt", TexelFormat::kInvalid},      {"rba8sint", TexelFormat::kInvalid},
+    {"rba8sqt", TexelFormat::kInvalid},        {"rgba8NNnom", TexelFormat::kInvalid},
+    {"rga8vvorm", TexelFormat::kInvalid},      {"rgba8snorQ", TexelFormat::kInvalid},
+    {"rgbauirf", TexelFormat::kInvalid},       {"rgbajuint", TexelFormat::kInvalid},
+    {"wNNgbauin2", TexelFormat::kInvalid},     {"rgba8unrm", TexelFormat::kInvalid},
+    {"rgba8urrorm", TexelFormat::kInvalid},    {"rgba8Gnorm", TexelFormat::kInvalid},
 };
 
 using TexelFormatParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/inspector/entry_point.cc b/src/tint/inspector/entry_point.cc
index 6d3419f..8cbca14 100644
--- a/src/tint/inspector/entry_point.cc
+++ b/src/tint/inspector/entry_point.cc
@@ -35,32 +35,5 @@
 EntryPoint::EntryPoint(EntryPoint&&) = default;
 EntryPoint::~EntryPoint() = default;
 
-InterpolationType ASTToInspectorInterpolationType(ast::InterpolationType ast_type) {
-    switch (ast_type) {
-        case ast::InterpolationType::kPerspective:
-            return InterpolationType::kPerspective;
-        case ast::InterpolationType::kLinear:
-            return InterpolationType::kLinear;
-        case ast::InterpolationType::kFlat:
-            return InterpolationType::kFlat;
-    }
-
-    return InterpolationType::kUnknown;
-}
-
-InterpolationSampling ASTToInspectorInterpolationSampling(ast::InterpolationSampling sampling) {
-    switch (sampling) {
-        case ast::InterpolationSampling::kNone:
-            return InterpolationSampling::kNone;
-        case ast::InterpolationSampling::kCenter:
-            return InterpolationSampling::kCenter;
-        case ast::InterpolationSampling::kCentroid:
-            return InterpolationSampling::kCentroid;
-        case ast::InterpolationSampling::kSample:
-            return InterpolationSampling::kSample;
-    }
-
-    return InterpolationSampling::kUnknown;
-}
 
 }  // namespace tint::inspector
diff --git a/src/tint/inspector/entry_point.h b/src/tint/inspector/entry_point.h
index 5d119d4..4a4706b 100644
--- a/src/tint/inspector/entry_point.h
+++ b/src/tint/inspector/entry_point.h
@@ -84,17 +84,6 @@
     InterpolationSampling interpolation_sampling = InterpolationSampling::kUnknown;
 };
 
-/// Convert from internal ast::InterpolationType to public ::InterpolationType.
-/// @param ast_type internal value to convert from
-/// @returns the publicly visible equivalent
-InterpolationType ASTToInspectorInterpolationType(ast::InterpolationType ast_type);
-
-/// Convert from internal ast::InterpolationSampling to public
-/// ::InterpolationSampling
-/// @param sampling internal value to convert from
-/// @returns the publicly visible equivalent
-InterpolationSampling ASTToInspectorInterpolationSampling(ast::InterpolationSampling sampling);
-
 /// Reflection data about an override variable referenced by an entry point
 struct Override {
     /// Name of the override
@@ -175,6 +164,8 @@
     bool sample_index_used = false;
     /// Does the entry point use the num_workgroups builtin
     bool num_workgroups_used = false;
+    /// Does the entry point use the frag_depth builtin
+    bool frag_depth_used = false;
 };
 
 }  // namespace tint::inspector
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index 3520ae3..ddb5b11 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -117,14 +117,45 @@
         return {InterpolationType::kPerspective, InterpolationSampling::kCenter};
     }
 
-    auto interpolation_type = interpolation_attribute->type;
-    auto sampling = interpolation_attribute->sampling;
-    if (interpolation_type != ast::InterpolationType::kFlat &&
-        sampling == ast::InterpolationSampling::kNone) {
-        sampling = ast::InterpolationSampling::kCenter;
+    auto ast_interpolation_type = interpolation_attribute->type;
+    auto ast_sampling_type = interpolation_attribute->sampling;
+    if (ast_interpolation_type != ast::InterpolationType::kFlat &&
+        ast_sampling_type == ast::InterpolationSampling::kInvalid) {
+        ast_sampling_type = ast::InterpolationSampling::kCenter;
     }
-    return {ASTToInspectorInterpolationType(interpolation_type),
-            ASTToInspectorInterpolationSampling(sampling)};
+
+    auto interpolation_type = InterpolationType::kUnknown;
+    switch (ast_interpolation_type) {
+        case ast::InterpolationType::kPerspective:
+            interpolation_type = InterpolationType::kPerspective;
+            break;
+        case ast::InterpolationType::kLinear:
+            interpolation_type = InterpolationType::kLinear;
+            break;
+        case ast::InterpolationType::kFlat:
+            interpolation_type = InterpolationType::kFlat;
+            break;
+        case ast::InterpolationType::kInvalid:
+            break;
+    }
+
+    auto sampling_type = InterpolationSampling::kUnknown;
+    switch (ast_sampling_type) {
+        case ast::InterpolationSampling::kInvalid:
+            sampling_type = InterpolationSampling::kNone;
+            break;
+        case ast::InterpolationSampling::kCenter:
+            sampling_type = InterpolationSampling::kCenter;
+            break;
+        case ast::InterpolationSampling::kCentroid:
+            sampling_type = InterpolationSampling::kCentroid;
+            break;
+        case ast::InterpolationSampling::kSample:
+            sampling_type = InterpolationSampling::kSample;
+            break;
+    }
+
+    return {interpolation_type, sampling_type};
 }
 
 }  // namespace
@@ -192,6 +223,8 @@
 
         entry_point.output_sample_mask_used = ContainsBuiltin(
             ast::BuiltinValue::kSampleMask, sem->ReturnType(), func->return_type_attributes);
+        entry_point.frag_depth_used = ContainsBuiltin(
+            ast::BuiltinValue::kFragDepth, sem->ReturnType(), func->return_type_attributes);
     }
 
     for (auto* var : sem->TransitivelyReferencedGlobals()) {
diff --git a/src/tint/inspector/inspector_test.cc b/src/tint/inspector/inspector_test.cc
index 7e81ba0..7aa017c 100644
--- a/src/tint/inspector/inspector_test.cc
+++ b/src/tint/inspector/inspector_test.cc
@@ -852,6 +852,7 @@
     EXPECT_FALSE(result[0].front_facing_used);
     EXPECT_FALSE(result[0].sample_index_used);
     EXPECT_FALSE(result[0].num_workgroups_used);
+    EXPECT_FALSE(result[0].frag_depth_used);
 }
 
 TEST_F(InspectorGetEntryPointTest, InputSampleMaskSimpleReferenced) {
@@ -1134,6 +1135,49 @@
     EXPECT_TRUE(result[0].num_workgroups_used);
 }
 
+TEST_F(InspectorGetEntryPointTest, FragDepthSimpleReferenced) {
+    Func("ep_func", {}, ty.f32(),
+         utils::Vector{
+             Return(Expr(0_f)),
+         },
+         utils::Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         utils::Vector{
+             Builtin(ast::BuiltinValue::kFragDepth),
+         });
+
+    Inspector& inspector = Build();
+
+    auto result = inspector.GetEntryPoints();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_TRUE(result[0].frag_depth_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, FragDepthStructReferenced) {
+    Structure("out_struct", utils::Vector{
+                                Member("inner_frag_depth", ty.f32(),
+                                       utils::Vector{Builtin(ast::BuiltinValue::kFragDepth)}),
+                            });
+
+    Func("ep_func", utils::Empty, ty.type_name("out_struct"),
+         utils::Vector{
+             Decl(Var("out_var", ty.type_name("out_struct"))),
+             Return("out_var"),
+         },
+         utils::Vector{
+             Stage(ast::PipelineStage::kFragment),
+         });
+
+    Inspector& inspector = Build();
+
+    auto result = inspector.GetEntryPoints();
+
+    ASSERT_EQ(1u, result.size());
+    EXPECT_TRUE(result[0].frag_depth_used);
+}
+
 TEST_F(InspectorGetEntryPointTest, ImplicitInterpolate) {
     Structure("in_struct", utils::Vector{
                                Member("struct_inner", ty.f32(), utils::Vector{Location(0_a)}),
@@ -1206,7 +1250,7 @@
             ast::InterpolationType::kPerspective, ast::InterpolationSampling::kSample,
             InterpolationType::kPerspective, InterpolationSampling::kSample},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kPerspective, ast::InterpolationSampling::kNone,
+            ast::InterpolationType::kPerspective, ast::InterpolationSampling::kInvalid,
             InterpolationType::kPerspective, InterpolationSampling::kCenter},
         InspectorGetEntryPointInterpolateTestParams{
             ast::InterpolationType::kLinear, ast::InterpolationSampling::kCenter,
@@ -1218,10 +1262,10 @@
             ast::InterpolationType::kLinear, ast::InterpolationSampling::kSample,
             InterpolationType::kLinear, InterpolationSampling::kSample},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kLinear, ast::InterpolationSampling::kNone,
+            ast::InterpolationType::kLinear, ast::InterpolationSampling::kInvalid,
             InterpolationType::kLinear, InterpolationSampling::kCenter},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone,
+            ast::InterpolationType::kFlat, ast::InterpolationSampling::kInvalid,
             InterpolationType::kFlat, InterpolationSampling::kNone}));
 
 TEST_F(InspectorGetOverrideDefaultValuesTest, Bool) {
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index f55b18c..493d224 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -94,6 +94,20 @@
   rgba32float
 }
 
+// https://www.w3.org/TR/WGSL/#interpolation
+enum interpolation_type {
+  perspective
+  linear
+  flat
+}
+
+// https://www.w3.org/TR/WGSL/#interpolation
+enum interpolation_sampling {
+  center
+  centroid
+  sample
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // WGSL primitive types                                                       //
 // Types may be decorated with @precedence(N) to prioritize which type        //
@@ -161,13 +175,13 @@
 // A type matcher that can match one or more types.                           //
 ////////////////////////////////////////////////////////////////////////////////
 
-match abstract_or_scalar: ia | fa | f32 | f16 | i32 | u32 | bool
-match scalar: f32 | f16 | i32 | u32 | bool
-match scalar_no_f32: i32 | f16 | u32 | bool
-match scalar_no_f16: f32 | i32 | u32 | bool
-match scalar_no_i32: f32 | f16 | u32 | bool
-match scalar_no_u32: f32 | f16 | i32 | bool
-match scalar_no_bool: f32 | f16 | i32 | u32
+match scalar: ia | fa | f32 | f16 | i32 | u32 | bool
+match concrete_scalar: f32 | f16 | i32 | u32 | bool
+match scalar_no_f32: ia | fa | i32 | f16 | u32 | bool
+match scalar_no_f16: ia | fa | f32 | i32 | u32 | bool
+match scalar_no_i32: ia | fa | f32 | f16 | u32 | bool
+match scalar_no_u32: ia | fa | f32 | f16 | i32 | bool
+match scalar_no_bool: ia | fa | f32 | f16 | i32 | u32
 match fia_fiu32_f16: fa | ia | f32 | i32 | u32 | f16
 match fia_fi32_f16: fa | ia | f32 | i32 | f16
 match fia_fiu32: fa | ia | f32 | i32 | u32
@@ -510,9 +524,9 @@
 fn round<N: num, T: f32_f16>(vec<N, T>) -> vec<N, T>
 fn saturate<T: f32_f16>(T) -> T
 fn saturate<T: f32_f16, N: num>(vec<N, T>) -> vec<N, T>
-@const("select_bool") fn select<T: abstract_or_scalar>(T, T, bool) -> T
-@const("select_bool") fn select<T: abstract_or_scalar, N: num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T>
-@const("select_boolvec") fn select<N: num, T: abstract_or_scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T>
+@const("select_bool") fn select<T: scalar>(T, T, bool) -> T
+@const("select_bool") fn select<T: scalar, N: num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T>
+@const("select_boolvec") fn select<N: num, T: scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T>
 fn sign<T: f32_f16>(T) -> T
 fn sign<N: num, T: f32_f16>(vec<N, T>) -> vec<N, T>
 fn sin<T: f32_f16>(T) -> T
@@ -706,9 +720,9 @@
 @const("Zero") ctor f32() -> f32
 @const("Zero") ctor f16() -> f16
 @const("Zero") ctor bool() -> bool
-@const("Zero") ctor vec2<T: scalar>() -> vec2<T>
-@const("Zero") ctor vec3<T: scalar>() -> vec3<T>
-@const("Zero") ctor vec4<T: scalar>() -> vec4<T>
+@const("Zero") ctor vec2<T: concrete_scalar>() -> vec2<T>
+@const("Zero") ctor vec3<T: concrete_scalar>() -> vec3<T>
+@const("Zero") ctor vec4<T: concrete_scalar>() -> vec4<T>
 @const("Zero") ctor mat2x2<T: f32_f16>() -> mat2x2<T>
 @const("Zero") ctor mat2x3<T: f32_f16>() -> mat2x3<T>
 @const("Zero") ctor mat2x4<T: f32_f16>() -> mat2x4<T>
@@ -725,9 +739,9 @@
 @const("Identity") ctor f32(f32) -> f32
 @const("Identity") ctor f16(f16) -> f16
 @const("Identity") ctor bool(bool) -> bool
-@const("Identity") ctor vec2<T: scalar>(vec2<T>) -> vec2<T>
-@const("Identity") ctor vec3<T: scalar>(vec3<T>) -> vec3<T>
-@const("Identity") ctor vec4<T: scalar>(vec4<T>) -> vec4<T>
+@const("Identity") ctor vec2<T: concrete_scalar>(vec2<T>) -> vec2<T>
+@const("Identity") ctor vec3<T: concrete_scalar>(vec3<T>) -> vec3<T>
+@const("Identity") ctor vec4<T: concrete_scalar>(vec4<T>) -> vec4<T>
 @const("Identity") ctor mat2x2<T: f32_f16>(mat2x2<T>) -> mat2x2<T>
 @const("Identity") ctor mat2x3<T: f32_f16>(mat2x3<T>) -> mat2x3<T>
 @const("Identity") ctor mat2x4<T: f32_f16>(mat2x4<T>) -> mat2x4<T>
@@ -739,24 +753,24 @@
 @const("Identity") ctor mat4x4<T: f32_f16>(mat4x4<T>) -> mat4x4<T>
 
 // Vector constructors (splat)
-@const("VecSplat") ctor vec2<T: abstract_or_scalar>(T) -> vec2<T>
-@const("VecSplat") ctor vec3<T: abstract_or_scalar>(T) -> vec3<T>
-@const("VecSplat") ctor vec4<T: abstract_or_scalar>(T) -> vec4<T>
+@const("VecSplat") ctor vec2<T: scalar>(T) -> vec2<T>
+@const("VecSplat") ctor vec3<T: scalar>(T) -> vec3<T>
+@const("VecSplat") ctor vec4<T: scalar>(T) -> vec4<T>
 
 // Vector constructors (scalar)
-@const("VecCtorS") ctor vec2<T: abstract_or_scalar>(x: T, y: T) -> vec2<T>
-@const("VecCtorS") ctor vec3<T: abstract_or_scalar>(x: T, y: T, z: T) -> vec3<T>
-@const("VecCtorS") ctor vec4<T: abstract_or_scalar>(x: T, y: T, z: T, w: T) -> vec4<T>
+@const("VecCtorS") ctor vec2<T: scalar>(x: T, y: T) -> vec2<T>
+@const("VecCtorS") ctor vec3<T: scalar>(x: T, y: T, z: T) -> vec3<T>
+@const("VecCtorS") ctor vec4<T: scalar>(x: T, y: T, z: T, w: T) -> vec4<T>
 
 // Vector constructors (mixed)
-@const("VecCtorM") ctor vec3<T: abstract_or_scalar>(xy: vec2<T>, z: T) -> vec3<T>
-@const("VecCtorM") ctor vec3<T: abstract_or_scalar>(x: T, yz: vec2<T>) -> vec3<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(xy: vec2<T>, z: T, w: T) -> vec4<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(x: T, yz: vec2<T>, w: T) -> vec4<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(x: T, y: T, zw: vec2<T>) -> vec4<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(xy: vec2<T>, zw: vec2<T>) -> vec4<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(xyz: vec3<T>, w: T) -> vec4<T>
-@const("VecCtorM") ctor vec4<T: abstract_or_scalar>(x: T, zyw: vec3<T>) -> vec4<T>
+@const("VecCtorM") ctor vec3<T: scalar>(xy: vec2<T>, z: T) -> vec3<T>
+@const("VecCtorM") ctor vec3<T: scalar>(x: T, yz: vec2<T>) -> vec3<T>
+@const("VecCtorM") ctor vec4<T: scalar>(xy: vec2<T>, z: T, w: T) -> vec4<T>
+@const("VecCtorM") ctor vec4<T: scalar>(x: T, yz: vec2<T>, w: T) -> vec4<T>
+@const("VecCtorM") ctor vec4<T: scalar>(x: T, y: T, zw: vec2<T>) -> vec4<T>
+@const("VecCtorM") ctor vec4<T: scalar>(xy: vec2<T>, zw: vec2<T>) -> vec4<T>
+@const("VecCtorM") ctor vec4<T: scalar>(xyz: vec3<T>, w: T) -> vec4<T>
+@const("VecCtorM") ctor vec4<T: scalar>(x: T, zyw: vec3<T>) -> vec4<T>
 
 // Matrix constructors (scalar)
 @const("MatCtorS")
@@ -938,11 +952,11 @@
 op && (bool, bool) -> bool
 op || (bool, bool) -> bool
 
-@const op == <T: abstract_or_scalar>(T, T) -> bool
-@const op == <T: abstract_or_scalar, N: num> (vec<N, T>, vec<N, T>) -> vec<N, bool>
+@const op == <T: scalar>(T, T) -> bool
+@const op == <T: scalar, N: num> (vec<N, T>, vec<N, T>) -> vec<N, bool>
 
-@const op != <T: abstract_or_scalar>(T, T) -> bool
-@const op != <T: abstract_or_scalar, N: num> (vec<N, T>, vec<N, T>) -> vec<N, bool>
+@const op != <T: scalar>(T, T) -> bool
+@const op != <T: scalar, N: num> (vec<N, T>, vec<N, T>) -> vec<N, bool>
 
 @const op < <T: fia_fiu32_f16>(T, T) -> bool
 @const op < <T: fia_fiu32_f16, N: num> (vec<N, T>, vec<N, T>) -> vec<N, bool>
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 116660f..2c99b3b 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -173,7 +173,7 @@
 
         const ast::Type* type = nullptr;
         ast::AddressSpace address_space = ast::AddressSpace::kNone;
-        ast::Access access = ast::Access::kUndefined;
+        ast::Access access = ast::Access::kInvalid;
         const ast::Expression* constructor = nullptr;
         utils::Vector<const ast::Attribute*, 4> attributes;
 
@@ -478,11 +478,6 @@
     /// returned`Type` will also be destructed.
     /// Types are unique (de-aliased), and so calling create() for the same `T`
     /// and arguments will return the same pointer.
-    /// @warning Use this method to acquire a type only if all of its type
-    /// information is provided in the constructor arguments `args`.<br>
-    /// If the type requires additional configuration after construction that
-    /// affect its fundamental type, build the type with `std::make_unique`, make
-    /// any necessary alterations and then call unique_type() instead.
     /// @param args the arguments to pass to the type constructor
     /// @returns the de-aliased type pointer
     template <typename T, typename... ARGS>
@@ -907,7 +902,7 @@
         /// @return the pointer to `type` with the given ast::AddressSpace
         const ast::Pointer* pointer(const ast::Type* type,
                                     ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kUndefined) const {
+                                    ast::Access access = ast::Access::kInvalid) const {
             return builder->create<ast::Pointer>(type, address_space, access);
         }
 
@@ -919,7 +914,7 @@
         const ast::Pointer* pointer(const Source& source,
                                     const ast::Type* type,
                                     ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kUndefined) const {
+                                    ast::Access access = ast::Access::kInvalid) const {
             return builder->create<ast::Pointer>(source, type, address_space, access);
         }
 
@@ -928,7 +923,7 @@
         /// @return the pointer to type `T` with the given ast::AddressSpace.
         template <typename T>
         const ast::Pointer* pointer(ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kUndefined) const {
+                                    ast::Access access = ast::Access::kInvalid) const {
             return pointer(Of<T>(), address_space, access);
         }
 
@@ -939,7 +934,7 @@
         template <typename T>
         const ast::Pointer* pointer(const Source& source,
                                     ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kUndefined) const {
+                                    ast::Access access = ast::Access::kInvalid) const {
             return pointer(source, Of<T>(), address_space, access);
         }
 
@@ -2914,7 +2909,7 @@
     const ast::InterpolateAttribute* Interpolate(
         const Source& source,
         ast::InterpolationType type,
-        ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone) {
+        ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid) {
         return create<ast::InterpolateAttribute>(source, type, sampling);
     }
 
@@ -2924,7 +2919,7 @@
     /// @returns the interpolate attribute pointer
     const ast::InterpolateAttribute* Interpolate(
         ast::InterpolationType type,
-        ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone) {
+        ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid) {
         return create<ast::InterpolateAttribute>(source_, type, sampling);
     }
 
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index 4b7b3d3..7bbb5df 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -327,6 +327,8 @@
             return "exp2";
         case GLSLstd450FaceForward:
             return "faceForward";
+        case GLSLstd450FindILsb:
+            return "firstTrailingBit";
         case GLSLstd450Floor:
             return "floor";
         case GLSLstd450Fma:
@@ -427,7 +429,6 @@
         case GLSLstd450PackDouble2x32:
         case GLSLstd450UnpackDouble2x32:
 
-        case GLSLstd450FindILsb:
         case GLSLstd450FindSMsb:
         case GLSLstd450FindUMsb:
 
@@ -2557,6 +2558,8 @@
     }
     auto type_it = identifier_types_.find(id);
     if (type_it != identifier_types_.end()) {
+        // We have a local named definition: function parameter, let, or var
+        // declaration.
         auto name = namer_.Name(id);
         auto* type = type_it->second;
         return TypedExpression{
@@ -2585,10 +2588,13 @@
     switch (inst->opcode()) {
         case SpvOpVariable: {
             // This occurs for module-scope variables.
-            auto name = namer_.Name(inst->result_id());
-            return TypedExpression{
-                parser_impl_.ConvertType(inst->type_id(), PtrAs::Ref),
-                create<ast::IdentifierExpression>(Source{}, builder_.Symbols().Register(name))};
+            auto name = namer_.Name(id);
+            // Construct the reference type, mapping storage class correctly.
+            const auto* type =
+                RemapPointerProperties(parser_impl_.ConvertType(inst->type_id(), PtrAs::Ref), id);
+            // TODO(crbug.com/tint/1041): Fix access mode
+            return TypedExpression{type, create<ast::IdentifierExpression>(
+                                             Source{}, builder_.Symbols().Register(name))};
         }
         case SpvOpUndef:
             // Substitute a null value for undef.
@@ -3356,11 +3362,13 @@
     for (auto id : sorted_by_index(block_info.hoisted_ids)) {
         const auto* def_inst = def_use_mgr_->GetDef(id);
         TINT_ASSERT(Reader, def_inst);
-        auto* storage_type = RemapAddressSpace(parser_impl_.ConvertType(def_inst->type_id()), id);
+        // Compute the store type.  Pointers are not storable, so there is
+        // no need to remap pointer properties.
+        auto* store_type = parser_impl_.ConvertType(def_inst->type_id());
         AddStatement(create<ast::VariableDeclStatement>(
-            Source{}, parser_impl_.MakeVar(id, ast::AddressSpace::kNone, storage_type, nullptr,
+            Source{}, parser_impl_.MakeVar(id, ast::AddressSpace::kNone, store_type, nullptr,
                                            AttributeList{})));
-        auto* type = ty_.Reference(storage_type, ast::AddressSpace::kNone);
+        auto* type = ty_.Reference(store_type, ast::AddressSpace::kNone);
         identifier_types_.emplace(id, type);
     }
 
@@ -3449,6 +3457,7 @@
     }
 
     expr = AddressOfIfNeeded(expr, &inst);
+    expr.type = RemapPointerProperties(expr.type, inst.result_id());
     auto* let = parser_impl_.MakeLet(inst.result_id(), expr.type, expr.expr);
     if (!let) {
         return false;
@@ -3720,7 +3729,6 @@
             if (!expr) {
                 return false;
             }
-            expr.type = RemapAddressSpace(expr.type, result_id);
             return EmitConstDefOrWriteToHoistedVar(inst, expr);
         }
 
@@ -3777,20 +3785,6 @@
     return parser_impl_.RectifyOperandSignedness(inst, std::move(expr));
 }
 
-TypedExpression FunctionEmitter::InferFunctionAddressSpace(TypedExpression expr) {
-    TypedExpression result(expr);
-    if (const auto* ref = expr.type->UnwrapAlias()->As<Reference>()) {
-        if (ref->address_space == ast::AddressSpace::kNone) {
-            expr.type = ty_.Reference(ref->type, ast::AddressSpace::kFunction);
-        }
-    } else if (const auto* ptr = expr.type->UnwrapAlias()->As<Pointer>()) {
-        if (ptr->address_space == ast::AddressSpace::kNone) {
-            expr.type = ty_.Pointer(ptr->type, ast::AddressSpace::kFunction);
-        }
-    }
-    return expr;
-}
-
 TypedExpression FunctionEmitter::MaybeEmitCombinatorialValue(
     const spvtools::opt::Instruction& inst) {
     if (inst.result_id() == 0) {
@@ -4350,6 +4344,10 @@
     const auto num_in_operands = inst.NumInOperands();
 
     bool sink_pointer = false;
+    // The current WGSL expression for the pointer, starting with the base
+    // pointer and updated as each index is incorported.  The important part
+    // is the pointee (or "store type").  The address space and access mode will
+    // be patched as needed at the very end, via RemapPointerProperties.
     TypedExpression current_expr;
 
     // If the variable was originally gl_PerVertex, then in the AST we
@@ -4418,7 +4416,7 @@
     // ever-deeper nested indexing expressions. Start off with an expression
     // for the base, and then bury that inside nested indexing expressions.
     if (!current_expr) {
-        current_expr = InferFunctionAddressSpace(MakeOperand(inst, 0));
+        current_expr = MakeOperand(inst, 0);
         if (current_expr.type->Is<Pointer>()) {
             current_expr = Dereference(current_expr);
         }
@@ -4533,6 +4531,7 @@
         GetDefInfo(inst.result_id())->sink_pointer_source_expr = current_expr;
     }
 
+    current_expr.type = RemapPointerProperties(current_expr.type, inst.result_id());
     return current_expr;
 }
 
@@ -4799,32 +4798,27 @@
             ++index;
             auto& info = def_info_[result_id];
 
-            // Determine address space for pointer values. Do this in order because
-            // we might rely on the address space for a previously-visited definition.
-            // Logical pointers can't be transmitted through OpPhi, so remaining
-            // pointer definitions are SSA values, and their definitions must be
-            // visited before their uses.
             const auto* type = type_mgr_->GetType(inst.type_id());
             if (type) {
+                // Determine address space and access mode for pointer values. Do this in
+                // order because we might rely on the storage class for a previously-visited
+                // definition.
+                // Logical pointers can't be transmitted through OpPhi, so remaining
+                // pointer definitions are SSA values, and their definitions must be
+                // visited before their uses.
                 if (type->AsPointer()) {
-                    if (auto* ast_type = parser_impl_.ConvertType(inst.type_id())) {
-                        if (auto* ptr = ast_type->As<Pointer>()) {
-                            info->pointer.address_space = ptr->address_space;
-                        }
-                    }
                     switch (inst.opcode()) {
                         case SpvOpUndef:
                             return Fail() << "undef pointer is not valid: " << inst.PrettyPrint();
                         case SpvOpVariable:
-                            // Keep the default decision based on the result type.
+                            info->pointer = GetPointerInfo(result_id);
                             break;
                         case SpvOpAccessChain:
                         case SpvOpInBoundsAccessChain:
                         case SpvOpCopyObject:
                             // Inherit from the first operand. We need this so we can pick up
                             // a remapped storage buffer.
-                            info->pointer.address_space =
-                                GetAddressSpaceForPointerValue(inst.GetSingleWordInOperand(0));
+                            info->pointer = GetPointerInfo(inst.GetSingleWordInOperand(0));
                             break;
                         default:
                             return Fail() << "pointer defined in function from unknown opcode: "
@@ -4846,32 +4840,71 @@
     return true;
 }
 
-ast::AddressSpace FunctionEmitter::GetAddressSpaceForPointerValue(uint32_t id) {
+DefInfo::Pointer FunctionEmitter::GetPointerInfo(uint32_t id) {
+    // Compute the result from first principles, for a variable.
+    auto get_from_root_identifier =
+        [&](const spvtools::opt::Instruction& inst) -> DefInfo::Pointer {
+        // WGSL root identifiers (or SPIR-V "memory object declarations") are
+        // either variables or function parameters.
+        switch (inst.opcode()) {
+            case SpvOpVariable: {
+                if (const auto* module_var = parser_impl_.GetModuleVariable(id)) {
+                    return DefInfo::Pointer{module_var->declared_address_space,
+                                            module_var->declared_access};
+                }
+                // Local variables are always Function storage class, with default
+                // access mode.
+                return DefInfo::Pointer{ast::AddressSpace::kFunction, ast::Access::kInvalid};
+            }
+            case SpvOpFunctionParameter: {
+                const auto* type = As<Pointer>(parser_impl_.ConvertType(inst.type_id()));
+                // TODO(crbug.com/tint/1041): Add access mode.
+                // Using kUndefined is ok for now, since the only non-default access mode
+                // on a pointer would be for a storage buffer, and baseline SPIR-V doesn't
+                // allow passing pointers to buffers as function parameters.
+                return DefInfo::Pointer{type->address_space, ast::Access::kInvalid};
+            }
+            default:
+                break;
+        }
+        TINT_ASSERT(Reader, false && "expected a memory object declaration");
+        return {};
+    };
+
     auto where = def_info_.find(id);
     if (where != def_info_.end()) {
-        auto candidate = where->second.get()->pointer.address_space;
-        if (candidate != ast::AddressSpace::kInvalid) {
-            return candidate;
+        const auto& info = where->second;
+        if (info->inst.opcode() == SpvOpVariable) {
+            // Ignore the cache in this case and compute it from scratch.
+            // That's because for a function-scope OpVariable is a
+            // locally-defined value.  So its cache entry has been created
+            // with a default PointerInfo object, which has invalid data.
+            //
+            // Instead, you might think that we could forget this weirdness
+            // and instead have more standard cache-like behaviour. But then
+            // for non-function-scope variables we look up information
+            // from a saved ast::Var. But some builtins don't correspond
+            // to a declared ast::Var. This is simpler and more reliable.
+            return get_from_root_identifier(info->inst);
         }
+        // Use the cached value.
+        return info->pointer;
     }
-    const auto type_id = def_use_mgr_->GetDef(id)->type_id();
-    if (type_id) {
-        auto* ast_type = parser_impl_.ConvertType(type_id);
-        if (auto* ptr = As<Pointer>(ast_type)) {
-            return ptr->address_space;
-        }
-    }
-    return ast::AddressSpace::kInvalid;
+    const auto* inst = def_use_mgr_->GetDef(id);
+    TINT_ASSERT(Reader, inst);
+    return get_from_root_identifier(*inst);
 }
 
-const Type* FunctionEmitter::RemapAddressSpace(const Type* type, uint32_t result_id) {
+const Type* FunctionEmitter::RemapPointerProperties(const Type* type, uint32_t result_id) {
     if (auto* ast_ptr_type = As<Pointer>(type)) {
-        // Remap an old-style storage buffer pointer to a new-style storage
-        // buffer pointer.
-        const auto addr_space = GetAddressSpaceForPointerValue(result_id);
-        if (ast_ptr_type->address_space != addr_space) {
-            return ty_.Pointer(ast_ptr_type->type, addr_space);
-        }
+        const auto pi = GetPointerInfo(result_id);
+        // TODO(crbug.com/tin/t1041): also do access mode
+        return ty_.Pointer(ast_ptr_type->type, pi.address_space);
+    }
+    if (auto* ast_ptr_type = As<Reference>(type)) {
+        const auto pi = GetPointerInfo(result_id);
+        // TODO(crbug.com/tin/t1041): also do access mode
+        return ty_.Reference(ast_ptr_type->type, pi.address_space);
     }
     return type;
 }
diff --git a/src/tint/reader/spirv/function.h b/src/tint/reader/spirv/function.h
index dc6002e..b8240cf 100644
--- a/src/tint/reader/spirv/function.h
+++ b/src/tint/reader/spirv/function.h
@@ -334,7 +334,8 @@
         /// This is kInvalid for non-pointers.
         ast::AddressSpace address_space = ast::AddressSpace::kInvalid;
 
-        // TODO(crbug.com/tint/1041) track access mode.
+        /// The declared access mode.
+        ast::Access access = ast::Access::kInvalid;
     };
 
     /// The expression to use when sinking pointers into their use.
@@ -619,19 +620,23 @@
     /// @returns false on failure
     bool RegisterLocallyDefinedValues();
 
-    /// Returns the Tint address space for the given SPIR-V ID that is a
-    /// pointer value.
+    /// Returns the pointer information needed for the given SPIR-V ID.
+    /// Assumes the given ID yields a value of pointer type.  For IDs
+    /// corresponding to WGSL root identifiers (i.e. OpVariable or
+    /// OpFunctionParameter), the info is computed from scratch.
+    /// Otherwise, this looks up pointer info from a base pointer whose
+    /// data is cached in def_info_.
     /// @param id a SPIR-V ID for a pointer value
-    /// @returns the address space
-    ast::AddressSpace GetAddressSpaceForPointerValue(uint32_t id);
+    /// @returns the associated Pointer info
+    DefInfo::Pointer GetPointerInfo(uint32_t id);
 
-    /// Remaps the address space for the type of a locally-defined value,
-    /// if necessary. If it's not a pointer type, or if its address space
-    /// already matches, then the result is a copy of the `type` argument.
+    /// Remaps the address space and access mode for the type of a
+    /// locally-defined value, if necessary. If it's not a pointer or reference
+    /// type, then the result is a copy of the `type` argument.
     /// @param type the AST type
     /// @param result_id the SPIR-V ID for the locally defined value
     /// @returns an possibly updated type
-    const Type* RemapAddressSpace(const Type* type, uint32_t result_id);
+    const Type* RemapPointerProperties(const Type* type, uint32_t result_id);
 
     /// Marks locally defined values when they should get a 'let'
     /// definition in WGSL, or a 'var' definition at an outer scope.
@@ -1011,13 +1016,6 @@
     /// @returns a new expression node
     TypedExpression MakeOperand(const spvtools::opt::Instruction& inst, uint32_t operand_index);
 
-    /// Copies a typed expression to the result, but when the type is a pointer
-    /// or reference type, ensures the address space is not defaulted.  That is,
-    /// it changes a address space of "none" to "function".
-    /// @param expr a typed expression
-    /// @results a copy of the expression, with possibly updated type
-    TypedExpression InferFunctionAddressSpace(TypedExpression expr);
-
     /// Returns an expression for a SPIR-V OpFMod instruction.
     /// @param inst the SPIR-V instruction
     /// @returns an expression
diff --git a/src/tint/reader/spirv/function_glsl_std_450_test.cc b/src/tint/reader/spirv/function_glsl_std_450_test.cc
index c5131e9..7a4fd94 100644
--- a/src/tint/reader/spirv/function_glsl_std_450_test.cc
+++ b/src/tint/reader/spirv/function_glsl_std_450_test.cc
@@ -179,6 +179,8 @@
     SpvParserTestBase<::testing::TestWithParam<GlslStd450Case>>;
 using SpvParserTest_GlslStd450_Inting_IntingIntingInting =
     SpvParserTestBase<::testing::TestWithParam<GlslStd450Case>>;
+using SpvParserTest_GlslStd450_Uinting_Uinting =
+    SpvParserTestBase<::testing::TestWithParam<GlslStd450Case>>;
 using SpvParserTest_GlslStd450_Uinting_UintingUinting =
     SpvParserTestBase<::testing::TestWithParam<GlslStd450Case>>;
 using SpvParserTest_GlslStd450_Uinting_UintingUintingUinting =
@@ -580,7 +582,8 @@
 
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Inting_Inting,
-                         ::testing::Values(GlslStd450Case{"SAbs", "abs"}));
+                         ::testing::Values(GlslStd450Case{"SAbs", "abs"},
+                                           GlslStd450Case{"FindILsb", "firstTrailingBit"}));
 
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Inting_IntingInting,
@@ -591,6 +594,41 @@
                          SpvParserTest_GlslStd450_Inting_IntingIntingInting,
                          ::testing::Values(GlslStd450Case{"SClamp", "clamp"}));
 
+TEST_P(SpvParserTest_GlslStd450_Uinting_Uinting, Scalar) {
+    const auto assembly = Preamble() + R"(
+     %1 = OpExtInst %uint %glsl )" +
+                          GetParam().opcode +
+                          R"( %u1
+     OpReturn
+     OpFunctionEnd
+  )";
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << assembly;
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    const auto body = test::ToString(p->program(), ast_body);
+    EXPECT_THAT(body, HasSubstr("let x_1 : u32 = " + GetParam().wgsl_func + "(u1);")) << body;
+}
+
+TEST_P(SpvParserTest_GlslStd450_Uinting_Uinting, Vector) {
+    const auto assembly = Preamble() + R"(
+     %1 = OpExtInst %v2uint %glsl )" +
+                          GetParam().opcode +
+                          R"( %v2u1
+     OpReturn
+     OpFunctionEnd
+  )";
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    const auto body = test::ToString(p->program(), ast_body);
+    EXPECT_THAT(body, HasSubstr("let x_1 : vec2<u32> = " + GetParam().wgsl_func + "(v2u1);"))
+        << body;
+}
+
 TEST_P(SpvParserTest_GlslStd450_Uinting_UintingUinting, Scalar) {
     const auto assembly = Preamble() + R"(
      %1 = OpExtInst %uint %glsl )" +
@@ -662,6 +700,10 @@
 }
 
 INSTANTIATE_TEST_SUITE_P(Samples,
+                         SpvParserTest_GlslStd450_Uinting_Uinting,
+                         ::testing::Values(GlslStd450Case{"FindILsb", "firstTrailingBit"}));
+
+INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Uinting_UintingUinting,
                          ::testing::Values(GlslStd450Case{"UMax", "max"},
                                            GlslStd450Case{"UMin", "min"}));
@@ -907,6 +949,34 @@
         << body;
 }
 
+TEST_F(SpvParserTest, RectifyOperandsAndResult_FindILsb) {
+    // Check conversion of:
+    //   signed results to unsigned result to match first arg.
+    //   unsigned results to signed result to match first arg.
+    // This is the first extended instruction we've supported which goes both
+    // ways.
+    const auto assembly = Preamble() + R"(
+     %1 = OpExtInst %uint %glsl FindILsb %i1
+     %2 = OpExtInst %v2uint %glsl FindILsb %v2i1
+     %3 = OpExtInst %int %glsl FindILsb %u1
+     %4 = OpExtInst %v2int %glsl FindILsb %v2u1
+     OpReturn
+     OpFunctionEnd
+  )";
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    const auto body = test::ToString(p->program(), ast_body);
+    EXPECT_THAT(body, HasSubstr(R"(
+let x_1 : u32 = bitcast<u32>(firstTrailingBit(i1));
+let x_2 : vec2<u32> = bitcast<vec2<u32>>(firstTrailingBit(v2i1));
+let x_3 : i32 = bitcast<i32>(firstTrailingBit(u1));
+let x_4 : vec2<i32> = bitcast<vec2<i32>>(firstTrailingBit(v2u1));)"))
+        << body;
+}
+
 struct DataPackingCase {
     std::string opcode;
     std::string wgsl_func;
diff --git a/src/tint/reader/spirv/function_memory_test.cc b/src/tint/reader/spirv/function_memory_test.cc
index dbf9219..a64aac5 100644
--- a/src/tint/reader/spirv/function_memory_test.cc
+++ b/src/tint/reader/spirv/function_memory_test.cc
@@ -938,6 +938,43 @@
 )"));
 }
 
+TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice) {
+    // Use the pointer value twice, which provokes the spirv-reader
+    // to make a let declaration for the pointer.  The storage class
+    // must be 'storage', not 'uniform'.
+    const auto assembly = OldStorageBufferPreamble() + R"(
+  %100 = OpFunction %void None %voidfn
+  %entry = OpLabel
+
+  ; the scalar element
+  %1 = OpAccessChain %ptr_uint %myvar %uint_0
+  OpStore %1 %uint_0
+  OpStore %1 %uint_0
+
+  ; element in the runtime array
+  %2 = OpAccessChain %ptr_uint %myvar %uint_1 %uint_1
+  ; Use the pointer twice
+  %3 = OpLoad %uint %2
+  OpStore %2 %uint_0
+
+  OpReturn
+  OpFunctionEnd
+)";
+    auto p = parser(test::Assemble(assembly));
+    ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
+    auto fe = p->function_emitter(100);
+    EXPECT_TRUE(fe.EmitBody()) << p->error();
+    auto ast_body = fe.ast_body();
+    const auto got = test::ToString(p->program(), ast_body);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32> = &(myvar.field0);
+*(x_1) = 0u;
+*(x_1) = 0u;
+let x_2 : ptr<storage, u32> = &(myvar.field1[1u]);
+let x_3 : u32 = *(x_2);
+*(x_2) = 0u;
+)"));
+}
+
 TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_InBoundsAccessChain) {
     // Like the previous test, but using OpInBoundsAccessChain.
     const auto assembly = OldStorageBufferPreamble() + R"(
@@ -1020,56 +1057,6 @@
     p->SkipDumpingPending("crbug.com/tint/1041 track access mode in spirv-reader parser type");
 }
 
-TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughCopyObject_WithHoisting) {
-    // TODO(dneto): Hoisting non-storable values (pointers) is not yet supported.
-    // It's debatable whether this test should run at all.
-    // crbug.com/tint/98
-
-    // Like the previous test, but the declaration for the copy-object
-    // has its declaration hoisted.
-    const auto assembly = OldStorageBufferPreamble() + R"(
-  %bool = OpTypeBool
-  %cond = OpConstantTrue %bool
-
-  %100 = OpFunction %void None %voidfn
-
-  %entry = OpLabel
-  OpSelectionMerge %99 None
-  OpBranchConditional %cond %20 %30
-
-  %20 = OpLabel
-  %1 = OpAccessChain %ptr_uint %myvar %uint_1 %uint_1
-  ; this definintion dominates the use in %99
-  %2 = OpCopyObject %ptr_uint %1
-  OpBranch %99
-
-  %30 = OpLabel
-  OpReturn
-
-  %99 = OpLabel
-  OpStore %2 %uint_0
-  OpReturn
-
-  OpFunctionEnd
-)";
-    auto p = parser(test::Assemble(assembly));
-    ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
-    auto fe = p->function_emitter(100);
-    EXPECT_TRUE(fe.EmitBody()) << p->error();
-    auto ast_body = fe.ast_body();
-    EXPECT_EQ(test::ToString(p->program(), ast_body),
-              R"(var x_2 : ptr<storage, u32>;
-if (true) {
-  x_2 = &(myvar.field1[1u]);
-} else {
-  return;
-}
-x_2 = 0u;
-return;
-)") << p->error();
-    p->SkipDumpingPending("crbug.com/tint/98");
-}
-
 std::string RuntimeArrayPreamble() {
     return R"(
      OpCapability Shader
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 5aa68eb..63e5cb4 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -221,6 +221,7 @@
         case GLSLstd450UMin:
         case GLSLstd450UMax:
         case GLSLstd450UClamp:
+        case GLSLstd450FindILsb:
             // TODO(dneto): FindSMsb?
             // TODO(dneto): FindUMsb?
             return true;
@@ -1566,7 +1567,7 @@
         return nullptr;
     }
 
-    ast::Access access = ast::Access::kUndefined;
+    ast::Access access = ast::Access::kInvalid;
     if (address_space == ast::AddressSpace::kStorage) {
         bool read_only = false;
         if (auto* tn = storage_type->As<Named>()) {
@@ -1749,7 +1750,7 @@
                                             AttributeList* attributes) {
     // Vulkan defaults to perspective-correct interpolation.
     ast::InterpolationType type = ast::InterpolationType::kPerspective;
-    ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone;
+    ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid;
 
     for (const auto& deco : decorations) {
         TINT_ASSERT(Reader, deco.size() > 0);
@@ -1804,7 +1805,7 @@
 
     // Apply interpolation.
     if (type == ast::InterpolationType::kPerspective &&
-        sampling == ast::InterpolationSampling::kNone) {
+        sampling == ast::InterpolationSampling::kInvalid) {
         // This is the default. Don't add a decoration.
     } else {
         attributes->Push(create<ast::InterpolateAttribute>(type, sampling));
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 50dd191..b042ac2 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -43,6 +43,7 @@
 #include "src/tint/sem/external_texture.h"
 #include "src/tint/sem/multisampled_texture.h"
 #include "src/tint/sem/sampled_texture.h"
+#include "src/tint/utils/string.h"
 
 namespace tint::reader::wgsl {
 namespace {
@@ -370,40 +371,36 @@
         }
 
         // Match the extension name.
-        Expect<std::string> name = {""};
         auto& t = peek();
-        if (t.IsIdentifier()) {
-            synchronized_ = true;
-            next();
-            name = {t.to_str(), t.source()};
-        } else if (t.Is(Token::Type::kF16)) {
-            // `f16` is a valid extension name and also a keyword
-            synchronized_ = true;
-            next();
-            name = {"f16", t.source()};
-        } else if (t.Is(Token::Type::kParenLeft)) {
+        if (handle_error(t)) {
+            // The token might itself be an error.
+            return Failure::kErrored;
+        }
+
+        if (t.Is(Token::Type::kParenLeft)) {
             // A common error case is writing `enable(foo);` instead of `enable foo;`.
             synchronized_ = false;
             return add_error(t.source(), "enable directives don't take parenthesis");
-        } else if (handle_error(t)) {
-            // The token might itself be an error.
-            return Failure::kErrored;
+        }
+
+        auto extension = ast::Extension::kInvalid;
+        if (t.Is(Token::Type::kF16)) {
+            // `f16` is a valid extension name and also a keyword
+            synchronized_ = true;
+            next();
+            extension = ast::Extension::kF16;
         } else {
-            // Failed to match an extension name.
-            synchronized_ = false;
-            return add_error(t.source(), "invalid extension name");
+            auto ext = expect_enum("extension", ast::ParseExtension, ast::kExtensionStrings);
+            if (ext.errored) {
+                return Failure::kErrored;
+            }
+            extension = ext.value;
         }
 
         if (!expect("enable directive", Token::Type::kSemicolon)) {
             return Failure::kErrored;
         }
-
-        auto extension = ast::ParseExtension(name.value);
-        if (extension == ast::Extension::kInvalid) {
-            return add_error(name.source, "unsupported extension: '" + name.value + "'");
-        }
-        builder_.AST().AddEnable(create<ast::Enable>(name.source, extension));
-
+        builder_.AST().AddEnable(create<ast::Enable>(t.source(), extension));
         return kSuccess;
     });
 
@@ -763,7 +760,7 @@
                 return Failure::kErrored;
             }
 
-            auto access = expect_access_mode("access control");
+            auto access = expect_access_mode(use);
             if (access.errored) {
                 return Failure::kErrored;
             }
@@ -920,12 +917,7 @@
 //  | 'rgba32sint'
 //  | 'rgba32float'
 Expect<ast::TexelFormat> ParserImpl::expect_texel_format(std::string_view use) {
-    auto& t = next();
-    auto fmt = ast::ParseTexelFormat(t.to_str());
-    if (fmt == ast::TexelFormat::kInvalid) {
-        return add_error(t.source(), "invalid format", use);
-    }
-    return fmt;
+    return expect_enum("texel format", ast::ParseTexelFormat, ast::kTexelFormatStrings, use);
 }
 
 Expect<ParserImpl::TypedIdentifier> ParserImpl::expect_ident_with_optional_type_specifier(
@@ -975,22 +967,7 @@
 //   | 'write'
 //   | 'read_write'
 Expect<ast::Access> ParserImpl::expect_access_mode(std::string_view use) {
-    auto ident = expect_ident(use);
-    if (ident.errored) {
-        return Failure::kErrored;
-    }
-
-    if (ident.value == "read") {
-        return {ast::Access::kRead, ident.source};
-    }
-    if (ident.value == "write") {
-        return {ast::Access::kWrite, ident.source};
-    }
-    if (ident.value == "read_write") {
-        return {ast::Access::kReadWrite, ident.source};
-    }
-
-    return add_error(ident.source, "invalid value for access control");
+    return expect_enum("access control", ast::ParseAccess, ast::kAccessStrings, use);
 }
 
 // variable_qualifier
@@ -1014,7 +991,7 @@
             }
             return VariableQualifier{sc.value, ac.value};
         }
-        return Expect<VariableQualifier>{VariableQualifier{sc.value, ast::Access::kUndefined},
+        return Expect<VariableQualifier>{VariableQualifier{sc.value, ast::Access::kInvalid},
                                          source};
     });
 
@@ -1194,6 +1171,66 @@
     return type_specifier_without_ident();
 }
 
+template <typename ENUM, size_t N>
+Expect<ENUM> ParserImpl::expect_enum(std::string_view name,
+                                     ENUM (*parse)(std::string_view str),
+                                     const char* const (&strings)[N],
+                                     std::string_view use) {
+    auto& t = peek();
+    if (t.IsIdentifier()) {
+        auto val = parse(t.to_str());
+        if (val != ENUM::kInvalid) {
+            synchronized_ = true;
+            next();
+            return {val, t.source()};
+        }
+    }
+
+    // Was the token itself an error?
+    if (handle_error(t)) {
+        return Failure::kErrored;
+    }
+
+    /// Create a sensible error message
+    std::stringstream err;
+    err << "expected " << name;
+
+    if (!use.empty()) {
+        err << " for " << use;
+    }
+
+    // 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;
+    constexpr size_t kSuggestionMaxLength = 64;
+    if (auto got = t.to_str(); !got.empty() && got.size() < kSuggestionMaxLength) {
+        size_t candidate_dist = kSuggestionDistance;
+        const char* candidate = nullptr;
+        for (auto* str : strings) {
+            auto dist = utils::Distance(str, got);
+            if (dist < candidate_dist) {
+                candidate = str;
+                candidate_dist = dist;
+            }
+        }
+        if (candidate) {
+            err << ". Did you mean '" << candidate << "'?";
+        }
+    }
+
+    // List all the possible enumerator values
+    err << "\nPossible values: ";
+    for (auto* str : strings) {
+        if (str != strings[0]) {
+            err << ", ";
+        }
+        err << "'" << str << "'";
+    }
+
+    synchronized_ = false;
+    return add_error(t.source(), err.str());
+}
+
 Expect<const ast::Type*> ParserImpl::expect_type(std::string_view use) {
     auto type = type_specifier();
     if (type.errored) {
@@ -1210,7 +1247,7 @@
     const char* use = "ptr declaration";
 
     auto address_space = ast::AddressSpace::kNone;
-    auto access = ast::Access::kUndefined;
+    auto access = ast::Access::kInvalid;
 
     auto subtype = expect_lt_gt_block(use, [&]() -> Expect<const ast::Type*> {
         auto sc = expect_address_space(use);
@@ -1229,7 +1266,7 @@
         }
 
         if (match(Token::Type::kComma)) {
-            auto ac = expect_access_mode("access control");
+            auto ac = expect_access_mode(use);
             if (ac.errored) {
                 return Failure::kErrored;
             }
@@ -1331,18 +1368,7 @@
 //
 // Note, we also parse `push_constant` from the experimental extension
 Expect<ast::AddressSpace> ParserImpl::expect_address_space(std::string_view use) {
-    auto& t = peek();
-    auto ident = expect_ident("address space");
-    if (ident.errored) {
-        return Failure::kErrored;
-    }
-
-    auto address_space = ast::ParseAddressSpace(ident.value);
-    if (address_space == ast::AddressSpace::kInvalid) {
-        return add_error(t.source(), "invalid address space", use);
-    }
-
-    return {address_space, t.source()};
+    return expect_enum("address space", ast::ParseAddressSpace, ast::kAddressSpaceStrings, use);
 }
 
 // struct_decl
@@ -1605,25 +1631,12 @@
 }
 
 // interpolation_sample_name
-//   :  'center'
+//   : 'center'
 //   | 'centroid'
 //   | 'sample'
 Expect<ast::InterpolationSampling> ParserImpl::expect_interpolation_sample_name() {
-    auto ident = expect_ident("interpolation sample name");
-    if (ident.errored) {
-        return Failure::kErrored;
-    }
-
-    if (ident.value == "center") {
-        return {ast::InterpolationSampling::kCenter, ident.source};
-    }
-    if (ident.value == "centroid") {
-        return {ast::InterpolationSampling::kCentroid, ident.source};
-    }
-    if (ident.value == "sample") {
-        return {ast::InterpolationSampling::kSample, ident.source};
-    }
-    return add_error(ident.source, "invalid interpolation sampling");
+    return expect_enum("interpolation sampling", ast::ParseInterpolationSampling,
+                       ast::kInterpolationSamplingStrings);
 }
 
 // interpolation_type_name
@@ -1631,22 +1644,8 @@
 //   | 'linear'
 //   | 'flat'
 Expect<ast::InterpolationType> ParserImpl::expect_interpolation_type_name() {
-    auto ident = expect_ident("interpolation type name");
-    if (ident.errored) {
-        return Failure::kErrored;
-    }
-
-    if (ident.value == "perspective") {
-        return {ast::InterpolationType::kPerspective, ident.source};
-    }
-    if (ident.value == "linear") {
-        return {ast::InterpolationType::kLinear, ident.source};
-    }
-    if (ident.value == "flat") {
-        return {ast::InterpolationType::kFlat, ident.source};
-    }
-
-    return add_error(ident.source, "invalid interpolation type");
+    return expect_enum("interpolation type", ast::ParseInterpolationType,
+                       ast::kInterpolationTypeStrings);
 }
 
 // builtin_value_name
@@ -1663,17 +1662,7 @@
 //   | vertex_index
 //   | workgroup_id
 Expect<ast::BuiltinValue> ParserImpl::expect_builtin() {
-    auto ident = expect_ident("builtin");
-    if (ident.errored) {
-        return Failure::kErrored;
-    }
-
-    ast::BuiltinValue builtin = ast::ParseBuiltinValue(ident.value);
-    if (builtin == ast::BuiltinValue::kInvalid) {
-        return add_error(ident.source, "invalid value for builtin attribute");
-    }
-
-    return {builtin, ident.source};
+    return expect_enum("builtin", ast::ParseBuiltinValue, ast::kBuiltinValueStrings);
 }
 
 // compound_statement
@@ -3523,7 +3512,7 @@
                 return Failure::kErrored;
             }
 
-            ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone;
+            ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid;
             if (match(Token::Type::kComma)) {
                 if (!peek_is(Token::Type::kParenRight)) {
                     auto sample = expect_interpolation_sample_name();
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index 5ae4c97..77196a0 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -296,7 +296,7 @@
         /// Variable address space
         ast::AddressSpace address_space = ast::AddressSpace::kNone;
         /// Variable access control
-        ast::Access access = ast::Access::kUndefined;
+        ast::Access access = ast::Access::kInvalid;
         /// Variable type
         const ast::Type* type = nullptr;
     };
@@ -306,7 +306,7 @@
         /// The variable's address space
         ast::AddressSpace address_space = ast::AddressSpace::kNone;
         /// The variable's access control
-        ast::Access access = ast::Access::kUndefined;
+        ast::Access access = ast::Access::kInvalid;
     };
 
     /// MatrixDimensions contains the column and row information for a matrix
@@ -868,6 +868,18 @@
     Expect<const ast::Type*> expect_type_specifier_matrix(const Source& s,
                                                           const MatrixDimensions& dims);
 
+    /// Parses the given enum, providing sensible error messages if the next token does not match
+    /// any of the enum values.
+    /// @param name the name of the enumerator
+    /// @param parse the optimized function used to parse the enum
+    /// @param strings the list of possible strings in the enum
+    /// @param use an optional description of what was being parsed if an error was raised.
+    template <typename ENUM, size_t N>
+    Expect<ENUM> expect_enum(std::string_view name,
+                             ENUM (*parse)(std::string_view str),
+                             const char* const (&strings)[N],
+                             std::string_view use = "");
+
     Expect<const ast::Type*> expect_type(std::string_view use);
 
     Maybe<const ast::Statement*> non_block_statement();
diff --git a/src/tint/reader/wgsl/parser_impl_address_space_test.cc b/src/tint/reader/wgsl/parser_impl_address_space_test.cc
index d9d6d5e..251c1d0 100644
--- a/src/tint/reader/wgsl/parser_impl_address_space_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_address_space_test.cc
@@ -54,7 +54,8 @@
     auto sc = p->expect_address_space("test");
     EXPECT_EQ(sc.errored, true);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:1: invalid address space for test");
+    EXPECT_EQ(p->error(), R"(1:1: expected address space for test
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 }  // namespace
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 ed57be8..aad8eb6 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -61,7 +61,21 @@
     p->enable_directive();
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:8: unsupported extension: 'NotAValidExtensionName'");
+    EXPECT_EQ(p->error(), R"(1:8: expected extension
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_push_constant', 'f16')");
+    auto program = p->program();
+    auto& ast = program.AST();
+    EXPECT_EQ(ast.Enables().Length(), 0u);
+    EXPECT_EQ(ast.GlobalDeclarations().Length(), 0u);
+}
+
+TEST_F(EnableDirectiveTest, InvalidIdentifierSuggest) {
+    auto p = parser("enable f15;");
+    p->enable_directive();
+    // Error when unknown extension found
+    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_push_constant', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
     EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -108,7 +122,8 @@
         auto p = parser("enable <f16;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
-        EXPECT_EQ(p->error(), "1:8: invalid extension name");
+        EXPECT_EQ(p->error(), R"(1:8: expected extension
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_push_constant', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -118,7 +133,8 @@
         auto p = parser("enable =;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
-        EXPECT_EQ(p->error(), "1:8: invalid extension name");
+        EXPECT_EQ(p->error(), R"(1:8: expected extension
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_push_constant', 'f16')");
         auto program = p->program();
         auto& ast = program.AST();
         EXPECT_EQ(ast.Enables().Length(), 0u);
@@ -128,7 +144,8 @@
         auto p = parser("enable vec4;");
         p->translation_unit();
         EXPECT_TRUE(p->has_error());
-        EXPECT_EQ(p->error(), "1:8: invalid extension name");
+        EXPECT_EQ(p->error(), R"(1:8: expected extension
+Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_push_constant', '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 7ea5aaa..af9734c 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -765,7 +765,8 @@
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclStaticAssertMissingLParen) {
-    EXPECT("static_assert true);", R"(test.wgsl:1:19 error: expected ';' for static assertion declaration
+    EXPECT("static_assert true);",
+           R"(test.wgsl:1:19 error: expected ';' for static assertion declaration
 static_assert true);
                   ^
 )");
@@ -804,7 +805,8 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclStorageTextureMissingSubtype) {
     EXPECT("var x : texture_storage_2d<>;",
-           R"(test.wgsl:1:28 error: invalid format for storage texture type
+           R"(test.wgsl:1:28 error: expected texel format for storage texture type
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm'
 var x : texture_storage_2d<>;
                            ^
 )");
@@ -812,7 +814,8 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclStorageTextureMissingInvalidSubtype) {
     EXPECT("var x : texture_storage_2d<1>;",
-           R"(test.wgsl:1:28 error: invalid format for storage texture type
+           R"(test.wgsl:1:28 error: expected texel format for storage texture type
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm'
 var x : texture_storage_2d<1>;
                            ^
 )");
@@ -989,17 +992,19 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarAttrBuiltinInvalidIdentifer) {
     EXPECT("@builtin(1) var i : i32;",
-           R"(test.wgsl:1:10 error: expected identifier for builtin
+           R"(test.wgsl:1:10 error: expected builtin
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id'
 @builtin(1) var i : i32;
          ^
 )");
 }
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarAttrBuiltinInvalidValue) {
-    EXPECT("@builtin(x) var i : i32;",
-           R"(test.wgsl:1:10 error: invalid value for builtin attribute
-@builtin(x) var i : i32;
-         ^
+    EXPECT("@builtin(frag_d3pth) var i : i32;",
+           R"(test.wgsl:1:10 error: expected builtin. Did you mean 'frag_depth'?
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id'
+@builtin(frag_d3pth) var i : i32;
+         ^^^^^^^^^^
 )");
 }
 
@@ -1107,7 +1112,8 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarPtrMissingAddressSpace) {
     EXPECT("var i : ptr<meow, u32>;",
-           R"(test.wgsl:1:13 error: invalid address space for ptr declaration
+           R"(test.wgsl:1:13 error: expected address space for ptr declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup'
 var i : ptr<meow, u32>;
             ^^^^
 )");
@@ -1139,7 +1145,8 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarStorageDeclInvalidClass) {
     EXPECT("var<fish> i : i32",
-           R"(test.wgsl:1:5 error: invalid address space for variable declaration
+           R"(test.wgsl:1:5 error: expected address space for variable declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup'
 var<fish> i : i32
     ^^^^
 )");
diff --git a/src/tint/reader/wgsl/parser_impl_global_variable_decl_test.cc b/src/tint/reader/wgsl/parser_impl_global_variable_decl_test.cc
index d6e475a..388ad67 100644
--- a/src/tint/reader/wgsl/parser_impl_global_variable_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_global_variable_decl_test.cc
@@ -165,7 +165,8 @@
     EXPECT_TRUE(e.errored);
     EXPECT_FALSE(e.matched);
     EXPECT_EQ(e.value, nullptr);
-    EXPECT_EQ(p->error(), "1:5: invalid address space for variable declaration");
+    EXPECT_EQ(p->error(), R"(1:5: expected address space for variable declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_texel_format_test.cc b/src/tint/reader/wgsl/parser_impl_texel_format_test.cc
index e7afb9e..1697359 100644
--- a/src/tint/reader/wgsl/parser_impl_texel_format_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texel_format_test.cc
@@ -17,15 +17,16 @@
 namespace tint::reader::wgsl {
 namespace {
 
-TEST_F(ParserImplTest, ImageStorageType_Invalid) {
+TEST_F(ParserImplTest, TexelFormat_Invalid) {
     auto p = parser("1234");
     auto t = p->expect_texel_format("test");
     EXPECT_TRUE(t.errored);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:1: invalid format for test");
+    EXPECT_EQ(p->error(), R"(1:1: expected texel format for test
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm')");
 }
 
-TEST_F(ParserImplTest, ImageStorageType_R32Uint) {
+TEST_F(ParserImplTest, TexelFormat_R32Uint) {
     auto p = parser("r32uint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -33,7 +34,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_R32Sint) {
+TEST_F(ParserImplTest, TexelFormat_R32Sint) {
     auto p = parser("r32sint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -41,7 +42,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_R32Float) {
+TEST_F(ParserImplTest, TexelFormat_R32Float) {
     auto p = parser("r32float");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -49,7 +50,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba8Unorm) {
+TEST_F(ParserImplTest, TexelFormat_Rgba8Unorm) {
     auto p = parser("rgba8unorm");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -57,7 +58,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba8Snorm) {
+TEST_F(ParserImplTest, TexelFormat_Rgba8Snorm) {
     auto p = parser("rgba8snorm");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -65,7 +66,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba8Uint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba8Uint) {
     auto p = parser("rgba8uint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -73,7 +74,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba8Sint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba8Sint) {
     auto p = parser("rgba8sint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -81,7 +82,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rg32Uint) {
+TEST_F(ParserImplTest, TexelFormat_Rg32Uint) {
     auto p = parser("rg32uint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -89,7 +90,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rg32Sint) {
+TEST_F(ParserImplTest, TexelFormat_Rg32Sint) {
     auto p = parser("rg32sint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -97,7 +98,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rg32Float) {
+TEST_F(ParserImplTest, TexelFormat_Rg32Float) {
     auto p = parser("rg32float");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -105,7 +106,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba16Uint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba16Uint) {
     auto p = parser("rgba16uint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -113,7 +114,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba16Sint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba16Sint) {
     auto p = parser("rgba16sint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -121,7 +122,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba16Float) {
+TEST_F(ParserImplTest, TexelFormat_Rgba16Float) {
     auto p = parser("rgba16float");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -129,7 +130,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba32Uint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba32Uint) {
     auto p = parser("rgba32uint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -137,7 +138,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba32Sint) {
+TEST_F(ParserImplTest, TexelFormat_Rgba32Sint) {
     auto p = parser("rgba32sint");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
@@ -145,7 +146,7 @@
     EXPECT_FALSE(p->has_error());
 }
 
-TEST_F(ParserImplTest, ImageStorageType_Rgba32Float) {
+TEST_F(ParserImplTest, TexelFormat_Rgba32Float) {
     auto p = parser("rgba32float");
     auto t = p->expect_texel_format("test");
     EXPECT_FALSE(t.errored);
diff --git a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
index 1143c52..ce31276 100644
--- a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
@@ -218,7 +218,19 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:20: invalid format for storage texture type");
+    EXPECT_EQ(p->error(), R"(1:20: expected texel format for storage texture type
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm')");
+}
+
+TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_InvalidTypeSuggest) {
+    auto p = parser("texture_storage_1d<rg32_float, read>");
+    auto t = p->texture_and_sampler_types();
+    EXPECT_EQ(t.value, nullptr);
+    EXPECT_FALSE(t.matched);
+    EXPECT_TRUE(t.errored);
+    EXPECT_EQ(p->error(),
+              R"(1:20: expected texel format for storage texture type. Did you mean 'rg32float'?
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm')");
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_InvalidAccess) {
@@ -227,7 +239,8 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:30: invalid value for access control");
+    EXPECT_EQ(p->error(), R"(1:30: expected access control for storage texture type. Did you mean 'read'?
+Possible values: 'read', 'read_write', 'write')");
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_MissingType) {
@@ -236,7 +249,8 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), "1:20: invalid format for storage texture type");
+    EXPECT_EQ(p->error(), R"(1:20: expected texel format for storage texture type
+Possible values: 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm')");
 }
 
 TEST_F(ParserImplTest, TextureSamplerTypes_StorageTexture_MissingLessThan) {
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
index 0e018e0..ad8946f 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
@@ -272,7 +272,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: expected identifier for address space");
+    ASSERT_EQ(p->error(), R"(1:5: expected address space for ptr declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_MissingType) {
@@ -292,7 +293,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:20: expected identifier for access control");
+    ASSERT_EQ(p->error(), R"(1:20: expected access control for ptr declaration
+Possible values: 'read', 'read_write', 'write')");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_MissingParams) {
@@ -302,7 +304,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: expected identifier for address space");
+    ASSERT_EQ(p->error(), R"(1:5: expected address space for ptr declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_BadAddressSpace) {
@@ -312,7 +315,9 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: invalid address space for ptr declaration");
+    ASSERT_EQ(p->error(),
+              R"(1:5: expected address space for ptr declaration. Did you mean 'uniform'?
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Ptr_BadAccess) {
@@ -322,7 +327,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:20: invalid value for access control");
+    ASSERT_EQ(p->error(), R"(1:20: expected access control for ptr declaration
+Possible values: 'read', 'read_write', 'write')");
 }
 
 TEST_F(ParserImplTest, TypeDecl_Atomic) {
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
index 186b364..e37a347 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
@@ -263,7 +263,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: expected identifier for address space");
+    ASSERT_EQ(p->error(), R"(1:5: expected address space for ptr declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingType) {
@@ -283,7 +284,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:20: expected identifier for access control");
+    ASSERT_EQ(p->error(), R"(1:20: expected access control for ptr declaration
+Possible values: 'read', 'read_write', 'write')");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_MissingParams) {
@@ -293,7 +295,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: expected identifier for address space");
+    ASSERT_EQ(p->error(), R"(1:5: expected address space for ptr declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_BadAddressSpace) {
@@ -303,7 +306,9 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:5: invalid address space for ptr declaration");
+    ASSERT_EQ(p->error(),
+              R"(1:5: expected address space for ptr declaration. Did you mean 'uniform'?
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Ptr_BadAccess) {
@@ -313,7 +318,8 @@
     EXPECT_FALSE(t.matched);
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
-    ASSERT_EQ(p->error(), "1:20: invalid value for access control");
+    ASSERT_EQ(p->error(), R"(1:20: expected access control for ptr declaration
+Possible values: 'read', 'read_write', 'write')");
 }
 
 TEST_F(ParserImplTest, TypeDeclWithoutIdent_Atomic) {
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
index a814ea6..79574a8 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -58,8 +58,19 @@
     EXPECT_TRUE(attrs.errored);
     EXPECT_FALSE(attrs.matched);
     EXPECT_TRUE(attrs.value.IsEmpty());
-    EXPECT_EQ(p->error(), "1:10: invalid value for builtin attribute");
+    EXPECT_EQ(p->error(), R"(1:10: expected builtin
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 
+TEST_F(ParserImplTest, AttributeList_InvalidValueSuggest) {
+    auto p = parser("@builtin(instanceindex)");
+    auto attrs = p->attribute_list();
+    EXPECT_TRUE(p->has_error());
+    EXPECT_TRUE(attrs.errored);
+    EXPECT_FALSE(attrs.matched);
+    EXPECT_TRUE(attrs.value.IsEmpty());
+    EXPECT_EQ(p->error(), R"(1:10: expected builtin. Did you mean 'instance_index'?
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
+}
 }  // namespace
 }  // namespace tint::reader::wgsl
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
index 7c52838..70af3b2 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
@@ -177,7 +177,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:9: expected identifier for builtin");
+    EXPECT_EQ(p->error(), R"(1:9: expected builtin
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 
 TEST_F(ParserImplTest, Attribute_Builtin_InvalidValue) {
@@ -187,7 +188,19 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:9: invalid value for builtin attribute");
+    EXPECT_EQ(p->error(), R"(1:9: expected builtin
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
+}
+
+TEST_F(ParserImplTest, Attribute_Builtin_InvalidValueSuggest) {
+    auto p = parser("builtin(front_face)");
+    auto attr = p->attribute();
+    EXPECT_FALSE(attr.matched);
+    EXPECT_TRUE(attr.errored);
+    EXPECT_EQ(attr.value, nullptr);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:9: expected builtin. Did you mean 'front_facing'?
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 
 TEST_F(ParserImplTest, Attribute_Builtin_MissingInvalid) {
@@ -197,7 +210,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:9: expected identifier for builtin");
+    EXPECT_EQ(p->error(), R"(1:9: expected builtin
+Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Flat) {
@@ -213,7 +227,7 @@
 
     auto* interp = var_attr->As<ast::InterpolateAttribute>();
     EXPECT_EQ(interp->type, ast::InterpolationType::kFlat);
-    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kNone);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kInvalid);
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Single_TrailingComma) {
@@ -229,7 +243,7 @@
 
     auto* interp = var_attr->As<ast::InterpolateAttribute>();
     EXPECT_EQ(interp->type, ast::InterpolationType::kFlat);
-    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kNone);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kInvalid);
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Single_DoubleTrailingComma) {
@@ -239,7 +253,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:18: expected identifier for interpolation sample name");
+    EXPECT_EQ(p->error(), R"(1:18: expected interpolation sampling
+Possible values: 'center', 'centroid', 'sample')");
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Perspective_Center) {
@@ -333,7 +348,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:13: expected identifier for interpolation type name");
+    EXPECT_EQ(p->error(), R"(1:13: expected interpolation type
+Possible values: 'flat', 'linear', 'perspective')");
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_InvalidFirstValue) {
@@ -343,7 +359,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:13: invalid interpolation type");
+    EXPECT_EQ(p->error(), R"(1:13: expected interpolation type
+Possible values: 'flat', 'linear', 'perspective')");
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_InvalidSecondValue) {
@@ -353,7 +370,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:26: invalid interpolation sampling");
+    EXPECT_EQ(p->error(), R"(1:26: expected interpolation sampling. Did you mean 'sample'?
+Possible values: 'center', 'centroid', 'sample')");
 }
 
 TEST_F(ParserImplTest, Attribute_Binding) {
diff --git a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
index 8427c6e..5cf038b 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -105,7 +105,9 @@
     EXPECT_FALSE(v.matched);
     EXPECT_TRUE(v.errored);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), "1:5: invalid address space for variable declaration");
+    EXPECT_EQ(p->error(),
+              R"(1:5: expected address space for variable declaration. Did you mean 'uniform'?
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc b/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
index 9f8663d..32c1a54 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_qualifier_test.cc
@@ -47,11 +47,11 @@
     ParserImplTest,
     VariableQualifierTest,
     testing::Values(
-        VariableStorageData{"uniform", ast::AddressSpace::kUniform, ast::Access::kUndefined},
-        VariableStorageData{"workgroup", ast::AddressSpace::kWorkgroup, ast::Access::kUndefined},
-        VariableStorageData{"storage", ast::AddressSpace::kStorage, ast::Access::kUndefined},
-        VariableStorageData{"private", ast::AddressSpace::kPrivate, ast::Access::kUndefined},
-        VariableStorageData{"function", ast::AddressSpace::kFunction, ast::Access::kUndefined},
+        VariableStorageData{"uniform", ast::AddressSpace::kUniform, ast::Access::kInvalid},
+        VariableStorageData{"workgroup", ast::AddressSpace::kWorkgroup, ast::Access::kInvalid},
+        VariableStorageData{"storage", ast::AddressSpace::kStorage, ast::Access::kInvalid},
+        VariableStorageData{"private", ast::AddressSpace::kPrivate, ast::Access::kInvalid},
+        VariableStorageData{"function", ast::AddressSpace::kFunction, ast::Access::kInvalid},
         VariableStorageData{"storage, read", ast::AddressSpace::kStorage, ast::Access::kRead},
         VariableStorageData{"storage, write", ast::AddressSpace::kStorage, ast::Access::kWrite},
         VariableStorageData{"storage, read_write", ast::AddressSpace::kStorage,
@@ -63,7 +63,8 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_TRUE(sc.errored);
     EXPECT_FALSE(sc.matched);
-    EXPECT_EQ(p->error(), "1:2: invalid address space for variable declaration");
+    EXPECT_EQ(p->error(), R"(1:2: expected address space for variable declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, VariableQualifier_Empty) {
@@ -72,7 +73,8 @@
     EXPECT_TRUE(p->has_error());
     EXPECT_TRUE(sc.errored);
     EXPECT_FALSE(sc.matched);
-    EXPECT_EQ(p->error(), "1:2: expected identifier for address space");
+    EXPECT_EQ(p->error(), R"(1:2: expected address space for variable declaration
+Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
 TEST_F(ParserImplTest, VariableQualifier_MissingLessThan) {
diff --git a/src/tint/reader/wgsl/parser_impl_while_stmt_test.cc b/src/tint/reader/wgsl/parser_impl_while_stmt_test.cc
index 247f340..3b83ecc 100644
--- a/src/tint/reader/wgsl/parser_impl_while_stmt_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_while_stmt_test.cc
@@ -23,6 +23,17 @@
 
 // Test an empty while loop.
 TEST_F(WhileStmtTest, Empty) {
+    auto p = parser("while true { }");
+    auto wl = p->while_statement();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_FALSE(wl.errored);
+    ASSERT_TRUE(wl.matched);
+    EXPECT_TRUE(Is<ast::Expression>(wl->condition));
+    EXPECT_TRUE(wl->body->Empty());
+}
+
+// Test an empty while loop with parentheses.
+TEST_F(WhileStmtTest, EmptyWithParentheses) {
     auto p = parser("while (true) { }");
     auto wl = p->while_statement();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -46,17 +57,6 @@
 
 // Test a while loop with complex condition.
 TEST_F(WhileStmtTest, ComplexCondition) {
-    auto p = parser("while ((a + 1 - 2) == 3) { }");
-    auto wl = p->while_statement();
-    EXPECT_FALSE(p->has_error()) << p->error();
-    EXPECT_FALSE(wl.errored);
-    ASSERT_TRUE(wl.matched);
-    EXPECT_TRUE(Is<ast::Expression>(wl->condition));
-    EXPECT_TRUE(wl->body->Empty());
-}
-
-// Test a while loop with no brackets.
-TEST_F(WhileStmtTest, NoBrackets) {
     auto p = parser("while (a + 1 - 2) == 3 { }");
     auto wl = p->while_statement();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -66,9 +66,20 @@
     EXPECT_TRUE(wl->body->Empty());
 }
 
+// Test a while loop with complex condition, with parentheses.
+TEST_F(WhileStmtTest, ComplexConditionWithParentheses) {
+    auto p = parser("while ((a + 1 - 2) == 3) { }");
+    auto wl = p->while_statement();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_FALSE(wl.errored);
+    ASSERT_TRUE(wl.matched);
+    EXPECT_TRUE(Is<ast::Expression>(wl->condition));
+    EXPECT_TRUE(wl->body->Empty());
+}
+
 class WhileStmtErrorTest : public ParserImplTest {
   public:
-    void TestForWithError(std::string for_str, std::string error_str) {
+    void TestWhileWithError(std::string for_str, std::string error_str) {
         auto p_for = parser(for_str);
         auto e_for = p_for->while_statement();
 
@@ -85,7 +96,7 @@
     std::string while_str = "while bool) { }";
     std::string error_str = "1:11: expected '(' for type constructor";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with missing condition is invalid.
@@ -93,7 +104,7 @@
     std::string while_str = "while () {}";
     std::string error_str = "1:8: unable to parse expression";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with missing right parenthesis is invalid.
@@ -101,7 +112,7 @@
     std::string while_str = "while (true {}";
     std::string error_str = "1:13: expected ')'";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with missing left brace is invalid.
@@ -109,7 +120,7 @@
     std::string while_str = "while (true) }";
     std::string error_str = "1:14: expected '{'";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a for loop with missing right brace is invalid.
@@ -117,7 +128,7 @@
     std::string while_str = "while (true) {";
     std::string error_str = "1:15: expected '}'";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with an invalid break condition.
@@ -125,7 +136,7 @@
     std::string while_str = "while ((0 == 1) { }";
     std::string error_str = "1:17: expected ')'";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with a break condition not matching
@@ -134,7 +145,7 @@
     std::string while_str = "while (var i: i32 = 0) { }";
     std::string error_str = "1:8: unable to parse expression";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a while loop with an invalid body.
@@ -142,7 +153,7 @@
     std::string while_str = "while (true) { let x: i32; }";
     std::string error_str = "1:26: expected '=' for 'let' declaration";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 // Test a for loop with a body not matching statements
@@ -150,7 +161,7 @@
     std::string while_str = "while (true) { fn main() {} }";
     std::string error_str = "1:16: expected '}'";
 
-    TestForWithError(while_str, error_str);
+    TestWhileWithError(while_str, error_str);
 }
 
 }  // namespace
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 796487b..018dbfd 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -721,7 +721,7 @@
 }
 
 TEST_F(StructMemberAttributeTest, Align_Attribute_Var) {
-    GlobalVar(Source{{1, 2}}, "val", ty.f32(), ast::AddressSpace::kPrivate, ast::Access::kUndefined,
+    GlobalVar(Source{{1, 2}}, "val", ty.f32(), ast::AddressSpace::kPrivate, ast::Access::kInvalid,
               Expr(1.23_f));
 
     Structure(Source{{6, 4}}, "mystruct",
@@ -1386,16 +1386,16 @@
     ResolverAttributeValidationTest,
     InterpolateParameterTest,
     testing::Values(
-        Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kNone, true},
+        Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kInvalid, true},
         Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kCenter, true},
         Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kCentroid, true},
         Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kSample, true},
-        Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kNone, true},
+        Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kInvalid, true},
         Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kCenter, true},
         Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kCentroid, true},
         Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kSample, true},
         // flat interpolation must not have a sampling type
-        Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone, true},
+        Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kInvalid, true},
         Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kCenter, false},
         Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kCentroid, false},
         Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kSample, false}));
@@ -1443,7 +1443,7 @@
                    utils::Vector{
                        Builtin(ast::BuiltinValue::kPosition),
                        Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                                   ast::InterpolationSampling::kNone),
+                                   ast::InterpolationSampling::kInvalid),
                    }),
          },
          ty.void_(), utils::Empty,
@@ -1467,7 +1467,7 @@
          utils::Vector{
              Builtin(ast::BuiltinValue::kPosition),
              Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                         ast::InterpolationSampling::kNone),
+                         ast::InterpolationSampling::kInvalid),
          });
 
     EXPECT_FALSE(r()->Resolve());
@@ -1480,7 +1480,7 @@
               utils::Vector{
                   Member("a", ty.f32(),
                          utils::Vector{Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                                                   ast::InterpolationSampling::kNone)}),
+                                                   ast::InterpolationSampling::kInvalid)}),
               });
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index 9ddf24f..d9fde25 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -231,27 +231,29 @@
             return this;
         }
         return ZeroTypeDispatch(target_ty, [&](auto zero_to) -> ImplResult {
-            // `T` is the source type, `value` is the source value.
+            // `value` is the source value.
+            // `FROM` is the source type.
             // `TO` is the target type.
             using TO = std::decay_t<decltype(zero_to)>;
+            using FROM = T;
             if constexpr (std::is_same_v<TO, bool>) {
                 // [x -> bool]
                 return builder.create<Element<TO>>(target_ty, !IsPositiveZero(value));
-            } else if constexpr (std::is_same_v<T, bool>) {
+            } else if constexpr (std::is_same_v<FROM, bool>) {
                 // [bool -> x]
                 return builder.create<Element<TO>>(target_ty, TO(value ? 1 : 0));
             } else if (auto conv = CheckedConvert<TO>(value)) {
                 // Conversion success
                 return builder.create<Element<TO>>(target_ty, conv.Get());
                 // --- Below this point are the failure cases ---
-            } else if constexpr (IsAbstract<T>) {
+            } else if constexpr (IsAbstract<FROM>) {
                 // [abstract-numeric -> x] - materialization failure
                 std::stringstream ss;
                 ss << "value " << value << " cannot be represented as ";
                 ss << "'" << builder.FriendlyName(target_ty) << "'";
                 builder.Diagnostics().add_error(tint::diag::System::Resolver, ss.str(), source);
                 return utils::Failure;
-            } else if constexpr (IsFloatingPoint<UnwrapNumber<TO>>) {
+            } else if constexpr (IsFloatingPoint<TO>) {
                 // [x -> floating-point] - number not exactly representable
                 // https://www.w3.org/TR/WGSL/#floating-point-conversion
                 switch (conv.Failure()) {
@@ -260,8 +262,8 @@
                     case ConversionFailure::kExceedsPositiveLimit:
                         return builder.create<Element<TO>>(target_ty, TO::Inf());
                 }
-            } else {
-                // [x -> integer] - number not exactly representable
+            } else if constexpr (IsFloatingPoint<FROM>) {
+                // [floating-point -> integer] - number not exactly representable
                 // https://www.w3.org/TR/WGSL/#floating-point-conversion
                 switch (conv.Failure()) {
                     case ConversionFailure::kExceedsNegativeLimit:
@@ -269,6 +271,10 @@
                     case ConversionFailure::kExceedsPositiveLimit:
                         return builder.create<Element<TO>>(target_ty, TO::Highest());
                 }
+            } else if constexpr (IsIntegral<FROM>) {
+                // [integer -> integer] - number not exactly representable
+                // Static cast
+                return builder.create<Element<TO>>(target_ty, static_cast<TO>(value));
             }
             return nullptr;  // Expression is not constant.
         });
@@ -842,11 +848,7 @@
         return nullptr;  // Single argument is not constant.
     }
 
-    if (auto conv = Convert(ty, args[0], source)) {
-        return conv.Get();
-    }
-
-    return nullptr;
+    return Convert(ty, args[0], source);
 }
 
 ConstEval::Result ConstEval::Zero(const sem::Type* ty,
diff --git a/src/tint/resolver/const_eval_test.cc b/src/tint/resolver/const_eval_test.cc
index 8bded4c..cf3cda0 100644
--- a/src/tint/resolver/const_eval_test.cc
+++ b/src/tint/resolver/const_eval_test.cc
@@ -44,6 +44,30 @@
 template <typename T>
 const auto k3PiOver4 = T(UnwrapNumber<T>(2.356194490192344928846));
 
+/// Walks the sem::Constant @p c, accumulating all the inner-most scalar values into @p args
+void CollectScalarArgs(const sem::Constant* c, builder::ScalarArgs& args) {
+    Switch(
+        c->Type(),  //
+        [&](const sem::Bool*) { args.values.Push(c->As<bool>()); },
+        [&](const sem::I32*) { args.values.Push(c->As<i32>()); },
+        [&](const sem::U32*) { args.values.Push(c->As<u32>()); },
+        [&](const sem::F32*) { args.values.Push(c->As<f32>()); },
+        [&](const sem::F16*) { args.values.Push(c->As<f16>()); },
+        [&](Default) {
+            size_t i = 0;
+            while (auto* child = c->Index(i++)) {
+                CollectScalarArgs(child, args);
+            }
+        });
+}
+
+/// Walks the sem::Constant @p c, returning all the inner-most scalar values.
+builder::ScalarArgs ScalarArgsFrom(const sem::Constant* c) {
+    builder::ScalarArgs out;
+    CollectScalarArgs(c, out);
+    return out;
+}
+
 template <typename T>
 constexpr auto Negate(const Number<T>& v) {
     if constexpr (std::is_integral_v<T>) {
@@ -1211,283 +1235,6 @@
     EXPECT_EQ(sem->ConstantValue()->Index(2)->As<bool>(), false);
 }
 
-TEST_F(ResolverConstEvalTest, Vec3_Convert_f32_to_i32) {
-    auto* expr = vec3<i32>(vec3<f32>(1.1_f, 2.2_f, 3.3_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::I32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), 1);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), 2);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), 3);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f32) {
-    auto* expr = vec3<f32>(vec3<u32>(10_u, 20_u, 30_u));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::F32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 10.f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), 20.f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 30.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_f16_to_i32) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<i32>(vec3<f16>(1.1_h, 2.2_h, 3.3_h));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    EXPECT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::I32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), 1_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), 2_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), 3_i);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(vec3<u32>(10_u, 20_u, 30_u));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    EXPECT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 10.f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), 20.f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 30.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_i32) {
-    auto* expr = vec3<i32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::I32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), i32::Highest());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), i32::Lowest());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), i32::Highest());
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_u32) {
-    auto* expr = vec3<u32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::U32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), u32::Highest());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), u32::Lowest());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), u32::Highest());
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    constexpr auto kInfinity = std::numeric_limits<double>::infinity();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), kInfinity);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), -kInfinity);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), kInfinity);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Convert_Small_f32_to_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(vec3<f32>(1e-20_f, -2e-30_f, 3e-40_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* vec = sem->Type()->As<sem::Vector>();
-    ASSERT_NE(vec, nullptr);
-    EXPECT_TRUE(vec->type()->Is<sem::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 0.0);
-    EXPECT_FALSE(std::signbit(sem->ConstantValue()->Index(0)->As<AFloat>().value));
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), -0.0);
-    EXPECT_TRUE(std::signbit(sem->ConstantValue()->Index(1)->As<AFloat>().value));
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 0.0);
-    EXPECT_FALSE(std::signbit(sem->ConstantValue()->Index(2)->As<AFloat>().value));
-}
-
 TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f32) {
     auto* expr = mat2x3<f32>();
     WrapInFunction(expr);
@@ -2475,6 +2222,519 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
+// Conversion
+////////////////////////////////////////////////////////////////////////////////////////////////////
+namespace conv {
+
+using Scalar = std::variant<  //
+    builder::Value<AInt>,
+    builder::Value<AFloat>,
+    builder::Value<u32>,
+    builder::Value<i32>,
+    builder::Value<f32>,
+    builder::Value<f16>,
+    builder::Value<bool>>;
+
+static std::ostream& operator<<(std::ostream& o, const Scalar& scalar) {
+    std::visit(
+        [&](auto&& v) {
+            using ValueType = std::decay_t<decltype(v)>;
+            o << ValueType::DataType::Name() << "(";
+            for (auto& a : v.args.values) {
+                o << std::get<typename ValueType::ElementType>(a);
+                if (&a != &v.args.values.Back()) {
+                    o << ", ";
+                }
+            }
+            o << ")";
+        },
+        scalar);
+    return o;
+}
+
+enum class Kind {
+    kScalar,
+    kVector,
+};
+
+static std::ostream& operator<<(std::ostream& o, const Kind& k) {
+    switch (k) {
+        case Kind::kScalar:
+            return o << "scalar";
+        case Kind::kVector:
+            return o << "vector";
+    }
+    return o << "<unknown>";
+}
+
+struct Case {
+    Scalar input;
+    Scalar expected;
+    builder::CreatePtrs type;
+    bool unrepresentable = false;
+};
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    if (c.unrepresentable) {
+        o << "[unrepresentable] input: " << c.input;
+    } else {
+        o << "input: " << c.input << ", expected: " << c.expected;
+    }
+    return o << ", type: " << c.type;
+}
+
+template <typename TO, typename FROM>
+Case Success(FROM input, TO expected) {
+    return {builder::Val(input), builder::Val(expected), builder::CreatePtrsFor<TO>()};
+}
+
+template <typename TO, typename FROM>
+Case Unrepresentable(FROM input) {
+    return {builder::Val(input), builder::Val(0_i), builder::CreatePtrsFor<TO>(),
+            /* unrepresentable */ true};
+}
+
+using ResolverConstEvalConvTest = ResolverTestWithParam<std::tuple<Kind, Case>>;
+
+TEST_P(ResolverConstEvalConvTest, Test) {
+    const auto& kind = std::get<0>(GetParam());
+    const auto& input = std::get<1>(GetParam()).input;
+    const auto& expected = std::get<1>(GetParam()).expected;
+    const auto& type = std::get<1>(GetParam()).type;
+    const auto unrepresentable = std::get<1>(GetParam()).unrepresentable;
+
+    auto* input_val = std::visit([&](auto val) { return val.Expr(*this); }, input);
+    auto* expr = Construct(type.ast(*this), input_val);
+    if (kind == Kind::kVector) {
+        expr = Construct(ty.vec(nullptr, 3), expr);
+    }
+    WrapInFunction(expr);
+
+    auto* target_sem_ty = type.sem(*this);
+    if (kind == Kind::kVector) {
+        target_sem_ty = create<sem::Vector>(target_sem_ty, 3u);
+    }
+
+    if (unrepresentable) {
+        ASSERT_FALSE(r()->Resolve());
+        EXPECT_THAT(r()->error(), testing::HasSubstr("cannot be represented as"));
+    } else {
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        auto* sem = Sem().Get(expr);
+        ASSERT_NE(sem, nullptr);
+        EXPECT_TYPE(sem->Type(), target_sem_ty);
+        ASSERT_NE(sem->ConstantValue(), nullptr);
+        EXPECT_TYPE(sem->ConstantValue()->Type(), target_sem_ty);
+
+        auto expected_values = std::visit([&](auto&& val) { return val.args; }, expected);
+        if (kind == Kind::kVector) {
+            expected_values.values.Push(expected_values.values[0]);
+            expected_values.values.Push(expected_values.values[0]);
+        }
+        auto got_values = ScalarArgsFrom(sem->ConstantValue());
+        EXPECT_EQ(expected_values, got_values);
+    }
+}
+INSTANTIATE_TEST_SUITE_P(ScalarAndVector,
+                         ResolverConstEvalConvTest,
+                         testing::Combine(testing::Values(Kind::kScalar, Kind::kVector),
+                                          testing::ValuesIn({
+                                              // TODO(crbug.com/tint/1502): Add f16 tests
+                                              // i32 -> u32
+                                              Success(0_i, 0_u),
+                                              Success(1_i, 1_u),
+                                              Success(-1_i, 0xffffffff_u),
+                                              Success(2_i, 2_u),
+                                              Success(-2_i, 0xfffffffe_u),
+                                              // i32 -> f32
+                                              Success(0_i, 0_f),
+                                              Success(1_i, 1_f),
+                                              Success(-1_i, -1_f),
+                                              Success(2_i, 2_f),
+                                              Success(-2_i, -2_f),
+                                              // i32 -> bool
+                                              Success(0_i, false),
+                                              Success(1_i, true),
+                                              Success(-1_i, true),
+                                              Success(2_i, true),
+                                              Success(-2_i, true),
+                                              // u32 -> i32
+                                              Success(0_u, 0_i),
+                                              Success(1_u, 1_i),
+                                              Success(0xffffffff_u, -1_i),
+                                              Success(2_u, 2_i),
+                                              Success(0xfffffffe_u, -2_i),
+                                              // u32 -> f32
+                                              Success(0_u, 0_f),
+                                              Success(1_u, 1_f),
+                                              Success(2_u, 2_f),
+                                              Success(0xffffffff_u, 0xffffffff_f),
+                                              // u32 -> bool
+                                              Success(0_u, false),
+                                              Success(1_u, true),
+                                              Success(2_u, true),
+                                              Success(0xffffffff_u, true),
+                                              // f32 -> i32
+                                              Success(0_f, 0_i),
+                                              Success(1_f, 1_i),
+                                              Success(2_f, 2_i),
+                                              Success(1e20_f, i32::Highest()),
+                                              Success(-1e20_f, i32::Lowest()),
+                                              // f32 -> u32
+                                              Success(0_f, 0_i),
+                                              Success(1_f, 1_i),
+                                              Success(-1_f, u32::Lowest()),
+                                              Success(2_f, 2_i),
+                                              Success(1e20_f, u32::Highest()),
+                                              Success(-1e20_f, u32::Lowest()),
+                                              // f32 -> bool
+                                              Success(0_f, false),
+                                              Success(1_f, true),
+                                              Success(-1_f, true),
+                                              Success(2_f, true),
+                                              Success(1e20_f, true),
+                                              Success(-1e20_f, true),
+                                              // abstract-int -> i32
+                                              Success(0_a, 0_i),
+                                              Success(1_a, 1_i),
+                                              Success(-1_a, -1_i),
+                                              Success(0x7fffffff_a, i32::Highest()),
+                                              Success(-0x80000000_a, i32::Lowest()),
+                                              Unrepresentable<i32>(0x80000000_a),
+                                              // abstract-int -> u32
+                                              Success(0_a, 0_u),
+                                              Success(1_a, 1_u),
+                                              Success(0xffffffff_a, 0xffffffff_u),
+                                              Unrepresentable<u32>(0x100000000_a),
+                                              Unrepresentable<u32>(-1_a),
+                                              // abstract-int -> f32
+                                              Success(0_a, 0_f),
+                                              Success(1_a, 1_f),
+                                              Success(0xffffffff_a, 0xffffffff_f),
+                                              Success(0x100000000_a, 0x100000000_f),
+                                              Success(-0x100000000_a, -0x100000000_f),
+                                              Success(0x7fffffffffffffff_a, 0x7fffffffffffffff_f),
+                                              Success(-0x7fffffffffffffff_a, -0x7fffffffffffffff_f),
+                                              // abstract-int -> bool
+                                              Success(0_a, false),
+                                              Success(1_a, true),
+                                              Success(0xffffffff_a, true),
+                                              Success(0x100000000_a, true),
+                                              Success(-0x100000000_a, true),
+                                              Success(0x7fffffffffffffff_a, true),
+                                              Success(-0x7fffffffffffffff_a, true),
+                                              // abstract-float -> i32
+                                              Success(0.0_a, 0_i),
+                                              Success(1.0_a, 1_i),
+                                              Success(-1.0_a, -1_i),
+                                              Success(AFloat(0x7fffffff), i32::Highest()),
+                                              Success(-AFloat(0x80000000), i32::Lowest()),
+                                              Unrepresentable<i32>(0x80000000_a),
+                                              // abstract-float -> u32
+                                              Success(0.0_a, 0_u),
+                                              Success(1.0_a, 1_u),
+                                              Success(AFloat(0xffffffff), 0xffffffff_u),
+                                              Unrepresentable<u32>(AFloat(0x100000000)),
+                                              Unrepresentable<u32>(AFloat(-1)),
+                                              // abstract-float -> f32
+                                              Success(0.0_a, 0_f),
+                                              Success(1.0_a, 1_f),
+                                              Success(AFloat(0xffffffff), 0xffffffff_f),
+                                              Success(AFloat(0x100000000), 0x100000000_f),
+                                              Success(-AFloat(0x100000000), -0x100000000_f),
+                                              Unrepresentable<f32>(1e40_a),
+                                              Unrepresentable<f32>(-1e40_a),
+                                              // abstract-float -> bool
+                                              Success(0.0_a, false),
+                                              Success(1.0_a, true),
+                                              Success(AFloat(0xffffffff), true),
+                                              Success(AFloat(0x100000000), true),
+                                              Success(-AFloat(0x100000000), true),
+                                              Success(1e40_a, true),
+                                              Success(-1e40_a, true),
+                                          })));
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_f32_to_i32) {
+    auto* expr = vec3<i32>(vec3<f32>(1.1_f, 2.2_f, 3.3_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::I32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), 1);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), 2);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), 3);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f32) {
+    auto* expr = vec3<f32>(vec3<u32>(10_u, 20_u, 30_u));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::F32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 10.f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), 20.f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 30.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_f16_to_i32) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<i32>(vec3<f16>(1.1_h, 2.2_h, 3.3_h));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    EXPECT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::I32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), 1_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), 2_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), 3_i);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_u32_to_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(vec3<u32>(10_u, 20_u, 30_u));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    EXPECT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 10.f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), 20.f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 30.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_i32) {
+    auto* expr = vec3<i32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::I32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), i32::Highest());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), i32::Lowest());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), i32::Highest());
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_u32) {
+    auto* expr = vec3<u32>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::U32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AInt>(), u32::Highest());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AInt>(), u32::Lowest());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AInt>(), u32::Highest());
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_Large_f32_to_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(vec3<f32>(1e10_f, -1e20_f, 1e30_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    constexpr auto kInfinity = std::numeric_limits<double>::infinity();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), kInfinity);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), -kInfinity);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), kInfinity);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Convert_Small_f32_to_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(vec3<f32>(1e-20_f, -2e-30_f, 3e-40_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* vec = sem->Type()->As<sem::Vector>();
+    ASSERT_NE(vec, nullptr);
+    EXPECT_TRUE(vec->type()->Is<sem::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<AFloat>(), 0.0);
+    EXPECT_FALSE(std::signbit(sem->ConstantValue()->Index(0)->As<AFloat>().value));
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<AFloat>(), -0.0);
+    EXPECT_TRUE(std::signbit(sem->ConstantValue()->Index(1)->As<AFloat>().value));
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<AFloat>(), 0.0);
+    EXPECT_FALSE(std::signbit(sem->ConstantValue()->Index(2)->As<AFloat>().value));
+}
+
+}  // namespace conv
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
 // Indexing
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -3020,12 +3280,14 @@
             using T = typename std::decay_t<decltype(expected)>::ElementType;
 
             auto* input_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.input);
-            auto* expected_expr = create<ast::UnaryOpExpression>(op, input_expr);
-            GlobalConst("C", expected_expr);
+            auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
 
-            EXPECT_TRUE(r()->Resolve()) << r()->error();
+            GlobalConst("C", expr);
+            auto* expected_expr = expected.Expr(*this);
+            GlobalConst("E", expected_expr);
+            ASSERT_TRUE(r()->Resolve()) << r()->error();
 
-            auto* sem = Sem().Get(expected_expr);
+            auto* sem = Sem().Get(expr);
             const sem::Constant* value = sem->ConstantValue();
             ASSERT_NE(value, nullptr);
             EXPECT_TYPE(value->Type(), sem->Type());
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index 0eaaa37..a82f9e8 100644
--- a/src/tint/resolver/intrinsic_table.cc
+++ b/src/tint/resolver/intrinsic_table.cc
@@ -1186,7 +1186,7 @@
         for (auto& p : match.parameters) {
             params.Push(builder.create<sem::Parameter>(
                 nullptr, static_cast<uint32_t>(params.Length()), p.type, ast::AddressSpace::kNone,
-                ast::Access::kUndefined, p.usage));
+                ast::Access::kInvalid, p.usage));
         }
         sem::PipelineStageSet supported_stages;
         if (match.overload->flags.Contains(OverloadFlag::kSupportsVertexPipeline)) {
@@ -1384,7 +1384,7 @@
         for (auto& p : match.parameters) {
             params.Push(builder.create<sem::Parameter>(
                 nullptr, static_cast<uint32_t>(params.Length()), p.type, ast::AddressSpace::kNone,
-                ast::Access::kUndefined, p.usage));
+                ast::Access::kInvalid, p.usage));
         }
         auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
                                                         : sem::EvaluationStage::kRuntime;
@@ -1397,9 +1397,9 @@
 
     // Conversion.
     auto* target = utils::GetOrCreate(converters, match, [&]() {
-        auto param = builder.create<sem::Parameter>(
-            nullptr, 0u, match.parameters[0].type, ast::AddressSpace::kNone,
-            ast::Access::kUndefined, match.parameters[0].usage);
+        auto param = builder.create<sem::Parameter>(nullptr, 0u, match.parameters[0].type,
+                                                    ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                                    match.parameters[0].usage);
         auto eval_stage = match.overload->const_eval_fn ? sem::EvaluationStage::kConstant
                                                         : sem::EvaluationStage::kRuntime;
         return builder.create<sem::TypeConversion>(match.return_type, param, eval_stage);
@@ -1643,7 +1643,13 @@
 
     auto earliest_eval_stage = sem::EvaluationStage::kConstant;
 
-    ss << intrinsic_name << "(";
+    ss << intrinsic_name;
+    if (overload->flags.Contains(OverloadFlag::kIsConverter) && overload->template_types) {
+        ss << "<";
+        ss << overload->template_types[0].name;
+        ss << ">";
+    }
+    ss << "(";
     for (size_t p = 0; p < overload->num_parameters; p++) {
         auto& parameter = overload->parameters[p];
         if (p > 0) {
diff --git a/src/tint/resolver/intrinsic_table.inl b/src/tint/resolver/intrinsic_table.inl
index 67cb23f..426b78b 100644
--- a/src/tint/resolver/intrinsic_table.inl
+++ b/src/tint/resolver/intrinsic_table.inl
@@ -1550,8 +1550,8 @@
   return "__atomic_compare_exchange_result<" + T + ">";
 }
 
-/// TypeMatcher for 'match abstract_or_scalar'
-class AbstractOrScalar : public TypeMatcher {
+/// TypeMatcher for 'match scalar'
+class Scalar : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
   /// expected, canonicalized type on success.
@@ -1566,7 +1566,7 @@
   std::string String(MatchState* state) const override;
 };
 
-const sem::Type* AbstractOrScalar::Match(MatchState& state, const sem::Type* ty) const {
+const sem::Type* Scalar::Match(MatchState& state, const sem::Type* ty) const {
   if (match_ia(state, ty)) {
     return build_ia(state);
   }
@@ -1591,7 +1591,7 @@
   return nullptr;
 }
 
-std::string AbstractOrScalar::String(MatchState*) const {
+std::string Scalar::String(MatchState*) const {
   std::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.
@@ -1599,8 +1599,8 @@
   return ss.str();
 }
 
-/// TypeMatcher for 'match scalar'
-class Scalar : public TypeMatcher {
+/// TypeMatcher for 'match concrete_scalar'
+class ConcreteScalar : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
   /// expected, canonicalized type on success.
@@ -1615,7 +1615,7 @@
   std::string String(MatchState* state) const override;
 };
 
-const sem::Type* Scalar::Match(MatchState& state, const sem::Type* ty) const {
+const sem::Type* ConcreteScalar::Match(MatchState& state, const sem::Type* ty) const {
   if (match_i32(state, ty)) {
     return build_i32(state);
   }
@@ -1634,7 +1634,7 @@
   return nullptr;
 }
 
-std::string Scalar::String(MatchState*) const {
+std::string ConcreteScalar::String(MatchState*) const {
   std::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.
@@ -1659,6 +1659,12 @@
 };
 
 const sem::Type* ScalarNoF32::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_ia(state, ty)) {
+    return build_ia(state);
+  }
+  if (match_fa(state, ty)) {
+    return build_fa(state);
+  }
   if (match_i32(state, ty)) {
     return build_i32(state);
   }
@@ -1678,7 +1684,7 @@
   std::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) << ", " << F16().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
+  ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << I32().String(nullptr) << ", " << F16().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
   return ss.str();
 }
 
@@ -1699,6 +1705,12 @@
 };
 
 const sem::Type* ScalarNoF16::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_ia(state, ty)) {
+    return build_ia(state);
+  }
+  if (match_fa(state, ty)) {
+    return build_fa(state);
+  }
   if (match_i32(state, ty)) {
     return build_i32(state);
   }
@@ -1718,7 +1730,7 @@
   std::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 " << Bool().String(nullptr);
+  ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
   return ss.str();
 }
 
@@ -1739,6 +1751,12 @@
 };
 
 const sem::Type* ScalarNoI32::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_ia(state, ty)) {
+    return build_ia(state);
+  }
+  if (match_fa(state, ty)) {
+    return build_fa(state);
+  }
   if (match_u32(state, ty)) {
     return build_u32(state);
   }
@@ -1758,7 +1776,7 @@
   std::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) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
+  ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << U32().String(nullptr) << " or " << Bool().String(nullptr);
   return ss.str();
 }
 
@@ -1779,6 +1797,12 @@
 };
 
 const sem::Type* ScalarNoU32::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_ia(state, ty)) {
+    return build_ia(state);
+  }
+  if (match_fa(state, ty)) {
+    return build_fa(state);
+  }
   if (match_i32(state, ty)) {
     return build_i32(state);
   }
@@ -1798,7 +1822,7 @@
   std::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) << " or " << Bool().String(nullptr);
+  ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << " or " << Bool().String(nullptr);
   return ss.str();
 }
 
@@ -1819,6 +1843,12 @@
 };
 
 const sem::Type* ScalarNoBool::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_ia(state, ty)) {
+    return build_ia(state);
+  }
+  if (match_fa(state, ty)) {
+    return build_fa(state);
+  }
   if (match_i32(state, ty)) {
     return build_i32(state);
   }
@@ -1838,7 +1868,7 @@
   std::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) << " or " << U32().String(nullptr);
+  ss << Ia().String(nullptr) << ", " << Fa().String(nullptr) << ", " << F32().String(nullptr) << ", " << F16().String(nullptr) << ", " << I32().String(nullptr) << " or " << U32().String(nullptr);
   return ss.str();
 }
 
@@ -2580,8 +2610,8 @@
   FrexpResult FrexpResult_;
   FrexpResultVec FrexpResultVec_;
   AtomicCompareExchangeResult AtomicCompareExchangeResult_;
-  AbstractOrScalar AbstractOrScalar_;
   Scalar Scalar_;
+  ConcreteScalar ConcreteScalar_;
   ScalarNoF32 ScalarNoF32_;
   ScalarNoF16 ScalarNoF16_;
   ScalarNoI32 ScalarNoI32_;
@@ -2666,8 +2696,8 @@
     /* [47] */ &FrexpResult_,
     /* [48] */ &FrexpResultVec_,
     /* [49] */ &AtomicCompareExchangeResult_,
-    /* [50] */ &AbstractOrScalar_,
-    /* [51] */ &Scalar_,
+    /* [50] */ &Scalar_,
+    /* [51] */ &ConcreteScalar_,
     /* [52] */ &ScalarNoF32_,
     /* [53] */ &ScalarNoF16_,
     /* [54] */ &ScalarNoI32_,
@@ -14348,9 +14378,9 @@
   },
   {
     /* [67] */
-    /* fn select<T : abstract_or_scalar>(T, T, bool) -> T */
-    /* fn select<T : abstract_or_scalar, N : num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T> */
-    /* fn select<N : num, T : abstract_or_scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T> */
+    /* fn select<T : scalar>(T, T, bool) -> T */
+    /* fn select<T : scalar, N : num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T> */
+    /* fn select<N : num, T : scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T> */
     /* num overloads */ 3,
     /* overloads */ &kOverloads[276],
   },
@@ -14876,15 +14906,15 @@
   },
   {
     /* [10] */
-    /* op ==<T : abstract_or_scalar>(T, T) -> bool */
-    /* op ==<T : abstract_or_scalar, N : num>(vec<N, T>, vec<N, T>) -> vec<N, bool> */
+    /* op ==<T : scalar>(T, T) -> bool */
+    /* op ==<T : scalar, N : num>(vec<N, T>, vec<N, T>) -> vec<N, bool> */
     /* num overloads */ 2,
     /* overloads */ &kOverloads[408],
   },
   {
     /* [11] */
-    /* op !=<T : abstract_or_scalar>(T, T) -> bool */
-    /* op !=<T : abstract_or_scalar, N : num>(vec<N, T>, vec<N, T>) -> vec<N, bool> */
+    /* op !=<T : scalar>(T, T) -> bool */
+    /* op !=<T : scalar, N : num>(vec<N, T>, vec<N, T>) -> vec<N, bool> */
     /* num overloads */ 2,
     /* overloads */ &kOverloads[394],
   },
@@ -14995,10 +15025,10 @@
   },
   {
     /* [5] */
-    /* ctor vec2<T : scalar>() -> vec2<T> */
-    /* ctor vec2<T : scalar>(vec2<T>) -> vec2<T> */
-    /* ctor vec2<T : abstract_or_scalar>(T) -> vec2<T> */
-    /* ctor vec2<T : abstract_or_scalar>(x: T, y: T) -> vec2<T> */
+    /* ctor vec2<T : concrete_scalar>() -> vec2<T> */
+    /* ctor vec2<T : concrete_scalar>(vec2<T>) -> vec2<T> */
+    /* ctor vec2<T : scalar>(T) -> vec2<T> */
+    /* ctor vec2<T : scalar>(x: T, y: T) -> vec2<T> */
     /* conv vec2<T : f32, U : scalar_no_f32>(vec2<U>) -> vec2<f32> */
     /* conv vec2<T : f16, U : scalar_no_f16>(vec2<U>) -> vec2<f16> */
     /* conv vec2<T : i32, U : scalar_no_i32>(vec2<U>) -> vec2<i32> */
@@ -15009,12 +15039,12 @@
   },
   {
     /* [6] */
-    /* ctor vec3<T : scalar>() -> vec3<T> */
-    /* ctor vec3<T : scalar>(vec3<T>) -> vec3<T> */
-    /* ctor vec3<T : abstract_or_scalar>(T) -> vec3<T> */
-    /* ctor vec3<T : abstract_or_scalar>(x: T, y: T, z: T) -> vec3<T> */
-    /* ctor vec3<T : abstract_or_scalar>(xy: vec2<T>, z: T) -> vec3<T> */
-    /* ctor vec3<T : abstract_or_scalar>(x: T, yz: vec2<T>) -> vec3<T> */
+    /* ctor vec3<T : concrete_scalar>() -> vec3<T> */
+    /* ctor vec3<T : concrete_scalar>(vec3<T>) -> vec3<T> */
+    /* ctor vec3<T : scalar>(T) -> vec3<T> */
+    /* ctor vec3<T : scalar>(x: T, y: T, z: T) -> vec3<T> */
+    /* ctor vec3<T : scalar>(xy: vec2<T>, z: T) -> vec3<T> */
+    /* ctor vec3<T : scalar>(x: T, yz: vec2<T>) -> vec3<T> */
     /* conv vec3<T : f32, U : scalar_no_f32>(vec3<U>) -> vec3<f32> */
     /* conv vec3<T : f16, U : scalar_no_f16>(vec3<U>) -> vec3<f16> */
     /* conv vec3<T : i32, U : scalar_no_i32>(vec3<U>) -> vec3<i32> */
@@ -15025,16 +15055,16 @@
   },
   {
     /* [7] */
-    /* ctor vec4<T : scalar>() -> vec4<T> */
-    /* ctor vec4<T : scalar>(vec4<T>) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(T) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(x: T, y: T, z: T, w: T) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(xy: vec2<T>, z: T, w: T) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(x: T, yz: vec2<T>, w: T) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(x: T, y: T, zw: vec2<T>) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(xy: vec2<T>, zw: vec2<T>) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(xyz: vec3<T>, w: T) -> vec4<T> */
-    /* ctor vec4<T : abstract_or_scalar>(x: T, zyw: vec3<T>) -> vec4<T> */
+    /* ctor vec4<T : concrete_scalar>() -> vec4<T> */
+    /* ctor vec4<T : concrete_scalar>(vec4<T>) -> vec4<T> */
+    /* ctor vec4<T : scalar>(T) -> vec4<T> */
+    /* ctor vec4<T : scalar>(x: T, y: T, z: T, w: T) -> vec4<T> */
+    /* ctor vec4<T : scalar>(xy: vec2<T>, z: T, w: T) -> vec4<T> */
+    /* ctor vec4<T : scalar>(x: T, yz: vec2<T>, w: T) -> vec4<T> */
+    /* ctor vec4<T : scalar>(x: T, y: T, zw: vec2<T>) -> vec4<T> */
+    /* ctor vec4<T : scalar>(xy: vec2<T>, zw: vec2<T>) -> vec4<T> */
+    /* ctor vec4<T : scalar>(xyz: vec3<T>, w: T) -> vec4<T> */
+    /* ctor vec4<T : scalar>(x: T, zyw: vec3<T>) -> vec4<T> */
     /* conv vec4<T : f32, U : scalar_no_f32>(vec4<U>) -> vec4<f32> */
     /* conv vec4<T : f16, U : scalar_no_f16>(vec4<U>) -> vec4<f16> */
     /* conv vec4<T : i32, U : scalar_no_i32>(vec4<U>) -> vec4<i32> */
diff --git a/src/tint/resolver/intrinsic_table.inl.tmpl b/src/tint/resolver/intrinsic_table.inl.tmpl
index fb95eae..7b69df0 100644
--- a/src/tint/resolver/intrinsic_table.inl.tmpl
+++ b/src/tint/resolver/intrinsic_table.inl.tmpl
@@ -1,6 +1,6 @@
 {{- /*
 --------------------------------------------------------------------------------
-Template file for use with tools/src/cmd/gen to generate builtin_table.inl
+Template file for use with tools/src/cmd/gen to generate intrinsic_table.inl
 Used by BuiltinTable.cc for builtin overload resolution.
 
 To update the generated file, run:
diff --git a/src/tint/resolver/intrinsic_table_test.cc b/src/tint/resolver/intrinsic_table_test.cc
index 34cec07..57a6731 100644
--- a/src/tint/resolver/intrinsic_table_test.cc
+++ b/src/tint/resolver/intrinsic_table_test.cc
@@ -793,11 +793,11 @@
   vec3() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
 
 5 candidate conversions:
-  vec3(vec3<U>) -> vec3<f32>  where: T is f32, U is i32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<f16>  where: T is f16, U is f32, i32, u32 or bool
-  vec3(vec3<U>) -> vec3<i32>  where: T is i32, U is f32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<u32>  where: T is u32, U is f32, f16, i32 or bool
-  vec3(vec3<U>) -> vec3<bool>  where: T is bool, U is f32, f16, i32 or u32
+  vec3<T>(vec3<U>) -> vec3<f32>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<f16>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<i32>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<u32>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
+  vec3<T>(vec3<U>) -> vec3<bool>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
 )");
 }
 
@@ -819,11 +819,11 @@
   vec3() -> vec3<T>  where: T is f32, f16, i32, u32 or bool
 
 5 candidate conversions:
-  vec3(vec3<U>) -> vec3<f32>  where: T is f32, U is i32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<f16>  where: T is f16, U is f32, i32, u32 or bool
-  vec3(vec3<U>) -> vec3<i32>  where: T is i32, U is f32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<u32>  where: T is u32, U is f32, f16, i32 or bool
-  vec3(vec3<U>) -> vec3<bool>  where: T is bool, U is f32, f16, i32 or u32
+  vec3<T>(vec3<U>) -> vec3<f32>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<f16>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<i32>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<u32>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
+  vec3<T>(vec3<U>) -> vec3<bool>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
 )");
 }
 
@@ -875,11 +875,11 @@
   vec3(x: T, y: T, z: T) -> vec3<T>  where: T is abstract-int, abstract-float, f32, f16, i32, u32 or bool
 
 5 candidate conversions:
-  vec3(vec3<U>) -> vec3<f32>  where: T is f32, U is i32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<f16>  where: T is f16, U is f32, i32, u32 or bool
-  vec3(vec3<U>) -> vec3<i32>  where: T is i32, U is f32, f16, u32 or bool
-  vec3(vec3<U>) -> vec3<u32>  where: T is u32, U is f32, f16, i32 or bool
-  vec3(vec3<U>) -> vec3<bool>  where: T is bool, U is f32, f16, i32 or u32
+  vec3<T>(vec3<U>) -> vec3<f32>  where: T is f32, U is abstract-int, abstract-float, i32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<f16>  where: T is f16, U is abstract-int, abstract-float, f32, i32, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<i32>  where: T is i32, U is abstract-int, abstract-float, f32, f16, u32 or bool
+  vec3<T>(vec3<U>) -> vec3<u32>  where: T is u32, U is abstract-int, abstract-float, f32, f16, i32 or bool
+  vec3<T>(vec3<U>) -> vec3<bool>  where: T is bool, U is abstract-int, abstract-float, f32, f16, i32 or u32
 )");
 }
 
@@ -904,7 +904,7 @@
     ASSERT_NE(result.target, nullptr);
     EXPECT_EQ(result.target->ReturnType(), i32);
     EXPECT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
+    EXPECT_EQ(result.target->Parameters()[0]->Type(), ai);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 4d31d70..0fe6691 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -251,7 +251,7 @@
         [&](const ast::Pointer* t) -> sem::Pointer* {
             if (auto* el = Type(t->type)) {
                 auto access = t->access;
-                if (access == ast::kUndefined) {
+                if (access == ast::Access::kInvalid) {
                     access = DefaultAccessForAddressSpace(t->address_space);
                 }
                 return builder_->create<sem::Pointer>(el, t->address_space, access);
@@ -386,13 +386,12 @@
     sem::Variable* sem = nullptr;
     if (is_global) {
         sem = builder_->create<sem::GlobalVariable>(
-            v, ty, sem::EvaluationStage::kRuntime, ast::AddressSpace::kNone,
-            ast::Access::kUndefined, /* constant_value */ nullptr, sem::BindingPoint{},
-            std::nullopt);
+            v, ty, sem::EvaluationStage::kRuntime, ast::AddressSpace::kNone, ast::Access::kInvalid,
+            /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
     } else {
         sem = builder_->create<sem::LocalVariable>(v, ty, sem::EvaluationStage::kRuntime,
-                                                   ast::AddressSpace::kNone,
-                                                   ast::Access::kUndefined, current_statement_,
+                                                   ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                                   current_statement_,
                                                    /* constant_value */ nullptr);
     }
 
@@ -442,7 +441,7 @@
     }
 
     auto* sem = builder_->create<sem::GlobalVariable>(
-        v, ty, sem::EvaluationStage::kOverride, ast::AddressSpace::kNone, ast::Access::kUndefined,
+        v, ty, sem::EvaluationStage::kOverride, ast::AddressSpace::kNone, ast::Access::kInvalid,
         /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
     sem->SetConstructor(rhs);
 
@@ -527,10 +526,10 @@
 
     auto* sem = is_global ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
                                 c, ty, sem::EvaluationStage::kConstant, ast::AddressSpace::kNone,
-                                ast::Access::kUndefined, value, sem::BindingPoint{}, std::nullopt))
+                                ast::Access::kInvalid, value, sem::BindingPoint{}, std::nullopt))
                           : static_cast<sem::Variable*>(builder_->create<sem::LocalVariable>(
                                 c, ty, sem::EvaluationStage::kConstant, ast::AddressSpace::kNone,
-                                ast::Access::kUndefined, current_statement_, value));
+                                ast::Access::kInvalid, current_statement_, value));
 
     sem->SetConstructor(rhs);
     builder_->Sem().Add(c, sem);
@@ -589,7 +588,7 @@
     }
 
     auto access = var->declared_access;
-    if (access == ast::Access::kUndefined) {
+    if (access == ast::Access::kInvalid) {
         access = DefaultAccessForAddressSpace(address_space);
     }
 
@@ -753,9 +752,9 @@
         location = c->As<uint32_t>();
     }
 
-    auto* sem = builder_->create<sem::Parameter>(
-        param, index, ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
-        sem::ParameterUsage::kNone, binding_point, location);
+    auto* sem = builder_->create<sem::Parameter>(param, index, ty, ast::AddressSpace::kNone,
+                                                 ast::Access::kInvalid, sem::ParameterUsage::kNone,
+                                                 binding_point, location);
     builder_->Sem().Add(param, sem);
     return sem;
 }
@@ -1789,7 +1788,7 @@
                                 static_cast<uint32_t>(i),  // index
                                 arr->ElemType(),           // type
                                 ast::AddressSpace::kNone,  // address_space
-                                ast::Access::kUndefined);
+                                ast::Access::kInvalid);
                         });
                         return builder_->create<sem::TypeConstructor>(arr, std::move(params),
                                                                       args_stage);
@@ -1818,7 +1817,7 @@
                                 static_cast<uint32_t>(i),   // index
                                 str->Members()[i]->Type(),  // type
                                 ast::AddressSpace::kNone,   // address_space
-                                ast::Access::kUndefined);   // access
+                                ast::Access::kInvalid);     // access
                         }
                         return builder_->create<sem::TypeConstructor>(str, std::move(params),
                                                                       args_stage);
diff --git a/src/tint/resolver/resolver_test_helper.h b/src/tint/resolver/resolver_test_helper.h
index 501cd0f..923f3ff 100644
--- a/src/tint/resolver/resolver_test_helper.h
+++ b/src/tint/resolver/resolver_test_helper.h
@@ -17,6 +17,7 @@
 
 #include <functional>
 #include <memory>
+#include <ostream>
 #include <string>
 #include <tuple>
 #include <utility>
@@ -174,13 +175,15 @@
 struct ptr {};
 
 /// Type used to accept scalars as arguments. Can be either a single value that gets splatted for
-/// composite types, or all values requried by the composite type.
+/// composite types, or all values required by the composite type.
 struct ScalarArgs {
     /// Constructor
+    ScalarArgs() = default;
+
+    /// Constructor
     /// @param single_value single value to initialize with
     template <typename T>
-    ScalarArgs(T single_value)  // NOLINT: implicit on purpose
-        : values(utils::Vector<Storage, 1>{single_value}) {}
+    explicit ScalarArgs(T single_value) : values(utils::Vector<Storage, 1>{single_value}) {}
 
     /// Constructor
     /// @param all_values all values to initialize the composite type with
@@ -192,6 +195,10 @@
         }
     }
 
+    /// @param other the other ScalarArgs to compare against
+    /// @returns true if all values are equal to the values in @p other
+    bool operator==(const ScalarArgs& other) const { return values == other.values; }
+
     /// Valid scalar types for args
     using Storage = std::variant<i32, u32, f32, f16, AInt, AFloat, bool>;
 
@@ -199,10 +206,28 @@
     utils::Vector<Storage, 16> values;
 };
 
+/// @param o the std::ostream to write to
+/// @param args the ScalarArgs
+/// @return the std::ostream so calls can be chained
+inline std::ostream& operator<<(std::ostream& o, const ScalarArgs& args) {
+    o << "[";
+    bool first = true;
+    for (auto& val : args.values) {
+        if (!first) {
+            o << ", ";
+        }
+        first = false;
+        std::visit([&](auto&& v) { o << v; }, val);
+    }
+    o << "]";
+    return o;
+}
+
 using ast_type_func_ptr = const ast::Type* (*)(ProgramBuilder& b);
 using ast_expr_func_ptr = const ast::Expression* (*)(ProgramBuilder& b, ScalarArgs args);
 using ast_expr_from_double_func_ptr = const ast::Expression* (*)(ProgramBuilder& b, double v);
 using sem_type_func_ptr = const sem::Type* (*)(ProgramBuilder& b);
+using type_name_func_ptr = std::string (*)();
 
 template <typename T>
 struct DataType {};
@@ -241,7 +266,7 @@
     /// @param v arg of type double that will be cast to bool.
     /// @return a new AST expression of the bool type
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "bool"; }
@@ -272,7 +297,7 @@
     /// @param v arg of type double that will be cast to i32.
     /// @return a new AST i32 literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "i32"; }
@@ -303,7 +328,7 @@
     /// @param v arg of type double that will be cast to u32.
     /// @return a new AST u32 literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "u32"; }
@@ -334,7 +359,7 @@
     /// @param v arg of type double that will be cast to f32.
     /// @return a new AST f32 literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<f32>(v));
+        return Expr(b, ScalarArgs{static_cast<f32>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "f32"; }
@@ -365,7 +390,7 @@
     /// @param v arg of type double that will be cast to f16.
     /// @return a new AST f16 literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "f16"; }
@@ -395,7 +420,7 @@
     /// @param v arg of type double that will be cast to AFloat.
     /// @return a new AST abstract-float literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "abstract-float"; }
@@ -425,7 +450,7 @@
     /// @param v arg of type double that will be cast to AInt.
     /// @return a new AST abstract-int literal value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() { return "abstract-int"; }
@@ -463,7 +488,7 @@
         const bool one_value = args.values.Length() == 1;
         utils::Vector<const ast::Expression*, N> r;
         for (size_t i = 0; i < N; ++i) {
-            r.Push(DataType<T>::Expr(b, one_value ? args.values[0] : args.values[i]));
+            r.Push(DataType<T>::Expr(b, ScalarArgs{one_value ? args.values[0] : args.values[i]}));
         }
         return r;
     }
@@ -471,7 +496,7 @@
     /// @param v arg of type double that will be cast to ElementType
     /// @return a new AST vector value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() {
@@ -514,7 +539,7 @@
         utils::Vector<const ast::Expression*, N> r;
         for (uint32_t i = 0; i < N; ++i) {
             if (one_value) {
-                r.Push(DataType<vec<M, T>>::Expr(b, args.values[0]));
+                r.Push(DataType<vec<M, T>>::Expr(b, ScalarArgs{args.values[0]}));
             } else {
                 utils::Vector<T, M> v;
                 for (size_t j = 0; j < M; ++j) {
@@ -529,7 +554,7 @@
     /// @param v arg of type double that will be cast to ElementType
     /// @return a new AST matrix value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() {
@@ -585,7 +610,7 @@
     /// @param v arg of type double that will be cast to ElementType
     /// @return a new AST expression of the alias type
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
 
     /// @returns the WGSL name for the type
@@ -626,7 +651,7 @@
     /// @param v arg of type double that will be cast to ElementType
     /// @return a new AST expression of the pointer type
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
 
     /// @returns the WGSL name for the type
@@ -680,7 +705,7 @@
         const bool one_value = args.values.Length() == 1;
         utils::Vector<const ast::Expression*, N> r;
         for (uint32_t i = 0; i < N; i++) {
-            r.Push(DataType<T>::Expr(b, one_value ? args.values[0] : args.values[i]));
+            r.Push(DataType<T>::Expr(b, ScalarArgs{one_value ? args.values[0] : args.values[i]}));
         }
         return r;
     }
@@ -688,7 +713,7 @@
     /// @param v arg of type double that will be cast to ElementType
     /// @return a new AST array value expression
     static inline const ast::Expression* ExprFromDouble(ProgramBuilder& b, double v) {
-        return Expr(b, static_cast<ElementType>(v));
+        return Expr(b, ScalarArgs{static_cast<ElementType>(v)});
     }
     /// @returns the WGSL name for the type
     static inline std::string Name() {
@@ -706,13 +731,23 @@
     ast_expr_from_double_func_ptr expr_from_double;
     /// sem type create function
     sem_type_func_ptr sem;
+    /// type name function
+    type_name_func_ptr name;
 };
 
+/// @param o the std::ostream to write to
+/// @param ptrs the CreatePtrs
+/// @return the std::ostream so calls can be chained
+inline std::ostream& operator<<(std::ostream& o, const CreatePtrs& ptrs) {
+    return o << (ptrs.name ? ptrs.name() : "<unknown>");
+}
+
 /// Returns a CreatePtrs struct instance with all creation pointer types for
 /// type `T`
 template <typename T>
 constexpr CreatePtrs CreatePtrsFor() {
-    return {DataType<T>::AST, DataType<T>::Expr, DataType<T>::ExprFromDouble, DataType<T>::Sem};
+    return {DataType<T>::AST, DataType<T>::Expr, DataType<T>::ExprFromDouble, DataType<T>::Sem,
+            DataType<T>::Name};
 }
 
 /// Value<T> is an instance of a value of type DataType<T>. Useful for storing values to create
@@ -729,15 +764,15 @@
     /// Creates a Value<T> with `args`
     /// @param args the args that will be passed to the expression
     /// @returns a Value<T>
-    static Value Create(ScalarArgs args) { return Value{DataType::Expr, std::move(args)}; }
+    static Value Create(ScalarArgs args) { return Value{CreatePtrsFor<T>(), std::move(args)}; }
 
     /// Creates an `ast::Expression` for the type T passing in previously stored args
     /// @param b the ProgramBuilder
     /// @returns an expression node
-    const ast::Expression* Expr(ProgramBuilder& b) const { return (*expr)(b, args); }
+    const ast::Expression* Expr(ProgramBuilder& b) const { return (*create.expr)(b, args); }
 
-    /// ast expression type create function
-    ast_expr_func_ptr expr;
+    /// functions to create values / types of the value
+    CreatePtrs create;
     /// args to create expression with
     ScalarArgs args;
 };
@@ -764,7 +799,7 @@
 /// Creates a `Value<T>` from a scalar `v`
 template <typename T>
 auto Val(T v) {
-    return Value<T>::Create(v);
+    return Value<T>::Create(ScalarArgs{v});
 }
 
 /// Creates a `Value<vec<N, T>>` from N scalar `args`
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index 7cc5cef..1271f06 100644
--- a/src/tint/resolver/type_validation_test.cc
+++ b/src/tint/resolver/type_validation_test.cc
@@ -1165,7 +1165,7 @@
     // var a : texture_storage_1d<ru32int>;
 
     auto* st = ty.storage_texture(Source{{12, 34}}, ast::TextureDimension::k1d,
-                                  ast::TexelFormat::kR32Uint, ast::Access::kUndefined);
+                                  ast::TexelFormat::kR32Uint, ast::Access::kInvalid);
 
     GlobalVar("a", st, Group(0_a), Binding(0_a));
 
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 687d17d..35ba7e9 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -263,7 +263,7 @@
     switch (t->access) {
         case ast::Access::kWrite:
             break;
-        case ast::Access::kUndefined:
+        case ast::Access::kInvalid:
             AddError("storage texture missing access control", t->source);
             return false;
         default:
@@ -344,8 +344,8 @@
                 break;  // Allowed an initializer
             default:
                 // https://gpuweb.github.io/gpuweb/wgsl/#var-and-let
-                // Optionally has an initializer expression, if the variable is in the
-                // private or function address spacees.
+                // Optionally has an initializer expression, if the variable is in the private or
+                // function address spacees.
                 AddError("var of address space '" + utils::ToString(address_space) +
                              "' cannot have an initializer. var initializers are only "
                              "supported for the address spacees "
@@ -440,9 +440,8 @@
                 return false;
             }
 
-            // For uniform buffers, validate that the number of bytes between the
-            // previous member of type struct and the current is a multiple of 16
-            // bytes.
+            // For uniform buffers, validate that the number of bytes between the previous member of
+            // type struct and the current is a multiple of 16 bytes.
             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();
@@ -469,24 +468,22 @@
         }
     }
 
-    // For uniform buffer array members, validate that array elements are
-    // aligned to 16 bytes
+    // For uniform buffer array members, validate that array elements are aligned to 16 bytes
     if (auto* arr = store_ty->As<sem::Array>()) {
         // Recurse into the element type.
-        // TODO(crbug.com/tint/1388): Ideally we'd pass the source for nested
-        // element type here, but we can't easily get that from the semantic node.
-        // We should consider recursing through the AST type nodes instead.
+        // TODO(crbug.com/tint/1388): Ideally we'd pass the source for nested element type here, but
+        // we can't easily get that from the semantic node. We should consider recursing through the
+        // AST type nodes instead.
         if (!AddressSpaceLayout(arr->ElemType(), address_space, source, layouts)) {
             return false;
         }
 
         if (address_space == ast::AddressSpace::kUniform) {
-            // 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.
+            // 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) {
-                // Since WGSL has no stride attribute, try to provide a useful hint
-                // for how the shader author can resolve the issue.
+                // Since WGSL has no stride attribute, try to provide a useful hint for how the
+                // shader author can resolve the issue.
                 std::string hint;
                 if (arr->ElemType()->is_scalar()) {
                     hint =
@@ -622,9 +619,9 @@
             }
 
             // https://gpuweb.github.io/gpuweb/wgsl/#variable-declaration
-            // The access mode always has a default, and except for variables in the
-            // storage address space, must not be written.
-            if (var->declared_access != ast::Access::kUndefined) {
+            // The access mode always has a default, and except for variables in the storage address
+            // space, must not be written.
+            if (var->declared_access != ast::Access::kInvalid) {
                 if (global->AddressSpace() == ast::AddressSpace::kStorage) {
                     // The access mode for the storage address space can only be 'read' or
                     // 'read_write'.
@@ -684,8 +681,7 @@
         case ast::AddressSpace::kStorage:
         case ast::AddressSpace::kHandle: {
             // https://gpuweb.github.io/gpuweb/wgsl/#resource-interface
-            // Each resource variable must be declared with both group and binding
-            // attributes.
+            // Each resource variable must be declared with both group and binding attributes.
             if (!decl->HasBindingPoint()) {
                 AddError("resource variables require @group and @binding attributes", decl->source);
                 return false;
@@ -709,8 +705,8 @@
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#atomic-types
-// Atomic types may only be instantiated by variables in the workgroup storage
-// class or by storage buffer variables with a read_write access mode.
+// Atomic types may only be instantiated by variables in the workgroup storage class or by storage
+// buffer variables with a read_write access mode.
 bool Validator::AtomicVariable(
     const sem::Variable* var,
     std::unordered_map<const sem::Type*, const Source&> atomic_composite_info) const {
@@ -763,9 +759,8 @@
 
     if (storage_ty->is_handle() && var->declared_address_space != ast::AddressSpace::kNone) {
         // https://gpuweb.github.io/gpuweb/wgsl/#module-scope-variables
-        // If the store type is a texture type or a sampler type, then the
-        // variable declaration must not have a address space attribute. The
-        // address space will always be handle.
+        // If the store type is a texture type or a sampler type, then the variable declaration must
+        // not have a address space attribute. The address space will always be handle.
         AddError(
             "variables of type '" + sem_.TypeNameOf(storage_ty) + "' must not have a address space",
             var->source);
@@ -1026,7 +1021,7 @@
     }
 
     if (attr->type == ast::InterpolationType::kFlat &&
-        attr->sampling != ast::InterpolationSampling::kNone) {
+        attr->sampling != ast::InterpolationSampling::kInvalid) {
         AddError("flat interpolation attribute must not have a sampling parameter", attr->source);
         return false;
     }
@@ -1127,10 +1122,10 @@
     auto* decl = func->Declaration();
 
     // Use a lambda to validate the entry point attributes for a type.
-    // Persistent state is used to track which builtins and locations have
-    // already been seen, in order to catch conflicts.
-    // TODO(jrprice): This state could be stored in sem::Function instead, and
-    // then passed to sem::Function since it would be useful there too.
+    // Persistent state is used to track which builtins and locations have already been seen, in
+    // order to catch conflicts.
+    // TODO(jrprice): This state could be stored in sem::Function instead, and then passed to
+    // sem::Function since it would be useful there too.
     std::unordered_set<ast::BuiltinValue> builtins;
     std::unordered_set<uint32_t> locations;
     enum class ParamOrRetType {
@@ -1145,8 +1140,7 @@
                                                      bool is_struct_member,
                                                      std::optional<uint32_t> location) {
         // Temporally forbid using f16 types in entry point IO.
-        // TODO(tint:1473, tint:1502): Remove this error after f16 is supported in entry point
-        // IO.
+        // TODO(tint:1473, tint:1502): Remove this error after f16 is supported in entry point IO.
         if (Is<sem::F16>(sem::Type::DeepestElementOf(ty))) {
             AddError("entry point IO of f16 types is not implemented yet", source);
             return false;
@@ -1324,9 +1318,8 @@
         }
     }
 
-    // Clear IO sets after parameter validation. Builtin and location attributes
-    // in return types should be validated independently from those used in
-    // parameters.
+    // Clear IO sets after parameter validation. Builtin and location attributes in return types
+    // should be validated independently from those used in parameters.
     builtins.clear();
     locations.clear();
 
@@ -1352,20 +1345,16 @@
             }
         }
         if (!found) {
-            AddError(
-                "a vertex shader must include the 'position' builtin in its return "
-                "type",
-                decl->source);
+            AddError("a vertex shader must include the 'position' builtin in its return type",
+                     decl->source);
             return false;
         }
     }
 
     if (decl->PipelineStage() == ast::PipelineStage::kCompute) {
         if (!ast::HasAttribute<ast::WorkgroupAttribute>(decl->attributes)) {
-            AddError(
-                "a compute shader must include 'workgroup_size' in its "
-                "attributes",
-                decl->source);
+            AddError("a compute shader must include 'workgroup_size' in its attributes",
+                     decl->source);
             return false;
         }
     }
@@ -1385,17 +1374,15 @@
             IsValidationEnabled(res.first->second->attributes,
                                 ast::DisabledValidation::kBindingPointCollision)) {
             // https://gpuweb.github.io/gpuweb/wgsl/#resource-interface
-            // Bindings must not alias within a shader stage: two different
-            // variables in the resource interface of a given shader must not have
-            // the same group and binding values, when considered as a pair of
-            // values.
+            // Bindings must not alias within a shader stage: two different variables in the
+            // resource interface of a given shader must not have the same group and binding values,
+            // when considered as a pair of values.
             auto func_name = symbols_.NameFor(decl->symbol);
-            AddError("entry point '" + func_name +
-                         "' references multiple variables that use the "
-                         "same resource binding @group(" +
-                         std::to_string(bp.group) + "), @binding(" + std::to_string(bp.binding) +
-                         ")",
-                     var_decl->source);
+            AddError(
+                "entry point '" + func_name +
+                    "' references multiple variables that use the same resource binding @group(" +
+                    std::to_string(bp.group) + "), @binding(" + std::to_string(bp.binding) + ")",
+                var_decl->source);
             AddNote("first resource binding usage declared here", res.first->second->source);
             return false;
         }
@@ -1453,9 +1440,9 @@
     if (auto* continuing = ClosestContinuing(/*stop_at_loop*/ true, current_statement)) {
         auto fail = [&](const char* note_msg, const Source& note_src) {
             constexpr const char* kErrorMsg =
-                "break statement in a continuing block must be the single statement "
-                "of an if statement's true or false block, and that if statement "
-                "must be the last statement of the continuing block";
+                "break statement in a continuing block must be the single statement of an if "
+                "statement's true or false block, and that if statement must be the last statement "
+                "of the continuing block";
             AddError(kErrorMsg, stmt->Declaration()->source);
             AddNote(note_msg, note_src);
             return false;
@@ -1491,15 +1478,14 @@
 
             if (if_stmt->Parent()->Declaration() != continuing) {
                 return fail(
-                    "if statement containing break statement is not directly in "
-                    "continuing block",
+                    "if statement containing break statement is not directly in continuing block",
                     if_stmt->Declaration()->source);
             }
             if (auto* cont_block = continuing->As<ast::BlockStatement>()) {
                 if (if_stmt->Declaration() != cont_block->Last()) {
                     return fail(
-                        "if statement containing break statement is not the last "
-                        "statement of the continuing block",
+                        "if statement containing break statement is not the last statement of the "
+                        "continuing block",
                         if_stmt->Declaration()->source);
                 }
             }
@@ -1570,10 +1556,8 @@
                     if (c->Declaration() != s->Declaration()->body.Back()) {
                         return true;
                     }
-                    AddError(
-                        "a fallthrough statement must not be used in the last switch "
-                        "case",
-                        stmt->Declaration()->source);
+                    AddError("a fallthrough statement must not be used in the last switch case",
+                             stmt->Declaration()->source);
                     return false;
                 }
             }
@@ -1826,8 +1810,8 @@
                 IsValidationEnabled(param->Declaration()->attributes,
                                     ast::DisabledValidation::kIgnoreInvalidPointerArgument)) {
                 AddError(
-                    "expected an address-of expression of a variable identifier "
-                    "expression or a function parameter",
+                    "expected an address-of expression of a variable identifier expression or a "
+                    "function parameter",
                     arg_expr->source);
                 return false;
             }
@@ -1886,8 +1870,7 @@
             auto* value_ty = sem_.TypeOf(value);
             if (member->Type() != value_ty->UnwrapRef()) {
                 AddError(
-                    "type in struct constructor does not match struct member type: "
-                    "expected '" +
+                    "type in struct constructor does not match struct member type: expected '" +
                         sem_.TypeNameOf(member->Type()) + "', found '" + sem_.TypeNameOf(value_ty) +
                         "'",
                     value->source);
@@ -2140,9 +2123,8 @@
         // at least the size of the element type, and be a multiple of the
         // element type's alignment value.
         AddError(
-            "arrays decorated with the stride attribute must have a stride "
-            "that is at least the size of the element type, and be a multiple "
-            "of the element type's alignment value",
+            "arrays decorated with the stride attribute must have a stride that is at least the "
+            "size of the element type, and be a multiple of the element type's alignment value",
             attr->source);
         return false;
     }
@@ -2189,8 +2171,7 @@
             }
         } else if (!IsFixedFootprint(member->Type())) {
             AddError(
-                "a struct that contains a runtime array cannot be nested inside "
-                "another struct",
+                "a struct that contains a runtime array cannot be nested inside another struct",
                 member->Declaration()->source);
             return false;
         }
@@ -2283,8 +2264,8 @@
         AddError("cannot apply 'location' attribute to declaration of type '" + invalid_type + "'",
                  source);
         AddNote(
-            "'location' attribute must only be applied to declarations of "
-            "numeric scalar or numeric vector type",
+            "'location' attribute must only be applied to declarations of numeric scalar or "
+            "numeric vector type",
             loc_attr->source);
         return false;
     }
@@ -2304,11 +2285,9 @@
                        const sem::Type* ret_type,
                        sem::Statement* current_statement) const {
     if (func_type->UnwrapRef() != ret_type) {
-        AddError(
-            "return statement type must match its function "
-            "return type, returned '" +
-                sem_.TypeNameOf(ret_type) + "', expected '" + sem_.TypeNameOf(func_type) + "'",
-            ret->source);
+        AddError("return statement type must match its function return type, returned '" +
+                     sem_.TypeNameOf(ret_type) + "', expected '" + sem_.TypeNameOf(func_type) + "'",
+                 ret->source);
         return false;
     }
 
@@ -2327,10 +2306,8 @@
 bool Validator::SwitchStatement(const ast::SwitchStatement* s) {
     auto* cond_ty = sem_.TypeOf(s->condition)->UnwrapRef();
     if (!cond_ty->is_integer_scalar()) {
-        AddError(
-            "switch statement selector expression must be of a "
-            "scalar integer type",
-            s->condition->source);
+        AddError("switch statement selector expression must be of a scalar integer type",
+                 s->condition->source);
         return false;
     }
 
@@ -2351,8 +2328,7 @@
         for (auto* selector : case_stmt->selectors) {
             if (cond_ty != sem_.TypeOf(selector)) {
                 AddError(
-                    "the case selector values must have the same "
-                    "type as the selector expression.",
+                    "the case selector values must have the same type as the selector expression.",
                     case_stmt->source);
                 return false;
             }
@@ -2397,8 +2373,8 @@
         if (!ty->IsConstructible() &&
             !ty->IsAnyOf<sem::Pointer, sem::Texture, sem::Sampler, sem::AbstractNumeric>()) {
             AddError("cannot assign '" + sem_.TypeNameOf(rhs_ty) +
-                         "' to '_'. '_' can only be assigned a constructible, pointer, "
-                         "texture or sampler type",
+                         "' to '_'. '_' can only be assigned a constructible, pointer, texture or "
+                         "sampler type",
                      rhs->source);
             return false;
         }
diff --git a/src/tint/sem/pointer.cc b/src/tint/sem/pointer.cc
index 3918233..cb73724 100644
--- a/src/tint/sem/pointer.cc
+++ b/src/tint/sem/pointer.cc
@@ -25,7 +25,7 @@
 Pointer::Pointer(const Type* subtype, ast::AddressSpace address_space, ast::Access access)
     : subtype_(subtype), address_space_(address_space), access_(access) {
     TINT_ASSERT(Semantic, !subtype->Is<Reference>());
-    TINT_ASSERT(Semantic, access != ast::Access::kUndefined);
+    TINT_ASSERT(Semantic, access != ast::Access::kInvalid);
 }
 
 size_t Pointer::Hash() const {
diff --git a/src/tint/sem/reference.cc b/src/tint/sem/reference.cc
index a3a9f24..297b779 100644
--- a/src/tint/sem/reference.cc
+++ b/src/tint/sem/reference.cc
@@ -24,7 +24,7 @@
 Reference::Reference(const Type* subtype, ast::AddressSpace address_space, ast::Access access)
     : subtype_(subtype), address_space_(address_space), access_(access) {
     TINT_ASSERT(Semantic, !subtype->Is<Reference>());
-    TINT_ASSERT(Semantic, access != ast::Access::kUndefined);
+    TINT_ASSERT(Semantic, access != ast::Access::kInvalid);
 }
 
 size_t Reference::Hash() const {
diff --git a/src/tint/templates/enums.tmpl.inc b/src/tint/templates/enums.tmpl.inc
index d05d763..4726d80 100644
--- a/src/tint/templates/enums.tmpl.inc
+++ b/src/tint/templates/enums.tmpl.inc
@@ -28,6 +28,14 @@
 /// @returns the parsed enum, or {{$enum}}::kInvalid if the string could not be parsed.
 {{$enum}} Parse{{$enum}}(std::string_view str);
 
+constexpr const char* k{{$enum}}Strings[] = {
+{{-   range $entry := $.Entries }}
+{{-     if not $entry.IsInternal}}
+    "{{$entry.Name}}",
+{{-     end }}
+{{-   end }}
+};
+
 {{- end -}}
 
 
diff --git a/src/tint/transform/binding_remapper.cc b/src/tint/transform/binding_remapper.cc
index a9a3dfb..66d8e99 100644
--- a/src/tint/transform/binding_remapper.cc
+++ b/src/tint/transform/binding_remapper.cc
@@ -121,7 +121,7 @@
             auto ac_it = remappings->access_controls.find(from);
             if (ac_it != remappings->access_controls.end()) {
                 ast::Access ac = ac_it->second;
-                if (ac > ast::Access::kLastValid) {
+                if (ac == ast::Access::kInvalid) {
                     ctx.dst->Diagnostics().add_error(
                         diag::System::Transform,
                         "invalid access mode (" + std::to_string(static_cast<uint32_t>(ac)) + ")");
diff --git a/src/tint/transform/canonicalize_entry_point_io.cc b/src/tint/transform/canonicalize_entry_point_io.cc
index 0ff1084..38a1b28 100644
--- a/src/tint/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/transform/canonicalize_entry_point_io.cc
@@ -45,6 +45,42 @@
     std::optional<uint32_t> location;
 };
 
+/// FXC is sensitive to field order in structures, this is used by StructMemberComparator to ensure
+/// that FXC is happy with the order of emitted fields.
+uint32_t BuiltinOrder(ast::BuiltinValue builtin) {
+    switch (builtin) {
+        case ast::BuiltinValue::kPosition:
+            return 1;
+        case ast::BuiltinValue::kVertexIndex:
+            return 2;
+        case ast::BuiltinValue::kInstanceIndex:
+            return 3;
+        case ast::BuiltinValue::kFrontFacing:
+            return 4;
+        case ast::BuiltinValue::kFragDepth:
+            return 5;
+        case ast::BuiltinValue::kLocalInvocationId:
+            return 6;
+        case ast::BuiltinValue::kLocalInvocationIndex:
+            return 7;
+        case ast::BuiltinValue::kGlobalInvocationId:
+            return 8;
+        case ast::BuiltinValue::kWorkgroupId:
+            return 9;
+        case ast::BuiltinValue::kNumWorkgroups:
+            return 10;
+        case ast::BuiltinValue::kSampleIndex:
+            return 11;
+        case ast::BuiltinValue::kSampleMask:
+            return 12;
+        case ast::BuiltinValue::kPointSize:
+            return 13;
+        default:
+            break;
+    }
+    return 0;
+}
+
 /// Comparison function used to reorder struct members such that all members with
 /// location attributes appear first (ordered by location slot), followed by
 /// those with builtin attributes.
@@ -68,8 +104,8 @@
             // `b` has location attribute and `a` does not: `b` goes first.
             return false;
         }
-        // Both are builtins: order doesn't matter, just use enum value.
-        return a_blt->builtin < b_blt->builtin;
+        // Both are builtins: order matters for FXC.
+        return BuiltinOrder(a_blt->builtin) < BuiltinOrder(b_blt->builtin);
     }
 }
 
@@ -187,7 +223,7 @@
                 (ast::HasAttribute<ast::LocationAttribute>(attributes) ||
                  cfg.shader_style == ShaderStyle::kSpirv)) {
                 attributes.Push(ctx.dst->Interpolate(ast::InterpolationType::kFlat,
-                                                     ast::InterpolationSampling::kNone));
+                                                     ast::InterpolationSampling::kInvalid));
             }
 
             // Disable validation for use of the `input` address space.
@@ -256,7 +292,7 @@
             ast::HasAttribute<ast::LocationAttribute>(attributes) &&
             !ast::HasAttribute<ast::InterpolateAttribute>(attributes)) {
             attributes.Push(ctx.dst->Interpolate(ast::InterpolationType::kFlat,
-                                                 ast::InterpolationSampling::kNone));
+                                                 ast::InterpolationSampling::kInvalid));
         }
 
         // In GLSL, if it's a builtin, override the name with the
diff --git a/src/tint/transform/clamp_frag_depth.cc b/src/tint/transform/clamp_frag_depth.cc
new file mode 100644
index 0000000..1be231a
--- /dev/null
+++ b/src/tint/transform/clamp_frag_depth.cc
@@ -0,0 +1,202 @@
+// Copyright 2022 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/transform/clamp_frag_depth.h"
+
+ #include <utility>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/builtin_attribute.h"
+#include "src/tint/ast/builtin_value.h"
+#include "src/tint/ast/function.h"
+#include "src/tint/ast/module.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/type.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/statement.h"
+#include "src/tint/sem/struct.h"
+#include "src/tint/utils/scoped_assignment.h"
+#include "src/tint/utils/vector.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::transform::ClampFragDepth);
+
+namespace tint::transform {
+
+namespace {
+
+bool ContainsFragDepth(utils::VectorRef<const ast::Attribute*> attributes) {
+    for (auto* attribute : attributes) {
+        if (auto* builtin_attribute = attribute->As<ast::BuiltinAttribute>()) {
+            if (builtin_attribute->builtin == ast::BuiltinValue::kFragDepth) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+bool ReturnsFragDepthAsValue(const ast::Function* fn) {
+    return ContainsFragDepth(fn->return_type_attributes);
+}
+
+bool ReturnsFragDepthInStruct(const sem::Info& sem, const ast::Function* fn) {
+    if (auto* struct_ty = sem.Get(fn)->ReturnType()->As<sem::Struct>()) {
+        for (auto* member : struct_ty->Members()) {
+            if (ContainsFragDepth(member->Declaration()->attributes)) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+}  // anonymous namespace
+
+ClampFragDepth::ClampFragDepth() = default;
+ClampFragDepth::~ClampFragDepth() = default;
+
+bool ClampFragDepth::ShouldRun(const Program* program, const DataMap&) const {
+    auto& sem = program->Sem();
+
+    for (auto* fn : program->AST().Functions()) {
+        if (fn->PipelineStage() == ast::PipelineStage::kFragment &&
+            (ReturnsFragDepthAsValue(fn) || ReturnsFragDepthInStruct(sem, fn))) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void ClampFragDepth::Run(CloneContext& ctx, const DataMap&, DataMap&) const {
+    // Abort on any use of push constants in the module.
+    for (auto* global : ctx.src->AST().GlobalVariables()) {
+        if (auto* var = global->As<ast::Var>()) {
+            if (var->declared_address_space == ast::AddressSpace::kPushConstant) {
+                TINT_ICE(Transform, ctx.dst->Diagnostics())
+                    << "ClampFragDepth doesn't know how to handle module that already use push "
+                       "constants.";
+                return;
+            }
+        }
+    }
+
+    auto& b = *ctx.dst;
+    auto& sem = ctx.src->Sem();
+    auto& sym = ctx.src->Symbols();
+
+    // At least one entry-point needs clamping. Add the following to the module:
+    //
+    //   enable chromium_experimental_push_constant;
+    //
+    //   struct FragDepthClampArgs {
+    //       min : f32,
+    //       max : f32,
+    //   }
+    //   var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+    //
+    //   fn clamp_frag_depth(v : f32) -> f32 {
+    //       return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+    //   }
+    b.Enable(ast::Extension::kChromiumExperimentalPushConstant);
+
+    b.Structure(b.Symbols().New("FragDepthClampArgs"),
+                utils::Vector{b.Member("min", b.ty.f32()), b.Member("max", b.ty.f32())});
+
+    auto args_sym = b.Symbols().New("frag_depth_clamp_args");
+    b.GlobalVar(args_sym, b.ty.type_name("FragDepthClampArgs"), ast::AddressSpace::kPushConstant);
+
+    auto base_fn_sym = b.Symbols().New("clamp_frag_depth");
+    b.Func(base_fn_sym, utils::Vector{b.Param("v", b.ty.f32())}, b.ty.f32(),
+           utils::Vector{b.Return(b.Call("clamp", "v", b.MemberAccessor(args_sym, "min"),
+                                         b.MemberAccessor(args_sym, "max")))});
+
+    // If true, the currently cloned function returns frag depth directly as a scalar
+    bool returns_frag_depth_as_value = false;
+
+    // If valid, the currently cloned function returns frag depth in a struct
+    // The symbol is the name of the helper function to apply the depth clamping.
+    Symbol returns_frag_depth_as_struct_helper;
+
+    // Map of io struct to helper function to return the structure with the depth clamping applied.
+    utils::Hashmap<const ast::Struct*, Symbol, 4u> io_structs_clamp_helpers;
+
+    // Register a callback that will be called for each visted AST function.
+    // This call wraps the cloning of the function's statements, and will assign to
+    // `returns_frag_depth_as_value` or `returns_frag_depth_as_struct_helper` if the function's
+    // return value requires depth clamping.
+    ctx.ReplaceAll([&](const ast::Function* fn) {
+        if (fn->PipelineStage() != ast::PipelineStage::kFragment) {
+            return ctx.CloneWithoutTransform(fn);
+        }
+
+        if (ReturnsFragDepthAsValue(fn)) {
+            TINT_SCOPED_ASSIGNMENT(returns_frag_depth_as_value, true);
+            return ctx.CloneWithoutTransform(fn);
+        }
+
+        if (ReturnsFragDepthInStruct(sem, fn)) {
+            // At most once per I/O struct, add the conversion function:
+            //
+            //   fn clamp_frag_depth_S(s : S) -> S {
+            //       return S(s.first, s.second, clamp_frag_depth(s.frag_depth), s.last);
+            //   }
+            auto* struct_ty = sem.Get(fn)->ReturnType()->As<sem::Struct>()->Declaration();
+            auto helper = io_structs_clamp_helpers.GetOrCreate(struct_ty, [&] {
+                auto* return_ty = fn->return_type;
+                auto fn_sym = b.Symbols().New("clamp_frag_depth_" +
+                                              sym.NameFor(return_ty->As<ast::TypeName>()->name));
+
+                utils::Vector<const ast::Expression*, 8u> constructor_args;
+                for (auto* member : struct_ty->members) {
+                    const ast::Expression* arg = b.MemberAccessor("s", ctx.Clone(member->symbol));
+                    if (ContainsFragDepth(member->attributes)) {
+                        arg = b.Call(base_fn_sym, arg);
+                    }
+                    constructor_args.Push(arg);
+                }
+                utils::Vector params{b.Param("s", ctx.Clone(return_ty))};
+                utils::Vector body{
+                    b.Return(b.Construct(ctx.Clone(return_ty), std::move(constructor_args))),
+                };
+                b.Func(fn_sym, params, ctx.Clone(return_ty), body);
+                return fn_sym;
+            });
+
+            TINT_SCOPED_ASSIGNMENT(returns_frag_depth_as_struct_helper, helper);
+            return ctx.CloneWithoutTransform(fn);
+        }
+
+        return ctx.CloneWithoutTransform(fn);
+    });
+
+    // Replace the return statements `return expr` with `return clamp_frag_depth(expr)`.
+    ctx.ReplaceAll([&](const ast::ReturnStatement* stmt) -> const ast::ReturnStatement* {
+        if (returns_frag_depth_as_value) {
+            return b.Return(stmt->source, b.Call(base_fn_sym, ctx.Clone(stmt->value)));
+        }
+        if (returns_frag_depth_as_struct_helper.IsValid()) {
+            return b.Return(stmt->source,
+                            b.Call(returns_frag_depth_as_struct_helper, ctx.Clone(stmt->value)));
+        }
+        return nullptr;
+    });
+
+    ctx.Clone();
+}
+
+}  // namespace tint::transform
diff --git a/src/tint/transform/clamp_frag_depth.h b/src/tint/transform/clamp_frag_depth.h
new file mode 100644
index 0000000..3b15f11
--- /dev/null
+++ b/src/tint/transform/clamp_frag_depth.h
@@ -0,0 +1,81 @@
+// Copyright 2022 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_TRANSFORM_CLAMP_FRAG_DEPTH_H_
+#define SRC_TINT_TRANSFORM_CLAMP_FRAG_DEPTH_H_
+
+#include "src/tint/transform/transform.h"
+
+// Forward declarations
+namespace tint {
+class CloneContext;
+}  // namespace tint
+
+namespace tint::transform {
+
+/// Add clamping of the `@builtin(frag_depth)` output of fragment shaders using two push constants
+/// provided by the outside environment. For example the following code:
+///
+/// ```
+///   @fragment fn main() -> @builtin(frag_depth) f32 {
+///       return 0.0;
+///   }
+/// ```
+///
+/// Is transformed to:
+///
+/// ```
+///   enable chromium_experimental_push_constant;
+///
+///   struct FragDepthClampArgs {
+///     min : f32,
+///     max : f32,
+///   }
+///
+///   var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+///
+///   fn clamp_frag_depth(v : f32) -> f32 {
+///     return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+///   }
+///
+///   @fragment
+///   fn main() -> @builtin(frag_depth) f32 {
+///     return clamp_frag_depth(0.0);
+///   }
+/// ```
+class ClampFragDepth final : public Castable<ClampFragDepth, Transform> {
+  public:
+    /// Constructor
+    ClampFragDepth();
+    /// Destructor
+    ~ClampFragDepth() override;
+
+    /// @param program the program to inspect
+    /// @param data optional extra transform-specific input data
+    /// @returns true if this transform should be run for the given program
+    bool ShouldRun(const Program* program, const DataMap& data = {}) const override;
+
+  protected:
+    /// Runs the transform using the CloneContext built for transforming a
+    /// program. Run() is responsible for calling Clone() on the CloneContext.
+    /// @param ctx the CloneContext primed with the input program and
+    /// ProgramBuilder
+    /// @param inputs optional extra transform-specific input data
+    /// @param outputs optional extra transform-specific output data
+    void Run(CloneContext& ctx, const DataMap& inputs, DataMap& outputs) const override;
+};
+
+}  // namespace tint::transform
+
+#endif  // SRC_TINT_TRANSFORM_CLAMP_FRAG_DEPTH_H_
diff --git a/src/tint/transform/clamp_frag_depth_test.cc b/src/tint/transform/clamp_frag_depth_test.cc
new file mode 100644
index 0000000..b94d5af
--- /dev/null
+++ b/src/tint/transform/clamp_frag_depth_test.cc
@@ -0,0 +1,381 @@
+// Copyright 2021 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/transform/clamp_frag_depth.h"
+
+#include "src/tint/transform/test_helper.h"
+
+namespace tint::transform {
+namespace {
+
+using ClampFragDepthTest = TransformTest;
+
+TEST_F(ClampFragDepthTest, ShouldRunEmptyModule) {
+    auto* src = R"()";
+
+    EXPECT_FALSE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, ShouldRunNoFragmentShader) {
+    auto* src = R"(
+        fn f() -> f32 {
+            return 0.0;
+        }
+
+        @compute @workgroup_size(1) fn cs() {
+        }
+
+        @vertex fn vs() -> @builtin(position) vec4<f32> {
+            return vec4<f32>();
+        }
+    )";
+
+    EXPECT_FALSE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, ShouldRunFragmentShaderNoReturnType) {
+    auto* src = R"(
+        @fragment fn main() {
+        }
+    )";
+
+    EXPECT_FALSE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, ShouldRunFragmentShaderNoFragDepth) {
+    auto* src = R"(
+        @fragment fn main() -> @location(0) f32 {
+            return 0.0;
+        }
+
+        struct S {
+            @location(0) a : f32,
+            @builtin(sample_mask) b : u32,
+        }
+        @fragment fn main2() -> S {
+            return S();
+        }
+    )";
+
+    EXPECT_FALSE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, ShouldRunFragDepthAsDirectReturn) {
+    auto* src = R"(
+        @fragment fn main() -> @builtin(frag_depth) f32 {
+            return 0.0;
+        }
+    )";
+
+    EXPECT_TRUE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, ShouldRunFragDepthInStruct) {
+    auto* src = R"(
+        struct S {
+            @location(0) a : f32,
+            @builtin(frag_depth) b : f32,
+            @location(1) c : f32,
+        }
+        @fragment fn main() -> S {
+            return S();
+        }
+    )";
+
+    EXPECT_TRUE(ShouldRun<ClampFragDepth>(src));
+}
+
+TEST_F(ClampFragDepthTest, SingleReturnOfFragDepth) {
+    auto* src = R"(
+        @fragment fn main() -> @builtin(frag_depth) f32 {
+            return 0.0;
+        }
+    )";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+@fragment
+fn main() -> @builtin(frag_depth) f32 {
+  return clamp_frag_depth(0.0);
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(ClampFragDepthTest, MultipleReturnOfFragDepth) {
+    auto* src = R"(
+        @fragment fn main() -> @builtin(frag_depth) f32 {
+            if (false) {
+                return 1.0;
+            }
+            return 0.0;
+        }
+    )";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+@fragment
+fn main() -> @builtin(frag_depth) f32 {
+  if (false) {
+    return clamp_frag_depth(1.0);
+  }
+  return clamp_frag_depth(0.0);
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(ClampFragDepthTest, OtherFunctionWithoutFragDepth) {
+    auto* src = R"(
+        @fragment fn main() -> @builtin(frag_depth) f32 {
+            return 0.0;
+        }
+        @fragment fn friend() -> @location(0) f32 {
+            return 0.0;
+        }
+    )";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+@fragment
+fn main() -> @builtin(frag_depth) f32 {
+  return clamp_frag_depth(0.0);
+}
+
+@fragment
+fn friend() -> @location(0) f32 {
+  return 0.0;
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(ClampFragDepthTest, SimpleReturnOfStruct) {
+    auto* src = R"(
+        struct S {
+            @builtin(frag_depth) frag_depth : f32,
+        }
+
+        @fragment fn main() -> S {
+            return S(0.0);
+        }
+    )";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+struct S {
+  @builtin(frag_depth)
+  frag_depth : f32,
+}
+
+fn clamp_frag_depth_S(s : S) -> S {
+  return S(clamp_frag_depth(s.frag_depth));
+}
+
+@fragment
+fn main() -> S {
+  return clamp_frag_depth_S(S(0.0));
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(ClampFragDepthTest, MixOfFunctionReturningStruct) {
+    auto* src = R"(
+        struct S {
+            @builtin(frag_depth) frag_depth : f32,
+        }
+        struct S2 {
+            @builtin(frag_depth) frag_depth : f32,
+        }
+
+        @fragment fn returnS() -> S {
+            return S(0.0);
+        }
+        @fragment fn againReturnS() -> S {
+            return S(0.0);
+        }
+        @fragment fn returnS2() -> S2 {
+            return S2(0.0);
+        }
+    )";
+
+    // clamp_frag_depth_S is emitted only once.
+    // S2 gets its own clamping function.
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+struct S {
+  @builtin(frag_depth)
+  frag_depth : f32,
+}
+
+struct S2 {
+  @builtin(frag_depth)
+  frag_depth : f32,
+}
+
+fn clamp_frag_depth_S(s : S) -> S {
+  return S(clamp_frag_depth(s.frag_depth));
+}
+
+@fragment
+fn returnS() -> S {
+  return clamp_frag_depth_S(S(0.0));
+}
+
+@fragment
+fn againReturnS() -> S {
+  return clamp_frag_depth_S(S(0.0));
+}
+
+fn clamp_frag_depth_S2(s : S2) -> S2 {
+  return S2(clamp_frag_depth(s.frag_depth));
+}
+
+@fragment
+fn returnS2() -> S2 {
+  return clamp_frag_depth_S2(S2(0.0));
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(ClampFragDepthTest, ComplexIOStruct) {
+    auto* src = R"(
+        struct S {
+            @location(0) blou : vec4<f32>,
+            @location(1) bi : vec4<f32>,
+            @builtin(frag_depth) frag_depth : f32,
+            @location(2) boul : i32,
+            @builtin(sample_mask) ga : u32,
+        }
+
+        @fragment fn main() -> S {
+            return S(vec4<f32>(), vec4<f32>(), 0.0, 1, 0u);
+        }
+    )";
+
+    auto* expect = R"(
+enable chromium_experimental_push_constant;
+
+struct FragDepthClampArgs {
+  min : f32,
+  max : f32,
+}
+
+var<push_constant> frag_depth_clamp_args : FragDepthClampArgs;
+
+fn clamp_frag_depth(v : f32) -> f32 {
+  return clamp(v, frag_depth_clamp_args.min, frag_depth_clamp_args.max);
+}
+
+struct S {
+  @location(0)
+  blou : vec4<f32>,
+  @location(1)
+  bi : vec4<f32>,
+  @builtin(frag_depth)
+  frag_depth : f32,
+  @location(2)
+  boul : i32,
+  @builtin(sample_mask)
+  ga : u32,
+}
+
+fn clamp_frag_depth_S(s : S) -> S {
+  return S(s.blou, s.bi, clamp_frag_depth(s.frag_depth), s.boul, s.ga);
+}
+
+@fragment
+fn main() -> S {
+  return clamp_frag_depth_S(S(vec4<f32>(), vec4<f32>(), 0.0, 1, 0u));
+}
+)";
+
+    auto got = Run<ClampFragDepth>(src);
+    EXPECT_EQ(expect, str(got));
+}
+
+}  // namespace
+}  // namespace tint::transform
diff --git a/src/tint/transform/std140_exhaustive_test.cc b/src/tint/transform/std140_exhaustive_test.cc
new file mode 100644
index 0000000..01d2dae
--- /dev/null
+++ b/src/tint/transform/std140_exhaustive_test.cc
@@ -0,0 +1,4872 @@
+// Copyright 2022 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/transform/std140.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/transform/test_helper.h"
+#include "src/tint/utils/string.h"
+
+namespace tint::transform {
+namespace {
+
+enum class MatrixType { f32, f16 };
+
+struct MatrixCase {
+    uint32_t columns;
+    uint32_t rows;
+    MatrixType type;
+
+    size_t ElementSize() const { return type == MatrixType::f16 ? 2 : 4; }
+
+    size_t ColumnVectorAlign() const { return (rows == 3 ? 4 : rows) * ElementSize(); }
+
+    bool NotStd140Compatible() const { return ColumnVectorAlign() != 16; }
+
+    // Return if this matrix type can be used as element type of an uniform buffer, i.e. the
+    // array stride is multiple of 16.
+    bool CanBeUsedAsUniformArrayElememts() const {
+        const size_t array_stride = columns * ColumnVectorAlign();
+        return (array_stride % 16 == 0);
+    }
+
+    std::string Shape() const { return std::to_string(columns) + "x" + std::to_string(rows); }
+
+    std::string ElementType() const { return type == MatrixType::f16 ? "f16" : "f32"; }
+
+    std::string Mat() const { return "mat" + Shape() + "<" + ElementType() + ">"; }
+
+    std::string ColumnVector() const {
+        return "vec" + std::to_string(rows) + "<" + (type == MatrixType::f32 ? "f32" : "f16") + ">";
+    }
+
+    std::string ColumnVectorSwizzle() const {
+        switch (rows) {
+            case 2:
+                return "yx";
+            case 3:
+                return "yzx";
+            case 4:
+                return "wzxy";
+        }
+        return "";
+    }
+
+    // For each column, replaces "${col_id_for_tmpl}" by column index in `tmpl` to get a string, and
+    // join all these strings with `seperator`. If `tmpl_for_last_column` is not empty, use it
+    // instead of `tmpl` for the last column.
+    std::string JoinTemplatedStringForEachMatrixColumn(
+        std::string tmpl,
+        std::string seperator,
+        std::string tmpl_for_last_column = "") const {
+        std::string result;
+        if (tmpl_for_last_column.size() == 0) {
+            tmpl_for_last_column = tmpl;
+        }
+        for (size_t c = 0; c < columns - 1; c++) {
+            if (c > 0) {
+                result += seperator;
+            }
+            std::string string_for_current_column =
+                utils::ReplaceAll(tmpl, "${col_id_for_tmpl}", std::to_string(c));
+            result += string_for_current_column;
+        }
+        result += seperator;
+        std::string string_for_last_column = utils::ReplaceAll(
+            tmpl_for_last_column, "${col_id_for_tmpl}", std::to_string(columns - 1));
+        result += string_for_last_column;
+        return result;
+    }
+
+    std::string ExpendedColumnVectors(uint32_t leading_space, std::string name) const {
+        std::string space(leading_space, ' ');
+        return JoinTemplatedStringForEachMatrixColumn(
+            space + name + "${col_id_for_tmpl} : " + ColumnVector() + ",", "\n");
+    }
+
+    std::string ExpendedColumnVectorsInline(std::string name, std::string seperator) const {
+        return JoinTemplatedStringForEachMatrixColumn(name + "${col_id_for_tmpl}", seperator);
+    }
+
+    std::string ExpendedColumnVectorsWithLastSize(uint32_t leading_space,
+                                                  std::string name,
+                                                  uint32_t last_size) const {
+        std::string space(leading_space, ' ');
+        return JoinTemplatedStringForEachMatrixColumn(
+            space + name + "${col_id_for_tmpl} : " + ColumnVector() + ",", "\n",
+            space + "@size(" + std::to_string(last_size) + ")\n" + space + name +
+                "${col_id_for_tmpl} : " + ColumnVector() + ",");
+    }
+
+    // Replace user-given fields and predefined fields in a given string `str`.
+    // First, for each pair of string in `replacement_pairs`, replace all occurrences of the first
+    // string of pair with second string. Then, replace several predefined fields with the matrix
+    // information. E.g. for a matrix mat4x3<f32>, would replace "${mat}" with "mat4x3<f32>",
+    // replace "${shape}" with "4x3", "${elem_type}" with "f32", "${col_vector_type}" with
+    // "vec3<f32>", and "${swizzle}" with "yzx".
+    std::string ReplaceFieldsInString(
+        std::string str,
+        std::initializer_list<std::pair<std::string, std::string>> replacement_pairs = {}) const {
+        for (auto& replace : replacement_pairs) {
+            str = utils::ReplaceAll(str, replace.first, replace.second);
+        }
+        str = utils::ReplaceAll(str, "${mat}", Mat());
+        str = utils::ReplaceAll(str, "${shape}", Shape());
+        str = utils::ReplaceAll(str, "${elem_type}", ElementType());
+        str = utils::ReplaceAll(str, "${col_vector_type}", ColumnVector());
+        str = utils::ReplaceAll(str, "${swizzle}", ColumnVectorSwizzle());
+        return str;
+    }
+};
+
+inline std::ostream& operator<<(std::ostream& os, const MatrixCase& c) {
+    return os << c.Mat();
+}
+
+using Std140Test_Matrix = TransformTestWithParam<MatrixCase>;
+
+TEST_P(Std140Test_Matrix, SingleStructMatUniform) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")}});
+
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, CustomAlign) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @align(128)
+  m : ${mat},
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @align(128)
+  m : ${mat},
+  after : i32,
+}
+
+struct S_std140 {
+  before : i32,
+  @align(128i)
+${col_vectors}
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, CustomSizeMat) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @size(128)
+  m : ${mat},
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        uint32_t last_size =
+            128 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+
+        expect = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @size(128)
+  m : ${mat},
+  after : i32,
+}
+
+struct S_std140 {
+  before : i32,
+${col_vectors}
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, CustomAlignAndSize) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @align(128) @size(128)
+  m : ${mat},
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        uint32_t last_size =
+            128 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+
+        expect = R"(
+enable f16;
+
+struct S {
+  before : i32,
+  @align(128) @size(128)
+  m : ${mat},
+  after : i32,
+}
+
+struct S_std140 {
+  before : i32,
+  @align(128i)
+${col_vectors}
+  after : i32,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, MatrixUsageInForLoop) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  for(var i = u32(s.m[0][0]); (i < u32(s.m[i][1])); i += u32(s.m[1][i])) {
+  }
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m_p0_1(p0 : u32) -> ${elem_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  for(var i = u32(s.m_0[0u]); (i < u32(load_s_m_p0_1(u32(i)))); i += u32(s.m_1[i])) {
+  }
+}
+)";
+
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.m_0[1u];
+        //   }
+        //   case 1u: {
+        //     return s.m_1[1u];
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.m_${col_id_for_tmpl}[1u];
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadMatrix) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> m : ${mat};
+
+fn f() {
+  let l = m;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> m : mat${shape}_${elem_type};
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = conv_mat${shape}_${elem_type}(m);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadColumn_ConstIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let l = a[${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn f() {
+  let l = a.col${cloumn_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${cloumn_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${cloumn_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadColumn_VariableIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn load_a_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a.col0;
+        //   }
+        //   case 1u: {
+        //     return a.col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a.col${col_id_for_tmpl};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadColumnSwizzle_ConstIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let l = a[${cloumn_index}].${swizzle};
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn f() {
+  let l = a.col${cloumn_index}.${swizzle};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${cloumn_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${cloumn_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadColumnSwizzle_VariableIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let I = 1;
+  let l = a[I].${swizzle};
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn load_a_p0_${swizzle}(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0_${swizzle}(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a.col0.${swizzle};
+        //   }
+        //   case 1u: {
+        //     return a.col1.${swizzle};
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a.col${col_id_for_tmpl}.${swizzle};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadScalar_ConstColumnIndex_ConstRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let l = a[${col_index}][${row_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn f() {
+  let l = a.col${col_index}[${row_index}u];
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        for (uint32_t row = 0; row < matrix.rows; row++) {
+            std::string src = utils::ReplaceAll(tmpl_src, "${col_index}", std::to_string(col));
+            src = utils::ReplaceAll(src, "${row_index}", std::to_string(row));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${col_index}", std::to_string(col));
+            expect = utils::ReplaceAll(expect, "${row_index}", std::to_string(row));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got)) << "accessing col " << col << " row " << row;
+        }
+    }
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadScalar_VariableColumnIndex_ConstRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let I = 0;
+  let l = a[I][${row_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn load_a_p0_${row_index}(p0 : u32) -> ${elem_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_a_p0_${row_index}(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a.col0[${row_index}u];
+        //   }
+        //   case 1u: {
+        //     return a.col1[${row_index}u];
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a.col${col_id_for_tmpl}[${row_index}u];
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t row = 0; row < matrix.rows; row++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${row_index}", std::to_string(row));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${row_index}", std::to_string(row));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing row " << row;
+    }
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadScalar_ConstColumnIndex_VariableRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let I = 0;
+  let l = a[${col_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn f() {
+  let I = 0;
+  let l = a.col${col_index}[I];
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${col_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${col_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, MatUniform_LoadScalar_VariableColumnIndex_VariableRowIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : ${mat};
+
+fn f() {
+  let I = 0;
+  let l = a[I][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : mat${shape}_${elem_type};
+
+fn load_a_p0_p1(p0 : u32, p1 : u32) -> ${elem_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_a_p0_p1(u32(I), u32(I));
+}
+)";
+
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a.col0[p1];
+        //   }
+        //   case 1u: {
+        //     return a.col1[p1];
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a.col${col_id_for_tmpl}[p1];
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_NameCollision) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m_1 : i32,
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m_1 : i32,
+  m : ${mat},
+}
+
+struct S_std140 {
+  m_1 : i32,
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m__")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadStruct) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn conv_S(val : S_std140) -> S {
+  return S(${mat}(${col_vectors_inline}));
+}
+
+fn f() {
+  let l = conv_S(s);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadMatrix) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.m;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m() -> ${mat} {
+  let s = &(s);
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = load_s_m();
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadColumn_ConstIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.m[${col_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn f() {
+  let l = s.m_${col_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${col_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${col_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadColumn_VariableIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 0;
+  let l = s.m[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_s_m_p0(u32(I));
+}
+)";
+
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.m_0;
+        //   }
+        //   case 1u: {
+        //     return s.m_1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.m_${col_id_for_tmpl};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vector_type}", matrix.ColumnVector()},
+                     {"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadScalar_ConstColumnIndex_ConstRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.m[${col_index}][${row_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn f() {
+  let l = s.m_${col_index}[${row_index}u];
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        for (uint32_t row = 0; row < matrix.rows; row++) {
+            std::string src = utils::ReplaceAll(tmpl_src, "${col_index}", std::to_string(col));
+            src = utils::ReplaceAll(src, "${row_index}", std::to_string(row));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${col_index}", std::to_string(col));
+            expect = utils::ReplaceAll(expect, "${row_index}", std::to_string(row));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got)) << "accessing col " << col << " row " << row;
+        }
+    }
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadScalar_VariableColumnIndex_ConstRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 0;
+  let l = s.m[I][${row_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m_p0_${row_index}(p0 : u32) -> ${elem_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_s_m_p0_${row_index}(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.m_0[${row_index}u];
+        //   }
+        //   case 1u: {
+        //     return s.m_1[${row_index}u];
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.m_${col_id_for_tmpl}[${row_index}u];
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t row = 0; row < matrix.rows; row++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${row_index}", std::to_string(row));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${row_index}", std::to_string(row));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing row " << row;
+    }
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadScalar_ConstColumnIndex_VariableRowIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 0;
+  let l = s.m[${col_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn f() {
+  let I = 0;
+  let l = s.m_${col_index}[I];
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${col_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${col_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, StructMatUniform_LoadScalar_VariableColumnIndex_VariableRowIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 0;
+  let l = s.m[I][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m_p0_p1(p0 : u32, p1 : u32) -> ${elem_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_s_m_p0_p1(u32(I), u32(I));
+}
+)";
+
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.m_0[p1];
+        //   }
+        //   case 1u: {
+        //     return s.m_1[p1];
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.m_${col_id_for_tmpl}[p1];
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "m_")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadArray) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let l = a;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(${mat}(${col_vectors_inline}));
+}
+
+fn conv_arr3_S(val : array<S_std140, 3u>) -> array<S, 3u> {
+  var arr : array<S, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_S(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr3_S(a);
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadStruct_ConstIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let l = a[${array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(${mat}(${col_vectors_inline}));
+}
+
+fn f() {
+  let l = conv_S(a[${array_index}u]);
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadStruct_VariableIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(${mat}(${col_vectors_inline}));
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_S(a[I]);
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadMatrix_ConstArrayIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let l = a[${array_index}].m;
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn load_a_${array_index}_m() -> ${mat} {
+  let s = &(a[${array_index}u]);
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = load_a_${array_index}_m();
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadMatrix_VariableArrayIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].m;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn load_a_p0_m(p0 : u32) -> ${mat} {
+  let s = &(a[p0]);
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0_m(u32(I));
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let l = a[${array_index}].m[${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn f() {
+  let l = a[${array_index}u].m_${cloumn_index};
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        for (uint32_t col = 0; col < matrix.columns; col++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+            src = utils::ReplaceAll(src, "${cloumn_index}", std::to_string(col));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+            expect = utils::ReplaceAll(expect, "${cloumn_index}", std::to_string(col));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element " << array_index << " col " << col;
+        }
+    }
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].m[${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].m_${cloumn_index};
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${cloumn_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${cloumn_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[${array_index}].m[I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn load_a_${array_index}_m_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_${array_index}_m_p0(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[${array_index}u].m_0;
+        //   }
+        //   case 1u: {
+        //     return a[${array_index}u].m_1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[${array_index}u].m_${col_id_for_tmpl};
+    })",
+            "\n");
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> a : array<S, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].m[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
+
+fn load_a_p0_m_p1(p0 : u32, p1 : u32) -> ${col_vector_type} {
+  switch(p1) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0_m_p1(u32(I), u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[p0].m_0;
+        //   }
+        //   case 1u: {
+        //     return a[p0].m_1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].m_${col_id_for_tmpl};
+    })",
+            "\n");
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructArrayStructMatUniform_Loads) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct Inner {
+  @size(64)
+  m : ${mat},
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let l_a : array<Outer, 4> = a;
+  let l_a_1 : Outer = a[1];
+  let l_a_I : Outer = a[I];
+  let l_a_2_a : array<Inner, 4> = a[2].a;
+  let l_a_I_a : array<Inner, 4> = a[I].a;
+  let l_a_3_a_1 : Inner = a[3].a[1];
+  let l_a_3_a_I : Inner = a[3].a[I];
+  let l_a_I_a_1 : Inner = a[I].a[1];
+  let l_a_I_a_J : Inner = a[I].a[J];
+  let l_a_0_a_2_m : ${mat} = a[0].a[2].m;
+  let l_a_0_a_I_m : ${mat} = a[0].a[I].m;
+  let l_a_I_a_2_m : ${mat} = a[I].a[2].m;
+  let l_a_I_a_J_m : ${mat} = a[I].a[J].m;
+  let l_a_1_a_3_m_0 : ${col_vector_type} = a[1].a[3].m[0];
+  let l_a_I_a_J_m_K : ${col_vector_type} = a[I].a[J].m[K];
+  let l_a_2_a_0_m_1_0 : ${elem_type} = a[2].a[0].m[1][0];
+  let l_a_I_a_J_m_K_I : ${elem_type} = a[I].a[J].m[K][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct Inner {
+  @size(64)
+  m : ${mat},
+}
+
+struct Inner_std140 {
+${col_vectors}
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+struct Outer_std140 {
+  a : array<Inner_std140, 4u>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
+
+fn conv_Inner(val : Inner_std140) -> Inner {
+  return Inner(${mat}(${col_vectors_inline_conv_Inner}));
+}
+
+fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
+  var arr : array<Inner, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Inner(val[i]);
+  }
+  return arr;
+}
+
+fn conv_Outer(val : Outer_std140) -> Outer {
+  return Outer(conv_arr4_Inner(val.a));
+}
+
+fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
+  var arr : array<Outer, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Outer(val[i]);
+  }
+  return arr;
+}
+
+fn load_a_0_a_2_m() -> ${mat} {
+  let s = &(a[0u].a[2u]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_0_a_p0_m(p0 : u32) -> ${mat} {
+  let s = &(a[0u].a[p0]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_2_m(p0 : u32) -> ${mat} {
+  let s = &(a[p0].a[2u]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_p1_m(p0 : u32, p1 : u32) -> ${mat} {
+  let s = &(a[p0].a[p1]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_p1_m_p2(p0 : u32, p1 : u32, p2 : u32) -> ${col_vector_type} {
+  switch(p2) {
+${col_table_load_column}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn load_a_p0_a_p1_m_p2_p3(p0 : u32, p1 : u32, p2 : u32, p3 : u32) -> ${elem_type} {
+  switch(p2) {
+${col_table_load_element}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
+  let l_a_1 : Outer = conv_Outer(a[1u]);
+  let l_a_I : Outer = conv_Outer(a[I]);
+  let l_a_2_a : array<Inner, 4> = conv_arr4_Inner(a[2u].a);
+  let l_a_I_a : array<Inner, 4> = conv_arr4_Inner(a[I].a);
+  let l_a_3_a_1 : Inner = conv_Inner(a[3u].a[1u]);
+  let l_a_3_a_I : Inner = conv_Inner(a[3u].a[I]);
+  let l_a_I_a_1 : Inner = conv_Inner(a[I].a[1u]);
+  let l_a_I_a_J : Inner = conv_Inner(a[I].a[J]);
+  let l_a_0_a_2_m : ${mat} = load_a_0_a_2_m();
+  let l_a_0_a_I_m : ${mat} = load_a_0_a_p0_m(u32(I));
+  let l_a_I_a_2_m : ${mat} = load_a_p0_a_2_m(u32(I));
+  let l_a_I_a_J_m : ${mat} = load_a_p0_a_p1_m(u32(I), u32(J));
+  let l_a_1_a_3_m_0 : ${col_vector_type} = a[1u].a[3u].m_0;
+  let l_a_I_a_J_m_K : ${col_vector_type} = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a_2_a_0_m_1_0 : ${elem_type} = a[2u].a[0u].m_1[0u];
+  let l_a_I_a_J_m_K_I : ${elem_type} = load_a_p0_a_p1_m_p2_p3(u32(I), u32(J), u32(K), u32(I));
+}
+)";
+        std::string col_tableLoadColumn = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].a[p1].m_${col_id_for_tmpl};
+    })",
+            "\n");
+        std::string col_tableLoadElement = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].a[p1].m_${col_id_for_tmpl}[p3];
+    })",
+            "\n");
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline_conv_Inner}",
+              matrix.ExpendedColumnVectorsInline("val.m_", ", ")},
+             {"${col_vectors_inline_load_matrix}",
+              matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")},
+             {"${col_table_load_column}", col_tableLoadColumn},
+             {"${col_table_load_element}", col_tableLoadElement}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructArrayStructMatUniform_LoadsViaPtrs) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct Inner {
+  @size(64)
+  m : ${mat},
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let p_a = &(a);
+  let p_a_3 = &((*(p_a))[3]);
+  let p_a_I = &((*(p_a))[I]);
+  let p_a_3_a = &((*(p_a_3)).a);
+  let p_a_I_a = &((*(p_a_I)).a);
+  let p_a_3_a_2 = &((*(p_a_3_a))[2]);
+  let p_a_3_a_I = &((*(p_a_3_a))[I]);
+  let p_a_I_a_2 = &((*(p_a_I_a))[2]);
+  let p_a_I_a_J = &((*(p_a_I_a))[J]);
+  let p_a_3_a_2_m = &((*(p_a_3_a_2)).m);
+  let p_a_3_a_I_m = &((*(p_a_3_a_I)).m);
+  let p_a_I_a_2_m = &((*(p_a_I_a_2)).m);
+  let p_a_I_a_J_m = &((*(p_a_I_a_J)).m);
+  let p_a_3_a_2_m_1 = &((*(p_a_3_a_2_m))[1]);
+  let p_a_I_a_J_m_K = &((*(p_a_I_a_J_m))[K]);
+  let l_a : array<Outer, 4> = *(p_a);
+  let l_a_3 : Outer = *(p_a_3);
+  let l_a_I : Outer = *(p_a_I);
+  let l_a_3_a : array<Inner, 4> = *(p_a_3_a);
+  let l_a_I_a : array<Inner, 4> = *(p_a_I_a);
+  let l_a_3_a_2 : Inner = *(p_a_3_a_2);
+  let l_a_3_a_I : Inner = *(p_a_3_a_I);
+  let l_a_I_a_2 : Inner = *(p_a_I_a_2);
+  let l_a_I_a_J : Inner = *(p_a_I_a_J);
+  let l_a_3_a_2_m : ${mat} = *(p_a_3_a_2_m);
+  let l_a_3_a_I_m : ${mat} = *(p_a_3_a_I_m);
+  let l_a_I_a_2_m : ${mat} = *(p_a_I_a_2_m);
+  let l_a_I_a_J_m : ${mat} = *(p_a_I_a_J_m);
+  let l_a_3_a_2_m_1 : ${col_vector_type} = *(p_a_3_a_2_m_1);
+  let l_a_I_a_J_m_K : ${col_vector_type} = *(p_a_I_a_J_m_K);
+  let l_a_2_a_0_m_1_0 : ${elem_type} = (*(p_a_3_a_2_m_1))[0];
+  let l_a_I_a_J_m_K_I : ${elem_type} = (*(p_a_I_a_J_m_K))[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct Inner {
+  @size(64)
+  m : ${mat},
+}
+
+struct Inner_std140 {
+${col_vectors}
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+struct Outer_std140 {
+  a : array<Inner_std140, 4u>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
+
+fn conv_Inner(val : Inner_std140) -> Inner {
+  return Inner(${mat}(${col_vectors_inline_conv_Inner}));
+}
+
+fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
+  var arr : array<Inner, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Inner(val[i]);
+  }
+  return arr;
+}
+
+fn conv_Outer(val : Outer_std140) -> Outer {
+  return Outer(conv_arr4_Inner(val.a));
+}
+
+fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
+  var arr : array<Outer, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Outer(val[i]);
+  }
+  return arr;
+}
+
+fn load_a_3_a_2_m() -> ${mat} {
+  let s = &(a[3u].a[2u]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_3_a_p0_m(p0 : u32) -> ${mat} {
+  let s = &(a[3u].a[p0]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_2_m(p0 : u32) -> ${mat} {
+  let s = &(a[p0].a[2u]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_p1_m(p0 : u32, p1 : u32) -> ${mat} {
+  let s = &(a[p0].a[p1]);
+  return ${mat}(${col_vectors_inline_load_matrix});
+}
+
+fn load_a_p0_a_p1_m_p2(p0 : u32, p1 : u32, p2 : u32) -> ${col_vector_type} {
+  switch(p2) {
+${col_table_load_column}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn load_a_p0_a_p1_m_p2_p3(p0 : u32, p1 : u32, p2 : u32, p3 : u32) -> ${elem_type} {
+  switch(p2) {
+${col_table_load_element}
+    default: {
+      return ${elem_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let p_a = conv_arr4_Outer(a);
+  let p_a_3 = conv_Outer(a[3u]);
+  let p_a_I = conv_Outer(a[I]);
+  let p_a_3_a = conv_arr4_Inner(a[3u].a);
+  let p_a_I_a = conv_arr4_Inner(a[I].a);
+  let p_a_3_a_2 = conv_Inner(a[3u].a[2u]);
+  let p_a_3_a_I = conv_Inner(a[3u].a[I]);
+  let p_a_I_a_2 = conv_Inner(a[I].a[2u]);
+  let p_a_I_a_J = conv_Inner(a[I].a[J]);
+  let p_a_3_a_2_m = load_a_3_a_2_m();
+  let p_a_3_a_I_m = load_a_3_a_p0_m(u32(I));
+  let p_a_I_a_2_m = load_a_p0_a_2_m(u32(I));
+  let p_a_I_a_J_m = load_a_p0_a_p1_m(u32(I), u32(J));
+  let p_a_3_a_2_m_1 = a[3u].a[2u].m_1;
+  let p_a_I_a_J_m_K = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
+  let l_a_3 : Outer = conv_Outer(a[3u]);
+  let l_a_I : Outer = conv_Outer(a[I]);
+  let l_a_3_a : array<Inner, 4> = conv_arr4_Inner(a[3u].a);
+  let l_a_I_a : array<Inner, 4> = conv_arr4_Inner(a[I].a);
+  let l_a_3_a_2 : Inner = conv_Inner(a[3u].a[2u]);
+  let l_a_3_a_I : Inner = conv_Inner(a[3u].a[I]);
+  let l_a_I_a_2 : Inner = conv_Inner(a[I].a[2u]);
+  let l_a_I_a_J : Inner = conv_Inner(a[I].a[J]);
+  let l_a_3_a_2_m : ${mat} = load_a_3_a_2_m();
+  let l_a_3_a_I_m : ${mat} = load_a_3_a_p0_m(u32(I));
+  let l_a_I_a_2_m : ${mat} = load_a_p0_a_2_m(u32(I));
+  let l_a_I_a_J_m : ${mat} = load_a_p0_a_p1_m(u32(I), u32(J));
+  let l_a_3_a_2_m_1 : ${col_vector_type} = a[3u].a[2u].m_1;
+  let l_a_I_a_J_m_K : ${col_vector_type} = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a_2_a_0_m_1_0 : ${elem_type} = a[3u].a[2u].m_1[0u];
+  let l_a_I_a_J_m_K_I : ${elem_type} = load_a_p0_a_p1_m_p2_p3(u32(I), u32(J), u32(K), u32(I));
+}
+)";
+        std::string col_tableLoadColumn = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].a[p1].m_${col_id_for_tmpl};
+    })",
+            "\n");
+        std::string col_tableLoadElement = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].a[p1].m_${col_id_for_tmpl}[p3];
+    })",
+            "\n");
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline_conv_Inner}",
+              matrix.ExpendedColumnVectorsInline("val.m_", ", ")},
+             {"${col_vectors_inline_load_matrix}",
+              matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")},
+             {"${col_table_load_column}", col_tableLoadColumn},
+             {"${col_table_load_element}", col_tableLoadElement}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyArray_UniformToStorage) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s = u;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(${mat}(${col_vectors_inline}));
+}
+
+fn conv_arr4_S(val : array<S_std140, 4u>) -> array<S, 4u> {
+  var arr : array<S, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_S(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  s = conv_arr4_S(u);
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyStruct_UniformToWorkgroup) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[0] = u[1];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+var<workgroup> w : array<S, 4>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(val.v, ${mat}(${col_vectors_inline}));
+}
+
+fn f() {
+  w[0] = conv_S(u[1u]);
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyMatrix_UniformToPrivate) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[2].m = u[1].m;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+var<private> p : array<S, 4>;
+
+fn load_u_1_m() -> ${mat} {
+  let s = &(u[1u]);
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  p[2].m = load_u_1_m();
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyColumn_UniformToStorage) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s[3].m[1] = u[2].m[0];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s[3].m[1] = u[2u].m_0;
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyColumnSwizzle_UniformToWorkgroup) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[3].m[1] = u[2].m[0].${swizzle}.${swizzle};
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[3].m[1] = u[2u].m_0.${swizzle}.${swizzle};
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_Matrix, ArrayStructMatUniform_CopyScalar_UniformToPrivate) {
+    auto matrix = GetParam();
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[3].m[1].x = u[2].m[0].y;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : ${mat},
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[3].m[1].x = u[2u].m_0[1u];
+}
+)";
+        uint32_t last_size =
+            64 - static_cast<uint32_t>(matrix.ColumnVectorAlign() * (matrix.columns - 1));
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectorsWithLastSize(2, "m_", last_size)},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("(*(s)).m_", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         Std140Test_Matrix,
+                         ::testing::ValuesIn(std::vector<MatrixCase>{
+                             {2, 2, MatrixType::f32},
+                             {2, 3, MatrixType::f32},
+                             {2, 4, MatrixType::f32},
+                             {3, 2, MatrixType::f32},
+                             {3, 3, MatrixType::f32},
+                             {3, 4, MatrixType::f32},
+                             {4, 2, MatrixType::f32},
+                             {4, 3, MatrixType::f32},
+                             {4, 4, MatrixType::f32},
+                         }));
+
+using Std140Test_MatrixArray = TransformTestWithParam<MatrixCase>;
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadArray) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let l = a;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr3_mat${shape}_${elem_type}(a);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadMatrix_ConstArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let l = a[${array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = conv_mat${shape}_${elem_type}(a[${array_index}u]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadMatrix_VariableArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat${shape}_${elem_type}(a[I]);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let l = a[${array_index}][${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn f() {
+  let l = a[${array_index}u].col${cloumn_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        for (uint32_t col = 0; col < matrix.columns; col++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+            src = utils::ReplaceAll(src, "${cloumn_index}", std::to_string(col));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+            expect = utils::ReplaceAll(expect, "${cloumn_index}", std::to_string(col));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element " << array_index << " col " << col;
+        }
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].col${cloumn_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${cloumn_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${cloumn_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[${array_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn load_a_${array_index}_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_${array_index}_p0(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[${array_index}u].col0;
+        //   }
+        //   case 1u: {
+        //     return a[${array_index}u].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[${array_index}u].col${col_id_for_tmpl};
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<${mat}, 3>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<mat${shape}_${elem_type}, 3u>;
+
+fn load_a_p0_p1(p0 : u32, p1 : u32) -> ${col_vector_type} {
+  switch(p1) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0_p1(u32(I), u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[p0].col0;
+        //   }
+        //   case 1u: {
+        //     return a[p0].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0].col${col_id_for_tmpl};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, StructArrayMatUniform_LoadStruct) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn conv_S(val : S_std140) -> S {
+  return S(conv_arr3_mat${shape}_${elem_type}(val.a));
+}
+
+fn f() {
+  let l = conv_S(s);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, StructArrayMatUniform_LoadArray) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.a;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr3_mat${shape}_${elem_type}(s.a);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, StructArrayMatUniform_LoadMatrix_ConstArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.a[${array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = conv_mat${shape}_${elem_type}(s.a[${array_index}u]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, StructArrayMatUniform_LoadMatrix_VariableArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 1;
+  let l = s.a[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat${shape}_${elem_type}(s.a[I]);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, StructArrayMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let l = s.a[${array_index}][${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn f() {
+  let l = s.a[${array_index}u].col${cloumn_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        for (uint32_t col = 0; col < matrix.columns; col++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+            src = utils::ReplaceAll(src, "${cloumn_index}", std::to_string(col));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+            expect = utils::ReplaceAll(expect, "${cloumn_index}", std::to_string(col));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element " << array_index << " col " << col;
+        }
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       StructArrayMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 1;
+  let l = s.a[I][${cloumn_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn f() {
+  let I = 1;
+  let l = s.a[I].col${cloumn_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${cloumn_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${cloumn_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing col " << col;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       StructArrayMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 1;
+  let l = s.a[${array_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_a_${array_index}_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_s_a_${array_index}_p0(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.a[${array_index}u].col0;
+        //   }
+        //   case 1u: {
+        //     return s.a[${array_index}u].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.a[${array_index}u].col${col_id_for_tmpl};
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t array_index = 0; array_index < 3; array_index++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${array_index}", std::to_string(array_index));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${array_index}", std::to_string(array_index));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << array_index;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       StructArrayMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  let I = 1;
+  let l = s.a[I][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+struct S {
+  a : array<${mat}, 3>,
+}
+
+struct S_std140 {
+  a : array<mat${shape}_${elem_type}, 3u>,
+}
+
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_a_p0_p1(p0 : u32, p1 : u32) -> ${col_vector_type} {
+  switch(p1) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_s_a_p0_p1(u32(I), u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return s.a[p0].col0;
+        //   }
+        //   case 1u: {
+        //     return s.a[p0].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return s.a[p0].col${col_id_for_tmpl};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayArrayMatUniform_LoadArrays) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let l = a;
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn conv_arr4_arr3_mat${shape}_${elem_type}(val : array<array<mat${shape}_${elem_type}, 3u>, 4u>) -> array<array<${mat}, 3u>, 4u> {
+  var arr : array<array<${mat}, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_arr3_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr4_arr3_mat${shape}_${elem_type}(a);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayArrayMatUniform_LoadArray_ConstOuterArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let l = a[${outer_array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr3_mat${shape}_${elem_type}(a[${outer_array_index}u]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element " << outer;
+    }
+}
+
+TEST_P(Std140Test_MatrixArray, ArrayArrayMatUniform_LoadArray_VariableOuterArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn conv_arr3_mat${shape}_${elem_type}(val : array<mat${shape}_${elem_type}, 3u>) -> array<${mat}, 3u> {
+  var arr : array<${mat}, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat${shape}_${elem_type}(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_arr3_mat${shape}_${elem_type}(a[I]);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray,
+       ArrayArrayMatUniform_LoadMatrix_ConstOuterArrayIndex_ConstInnerArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let l = a[${outer_array_index}][${inner_array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let l = conv_mat${shape}_${elem_type}(a[${outer_array_index}u][${inner_array_index}u]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        for (uint32_t inner = 0; inner < 3; inner++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+            src = utils::ReplaceAll(src, "${inner_array_index}", std::to_string(inner));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+            expect = utils::ReplaceAll(expect, "${inner_array_index}", std::to_string(inner));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element [" << outer << "][" << inner << "]";
+        }
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       ArrayArrayMatUniform_LoadMatrix_ConstOuterArrayIndex_VariableInnerArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[${outer_array_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat${shape}_${elem_type}(a[${outer_array_index}u][I]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element [" << outer << "][I]";
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       ArrayArrayMatUniform_LoadMatrix_VariableOuterArrayIndex_ConstInnerArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][${inner_array_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat${shape}_${elem_type}(a[I][${inner_array_index}u]);
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t inner = 0; inner < 3; inner++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${inner_array_index}", std::to_string(inner));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${inner_array_index}", std::to_string(inner));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element [I][" << inner << "]";
+    }
+}
+
+TEST_P(Std140Test_MatrixArray,
+       ArrayArrayMatUniform_LoadMatrix_VariableOuterArrayIndex_VariableInnerArrayIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][I];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn conv_mat${shape}_${elem_type}(val : mat${shape}_${elem_type}) -> ${mat} {
+  return ${mat}(${col_vectors_inline});
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat${shape}_${elem_type}(a[I][I]);
+}
+)";
+        expect = matrix.ReplaceFieldsInString(
+            expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_P(Std140Test_MatrixArray,
+       ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_ConstInnerArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let l = a[${outer_array_index}][${inner_array_index}][${column_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn f() {
+  let l = a[${outer_array_index}u][${inner_array_index}u].col${column_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect,
+            {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+             {"${col_vectors_inline}", matrix.ExpendedColumnVectorsInline("val.col", ", ")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        for (uint32_t inner = 0; inner < 3; inner++) {
+            for (uint32_t col = 0; col < matrix.columns; col++) {
+                std::string src =
+                    utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+                src = utils::ReplaceAll(src, "${inner_array_index}", std::to_string(inner));
+                src = utils::ReplaceAll(src, "${column_index}", std::to_string(col));
+                std::string expect =
+                    utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+                expect = utils::ReplaceAll(expect, "${inner_array_index}", std::to_string(inner));
+                expect = utils::ReplaceAll(expect, "${column_index}", std::to_string(col));
+
+                auto got = Run<Std140>(src);
+
+                EXPECT_EQ(expect, str(got))
+                    << "accessing array element [" << outer << "][" << inner << "] col " << col;
+            }
+        }
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_ConstInnerArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[${outer_array_index}][${inner_array_index}][I];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn load_a_${outer_array_index}_${inner_array_index}_p0(p0 : u32) -> ${col_vector_type} {
+  switch(p0) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_${outer_array_index}_${inner_array_index}_p0(u32(I));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[${outer_array_index}u][${inner_array_index}u].col0;
+        //   }
+        //   case 1u: {
+        //     return a[${outer_array_index}u][${inner_array_index}u].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[${outer_array_index}u][${inner_array_index}u].col${col_id_for_tmpl};
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        for (uint32_t inner = 0; inner < 3; inner++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+            src = utils::ReplaceAll(src, "${inner_array_index}", std::to_string(inner));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+            expect = utils::ReplaceAll(expect, "${inner_array_index}", std::to_string(inner));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element [" << outer << "][" << inner << "]";
+        }
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_VariableInnerArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[${outer_array_index}][I][${column_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let l = a[${outer_array_index}u][I].col${column_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        for (uint32_t col = 0; col < matrix.columns; col++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+            src = utils::ReplaceAll(src, "${column_index}", std::to_string(col));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+            expect = utils::ReplaceAll(expect, "${column_index}", std::to_string(col));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element [" << outer << "][I] col " << col;
+        }
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_VariableInnerArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[${outer_array_index}][I][J];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn load_a_${outer_array_index}_p0_p1(p0 : u32, p1 : u32) -> ${col_vector_type} {
+  switch(p1) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = load_a_${outer_array_index}_p0_p1(u32(I), u32(J));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[${outer_array_index}u][p0].col0;
+        //   }
+        //   case 1u: {
+        //     return a[${outer_array_index}u][p0].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[${outer_array_index}u][p0].col${col_id_for_tmpl};
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t outer = 0; outer < 4; outer++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${outer_array_index}", std::to_string(outer));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${outer_array_index}", std::to_string(outer));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element [" << outer << "][I]";
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_ConstInnerArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][${inner_array_index}][${column_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][${inner_array_index}u].col${column_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t inner = 0; inner < 3; inner++) {
+        for (uint32_t col = 0; col < matrix.columns; col++) {
+            std::string src =
+                utils::ReplaceAll(tmpl_src, "${inner_array_index}", std::to_string(inner));
+            src = utils::ReplaceAll(src, "${column_index}", std::to_string(col));
+            std::string expect =
+                utils::ReplaceAll(tmpl_expect, "${inner_array_index}", std::to_string(inner));
+            expect = utils::ReplaceAll(expect, "${column_index}", std::to_string(col));
+
+            auto got = Run<Std140>(src);
+
+            EXPECT_EQ(expect, str(got))
+                << "accessing array element [I][" << inner << "] col " << col;
+        }
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_ConstInnerArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][${inner_array_index}][J];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn load_a_p0_${inner_array_index}_p1(p0 : u32, p1 : u32) -> ${col_vector_type} {
+  switch(p1) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = load_a_p0_${inner_array_index}_p1(u32(I), u32(J));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[p0][${inner_array_index}u].col0;
+        //   }
+        //   case 1u: {
+        //     return a[p0][${inner_array_index}u].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0][${inner_array_index}u].col${col_id_for_tmpl};
+    })",
+            "\n");
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                          {"${col_table}", col_table}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t inner = 0; inner < 3; inner++) {
+        std::string src =
+            utils::ReplaceAll(tmpl_src, "${inner_array_index}", std::to_string(inner));
+        std::string expect =
+            utils::ReplaceAll(tmpl_expect, "${inner_array_index}", std::to_string(inner));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element [I][" << inner << "]";
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_VariableInnerArrayIndex_ConstColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string tmpl_src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][J][${column_index}];
+}
+)";
+    tmpl_src = matrix.ReplaceFieldsInString(tmpl_src);
+
+    std::string tmpl_expect;
+    if (matrix.NotStd140Compatible()) {
+        tmpl_expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][J].col${column_index};
+}
+)";
+        tmpl_expect = matrix.ReplaceFieldsInString(
+            tmpl_expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")}});
+    } else {
+        tmpl_expect = tmpl_src;
+    }
+
+    for (uint32_t col = 0; col < matrix.columns; col++) {
+        std::string src = utils::ReplaceAll(tmpl_src, "${column_index}", std::to_string(col));
+        std::string expect = utils::ReplaceAll(tmpl_expect, "${column_index}", std::to_string(col));
+
+        auto got = Run<Std140>(src);
+
+        EXPECT_EQ(expect, str(got)) << "accessing array element [I][J] col " << col;
+    }
+}
+
+TEST_P(
+    Std140Test_MatrixArray,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_VariableInnerArrayIndex_VariableColumnIndex) {
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
+        return;
+    }
+
+    std::string src = R"(
+enable f16;
+
+@group(0) @binding(0) var<uniform> a : array<array<${mat}, 3>, 4>;
+
+fn f() {
+  let I = 0;
+  let J = 1;
+  let K = 2;
+  let l = a[I][J][K];
+}
+)";
+    src = matrix.ReplaceFieldsInString(src);
+
+    std::string expect;
+    if (matrix.NotStd140Compatible()) {
+        expect = R"(
+enable f16;
+
+struct mat${shape}_${elem_type} {
+${col_vectors}
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat${shape}_${elem_type}, 3u>, 4u>;
+
+fn load_a_p0_p1_p2(p0 : u32, p1 : u32, p2 : u32) -> ${col_vector_type} {
+  switch(p2) {
+${col_table}
+    default: {
+      return ${col_vector_type}();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let J = 1;
+  let K = 2;
+  let l = load_a_p0_p1_p2(u32(I), u32(J), u32(K));
+}
+)";
+        // col_table is the switch cases for all column index.
+        // Example for a matrix having 2 columns:
+        //   case 0u: {
+        //     return a[p0][p1].col0;
+        //   }
+        //   case 1u: {
+        //     return a[p0][p1].col1;
+        //   }
+        std::string col_table = matrix.JoinTemplatedStringForEachMatrixColumn(  //
+            R"(    case ${col_id_for_tmpl}u: {
+      return a[p0][p1].col${col_id_for_tmpl};
+    })",
+            "\n");
+        expect = matrix.ReplaceFieldsInString(
+            expect, {{"${col_vectors}", matrix.ExpendedColumnVectors(2, "col")},
+                     {"${col_table}", col_table}});
+    } else {
+        expect = src;
+    }
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         Std140Test_MatrixArray,
+                         ::testing::ValuesIn(std::vector<MatrixCase>{
+                             {2, 2, MatrixType::f32},
+                             {2, 3, MatrixType::f32},
+                             {2, 4, MatrixType::f32},
+                             {3, 2, MatrixType::f32},
+                             {3, 3, MatrixType::f32},
+                             {3, 4, MatrixType::f32},
+                             {4, 2, MatrixType::f32},
+                             {4, 3, MatrixType::f32},
+                             {4, 4, MatrixType::f32},
+                         }));
+
+}  // namespace
+}  // namespace tint::transform
diff --git a/src/tint/transform/std140_test.cc b/src/tint/transform/std140_test.cc
index 6e0b170..1ec2e09 100644
--- a/src/tint/transform/std140_test.cc
+++ b/src/tint/transform/std140_test.cc
@@ -32,7 +32,7 @@
     EXPECT_FALSE(ShouldRun<Std140>(src));
 }
 
-TEST_F(Std140Test, ShouldRunStructMat2x2Unused) {
+TEST_F(Std140Test, ShouldRunStructMat2x2F32Unused) {
     auto* src = R"(
 struct Unused {
   m : mat2x2<f32>,
@@ -42,30 +42,68 @@
     EXPECT_FALSE(ShouldRun<Std140>(src));
 }
 
-struct ShouldRunCase {
+TEST_F(Std140Test, ShouldRunStructMat2x2F16Unused) {
+    auto* src = R"(
+enable f16;
+
+struct Unused {
+  m : mat2x2<f16>,
+}
+)";
+
+    EXPECT_FALSE(ShouldRun<Std140>(src));
+}
+
+enum class MatrixType { f32, f16 };
+
+struct MatrixCase {
     uint32_t columns;
     uint32_t rows;
-    bool should_run;
+    MatrixType type;
 
-    std::string Mat() const { return "mat" + std::to_string(columns) + "x" + std::to_string(rows); }
+    size_t ElementSize() const { return type == MatrixType::f16 ? 2 : 4; }
+
+    size_t ColumnVectorAlign() const { return (rows == 3 ? 4 : rows) * ElementSize(); }
+
+    bool NotStd140Compatible() const { return ColumnVectorAlign() != 16; }
+
+    // Return if this matrix type can be used as element type of an uniform buffer, i.e. the
+    // array stride is multiple of 16.
+    bool CanBeUsedAsUniformArrayElememts() const {
+        const size_t arrayStride = columns * ColumnVectorAlign();
+        return (arrayStride % 16 == 0);
+    }
+
+    std::string Shape() const { return std::to_string(columns) + "x" + std::to_string(rows); }
+
+    std::string ElementType() const { return type == MatrixType::f16 ? "f16" : "f32"; }
+
+    std::string Mat() const { return "mat" + Shape() + "<" + ElementType() + ">"; }
+
+    // Replace predefined field `${mat}` with the matrix shape. E.g. for a matrix mat4x3<f32>, would
+    // replace "${mat}" with "mat4x3<f32>".
+    std::string ReplaceMatInString(std::string str) const {
+        str = utils::ReplaceAll(str, "${mat}", Mat());
+        return str;
+    }
 };
 
-inline std::ostream& operator<<(std::ostream& os, const ShouldRunCase& c) {
+inline std::ostream& operator<<(std::ostream& os, const MatrixCase& c) {
     return os << c.Mat();
 }
 
-using Std140TestShouldRun = TransformTestWithParam<ShouldRunCase>;
+using Std140TestShouldRun = TransformTestWithParam<MatrixCase>;
 
 TEST_P(Std140TestShouldRun, StructStorage) {
     std::string src = R"(
 struct S {
-  m : ${mat}<f32>,
+  m : ${mat},
 }
 
 @group(0) @binding(0) var<storage> s : S;
 )";
 
-    src = utils::ReplaceAll(src, "${mat}", GetParam().Mat());
+    src = GetParam().ReplaceMatInString(src);
 
     EXPECT_FALSE(ShouldRun<Std140>(src));
 }
@@ -73,57 +111,56 @@
 TEST_P(Std140TestShouldRun, StructUniform) {
     std::string src = R"(
 struct S {
-  m : ${mat}<f32>,
+  m : ${mat},
 }
 
 @group(0) @binding(0) var<uniform> s : S;
 )";
 
-    src = utils::ReplaceAll(src, "${mat}", GetParam().Mat());
+    src = GetParam().ReplaceMatInString(src);
 
-    EXPECT_EQ(ShouldRun<Std140>(src), GetParam().should_run);
+    EXPECT_EQ(ShouldRun<Std140>(src), GetParam().NotStd140Compatible());
 }
 
 TEST_P(Std140TestShouldRun, ArrayStorage) {
     std::string src = R"(
-@group(0) @binding(0) var<storage> s : array<${mat}<f32>, 2>;
+@group(0) @binding(0) var<storage> s : array<${mat}, 2>;
 )";
 
-    src = utils::ReplaceAll(src, "${mat}", GetParam().Mat());
+    src = GetParam().ReplaceMatInString(src);
 
     EXPECT_FALSE(ShouldRun<Std140>(src));
 }
 
 TEST_P(Std140TestShouldRun, ArrayUniform) {
-    if (GetParam().columns == 3u && GetParam().rows == 2u) {
-        // This permutation is invalid. Skip the test:
-        // error: uniform storage requires that array elements be aligned to 16 bytes, but array
-        // element alignment is currently 24. Consider wrapping the element type in a struct and
-        // using the @size attribute.
+    auto matrix = GetParam();
+
+    if (!matrix.CanBeUsedAsUniformArrayElememts()) {
+        // This permutation is invalid, skip the test.
         return;
     }
 
     std::string src = R"(
-@group(0) @binding(0) var<uniform> s : array<${mat}<f32>, 2>;
+@group(0) @binding(0) var<uniform> s : array<${mat}, 2>;
 )";
 
-    src = utils::ReplaceAll(src, "${mat}", GetParam().Mat());
+    src = GetParam().ReplaceMatInString(src);
 
-    EXPECT_EQ(ShouldRun<Std140>(src), GetParam().should_run);
+    EXPECT_EQ(ShouldRun<Std140>(src), matrix.NotStd140Compatible());
 }
 
 INSTANTIATE_TEST_SUITE_P(Std140TestShouldRun,
                          Std140TestShouldRun,
-                         ::testing::ValuesIn(std::vector<ShouldRunCase>{
-                             {2, 2, true},
-                             {2, 3, false},
-                             {2, 4, false},
-                             {3, 2, true},
-                             {3, 3, false},
-                             {3, 4, false},
-                             {4, 2, true},
-                             {4, 3, false},
-                             {4, 4, false},
+                         ::testing::ValuesIn(std::vector<MatrixCase>{
+                             {2, 2, MatrixType::f32},
+                             {2, 3, MatrixType::f32},
+                             {2, 4, MatrixType::f32},
+                             {3, 2, MatrixType::f32},
+                             {3, 3, MatrixType::f32},
+                             {3, 4, MatrixType::f32},
+                             {4, 2, MatrixType::f32},
+                             {4, 3, MatrixType::f32},
+                             {4, 4, MatrixType::f32},
                          }));
 
 TEST_F(Std140Test, EmptyModule) {
@@ -136,26 +173,137 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, SingleStructMat4x4Uniform) {
+using Std140Test_F32 = Std140Test;
+
+TEST_F(Std140Test_F32, StructMatricesUniform) {
     auto* src = R"(
-struct S {
+struct S2x2F32 {
+  m : mat2x2<f32>,
+}
+struct S3x2F32 {
+  m : mat3x2<f32>,
+}
+struct S4x2F32 {
+  m : mat4x2<f32>,
+}
+struct S2x3F32 {
+  m : mat2x3<f32>,
+}
+struct S3x3F32 {
+  m : mat3x3<f32>,
+}
+struct S4x3F32 {
+  m : mat4x3<f32>,
+}
+struct S2x4F32 {
+  m : mat2x4<f32>,
+}
+struct S3x4F32 {
+  m : mat3x4<f32>,
+}
+struct S4x4F32 {
   m : mat4x4<f32>,
 }
 
-@group(0) @binding(0) var<uniform> s : S;
+@group(2) @binding(2) var<uniform> s2x2f32 : S2x2F32;
+@group(3) @binding(2) var<uniform> s3x2f32 : S3x2F32;
+@group(4) @binding(2) var<uniform> s4x2f32 : S4x2F32;
+@group(2) @binding(3) var<uniform> s2x3f32 : S2x3F32;
+@group(3) @binding(3) var<uniform> s3x3f32 : S3x3F32;
+@group(4) @binding(3) var<uniform> s4x3f32 : S4x3F32;
+@group(2) @binding(4) var<uniform> s2x4f32 : S2x4F32;
+@group(3) @binding(4) var<uniform> s3x4f32 : S3x4F32;
+@group(4) @binding(4) var<uniform> s4x4f32 : S4x4F32;
 )";
 
-    auto* expect = src;  // Nothing violates std140 layout
+    auto* expect = R"(
+struct S2x2F32 {
+  m : mat2x2<f32>,
+}
+
+struct S2x2F32_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+}
+
+struct S3x2F32 {
+  m : mat3x2<f32>,
+}
+
+struct S3x2F32_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
+}
+
+struct S4x2F32 {
+  m : mat4x2<f32>,
+}
+
+struct S4x2F32_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
+  m_3 : vec2<f32>,
+}
+
+struct S2x3F32 {
+  m : mat2x3<f32>,
+}
+
+struct S3x3F32 {
+  m : mat3x3<f32>,
+}
+
+struct S4x3F32 {
+  m : mat4x3<f32>,
+}
+
+struct S2x4F32 {
+  m : mat2x4<f32>,
+}
+
+struct S3x4F32 {
+  m : mat3x4<f32>,
+}
+
+struct S4x4F32 {
+  m : mat4x4<f32>,
+}
+
+@group(2) @binding(2) var<uniform> s2x2f32 : S2x2F32_std140;
+
+@group(3) @binding(2) var<uniform> s3x2f32 : S3x2F32_std140;
+
+@group(4) @binding(2) var<uniform> s4x2f32 : S4x2F32_std140;
+
+@group(2) @binding(3) var<uniform> s2x3f32 : S2x3F32;
+
+@group(3) @binding(3) var<uniform> s3x3f32 : S3x3F32;
+
+@group(4) @binding(3) var<uniform> s4x3f32 : S4x3F32;
+
+@group(2) @binding(4) var<uniform> s2x4f32 : S2x4F32;
+
+@group(3) @binding(4) var<uniform> s3x4f32 : S3x4F32;
+
+@group(4) @binding(4) var<uniform> s4x4f32 : S4x4F32;
+)";
 
     auto got = Run<Std140>(src);
 
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, SingleStructMat2x2Uniform) {
+// In the following tests we only test `mat2x2<f32>` for matrix used as array element type and
+// `mat3x2<f32>` otherwise, and set all constant column index to 1, row index 0, inner array index
+// 2, and outer array index 3. For exhaustive tests, i.e. tests on all matrix shape and different
+// valid constant index, please refer to std140_exhaustive_test.cc
+
+TEST_F(Std140Test_F32, SingleStructMatUniform_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -163,12 +311,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -179,11 +328,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, CustomAlignMat3x2) {
+TEST_F(Std140Test_F32, CustomAlign_Mat3x2F32) {
     auto* src = R"(
 struct S {
   before : i32,
-  @align(128) m : mat3x2<f32>,
+  @align(128)
+  m : mat3x2<f32>,
   after : i32,
 }
 
@@ -215,11 +365,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, CustomSizeMat3x2) {
+TEST_F(Std140Test_F32, CustomSizeMat_Mat3x2F32) {
     auto* src = R"(
 struct S {
   before : i32,
-  @size(128) m : mat3x2<f32>,
+  @size(128)
+  m : mat3x2<f32>,
   after : i32,
 }
 
@@ -251,11 +402,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, CustomAlignAndSizeMat3x2) {
+TEST_F(Std140Test_F32, CustomAlignAndSize_Mat3x2F32) {
     auto* src = R"(
 struct S {
   before : i32,
-  @align(128) @size(128) m : mat3x2<f32>,
+  @align(128) @size(128)
+  m : mat3x2<f32>,
   after : i32,
 }
 
@@ -288,119 +440,54 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMatricesUniform) {
+TEST_F(Std140Test_F32, MatrixUsageInForLoop_Mat3x2F32) {
     auto* src = R"(
-struct S2x2 {
-  m : mat2x2<f32>,
-}
-struct S3x2 {
+struct S {
   m : mat3x2<f32>,
 }
-struct S4x2 {
-  m : mat4x2<f32>,
-}
-struct S2x3 {
-  m : mat2x3<f32>,
-}
-struct S3x3 {
-  m : mat3x3<f32>,
-}
-struct S4x3 {
-  m : mat4x3<f32>,
-}
-struct S2x4 {
-  m : mat2x4<f32>,
-}
-struct S3x4 {
-  m : mat3x4<f32>,
-}
-struct S4x4 {
-  m : mat4x4<f32>,
-}
 
-@group(2) @binding(2) var<uniform> s2x2 : S2x2;
-@group(3) @binding(2) var<uniform> s3x2 : S3x2;
-@group(4) @binding(2) var<uniform> s4x2 : S4x2;
-@group(2) @binding(3) var<uniform> s2x3 : S2x3;
-@group(3) @binding(3) var<uniform> s3x3 : S3x3;
-@group(4) @binding(3) var<uniform> s4x3 : S4x3;
-@group(2) @binding(4) var<uniform> s2x4 : S2x4;
-@group(3) @binding(4) var<uniform> s3x4 : S3x4;
-@group(4) @binding(4) var<uniform> s4x4 : S4x4;
+@group(0) @binding(0) var<uniform> s : S;
+
+fn f() {
+  for(var i = u32(s.m[0][0]); (i < u32(s.m[i][1])); i += u32(s.m[1][i])) {
+  }
+}
 )";
 
     auto* expect = R"(
-struct S2x2 {
-  m : mat2x2<f32>,
-}
-
-struct S2x2_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-struct S3x2 {
+struct S {
   m : mat3x2<f32>,
 }
 
-struct S3x2_std140 {
+struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
   m_2 : vec2<f32>,
 }
 
-struct S4x2 {
-  m : mat4x2<f32>,
+@group(0) @binding(0) var<uniform> s : S_std140;
+
+fn load_s_m_p0_1(p0 : u32) -> f32 {
+  switch(p0) {
+    case 0u: {
+      return s.m_0[1u];
+    }
+    case 1u: {
+      return s.m_1[1u];
+    }
+    case 2u: {
+      return s.m_2[1u];
+    }
+    default: {
+      return f32();
+    }
+  }
 }
 
-struct S4x2_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  m_2 : vec2<f32>,
-  m_3 : vec2<f32>,
+fn f() {
+  for(var i = u32(s.m_0[0u]); (i < u32(load_s_m_p0_1(u32(i)))); i += u32(s.m_1[i])) {
+  }
 }
-
-struct S2x3 {
-  m : mat2x3<f32>,
-}
-
-struct S3x3 {
-  m : mat3x3<f32>,
-}
-
-struct S4x3 {
-  m : mat4x3<f32>,
-}
-
-struct S2x4 {
-  m : mat2x4<f32>,
-}
-
-struct S3x4 {
-  m : mat3x4<f32>,
-}
-
-struct S4x4 {
-  m : mat4x4<f32>,
-}
-
-@group(2) @binding(2) var<uniform> s2x2 : S2x2_std140;
-
-@group(3) @binding(2) var<uniform> s3x2 : S3x2_std140;
-
-@group(4) @binding(2) var<uniform> s4x2 : S4x2_std140;
-
-@group(2) @binding(3) var<uniform> s2x3 : S2x3;
-
-@group(3) @binding(3) var<uniform> s3x3 : S3x3;
-
-@group(4) @binding(3) var<uniform> s4x3 : S4x3;
-
-@group(2) @binding(4) var<uniform> s2x4 : S2x4;
-
-@group(3) @binding(4) var<uniform> s3x4 : S3x4;
-
-@group(4) @binding(4) var<uniform> s4x4 : S4x4;
 )";
 
     auto got = Run<Std140>(src);
@@ -408,11 +495,345 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_NameCollision) {
+TEST_F(Std140Test_F32, MatUniform_LoadMatrix_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> m : mat3x2<f32>;
+
+fn f() {
+  let l = m;
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> m : mat3x2_f32;
+
+fn conv_mat3x2_f32(val : mat3x2_f32) -> mat3x2<f32> {
+  return mat3x2<f32>(val.col0, val.col1, val.col2);
+}
+
+fn f() {
+  let l = conv_mat3x2_f32(m);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadColumn_ConstIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let l = a[1];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn f() {
+  let l = a.col1;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadColumn_VariableIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn load_a_p0(p0 : u32) -> vec2<f32> {
+  switch(p0) {
+    case 0u: {
+      return a.col0;
+    }
+    case 1u: {
+      return a.col1;
+    }
+    case 2u: {
+      return a.col2;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0(u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadColumnSwizzle_ConstIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let l = a[1].yx;
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn f() {
+  let l = a.col1.yx;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadColumnSwizzle_VariableIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let I = 1;
+  let l = a[I].yx;
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn load_a_p0_yx(p0 : u32) -> vec2<f32> {
+  switch(p0) {
+    case 0u: {
+      return a.col0.yx;
+    }
+    case 1u: {
+      return a.col1.yx;
+    }
+    case 2u: {
+      return a.col2.yx;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_p0_yx(u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadScalar_ConstColumnIndex_ConstRowIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let l = a[1][0];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn f() {
+  let l = a.col1[0u];
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadScalar_VariableColumnIndex_ConstRowIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let I = 0;
+  let l = a[I][0];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn load_a_p0_0(p0 : u32) -> f32 {
+  switch(p0) {
+    case 0u: {
+      return a.col0[0u];
+    }
+    case 1u: {
+      return a.col1[0u];
+    }
+    case 2u: {
+      return a.col2[0u];
+    }
+    default: {
+      return f32();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_a_p0_0(u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadScalar_ConstColumnIndex_VariableRowIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let I = 0;
+  let l = a[1][I];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn f() {
+  let I = 0;
+  let l = a.col1[I];
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, MatUniform_LoadScalar_VariableColumnIndex_VariableRowIndex_Mat3x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : mat3x2<f32>;
+
+fn f() {
+  let I = 0;
+  let l = a[I][I];
+}
+)";
+
+    auto* expect = R"(
+struct mat3x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+  col2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : mat3x2_f32;
+
+fn load_a_p0_p1(p0 : u32, p1 : u32) -> f32 {
+  switch(p0) {
+    case 0u: {
+      return a.col0[p1];
+    }
+    case 1u: {
+      return a.col1[p1];
+    }
+    case 2u: {
+      return a.col2[p1];
+    }
+    default: {
+      return f32();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let l = load_a_p0_p1(u32(I), u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, StructMatUniform_NameCollision_Mat3x2F32) {
     auto* src = R"(
 struct S {
   m_1 : i32,
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -421,13 +842,14 @@
     auto* expect = R"(
 struct S {
   m_1 : i32,
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_1 : i32,
   m__0 : vec2<f32>,
   m__1 : vec2<f32>,
+  m__2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -438,10 +860,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadStruct) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadStruct_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -453,18 +875,19 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
 
 fn conv_S(val : S_std140) -> S {
-  return S(mat2x2<f32>(val.m_0, val.m_1));
+  return S(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
 }
 
 fn f() {
@@ -477,10 +900,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadMatrix) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadMatrix_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -492,19 +915,20 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
 
-fn load_s_m() -> mat2x2<f32> {
+fn load_s_m() -> mat3x2<f32> {
   let s = &(s);
-  return mat2x2<f32>((*(s)).m_0, (*(s)).m_1);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
 }
 
 fn f() {
@@ -517,45 +941,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadColumn0) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadColumn_ConstIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.m[0];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let l = s.m_0;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadColumn1) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -567,12 +956,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -587,10 +977,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadColumnI) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadColumn_VariableIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -603,12 +993,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -621,6 +1012,9 @@
     case 1u: {
       return s.m_1;
     }
+    case 2u: {
+      return s.m_2;
+    }
     default: {
       return vec2<f32>();
     }
@@ -638,45 +1032,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar00) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadScalar_ConstColumnIndex_ConstRowIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.m[0][0];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let l = s.m_0[0u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar10) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -688,12 +1047,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -708,10 +1068,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalarI0) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadScalar_VariableColumnIndex_ConstRowIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -724,12 +1084,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -742,6 +1103,9 @@
     case 1u: {
       return s.m_1[0u];
     }
+    case 2u: {
+      return s.m_2[0u];
+    }
     default: {
       return f32();
     }
@@ -759,168 +1123,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar01) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadScalar_ConstColumnIndex_VariableRowIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.m[0][1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let l = s.m_0[1u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar11) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.m[1][1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let l = s.m_1[1u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalarI1) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let I = 0;
-  let l = s.m[I][1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn load_s_m_p0_1(p0 : u32) -> f32 {
-  switch(p0) {
-    case 0u: {
-      return s.m_0[1u];
-    }
-    case 1u: {
-      return s.m_1[1u];
-    }
-    default: {
-      return f32();
-    }
-  }
-}
-
-fn f() {
-  let I = 0;
-  let l = load_s_m_p0_1(u32(I));
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar0I) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let I = 0;
-  let l = s.m[0][I];
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  m : mat2x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let I = 0;
-  let l = s.m_0[I];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalar1I) {
-    auto* src = R"(
-struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -933,12 +1139,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -954,10 +1161,10 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructMat2x2Uniform_LoadScalarII) {
+TEST_F(Std140Test_F32, StructMatUniform_LoadScalar_VariableColumnIndex_VariableRowIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S;
@@ -970,12 +1177,13 @@
 
     auto* expect = R"(
 struct S {
-  m : mat2x2<f32>,
+  m : mat3x2<f32>,
 }
 
 struct S_std140 {
   m_0 : vec2<f32>,
   m_1 : vec2<f32>,
+  m_2 : vec2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
@@ -988,6 +1196,9 @@
     case 1u: {
       return s.m_1[p1];
     }
+    case 2u: {
+      return s.m_2[p1];
+    }
     default: {
       return f32();
     }
@@ -1005,10 +1216,11 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadArray) {
+TEST_F(Std140Test_F32, ArrayStructMatUniform_LoadArray_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
+  @size(64)
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
@@ -1055,16 +1267,17 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadStruct0) {
+TEST_F(Std140Test_F32, ArrayStructMatUniform_LoadStruct_ConstIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
+  @size(64)
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
-  let l = a[0];
+  let l = a[2];
 }
 )";
 
@@ -1088,7 +1301,7 @@
 }
 
 fn f() {
-  let l = conv_S(a[0u]);
+  let l = conv_S(a[2u]);
 }
 )";
 
@@ -1097,54 +1310,13 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadStruct1) {
+TEST_F(Std140Test_F32, ArrayStructMatUniform_LoadStruct_VariableIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S, 3>;
-
-fn f() {
-  let l = a[1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
   @size(64)
   m : mat3x2<f32>,
 }
 
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
-
-fn conv_S(val : S_std140) -> S {
-  return S(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
-}
-
-fn f() {
-  let l = conv_S(a[1u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadStructI) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
@@ -1183,16 +1355,17 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix0) {
+TEST_F(Std140Test_F32, ArrayStructMatUniform_LoadMatrix_ConstArrayIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
+  @size(64)
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
-  let l = a[0].m;
+  let l = a[2].m;
 }
 )";
 
@@ -1211,13 +1384,13 @@
 
 @group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
 
-fn load_a_0_m() -> mat3x2<f32> {
-  let s = &(a[0u]);
+fn load_a_2_m() -> mat3x2<f32> {
+  let s = &(a[2u]);
   return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
 }
 
 fn f() {
-  let l = load_a_0_m();
+  let l = load_a_2_m();
 }
 )";
 
@@ -1226,55 +1399,13 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix1) {
+TEST_F(Std140Test_F32, ArrayStructMatUniform_LoadMatrix_VariableArrayIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S, 3>;
-
-fn f() {
-  let l = a[1].m;
-}
-)";
-
-    auto* expect = R"(
-struct S {
   @size(64)
   m : mat3x2<f32>,
 }
 
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
-
-fn load_a_1_m() -> mat3x2<f32> {
-  let s = &(a[1u]);
-  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
-}
-
-fn f() {
-  let l = load_a_1_m();
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrixI) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
@@ -1314,16 +1445,18 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix0Column0) {
+TEST_F(Std140Test_F32,
+       ArrayStructMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
+  @size(64)
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
-  let l = a[0].m[0];
+  let l = a[2].m[1];
 }
 )";
 
@@ -1343,7 +1476,7 @@
 @group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
 
 fn f() {
-  let l = a[0u].m_0;
+  let l = a[2u].m_1;
 }
 )";
 
@@ -1352,55 +1485,19 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix1Column0) {
+TEST_F(Std140Test_F32,
+       ArrayStructMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S, 3>;
-
-fn f() {
-  let l = a[1].m[0];
-}
-)";
-
-    auto* expect = R"(
-struct S {
   @size(64)
   m : mat3x2<f32>,
 }
 
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
-
-fn f() {
-  let l = a[1u].m_0;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrixIColumn0) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
   let I = 1;
-  let l = a[I].m[0];
+  let l = a[I].m[1];
 }
 )";
 
@@ -1421,7 +1518,7 @@
 
 fn f() {
   let I = 1;
-  let l = a[I].m_0;
+  let l = a[I].m_1;
 }
 )";
 
@@ -1430,16 +1527,19 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix0Column1) {
+TEST_F(Std140Test_F32,
+       ArrayStructMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
+  @size(64)
+  m : mat3x2<f32>,
 }
 
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
-  let l = a[0].m[1];
+  let I = 1;
+  let l = a[2].m[I];
 }
 )";
 
@@ -1458,8 +1558,26 @@
 
 @group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
 
+fn load_a_2_m_p0(p0 : u32) -> vec2<f32> {
+  switch(p0) {
+    case 0u: {
+      return a[2u].m_0;
+    }
+    case 1u: {
+      return a[2u].m_1;
+    }
+    case 2u: {
+      return a[2u].m_2;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
 fn f() {
-  let l = a[0u].m_1;
+  let I = 1;
+  let l = load_a_2_m_p0(u32(I));
 }
 )";
 
@@ -1468,50 +1586,14 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrix1Column1) {
+TEST_F(Std140Test_F32,
+       ArrayStructMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex_Mat3x2F32) {
     auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S, 3>;
-
-fn f() {
-  let l = a[1].m[1];
-}
-)";
-
-    auto* expect = R"(
-struct S {
   @size(64)
   m : mat3x2<f32>,
 }
 
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<S_std140, 3u>;
-
-fn f() {
-  let l = a[1u].m_1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_LoadMatrixIColumnI) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
 @group(0) @binding(0) var<uniform> a : array<S, 3>;
 
 fn f() {
@@ -1563,121 +1645,120 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, Mat4x2Uniform_LoadMatrix) {
+TEST_F(Std140Test_F32, ArrayStructArrayStructMatUniform_Loads_Mat3x2F32) {
     auto* src = R"(
-@group(0) @binding(0) var<uniform> m : mat4x2<f32>;
-
-fn f() {
-  let l = m;
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
+struct Inner {
+  @size(64)
+  m : mat3x2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> m : mat4x2_f32;
-
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
+struct Outer {
+  a : array<Inner, 4>,
 }
 
-fn f() {
-  let l = conv_mat4x2_f32(m);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat2x2Uniform_LoadColumn0) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat2x2<f32>;
-
-fn f() {
-  let l = a[0];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat2x2_f32;
-
-fn f() {
-  let l = a.col0;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat4x2Uniform_LoadColumn1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat4x2<f32>;
-
-fn f() {
-  let l = a[1];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat4x2_f32;
-
-fn f() {
-  let l = a.col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat2x2Uniform_LoadColumnI) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat2x2<f32>;
+@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
 
 fn f() {
   let I = 1;
-
-  let l = a[I];
+  let J = 2;
+  let K = 0;
+  let l_a : array<Outer, 4> = a;
+  let l_a_1 : Outer = a[1];
+  let l_a_I : Outer = a[I];
+  let l_a_2_a : array<Inner, 4> = a[2].a;
+  let l_a_I_a : array<Inner, 4> = a[I].a;
+  let l_a_3_a_1 : Inner = a[3].a[1];
+  let l_a_3_a_I : Inner = a[3].a[I];
+  let l_a_I_a_1 : Inner = a[I].a[1];
+  let l_a_I_a_J : Inner = a[I].a[J];
+  let l_a_0_a_2_m : mat3x2<f32> = a[0].a[2].m;
+  let l_a_0_a_I_m : mat3x2<f32> = a[0].a[I].m;
+  let l_a_I_a_2_m : mat3x2<f32> = a[I].a[2].m;
+  let l_a_I_a_J_m : mat3x2<f32> = a[I].a[J].m;
+  let l_a_1_a_3_m_0 : vec2<f32> = a[1].a[3].m[0];
+  let l_a_I_a_J_m_K : vec2<f32> = a[I].a[J].m[K];
+  let l_a_2_a_0_m_1_0 : f32 = a[2].a[0].m[1][0];
+  let l_a_I_a_J_m_K_I : f32 = a[I].a[J].m[K][I];
 }
 )";
 
     auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
+struct Inner {
+  @size(64)
+  m : mat3x2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> a : mat2x2_f32;
+struct Inner_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
 
-fn load_a_p0(p0 : u32) -> vec2<f32> {
-  switch(p0) {
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+struct Outer_std140 {
+  a : array<Inner_std140, 4u>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
+
+fn conv_Inner(val : Inner_std140) -> Inner {
+  return Inner(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
+}
+
+fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
+  var arr : array<Inner, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Inner(val[i]);
+  }
+  return arr;
+}
+
+fn conv_Outer(val : Outer_std140) -> Outer {
+  return Outer(conv_arr4_Inner(val.a));
+}
+
+fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
+  var arr : array<Outer, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Outer(val[i]);
+  }
+  return arr;
+}
+
+fn load_a_0_a_2_m() -> mat3x2<f32> {
+  let s = &(a[0u].a[2u]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_0_a_p0_m(p0 : u32) -> mat3x2<f32> {
+  let s = &(a[0u].a[p0]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_2_m(p0 : u32) -> mat3x2<f32> {
+  let s = &(a[p0].a[2u]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_p1_m(p0 : u32, p1 : u32) -> mat3x2<f32> {
+  let s = &(a[p0].a[p1]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_p1_m_p2(p0 : u32, p1 : u32, p2 : u32) -> vec2<f32> {
+  switch(p2) {
     case 0u: {
-      return a.col0;
+      return a[p0].a[p1].m_0;
     }
     case 1u: {
-      return a.col1;
+      return a[p0].a[p1].m_1;
+    }
+    case 2u: {
+      return a[p0].a[p1].m_2;
     }
     default: {
       return vec2<f32>();
@@ -1685,157 +1766,16 @@
   }
 }
 
-fn f() {
-  let I = 1;
-  let l = load_a_p0(u32(I));
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat2x2Uniform_LoadColumn1Swizzle) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat2x2<f32>;
-
-fn f() {
-  let l = a[1].yx;
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat2x2_f32;
-
-fn f() {
-  let l = a.col1.yx;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat4x2Uniform_LoadColumnISwizzle) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat4x2<f32>;
-
-fn f() {
-  let I = 1;
-
-  let l = a[I].yx;
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat4x2_f32;
-
-fn load_a_p0_yx(p0 : u32) -> vec2<f32> {
-  switch(p0) {
+fn load_a_p0_a_p1_m_p2_p3(p0 : u32, p1 : u32, p2 : u32, p3 : u32) -> f32 {
+  switch(p2) {
     case 0u: {
-      return a.col0.yx;
+      return a[p0].a[p1].m_0[p3];
     }
     case 1u: {
-      return a.col1.yx;
+      return a[p0].a[p1].m_1[p3];
     }
     case 2u: {
-      return a.col2.yx;
-    }
-    case 3u: {
-      return a.col3.yx;
-    }
-    default: {
-      return vec2<f32>();
-    }
-  }
-}
-
-fn f() {
-  let I = 1;
-  let l = load_a_p0_yx(u32(I));
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat2x2Uniform_LoadColumn1Element1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat2x2<f32>;
-
-fn f() {
-  let l = a[1][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat2x2_f32;
-
-fn f() {
-  let l = a.col1[1u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, Mat4x2Uniform_LoadColumnIElementI) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : mat4x2<f32>;
-
-fn f() {
-  let I = 1;
-
-  let l = a[I][I];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : mat4x2_f32;
-
-fn load_a_p0_p1(p0 : u32, p1 : u32) -> f32 {
-  switch(p0) {
-    case 0u: {
-      return a.col0[p1];
-    }
-    case 1u: {
-      return a.col1[p1];
-    }
-    case 2u: {
-      return a.col2[p1];
-    }
-    case 3u: {
-      return a.col3[p1];
+      return a[p0].a[p1].m_2[p3];
     }
     default: {
       return f32();
@@ -1845,7 +1785,25 @@
 
 fn f() {
   let I = 1;
-  let l = load_a_p0_p1(u32(I), u32(I));
+  let J = 2;
+  let K = 0;
+  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
+  let l_a_1 : Outer = conv_Outer(a[1u]);
+  let l_a_I : Outer = conv_Outer(a[I]);
+  let l_a_2_a : array<Inner, 4> = conv_arr4_Inner(a[2u].a);
+  let l_a_I_a : array<Inner, 4> = conv_arr4_Inner(a[I].a);
+  let l_a_3_a_1 : Inner = conv_Inner(a[3u].a[1u]);
+  let l_a_3_a_I : Inner = conv_Inner(a[3u].a[I]);
+  let l_a_I_a_1 : Inner = conv_Inner(a[I].a[1u]);
+  let l_a_I_a_J : Inner = conv_Inner(a[I].a[J]);
+  let l_a_0_a_2_m : mat3x2<f32> = load_a_0_a_2_m();
+  let l_a_0_a_I_m : mat3x2<f32> = load_a_0_a_p0_m(u32(I));
+  let l_a_I_a_2_m : mat3x2<f32> = load_a_p0_a_2_m(u32(I));
+  let l_a_I_a_J_m : mat3x2<f32> = load_a_p0_a_p1_m(u32(I), u32(J));
+  let l_a_1_a_3_m_0 : vec2<f32> = a[1u].a[3u].m_0;
+  let l_a_I_a_J_m_K : vec2<f32> = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a_2_a_0_m_1_0 : f32 = a[2u].a[0u].m_1[0u];
+  let l_a_I_a_J_m_K_I : f32 = load_a_p0_a_p1_m_p2_p3(u32(I), u32(J), u32(K), u32(I));
 }
 )";
 
@@ -1854,7 +1812,492 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat2x2Uniform_LoadArray) {
+TEST_F(Std140Test_F32, ArrayStructArrayStructMatUniform_LoadsViaPtrs_Mat3x2F32) {
+    auto* src = R"(
+struct Inner {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let p_a = &(a);
+  let p_a_3 = &((*(p_a))[3]);
+  let p_a_I = &((*(p_a))[I]);
+  let p_a_3_a = &((*(p_a_3)).a);
+  let p_a_I_a = &((*(p_a_I)).a);
+  let p_a_3_a_2 = &((*(p_a_3_a))[2]);
+  let p_a_3_a_I = &((*(p_a_3_a))[I]);
+  let p_a_I_a_2 = &((*(p_a_I_a))[2]);
+  let p_a_I_a_J = &((*(p_a_I_a))[J]);
+  let p_a_3_a_2_m = &((*(p_a_3_a_2)).m);
+  let p_a_3_a_I_m = &((*(p_a_3_a_I)).m);
+  let p_a_I_a_2_m = &((*(p_a_I_a_2)).m);
+  let p_a_I_a_J_m = &((*(p_a_I_a_J)).m);
+  let p_a_3_a_2_m_1 = &((*(p_a_3_a_2_m))[1]);
+  let p_a_I_a_J_m_K = &((*(p_a_I_a_J_m))[K]);
+  let l_a : array<Outer, 4> = *(p_a);
+  let l_a_3 : Outer = *(p_a_3);
+  let l_a_I : Outer = *(p_a_I);
+  let l_a_3_a : array<Inner, 4> = *(p_a_3_a);
+  let l_a_I_a : array<Inner, 4> = *(p_a_I_a);
+  let l_a_3_a_2 : Inner = *(p_a_3_a_2);
+  let l_a_3_a_I : Inner = *(p_a_3_a_I);
+  let l_a_I_a_2 : Inner = *(p_a_I_a_2);
+  let l_a_I_a_J : Inner = *(p_a_I_a_J);
+  let l_a_3_a_2_m : mat3x2<f32> = *(p_a_3_a_2_m);
+  let l_a_3_a_I_m : mat3x2<f32> = *(p_a_3_a_I_m);
+  let l_a_I_a_2_m : mat3x2<f32> = *(p_a_I_a_2_m);
+  let l_a_I_a_J_m : mat3x2<f32> = *(p_a_I_a_J_m);
+  let l_a_3_a_2_m_1 : vec2<f32> = *(p_a_3_a_2_m_1);
+  let l_a_I_a_J_m_K : vec2<f32> = *(p_a_I_a_J_m_K);
+  let l_a_2_a_0_m_1_0 : f32 = (*(p_a_3_a_2_m_1))[0];
+  let l_a_I_a_J_m_K_I : f32 = (*(p_a_I_a_J_m_K))[I];
+}
+)";
+
+    auto* expect = R"(
+struct Inner {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct Inner_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+struct Outer {
+  a : array<Inner, 4>,
+}
+
+struct Outer_std140 {
+  a : array<Inner_std140, 4u>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
+
+fn conv_Inner(val : Inner_std140) -> Inner {
+  return Inner(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
+}
+
+fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
+  var arr : array<Inner, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Inner(val[i]);
+  }
+  return arr;
+}
+
+fn conv_Outer(val : Outer_std140) -> Outer {
+  return Outer(conv_arr4_Inner(val.a));
+}
+
+fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
+  var arr : array<Outer, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_Outer(val[i]);
+  }
+  return arr;
+}
+
+fn load_a_3_a_2_m() -> mat3x2<f32> {
+  let s = &(a[3u].a[2u]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_3_a_p0_m(p0 : u32) -> mat3x2<f32> {
+  let s = &(a[3u].a[p0]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_2_m(p0 : u32) -> mat3x2<f32> {
+  let s = &(a[p0].a[2u]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_p1_m(p0 : u32, p1 : u32) -> mat3x2<f32> {
+  let s = &(a[p0].a[p1]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn load_a_p0_a_p1_m_p2(p0 : u32, p1 : u32, p2 : u32) -> vec2<f32> {
+  switch(p2) {
+    case 0u: {
+      return a[p0].a[p1].m_0;
+    }
+    case 1u: {
+      return a[p0].a[p1].m_1;
+    }
+    case 2u: {
+      return a[p0].a[p1].m_2;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn load_a_p0_a_p1_m_p2_p3(p0 : u32, p1 : u32, p2 : u32, p3 : u32) -> f32 {
+  switch(p2) {
+    case 0u: {
+      return a[p0].a[p1].m_0[p3];
+    }
+    case 1u: {
+      return a[p0].a[p1].m_1[p3];
+    }
+    case 2u: {
+      return a[p0].a[p1].m_2[p3];
+    }
+    default: {
+      return f32();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let K = 0;
+  let p_a = conv_arr4_Outer(a);
+  let p_a_3 = conv_Outer(a[3u]);
+  let p_a_I = conv_Outer(a[I]);
+  let p_a_3_a = conv_arr4_Inner(a[3u].a);
+  let p_a_I_a = conv_arr4_Inner(a[I].a);
+  let p_a_3_a_2 = conv_Inner(a[3u].a[2u]);
+  let p_a_3_a_I = conv_Inner(a[3u].a[I]);
+  let p_a_I_a_2 = conv_Inner(a[I].a[2u]);
+  let p_a_I_a_J = conv_Inner(a[I].a[J]);
+  let p_a_3_a_2_m = load_a_3_a_2_m();
+  let p_a_3_a_I_m = load_a_3_a_p0_m(u32(I));
+  let p_a_I_a_2_m = load_a_p0_a_2_m(u32(I));
+  let p_a_I_a_J_m = load_a_p0_a_p1_m(u32(I), u32(J));
+  let p_a_3_a_2_m_1 = a[3u].a[2u].m_1;
+  let p_a_I_a_J_m_K = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
+  let l_a_3 : Outer = conv_Outer(a[3u]);
+  let l_a_I : Outer = conv_Outer(a[I]);
+  let l_a_3_a : array<Inner, 4> = conv_arr4_Inner(a[3u].a);
+  let l_a_I_a : array<Inner, 4> = conv_arr4_Inner(a[I].a);
+  let l_a_3_a_2 : Inner = conv_Inner(a[3u].a[2u]);
+  let l_a_3_a_I : Inner = conv_Inner(a[3u].a[I]);
+  let l_a_I_a_2 : Inner = conv_Inner(a[I].a[2u]);
+  let l_a_I_a_J : Inner = conv_Inner(a[I].a[J]);
+  let l_a_3_a_2_m : mat3x2<f32> = load_a_3_a_2_m();
+  let l_a_3_a_I_m : mat3x2<f32> = load_a_3_a_p0_m(u32(I));
+  let l_a_I_a_2_m : mat3x2<f32> = load_a_p0_a_2_m(u32(I));
+  let l_a_I_a_J_m : mat3x2<f32> = load_a_p0_a_p1_m(u32(I), u32(J));
+  let l_a_3_a_2_m_1 : vec2<f32> = a[3u].a[2u].m_1;
+  let l_a_I_a_J_m_K : vec2<f32> = load_a_p0_a_p1_m_p2(u32(I), u32(J), u32(K));
+  let l_a_2_a_0_m_1_0 : f32 = a[3u].a[2u].m_1[0u];
+  let l_a_I_a_J_m_K_I : f32 = load_a_p0_a_p1_m_p2_p3(u32(I), u32(J), u32(K), u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyArray_UniformToStorage_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s = u;
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
+}
+
+fn conv_arr4_S(val : array<S_std140, 4u>) -> array<S, 4u> {
+  var arr : array<S, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_S(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  s = conv_arr4_S(u);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyStruct_UniformToWorkgroup_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[0] = u[1];
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+var<workgroup> w : array<S, 4>;
+
+fn conv_S(val : S_std140) -> S {
+  return S(val.v, mat3x2<f32>(val.m_0, val.m_1, val.m_2));
+}
+
+fn f() {
+  w[0] = conv_S(u[1u]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyMatrix_UniformToPrivate_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[2].m = u[1].m;
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+var<private> p : array<S, 4>;
+
+fn load_u_1_m() -> mat3x2<f32> {
+  let s = &(u[1u]);
+  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
+}
+
+fn f() {
+  p[2].m = load_u_1_m();
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyColumn_UniformToStorage_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s[3].m[1] = u[2].m[0];
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
+
+fn f() {
+  s[3].m[1] = u[2u].m_0;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyColumnSwizzle_UniformToWorkgroup_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 4>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[3].m[1] = u[2].m[0].yx.yx;
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+
+var<workgroup> w : array<S, 4>;
+
+fn f() {
+  w[3].m[1] = u[2u].m_0.yx.yx;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayStructMatUniform_CopyScalar_UniformToPrivate_Mat3x2F32) {
+    auto* src = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S, 3>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[3].m[1].x = u[2].m[0].y;
+}
+)";
+
+    auto* expect = R"(
+struct S {
+  v : vec4<i32>,
+  @size(64)
+  m : mat3x2<f32>,
+}
+
+struct S_std140 {
+  v : vec4<i32>,
+  m_0 : vec2<f32>,
+  m_1 : vec2<f32>,
+  @size(48)
+  m_2 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> u : array<S_std140, 3u>;
+
+var<private> p : array<S, 4>;
+
+fn f() {
+  p[3].m[1].x = u[2u].m_0[1u];
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadArray_Mat2x2F32) {
     auto* src = R"(
 @group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
@@ -1893,45 +2336,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat4x2Uniform_LoadMatrix0) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<mat4x2<f32>, 3>;
-
-fn f() {
-  let l = a[0];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<mat4x2_f32, 3u>;
-
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
-}
-
-fn f() {
-  let l = conv_mat4x2_f32(a[0u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayMat2x2Uniform_LoadMatrix1) {
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadMatrix_ConstArrayIndex_Mat2x2F32) {
     auto* src = R"(
 @group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
 fn f() {
-  let l = a[1];
+  let l = a[2];
 }
 )";
 
@@ -1948,7 +2358,7 @@
 }
 
 fn f() {
-  let l = conv_mat2x2_f32(a[1u]);
+  let l = conv_mat2x2_f32(a[2u]);
 }
 )";
 
@@ -1957,9 +2367,9 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat4x2Uniform_LoadMatrixI) {
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadMatrix_VariableArrayIndex_Mat2x2F32) {
     auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<mat4x2<f32>, 3>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
 fn f() {
   let I = 1;
@@ -1968,22 +2378,20 @@
 )";
 
     auto* expect = R"(
-struct mat4x2_f32 {
+struct mat2x2_f32 {
   col0 : vec2<f32>,
   col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> a : array<mat4x2_f32, 3u>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2_f32, 3u>;
 
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
 }
 
 fn f() {
   let I = 1;
-  let l = conv_mat4x2_f32(a[I]);
+  let l = conv_mat2x2_f32(a[I]);
 }
 )";
 
@@ -1992,12 +2400,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat2x2Uniform_LoadMatrix1Column0) {
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex_Mat2x2F32) {
     auto* src = R"(
 @group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
 fn f() {
-  let l = a[1][0];
+  let l = a[2][1];
 }
 )";
 
@@ -2010,7 +2418,7 @@
 @group(0) @binding(0) var<uniform> a : array<mat2x2_f32, 3u>;
 
 fn f() {
-  let l = a[1u].col0;
+  let l = a[2u].col1;
 }
 )";
 
@@ -2019,36 +2427,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat4x2Uniform_LoadMatrix0Column1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<mat4x2<f32>, 3>;
-
-fn f() {
-  let l = a[0][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<mat4x2_f32, 3u>;
-
-fn f() {
-  let l = a[0u].col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayMat2x2Uniform_LoadMatrixIColumn1) {
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex_Mat2x2F32) {
     auto* src = R"(
 @group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
@@ -2077,39 +2456,31 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayMat4x2Uniform_LoadMatrix1ColumnI) {
+TEST_F(Std140Test_F32, ArrayMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex_Mat2x2F32) {
     auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<mat4x2<f32>, 3>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
 fn f() {
   let I = 1;
-  let l = a[1][I];
+  let l = a[2][I];
 }
 )";
 
     auto* expect = R"(
-struct mat4x2_f32 {
+struct mat2x2_f32 {
   col0 : vec2<f32>,
   col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> a : array<mat4x2_f32, 3u>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2_f32, 3u>;
 
-fn load_a_1_p0(p0 : u32) -> vec2<f32> {
+fn load_a_2_p0(p0 : u32) -> vec2<f32> {
   switch(p0) {
     case 0u: {
-      return a[1u].col0;
+      return a[2u].col0;
     }
     case 1u: {
-      return a[1u].col1;
-    }
-    case 2u: {
-      return a[1u].col2;
-    }
-    case 3u: {
-      return a[1u].col3;
+      return a[2u].col1;
     }
     default: {
       return vec2<f32>();
@@ -2119,7 +2490,7 @@
 
 fn f() {
   let I = 1;
-  let l = load_a_1_p0(u32(I));
+  let l = load_a_2_p0(u32(I));
 }
 )";
 
@@ -2128,12 +2499,14 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadArrays) {
+TEST_F(Std140Test_F32,
+       ArrayMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex_Mat2x2F32) {
     auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2<f32>, 3>;
 
 fn f() {
-  let l = a;
+  let I = 1;
+  let l = a[I][I];
 }
 )";
 
@@ -2143,307 +2516,15 @@
   col1 : vec2<f32>,
 }
 
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+@group(0) @binding(0) var<uniform> a : array<mat2x2_f32, 3u>;
 
-fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
-  return mat2x2<f32>(val.col0, val.col1);
-}
-
-fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
-  var arr : array<mat2x2<f32>, 3u>;
-  for(var i : u32; (i < 3u); i = (i + 1)) {
-    arr[i] = conv_mat2x2_f32(val[i]);
-  }
-  return arr;
-}
-
-fn conv_arr4_arr3_mat2x2_f32(val : array<array<mat2x2_f32, 3u>, 4u>) -> array<array<mat2x2<f32>, 3u>, 4u> {
-  var arr : array<array<mat2x2<f32>, 3u>, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_arr3_mat2x2_f32(val[i]);
-  }
-  return arr;
-}
-
-fn f() {
-  let l = conv_arr4_arr3_mat2x2_f32(a);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat4x2Uniform_LoadArray0) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2<f32>, 3>, 4>;
-
-fn f() {
-  let l = a[0];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2_f32, 3u>, 4u>;
-
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
-}
-
-fn conv_arr3_mat4x2_f32(val : array<mat4x2_f32, 3u>) -> array<mat4x2<f32>, 3u> {
-  var arr : array<mat4x2<f32>, 3u>;
-  for(var i : u32; (i < 3u); i = (i + 1)) {
-    arr[i] = conv_mat4x2_f32(val[i]);
-  }
-  return arr;
-}
-
-fn f() {
-  let l = conv_arr3_mat4x2_f32(a[0u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadArray1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>,4>;
-
-fn f() {
-  let l = a[1];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
-
-fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
-  return mat2x2<f32>(val.col0, val.col1);
-}
-
-fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
-  var arr : array<mat2x2<f32>, 3u>;
-  for(var i : u32; (i < 3u); i = (i + 1)) {
-    arr[i] = conv_mat2x2_f32(val[i]);
-  }
-  return arr;
-}
-
-fn f() {
-  let l = conv_arr3_mat2x2_f32(a[1u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadArrayI) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>,4>;
-
-fn f() {
-  let I = 1;
-  let l = a[I];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
-
-fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
-  return mat2x2<f32>(val.col0, val.col1);
-}
-
-fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
-  var arr : array<mat2x2<f32>, 3u>;
-  for(var i : u32; (i < 3u); i = (i + 1)) {
-    arr[i] = conv_mat2x2_f32(val[i]);
-  }
-  return arr;
-}
-
-fn f() {
-  let I = 1;
-  let l = conv_arr3_mat2x2_f32(a[I]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadMatrix12Column0) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
-
-fn f() {
-  let l = a[1][2][0];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
-
-fn f() {
-  let l = a[1u][2u].col0;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat4x2Uniform_LoadMatrix2IColumn1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2<f32>, 3>, 4>;
-
-fn f() {
-  let I = 1;
-  let l = a[2][I][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2_f32, 3u>, 4u>;
-
-fn f() {
-  let I = 1;
-  let l = a[2u][I].col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadMatrixI2Column1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
-
-fn f() {
-  let I = 1;
-  let l = a[I][2][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
-
-fn f() {
-  let I = 1;
-  let l = a[I][2u].col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat2x2Uniform_LoadMatrixIIColumn1) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
-
-fn f() {
-  let I = 1;
-  let l = a[I][I][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat2x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
-
-fn f() {
-  let I = 1;
-  let l = a[I][I].col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayArrayMat4x2Uniform_LoadMatrix12ColumnI) {
-    auto* src = R"(
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2<f32>, 3>, 4>;
-
-fn f() {
-  let I = 1;
-  let l = a[1][2][I];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<array<mat4x2_f32, 3u>, 4u>;
-
-fn load_a_1_2_p0(p0 : u32) -> vec2<f32> {
-  switch(p0) {
+fn load_a_p0_p1(p0 : u32, p1 : u32) -> vec2<f32> {
+  switch(p1) {
     case 0u: {
-      return a[1u][2u].col0;
+      return a[p0].col0;
     }
     case 1u: {
-      return a[1u][2u].col1;
-    }
-    case 2u: {
-      return a[1u][2u].col2;
-    }
-    case 3u: {
-      return a[1u][2u].col3;
+      return a[p0].col1;
     }
     default: {
       return vec2<f32>();
@@ -2453,7 +2534,7 @@
 
 fn f() {
   let I = 1;
-  let l = load_a_1_2_p0(u32(I));
+  let l = load_a_p0_p1(u32(I), u32(I));
 }
 )";
 
@@ -2462,11 +2543,11 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat2x2Uniform_LoadStruct) {
+TEST_F(Std140Test_F32, StructArrayMatUniform_LoadStruct_Mat2x2F32) {
     auto* src = R"(
 struct S {
   a : array<mat2x2<f32>, 3>,
-};
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
@@ -2517,11 +2598,11 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat2x2Uniform_LoadArray) {
+TEST_F(Std140Test_F32, StructArrayMatUniform_LoadArray_Mat2x2F32) {
     auto* src = R"(
 struct S {
   a : array<mat2x2<f32>, 3>,
-};
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
@@ -2568,61 +2649,16 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat4x2Uniform_LoadMatrix0) {
-    auto* src = R"(
-struct S {
-  a : array<mat4x2<f32>, 3>,
-};
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.a[0];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-struct S {
-  a : array<mat4x2<f32>, 3>,
-}
-
-struct S_std140 {
-  a : array<mat4x2_f32, 3u>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
-}
-
-fn f() {
-  let l = conv_mat4x2_f32(s.a[0u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructArrayMat2x2Uniform_LoadMatrix1) {
+TEST_F(Std140Test_F32, StructArrayMatUniform_LoadMatrix_ConstArrayIndex_Mat2x2F32) {
     auto* src = R"(
 struct S {
   a : array<mat2x2<f32>, 3>,
-};
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
 fn f() {
-  let l = s.a[1];
+  let l = s.a[2];
 }
 )";
 
@@ -2647,7 +2683,7 @@
 }
 
 fn f() {
-  let l = conv_mat2x2_f32(s.a[1u]);
+  let l = conv_mat2x2_f32(s.a[2u]);
 }
 )";
 
@@ -2656,11 +2692,11 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat4x2Uniform_LoadMatrixI) {
+TEST_F(Std140Test_F32, StructArrayMatUniform_LoadMatrix_VariableArrayIndex_Mat2x2F32) {
     auto* src = R"(
 struct S {
-  a : array<mat4x2<f32>, 3>,
-};
+  a : array<mat2x2<f32>, 3>,
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
@@ -2671,30 +2707,28 @@
 )";
 
     auto* expect = R"(
-struct mat4x2_f32 {
+struct mat2x2_f32 {
   col0 : vec2<f32>,
   col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
 }
 
 struct S {
-  a : array<mat4x2<f32>, 3>,
+  a : array<mat2x2<f32>, 3>,
 }
 
 struct S_std140 {
-  a : array<mat4x2_f32, 3u>,
+  a : array<mat2x2_f32, 3u>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
 
-fn conv_mat4x2_f32(val : mat4x2_f32) -> mat4x2<f32> {
-  return mat4x2<f32>(val.col0, val.col1, val.col2, val.col3);
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
 }
 
 fn f() {
   let I = 1;
-  let l = conv_mat4x2_f32(s.a[I]);
+  let l = conv_mat2x2_f32(s.a[I]);
 }
 )";
 
@@ -2703,16 +2737,17 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat2x2Uniform_LoadMatrix1Column0) {
+TEST_F(Std140Test_F32,
+       StructArrayMatUniform_LoadColumn_ConstArrayIndex_ConstColumnIndex_Mat2x2F32) {
     auto* src = R"(
 struct S {
   a : array<mat2x2<f32>, 3>,
-};
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
 fn f() {
-  let l = s.a[1][0];
+  let l = s.a[2][1];
 }
 )";
 
@@ -2733,7 +2768,7 @@
 @group(0) @binding(0) var<uniform> s : S_std140;
 
 fn f() {
-  let l = s.a[1u].col0;
+  let l = s.a[2u].col1;
 }
 )";
 
@@ -2742,52 +2777,12 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat4x2Uniform_LoadMatrix0Column1) {
-    auto* src = R"(
-struct S {
-  a : array<mat4x2<f32>, 3>,
-};
-
-@group(0) @binding(0) var<uniform> s : S;
-
-fn f() {
-  let l = s.a[0][1];
-}
-)";
-
-    auto* expect = R"(
-struct mat4x2_f32 {
-  col0 : vec2<f32>,
-  col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
-}
-
-struct S {
-  a : array<mat4x2<f32>, 3>,
-}
-
-struct S_std140 {
-  a : array<mat4x2_f32, 3u>,
-}
-
-@group(0) @binding(0) var<uniform> s : S_std140;
-
-fn f() {
-  let l = s.a[0u].col1;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, StructArrayMat2x2Uniform_LoadMatrixIColumn1) {
+TEST_F(Std140Test_F32,
+       StructArrayMatUniform_LoadColumn_VariableArrayIndex_ConstColumnIndex_Mat2x2F32) {
     auto* src = R"(
 struct S {
   a : array<mat2x2<f32>, 3>,
-};
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
@@ -2824,51 +2819,44 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, StructArrayMat4x2Uniform_LoadMatrix1ColumnI) {
+TEST_F(Std140Test_F32,
+       StructArrayMatUniform_LoadColumn_ConstArrayIndex_VariableColumnIndex_Mat2x2F32) {
     auto* src = R"(
 struct S {
-  a : array<mat4x2<f32>, 3>,
-};
+  a : array<mat2x2<f32>, 3>,
+}
 
 @group(0) @binding(0) var<uniform> s : S;
 
 fn f() {
   let I = 1;
-  let l = s.a[1][I];
+  let l = s.a[2][I];
 }
 )";
 
     auto* expect = R"(
-struct mat4x2_f32 {
+struct mat2x2_f32 {
   col0 : vec2<f32>,
   col1 : vec2<f32>,
-  col2 : vec2<f32>,
-  col3 : vec2<f32>,
 }
 
 struct S {
-  a : array<mat4x2<f32>, 3>,
+  a : array<mat2x2<f32>, 3>,
 }
 
 struct S_std140 {
-  a : array<mat4x2_f32, 3u>,
+  a : array<mat2x2_f32, 3u>,
 }
 
 @group(0) @binding(0) var<uniform> s : S_std140;
 
-fn load_s_a_1_p0(p0 : u32) -> vec2<f32> {
+fn load_s_a_2_p0(p0 : u32) -> vec2<f32> {
   switch(p0) {
     case 0u: {
-      return s.a[1u].col0;
+      return s.a[2u].col0;
     }
     case 1u: {
-      return s.a[1u].col1;
-    }
-    case 2u: {
-      return s.a[1u].col2;
-    }
-    case 3u: {
-      return s.a[1u].col3;
+      return s.a[2u].col1;
     }
     default: {
       return vec2<f32>();
@@ -2878,7 +2866,7 @@
 
 fn f() {
   let I = 1;
-  let l = load_s_a_1_p0(u32(I));
+  let l = load_s_a_2_p0(u32(I));
 }
 )";
 
@@ -2887,536 +2875,627 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(Std140Test, ArrayStructArrayStructMat4x2Uniform_Loads) {
+TEST_F(Std140Test_F32,
+       StructArrayMatUniform_LoadColumn_VariableArrayIndex_VariableColumnIndex_Mat2x2F32) {
     auto* src = R"(
-struct Inner {
-  m : mat4x2<f32>,
+struct S {
+  a : array<mat2x2<f32>, 3>,
 }
 
-struct Outer {
-  a : array<Inner, 4>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
+@group(0) @binding(0) var<uniform> s : S;
 
 fn f() {
   let I = 1;
-
-  let l_a             : array<Outer, 4>  = a;
-  let l_a_1           : Outer            = a[1];
-  let l_a_2_a         : array<Inner, 4>  = a[2].a;
-  let l_a_3_a_1       : Inner            = a[3].a[1];
-  let l_a_0_a_2_m     : mat4x2<f32>      = a[0].a[2].m;
-  let l_a_1_a_3_m_0   : vec2<f32>        = a[1].a[3].m[0];
-  let l_a_2_a_0_m_1_0 : f32              = a[2].a[0].m[1][0];
+  let l = s.a[I][I];
 }
 )";
 
     auto* expect = R"(
-struct Inner {
-  m : mat4x2<f32>,
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
 }
 
-struct Inner_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  m_2 : vec2<f32>,
-  m_3 : vec2<f32>,
-}
-
-struct Outer {
-  a : array<Inner, 4>,
-}
-
-struct Outer_std140 {
-  a : array<Inner_std140, 4u>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
-
-fn conv_Inner(val : Inner_std140) -> Inner {
-  return Inner(mat4x2<f32>(val.m_0, val.m_1, val.m_2, val.m_3));
-}
-
-fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
-  var arr : array<Inner, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_Inner(val[i]);
-  }
-  return arr;
-}
-
-fn conv_Outer(val : Outer_std140) -> Outer {
-  return Outer(conv_arr4_Inner(val.a));
-}
-
-fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
-  var arr : array<Outer, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_Outer(val[i]);
-  }
-  return arr;
-}
-
-fn load_a_0_a_2_m() -> mat4x2<f32> {
-  let s = &(a[0u].a[2u]);
-  return mat4x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2, (*(s)).m_3);
-}
-
-fn f() {
-  let I = 1;
-  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
-  let l_a_1 : Outer = conv_Outer(a[1u]);
-  let l_a_2_a : array<Inner, 4> = conv_arr4_Inner(a[2u].a);
-  let l_a_3_a_1 : Inner = conv_Inner(a[3u].a[1u]);
-  let l_a_0_a_2_m : mat4x2<f32> = load_a_0_a_2_m();
-  let l_a_1_a_3_m_0 : vec2<f32> = a[1u].a[3u].m_0;
-  let l_a_2_a_0_m_1_0 : f32 = a[2u].a[0u].m_1[0u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructArrayStructMat4x2Uniform_LoadsViaPtrs) {
-    // Note: Std140Test requires the PromoteSideEffectsToDecl transform to have been run first, so
-    // side-effects in the let-chain will not be a problem.
-    auto* src = R"(
-struct Inner {
-  m : mat4x2<f32>,
-}
-
-struct Outer {
-  a : array<Inner, 4>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<Outer, 4>;
-
-fn f() {
-  let I = 1;
-
-  let p_a = &a;
-  let p_a_3 = &((*p_a)[3]);
-  let p_a_3_a = &((*p_a_3).a);
-  let p_a_3_a_2 = &((*p_a_3_a)[2]);
-  let p_a_3_a_2_m = &((*p_a_3_a_2).m);
-  let p_a_3_a_2_m_1 = &((*p_a_3_a_2_m)[1]);
-
-
-  let l_a             : array<Outer, 4> = *p_a;
-  let l_a_3           : Outer           = *p_a_3;
-  let l_a_3_a         : array<Inner, 4> = *p_a_3_a;
-  let l_a_3_a_2       : Inner           = *p_a_3_a_2;
-  let l_a_3_a_2_m     : mat4x2<f32>     = *p_a_3_a_2_m;
-  let l_a_3_a_2_m_1   : vec2<f32>       = *p_a_3_a_2_m_1;
-  let l_a_2_a_0_m_1_0 : f32             = (*p_a_3_a_2_m_1)[0];
-}
-)";
-
-    auto* expect = R"(
-struct Inner {
-  m : mat4x2<f32>,
-}
-
-struct Inner_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  m_2 : vec2<f32>,
-  m_3 : vec2<f32>,
-}
-
-struct Outer {
-  a : array<Inner, 4>,
-}
-
-struct Outer_std140 {
-  a : array<Inner_std140, 4u>,
-}
-
-@group(0) @binding(0) var<uniform> a : array<Outer_std140, 4u>;
-
-fn conv_Inner(val : Inner_std140) -> Inner {
-  return Inner(mat4x2<f32>(val.m_0, val.m_1, val.m_2, val.m_3));
-}
-
-fn conv_arr4_Inner(val : array<Inner_std140, 4u>) -> array<Inner, 4u> {
-  var arr : array<Inner, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_Inner(val[i]);
-  }
-  return arr;
-}
-
-fn conv_Outer(val : Outer_std140) -> Outer {
-  return Outer(conv_arr4_Inner(val.a));
-}
-
-fn conv_arr4_Outer(val : array<Outer_std140, 4u>) -> array<Outer, 4u> {
-  var arr : array<Outer, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_Outer(val[i]);
-  }
-  return arr;
-}
-
-fn load_a_3_a_2_m() -> mat4x2<f32> {
-  let s = &(a[3u].a[2u]);
-  return mat4x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2, (*(s)).m_3);
-}
-
-fn f() {
-  let I = 1;
-  let p_a = conv_arr4_Outer(a);
-  let p_a_3 = conv_Outer(a[3u]);
-  let p_a_3_a = conv_arr4_Inner(a[3u].a);
-  let p_a_3_a_2 = conv_Inner(a[3u].a[2u]);
-  let p_a_3_a_2_m = load_a_3_a_2_m();
-  let p_a_3_a_2_m_1 = a[3u].a[2u].m_1;
-  let l_a : array<Outer, 4> = conv_arr4_Outer(a);
-  let l_a_3 : Outer = conv_Outer(a[3u]);
-  let l_a_3_a : array<Inner, 4> = conv_arr4_Inner(a[3u].a);
-  let l_a_3_a_2 : Inner = conv_Inner(a[3u].a[2u]);
-  let l_a_3_a_2_m : mat4x2<f32> = load_a_3_a_2_m();
-  let l_a_3_a_2_m_1 : vec2<f32> = a[3u].a[2u].m_1;
-  let l_a_2_a_0_m_1_0 : f32 = a[3u].a[2u].m_1[0u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopyArray_UniformToStorage) {
-    auto* src = R"(
 struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
-
-fn f() {
-    s = u;
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  @size(64)
-  m : mat3x2<f32>,
+  a : array<mat2x2<f32>, 3>,
 }
 
 struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
+  a : array<mat2x2_f32, 3u>,
 }
 
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
+@group(0) @binding(0) var<uniform> s : S_std140;
 
-@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
-
-fn conv_S(val : S_std140) -> S {
-  return S(mat3x2<f32>(val.m_0, val.m_1, val.m_2));
-}
-
-fn conv_arr4_S(val : array<S_std140, 4u>) -> array<S, 4u> {
-  var arr : array<S, 4u>;
-  for(var i : u32; (i < 4u); i = (i + 1)) {
-    arr[i] = conv_S(val[i]);
-  }
-  return arr;
-}
-
-fn f() {
-  s = conv_arr4_S(u);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopyStruct_UniformToWorkgroup) {
-    auto* src = R"(
-struct S {
-  v : vec4<i32>,
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-var<workgroup> w : array<S, 4>;
-
-fn f() {
-    w[0] = u[1];
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  v : vec4<i32>,
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  v : vec4<i32>,
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
-
-var<workgroup> w : array<S, 4>;
-
-fn conv_S(val : S_std140) -> S {
-  return S(val.v, mat3x2<f32>(val.m_0, val.m_1, val.m_2));
-}
-
-fn f() {
-  w[0] = conv_S(u[1u]);
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopyMatrix_UniformToPrivate) {
-    auto* src = R"(
-struct S {
-  v : vec4<i32>,
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-var<private> p : array<S, 4>;
-
-fn f() {
-    p[2].m = u[1].m;
-}
-)";
-
-    auto* expect = R"(
-struct S {
-  v : vec4<i32>,
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  v : vec4<i32>,
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
-
-var<private> p : array<S, 4>;
-
-fn load_u_1_m() -> mat3x2<f32> {
-  let s = &(u[1u]);
-  return mat3x2<f32>((*(s)).m_0, (*(s)).m_1, (*(s)).m_2);
-}
-
-fn f() {
-  p[2].m = load_u_1_m();
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopyColumn_UniformToStorage) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
-
-fn f() {
-    s[3].m[1] = u[2].m[0];
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
-
-@group(0) @binding(1) var<storage, read_write> s : array<S, 4>;
-
-fn f() {
-  s[3].m[1] = u[2u].m_0;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopySwizzle_UniformToWorkgroup) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-var<workgroup> w : array<S, 4>;
-
-fn f() {
-    w[3].m[1] = u[2].m[0].yx.xy;
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
-
-var<workgroup> w : array<S, 4>;
-
-fn f() {
-  w[3].m[1] = u[2u].m_0.yx.xy;
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, ArrayStructMat3x2Uniform_CopyScalar_UniformToPrivate) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S, 4>;
-var<private> w : array<S, 4>;
-
-fn f() {
-    w[3].m[1].x = u[2].m[0].y;
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : array<S_std140, 4u>;
-
-var<private> w : array<S, 4>;
-
-fn f() {
-  w[3].m[1].x = u[2u].m_0[1u];
-}
-)";
-
-    auto got = Run<Std140>(src);
-
-    EXPECT_EQ(expect, str(got));
-}
-
-TEST_F(Std140Test, MatrixUsageInForLoop) {
-    auto* src = R"(
-struct S {
-  @size(64) m : mat3x2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : S;
-
-fn f() {
-    for (var i = u32(u.m[0][0]); i < u32(u.m[i][1]); i += u32(u.m[1][i])) {
-    }
-}
-)";
-
-    auto* expect =
-        R"(
-struct S {
-  @size(64)
-  m : mat3x2<f32>,
-}
-
-struct S_std140 {
-  m_0 : vec2<f32>,
-  m_1 : vec2<f32>,
-  @size(48)
-  m_2 : vec2<f32>,
-}
-
-@group(0) @binding(0) var<uniform> u : S_std140;
-
-fn load_u_m_p0_1(p0 : u32) -> f32 {
-  switch(p0) {
+fn load_s_a_p0_p1(p0 : u32, p1 : u32) -> vec2<f32> {
+  switch(p1) {
     case 0u: {
-      return u.m_0[1u];
+      return s.a[p0].col0;
     }
     case 1u: {
-      return u.m_1[1u];
-    }
-    case 2u: {
-      return u.m_2[1u];
+      return s.a[p0].col1;
     }
     default: {
-      return f32();
+      return vec2<f32>();
     }
   }
 }
 
 fn f() {
-  for(var i = u32(u.m_0[0u]); (i < u32(load_u_m_p0_1(u32(i)))); i += u32(u.m_1[i])) {
+  let I = 1;
+  let l = load_s_a_p0_p1(u32(I), u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayArrayMatUniform_LoadArrays_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let l = a;
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
+  var arr : array<mat2x2<f32>, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat2x2_f32(val[i]);
   }
+  return arr;
+}
+
+fn conv_arr4_arr3_mat2x2_f32(val : array<array<mat2x2_f32, 3u>, 4u>) -> array<array<mat2x2<f32>, 3u>, 4u> {
+  var arr : array<array<mat2x2<f32>, 3u>, 4u>;
+  for(var i : u32; (i < 4u); i = (i + 1)) {
+    arr[i] = conv_arr3_mat2x2_f32(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr4_arr3_mat2x2_f32(a);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayArrayMatUniform_LoadArray_ConstOuterArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let l = a[3];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
+  var arr : array<mat2x2<f32>, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat2x2_f32(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let l = conv_arr3_mat2x2_f32(a[3u]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32, ArrayArrayMatUniform_LoadArray_VariableOuterArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn conv_arr3_mat2x2_f32(val : array<mat2x2_f32, 3u>) -> array<mat2x2<f32>, 3u> {
+  var arr : array<mat2x2<f32>, 3u>;
+  for(var i : u32; (i < 3u); i = (i + 1)) {
+    arr[i] = conv_mat2x2_f32(val[i]);
+  }
+  return arr;
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_arr3_mat2x2_f32(a[I]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32,
+       ArrayArrayMatUniform_LoadMatrix_ConstOuterArrayIndex_ConstInnerArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let l = a[3][2];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn f() {
+  let l = conv_mat2x2_f32(a[3u][2u]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32,
+       ArrayArrayMatUniform_LoadMatrix_ConstOuterArrayIndex_VariableInnerArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[3][I];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat2x2_f32(a[3u][I]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32,
+       ArrayArrayMatUniform_LoadMatrix_VariableOuterArrayIndex_ConstInnerArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][2];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat2x2_f32(a[I][2u]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(Std140Test_F32,
+       ArrayArrayMatUniform_LoadMatrix_VariableOuterArrayIndex_VariableInnerArrayIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][I];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn conv_mat2x2_f32(val : mat2x2_f32) -> mat2x2<f32> {
+  return mat2x2<f32>(val.col0, val.col1);
+}
+
+fn f() {
+  let I = 1;
+  let l = conv_mat2x2_f32(a[I][I]);
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_ConstInnerArrayIndex_ConstColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let l = a[3][2][1];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn f() {
+  let l = a[3u][2u].col1;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_ConstInnerArrayIndex_VariableColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[3][2][I];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn load_a_3_2_p0(p0 : u32) -> vec2<f32> {
+  switch(p0) {
+    case 0u: {
+      return a[3u][2u].col0;
+    }
+    case 1u: {
+      return a[3u][2u].col1;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let l = load_a_3_2_p0(u32(I));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_VariableInnerArrayIndex_ConstColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[3][I][1];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let l = a[3u][I].col1;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_ConstOuterArrayIndex_VariableInnerArrayIndex_VariableColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[3][I][J];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn load_a_3_p0_p1(p0 : u32, p1 : u32) -> vec2<f32> {
+  switch(p1) {
+    case 0u: {
+      return a[3u][p0].col0;
+    }
+    case 1u: {
+      return a[3u][p0].col1;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = load_a_3_p0_p1(u32(I), u32(J));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_ConstInnerArrayIndex_ConstColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][2][1];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let l = a[I][2u].col1;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_ConstInnerArrayIndex_VariableColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][2][J];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn load_a_p0_2_p1(p0 : u32, p1 : u32) -> vec2<f32> {
+  switch(p1) {
+    case 0u: {
+      return a[p0][2u].col0;
+    }
+    case 1u: {
+      return a[p0][2u].col1;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = load_a_p0_2_p1(u32(I), u32(J));
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_VariableInnerArrayIndex_ConstColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][J][1];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn f() {
+  let I = 1;
+  let J = 2;
+  let l = a[I][J].col1;
+}
+)";
+
+    auto got = Run<Std140>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(
+    Std140Test_F32,
+    ArrayArrayMatUniform_LoadColumn_VariableOuterArrayIndex_VariableInnerArrayIndex_VariableColumnIndex_Mat2x2F32) {
+    auto* src = R"(
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2<f32>, 3>, 4>;
+
+fn f() {
+  let I = 0;
+  let J = 1;
+  let K = 2;
+  let l = a[I][J][K];
+}
+)";
+
+    auto* expect = R"(
+struct mat2x2_f32 {
+  col0 : vec2<f32>,
+  col1 : vec2<f32>,
+}
+
+@group(0) @binding(0) var<uniform> a : array<array<mat2x2_f32, 3u>, 4u>;
+
+fn load_a_p0_p1_p2(p0 : u32, p1 : u32, p2 : u32) -> vec2<f32> {
+  switch(p2) {
+    case 0u: {
+      return a[p0][p1].col0;
+    }
+    case 1u: {
+      return a[p0][p1].col1;
+    }
+    default: {
+      return vec2<f32>();
+    }
+  }
+}
+
+fn f() {
+  let I = 0;
+  let J = 1;
+  let K = 2;
+  let l = load_a_p0_p1_p2(u32(I), u32(J), u32(K));
 }
 )";
 
diff --git a/src/tint/utils/string.cc b/src/tint/utils/string.cc
new file mode 100644
index 0000000..a0d3b85
--- /dev/null
+++ b/src/tint/utils/string.cc
@@ -0,0 +1,51 @@
+// Copyright 2022 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 <algorithm>
+
+#include "src/tint/utils/string.h"
+#include "src/tint/utils/vector.h"
+
+namespace tint::utils {
+
+size_t Distance(const std::string& str_a, const std::string& str_b) {
+    const auto len_a = str_a.size();
+    const auto len_b = str_b.size();
+
+    Vector<size_t, 64> mat;
+    mat.Reserve((len_a + 1) * (len_b + 1));
+
+    auto at = [&](size_t a, size_t b) -> size_t& { return mat[a + b * (len_a + 1)]; };
+
+    at(0, 0) = 0;
+    for (size_t a = 1; a <= len_a; a++) {
+        at(a, 0) = a;
+    }
+    for (size_t b = 1; b <= len_b; b++) {
+        at(0, b) = b;
+    }
+    for (size_t b = 1; b <= len_b; b++) {
+        for (size_t a = 1; a <= len_a; a++) {
+            bool eq = str_a[a - 1] == str_b[b - 1];
+            at(a, b) = std::min({
+                at(a - 1, b) + 1,
+                at(a, b - 1) + 1,
+                at(a - 1, b - 1) + (eq ? 0 : 1),
+            });
+        }
+    }
+    return at(len_a, len_b);
+}
+
+}  // namespace tint::utils
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index de1f4d6..9f65395 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -44,6 +44,11 @@
     return s.str();
 }
 
+/// @param a the first string
+/// @param b the second string
+/// @returns the Levenshtein distance between @p a and @p b
+size_t Distance(const std::string& a, const std::string& b);
+
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_STRING_H_
diff --git a/src/tint/utils/string_test.cc b/src/tint/utils/string_test.cc
index d1cab51..6c8008c 100644
--- a/src/tint/utils/string_test.cc
+++ b/src/tint/utils/string_test.cc
@@ -20,21 +20,33 @@
 namespace {
 
 TEST(StringTest, ReplaceAll) {
-    ASSERT_EQ("xybbcc", ReplaceAll("aabbcc", "aa", "xy"));
-    ASSERT_EQ("aaxycc", ReplaceAll("aabbcc", "bb", "xy"));
-    ASSERT_EQ("aabbxy", ReplaceAll("aabbcc", "cc", "xy"));
-    ASSERT_EQ("xyxybbcc", ReplaceAll("aabbcc", "a", "xy"));
-    ASSERT_EQ("aaxyxycc", ReplaceAll("aabbcc", "b", "xy"));
-    ASSERT_EQ("aabbxyxy", ReplaceAll("aabbcc", "c", "xy"));
+    EXPECT_EQ("xybbcc", ReplaceAll("aabbcc", "aa", "xy"));
+    EXPECT_EQ("aaxycc", ReplaceAll("aabbcc", "bb", "xy"));
+    EXPECT_EQ("aabbxy", ReplaceAll("aabbcc", "cc", "xy"));
+    EXPECT_EQ("xyxybbcc", ReplaceAll("aabbcc", "a", "xy"));
+    EXPECT_EQ("aaxyxycc", ReplaceAll("aabbcc", "b", "xy"));
+    EXPECT_EQ("aabbxyxy", ReplaceAll("aabbcc", "c", "xy"));
     // Replacement string includes the searched-for string.
     // This proves that the algorithm needs to advance 'pos'
     // past the replacement.
-    ASSERT_EQ("aabxybbxybcc", ReplaceAll("aabbcc", "b", "bxyb"));
+    EXPECT_EQ("aabxybbxybcc", ReplaceAll("aabbcc", "b", "bxyb"));
 }
 
 TEST(StringTest, ToString) {
-    ASSERT_EQ("123", ToString(123));
-    ASSERT_EQ("hello", ToString("hello"));
+    EXPECT_EQ("123", ToString(123));
+    EXPECT_EQ("hello", ToString("hello"));
+}
+
+TEST(StringTest, Distance) {
+    EXPECT_EQ(Distance("hello world", "hello world"), 0u);
+    EXPECT_EQ(Distance("hello world", "helloworld"), 1u);
+    EXPECT_EQ(Distance("helloworld", "hello world"), 1u);
+    EXPECT_EQ(Distance("hello world", "hello  world"), 1u);
+    EXPECT_EQ(Distance("hello  world", "hello world"), 1u);
+    EXPECT_EQ(Distance("Hello World", "hello world"), 2u);
+    EXPECT_EQ(Distance("hello world", "Hello World"), 2u);
+    EXPECT_EQ(Distance("Hello world", ""), 11u);
+    EXPECT_EQ(Distance("", "Hello world"), 11u);
 }
 
 }  // namespace
diff --git a/src/tint/utils/vector.h b/src/tint/utils/vector.h
index f0cbf58..705817a 100644
--- a/src/tint/utils/vector.h
+++ b/src/tint/utils/vector.h
@@ -463,13 +463,15 @@
     /// Equality operator
     /// @param other the other vector
     /// @returns true if this vector is the same length as `other`, and all elements are equal.
-    bool operator==(const Vector& other) const {
+    template <typename T2, size_t N2>
+    bool operator==(const Vector<T2, N2>& other) const {
         const size_t len = Length();
-        if (len == other.Length()) {
-            for (size_t i = 0; i < len; i++) {
-                if ((*this)[i] != other[i]) {
-                    return false;
-                }
+        if (len != other.Length()) {
+            return false;
+        }
+        for (size_t i = 0; i < len; i++) {
+            if ((*this)[i] != other[i]) {
+                return false;
             }
         }
         return true;
@@ -479,7 +481,10 @@
     /// @param other the other vector
     /// @returns true if this vector is not the same length as `other`, or all elements are not
     ///          equal.
-    bool operator!=(const Vector& other) const { return !(*this == other); }
+    template <typename T2, size_t N2>
+    bool operator!=(const Vector<T2, N2>& other) const {
+        return !(*this == other);
+    }
 
   private:
     /// Friend class (differing specializations of this class)
diff --git a/src/tint/utils/vector_test.cc b/src/tint/utils/vector_test.cc
index b4e9fb5..a3cc1f7 100644
--- a/src/tint/utils/vector_test.cc
+++ b/src/tint/utils/vector_test.cc
@@ -2060,6 +2060,15 @@
     EXPECT_EQ(vec_ref.end(), &vec[0] + 3);
 }
 
+TEST(TintVectorTest, Equality) {
+    EXPECT_EQ((Vector<int, 2>{1, 2}), (Vector<int, 2>{1, 2}));
+    EXPECT_EQ((Vector<int, 1>{1, 2}), (Vector<int, 3>{1, 2}));
+    EXPECT_NE((Vector{1, 2}), (Vector{1}));
+    EXPECT_NE((Vector{1}), (Vector{1, 2}));
+    EXPECT_NE((Vector{1, 2}), (Vector{2, 1}));
+    EXPECT_NE((Vector{2, 1}), (Vector{1, 2}));
+}
+
 }  // namespace
 }  // namespace tint::utils
 
diff --git a/src/tint/writer/append_vector.cc b/src/tint/writer/append_vector.cc
index edf9132..98e9efc 100644
--- a/src/tint/writer/append_vector.cc
+++ b/src/tint/writer/append_vector.cc
@@ -137,7 +137,7 @@
         auto* scalar_cast_target = b->create<sem::TypeConversion>(
             packed_el_sem_ty,
             b->create<sem::Parameter>(nullptr, 0u, scalar_sem->Type()->UnwrapRef(),
-                                      ast::AddressSpace::kNone, ast::Access::kUndefined),
+                                      ast::AddressSpace::kNone, ast::Access::kInvalid),
             sem::EvaluationStage::kRuntime);
         auto* scalar_cast_sem = b->create<sem::Call>(
             scalar_cast_ast, scalar_cast_target, sem::EvaluationStage::kRuntime,
@@ -158,7 +158,7 @@
                          [&](const tint::sem::Expression* arg, size_t i) -> const sem::Parameter* {
                              return b->create<sem::Parameter>(
                                  nullptr, static_cast<uint32_t>(i), arg->Type()->UnwrapRef(),
-                                 ast::AddressSpace::kNone, ast::Access::kUndefined);
+                                 ast::AddressSpace::kNone, ast::Access::kInvalid);
                          }),
         sem::EvaluationStage::kRuntime);
     auto* constructor_sem =
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index 429249d..da67b3d 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -509,20 +509,20 @@
                                 {
                                     auto decl = line(&b);
                                     if (!EmitTypeAndName(decl, ret_ty, ast::AddressSpace::kNone,
-                                                         ast::Access::kUndefined, fn_name)) {
+                                                         ast::Access::kInvalid, fn_name)) {
                                         return "";
                                     }
                                     {
                                         ScopedParen sp(decl);
                                         const auto* ty = TypeOf(expr->lhs)->UnwrapRef();
                                         if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                                             ast::Access::kUndefined, "lhs")) {
+                                                             ast::Access::kInvalid, "lhs")) {
                                             return "";
                                         }
                                         decl << ", ";
                                         ty = TypeOf(expr->rhs)->UnwrapRef();
                                         if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                                             ast::Access::kUndefined, "rhs")) {
+                                                             ast::Access::kInvalid, "rhs")) {
                                             return "";
                                         }
                                     }
@@ -935,7 +935,7 @@
             {
                 auto pre = line();
                 if (!EmitTypeAndName(pre, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, result)) {
+                                     ast::Access::kInvalid, result)) {
                     return false;
                 }
                 pre << ";";
@@ -1213,7 +1213,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kUndefined, "")) {
+                              ast::Access::kInvalid, "")) {
                     return false;
                 }
                 l << " result;";
@@ -1239,7 +1239,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kUndefined, "")) {
+                              ast::Access::kInvalid, "")) {
                     return false;
                 }
                 l << " result;";
@@ -2049,6 +2049,7 @@
             switch (interpolate->type) {
                 case ast::InterpolationType::kPerspective:
                 case ast::InterpolationType::kLinear:
+                case ast::InterpolationType::kInvalid:
                     break;
                 case ast::InterpolationType::kFlat:
                     out << "flat ";
@@ -2060,7 +2061,7 @@
                     break;
                 case ast::InterpolationSampling::kSample:
                 case ast::InterpolationSampling::kCenter:
-                case ast::InterpolationSampling::kNone:
+                case ast::InterpolationSampling::kInvalid:
                     break;
             }
         }
@@ -2197,7 +2198,7 @@
             return true;
         },
         [&](const sem::Vector* v) {
-            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -2218,7 +2219,7 @@
             return true;
         },
         [&](const sem::Matrix* m) {
-            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -2235,7 +2236,7 @@
             return true;
         },
         [&](const sem::Array* a) {
-            if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -2259,7 +2260,7 @@
             return true;
         },
         [&](const sem::Struct* s) {
-            if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -2350,7 +2351,7 @@
             }
         }
     } else if (auto* str = type->As<sem::Struct>()) {
-        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
             return false;
         }
         bool first = true;
@@ -2364,7 +2365,7 @@
             EmitZeroValue(out, member->Type());
         }
     } else if (auto* arr = type->As<sem::Array>()) {
-        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
             return false;
         }
         ScopedParen sp(out);
@@ -2971,7 +2972,7 @@
 
     auto out = line();
     // TODO(senorblanco): handle const
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
                          builder_.Symbols().NameFor(let->symbol))) {
         return false;
     }
@@ -2993,7 +2994,7 @@
 
     auto out = line();
     out << "const ";
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
                          builder_.Symbols().NameFor(var->symbol))) {
         return false;
     }
@@ -3021,7 +3022,7 @@
         {
             auto decl = line(&b);
             if (!EmitTypeAndName(decl, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                 ast::Access::kUndefined, fn_name)) {
+                                 ast::Access::kInvalid, fn_name)) {
                 return "";
             }
             {
@@ -3036,8 +3037,8 @@
                         decl << "inout ";
                         ty = ptr->StoreType();
                     }
-                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                         ast::Access::kUndefined, param_name)) {
+                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                         param_name)) {
                         return "";
                     }
                     parameter_names.emplace_back(std::move(param_name));
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 4ada084..94e80e8 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -325,8 +325,7 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, vec, tint::ast::AddressSpace::kInvalid, ast::Access::kUndefined,
-                          "")) {
+            if (!EmitType(ss, vec, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_" + ss.str());
@@ -334,13 +333,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, vec, ast::AddressSpace::kInvalid, ast::Access::kUndefined,
+            if (!EmitTypeAndName(out, vec, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
                                  "vec")) {
                 return "";
             }
             out << ", int idx, ";
             if (!EmitTypeAndName(out, vec->type(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kUndefined, "val")) {
+                                 ast::Access::kInvalid, "val")) {
                 return "";
             }
             out << ") {";
@@ -399,8 +398,7 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kUndefined,
-                          "")) {
+            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_vector_" + ss.str());
@@ -408,13 +406,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kUndefined,
+            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
                                  "mat")) {
                 return "";
             }
             out << ", int col, ";
             if (!EmitTypeAndName(out, mat->ColumnType(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kUndefined, "val")) {
+                                 ast::Access::kInvalid, "val")) {
                 return "";
             }
             out << ") {";
@@ -468,8 +466,7 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kUndefined,
-                          "")) {
+            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_scalar_" + ss.str());
@@ -477,13 +474,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kUndefined,
+            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
                                  "mat")) {
                 return "";
             }
             out << ", int col, int row, ";
             if (!EmitTypeAndName(out, mat->type(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kUndefined, "val")) {
+                                 ast::Access::kInvalid, "val")) {
                 return "";
             }
             out << ") {";
@@ -658,7 +655,7 @@
         if (auto* vec = ty->As<sem::Vector>()) {
             auto* elem_ty = vec->type();
 
-            if (!EmitType(out, ty, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, ty, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -723,7 +720,7 @@
         std::string ty_name;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, ty, tint::ast::AddressSpace::kInvalid, ast::Access::kUndefined, "")) {
+            if (!EmitType(ss, ty, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
                 return "";
             }
             ty_name = ss.str();
@@ -1431,12 +1428,12 @@
     auto rmw = [&](const char* hlsl) -> bool {
         {
             auto fn = line(&buf);
-            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
+            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
                                  name)) {
                 return false;
             }
             fn << "(RWByteAddressBuffer buffer, uint offset, ";
-            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
+            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
                                  "value")) {
                 return false;
             }
@@ -1452,7 +1449,7 @@
 
         {
             auto l = line(&buf);
-            if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
+            if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
                                  "original_value")) {
                 return false;
             }
@@ -1501,8 +1498,8 @@
             // InterlockedOr using 0 as the OR value
             {
                 auto fn = line(&buf);
-                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, name)) {
+                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     name)) {
                     return false;
                 }
                 fn << "(RWByteAddressBuffer buffer, uint offset) {";
@@ -1517,8 +1514,8 @@
 
             {
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, "value")) {
+                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     "value")) {
                     return false;
                 }
                 l << " = 0;";
@@ -1535,8 +1532,8 @@
             {
                 auto fn = line(&buf);
                 fn << "void " << name << "(RWByteAddressBuffer buffer, uint offset, ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, "value")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     "value")) {
                     return false;
                 }
                 fn << ") {";
@@ -1551,7 +1548,7 @@
 
             {
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, value_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
+                if (!EmitTypeAndName(l, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
                                      "ignored")) {
                     return false;
                 }
@@ -1566,18 +1563,18 @@
             auto* value_ty = params[2]->Type()->UnwrapRef();
             {
                 auto fn = line(&buf);
-                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, name)) {
+                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     name)) {
                     return false;
                 }
                 fn << "(RWByteAddressBuffer buffer, uint offset, ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, "compare")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     "compare")) {
                     return false;
                 }
                 fn << ", ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, "value")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     "value")) {
                     return false;
                 }
                 fn << ") {";
@@ -1592,8 +1589,8 @@
 
             {  // T result = {0};
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, "result")) {
+                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     "result")) {
                     return false;
                 }
                 l << "=";
@@ -1628,7 +1625,7 @@
     if (!builtin->ReturnType()->Is<sem::Void>()) {
         auto pre = line();
         if (!EmitTypeAndName(pre, builtin->ReturnType(), ast::AddressSpace::kNone,
-                             ast::Access::kUndefined, result)) {
+                             ast::Access::kInvalid, result)) {
             return false;
         }
         pre << " = ";
@@ -1691,8 +1688,8 @@
             {  // T result = 0;
                 auto pre = line();
                 auto* value_ty = builtin->Parameters()[1]->Type()->UnwrapRef();
-                if (!EmitTypeAndName(pre, value_ty, ast::AddressSpace::kNone,
-                                     ast::Access::kUndefined, result)) {
+                if (!EmitTypeAndName(pre, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                     result)) {
                     return false;
                 }
                 pre << " = ";
@@ -1732,7 +1729,7 @@
             {  // T compare_value = <compare_value>;
                 auto pre = line();
                 if (!EmitTypeAndName(pre, TypeOf(compare_value)->UnwrapRef(),
-                                     ast::AddressSpace::kNone, ast::Access::kUndefined, compare)) {
+                                     ast::AddressSpace::kNone, ast::Access::kInvalid, compare)) {
                     return false;
                 }
                 pre << " = ";
@@ -1842,7 +1839,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kUndefined, "")) {
+                              ast::Access::kInvalid, "")) {
                     return false;
                 }
                 l << " result;";
@@ -1884,7 +1881,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kUndefined, "")) {
+                              ast::Access::kInvalid, "")) {
                     return false;
                 }
                 l << " result = {sig, int" << width << "(exp)};";
@@ -2721,7 +2718,7 @@
 
             auto const* type = v->Type();
             auto address_space = ast::AddressSpace::kNone;
-            auto access = ast::Access::kUndefined;
+            auto access = ast::Access::kInvalid;
 
             if (auto* ptr = type->As<sem::Pointer>()) {
                 type = ptr->StoreType();
@@ -3010,6 +3007,8 @@
         case ast::InterpolationType::kFlat:
             modifiers += "nointerpolation ";
             break;
+        case ast::InterpolationType::kInvalid:
+            break;
     }
     switch (sampling) {
         case ast::InterpolationSampling::kCentroid:
@@ -3019,7 +3018,7 @@
             modifiers += "sample ";
             break;
         case ast::InterpolationSampling::kCenter:
-        case ast::InterpolationSampling::kNone:
+        case ast::InterpolationSampling::kInvalid:
             break;
     }
     return modifiers;
@@ -3141,7 +3140,7 @@
                 return true;
             }
 
-            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -3158,7 +3157,7 @@
             return true;
         },
         [&](const sem::Matrix* m) {
-            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                 return false;
             }
 
@@ -3177,7 +3176,7 @@
         [&](const sem::Array* a) {
             if (constant->AllZero()) {
                 out << "(";
-                if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+                if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                     return false;
                 }
                 out << ")0";
@@ -3207,7 +3206,7 @@
         [&](const sem::Struct* s) {
             if (constant->AllZero()) {
                 out << "(";
-                if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
+                if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
                     return false;
                 }
                 out << ")0";
@@ -3328,12 +3327,12 @@
         [&](const sem::Struct*) {
             out << "(";
             TINT_DEFER(out << ")" << value);
-            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "");
+            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "");
         },
         [&](const sem::Array*) {
             out << "(";
             TINT_DEFER(out << ")" << value);
-            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "");
+            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "");
         },
         [&](Default) {
             diagnostics_.add_error(
@@ -4081,7 +4080,7 @@
 
     auto out = line();
     out << "const ";
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
                          builder_.Symbols().NameFor(let->symbol))) {
         return false;
     }
@@ -4109,7 +4108,7 @@
         {
             auto decl = line(&b);
             if (!EmitTypeAndName(decl, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                 ast::Access::kUndefined, fn_name)) {
+                                 ast::Access::kInvalid, fn_name)) {
                 return "";
             }
             {
@@ -4124,8 +4123,8 @@
                         decl << "inout ";
                         ty = ptr->StoreType();
                     }
-                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                         ast::Access::kUndefined, param_name)) {
+                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                                         param_name)) {
                         return "";
                     }
                     parameter_names.emplace_back(std::move(param_name));
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 9517ec8..5db1d8f 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -1947,7 +1947,7 @@
         case ast::InterpolationSampling::kSample:
             attr = "sample_";
             break;
-        case ast::InterpolationSampling::kNone:
+        case ast::InterpolationSampling::kInvalid:
             break;
     }
     switch (type) {
@@ -1960,6 +1960,8 @@
         case ast::InterpolationType::kFlat:
             attr += "flat";
             break;
+        case ast::InterpolationType::kInvalid:
+            break;
     }
     return attr;
 }
diff --git a/src/tint/writer/msl/generator_impl_cast_test.cc b/src/tint/writer/msl/generator_impl_cast_test.cc
index 4b9e3f2..74b3be6 100644
--- a/src/tint/writer/msl/generator_impl_cast_test.cc
+++ b/src/tint/writer/msl/generator_impl_cast_test.cc
@@ -51,7 +51,7 @@
 
     std::stringstream out;
     ASSERT_TRUE(gen.EmitExpression(out, cast)) << gen.error();
-    EXPECT_EQ(out.str(), "0u");
+    EXPECT_EQ(out.str(), "2147483648u");
 }
 
 }  // namespace
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 5bd6407..57a363d 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -766,7 +766,7 @@
                     push_annot(spv::Op::OpDecorate,
                                {Operand(var_id), U32Operand(SpvDecorationNonWritable)});
                     break;
-                case ast::Access::kUndefined:
+                case ast::Access::kInvalid:
                 case ast::Access::kReadWrite:
                     break;
             }
@@ -3684,11 +3684,11 @@
     // references are not legal in WGSL, so only considering the top-level type is
     // fine.
     if (auto* ptr = type->As<sem::Pointer>()) {
-        type =
-            builder_.create<sem::Pointer>(ptr->StoreType(), ptr->AddressSpace(), ast::kReadWrite);
+        type = builder_.create<sem::Pointer>(ptr->StoreType(), ptr->AddressSpace(),
+                                             ast::Access::kReadWrite);
     } else if (auto* ref = type->As<sem::Reference>()) {
-        type =
-            builder_.create<sem::Pointer>(ref->StoreType(), ref->AddressSpace(), ast::kReadWrite);
+        type = builder_.create<sem::Pointer>(ref->StoreType(), ref->AddressSpace(),
+                                             ast::Access::kReadWrite);
     }
 
     return utils::GetOrCreate(type_to_id_, type, [&]() -> uint32_t {
@@ -4088,6 +4088,7 @@
             push_annot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationFlat)});
             break;
         case ast::InterpolationType::kPerspective:
+        case ast::InterpolationType::kInvalid:
             break;
     }
     switch (sampling) {
@@ -4099,7 +4100,7 @@
             push_annot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationSample)});
             break;
         case ast::InterpolationSampling::kCenter:
-        case ast::InterpolationSampling::kNone:
+        case ast::InterpolationSampling::kInvalid:
             break;
     }
 }
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 05f1a5a..cf43f23 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -433,7 +433,7 @@
             if (!EmitType(out, ptr->type)) {
                 return false;
             }
-            if (ptr->access != ast::Access::kUndefined) {
+            if (ptr->access != ast::Access::kInvalid) {
                 out << ", ";
                 if (!EmitAccess(out, ptr->access)) {
                     return false;
@@ -656,9 +656,9 @@
             out << "var";
             auto address_space = var->declared_address_space;
             auto ac = var->declared_access;
-            if (address_space != ast::AddressSpace::kNone || ac != ast::Access::kUndefined) {
+            if (address_space != ast::AddressSpace::kNone || ac != ast::Access::kInvalid) {
                 out << "<" << address_space;
-                if (ac != ast::Access::kUndefined) {
+                if (ac != ast::Access::kInvalid) {
                     out << ", ";
                     if (!EmitAccess(out, ac)) {
                         return false;
@@ -769,7 +769,7 @@
             },
             [&](const ast::InterpolateAttribute* interpolate) {
                 out << "interpolate(" << interpolate->type;
-                if (interpolate->sampling != ast::InterpolationSampling::kNone) {
+                if (interpolate->sampling != ast::InterpolationSampling::kInvalid) {
                     out << ", " << interpolate->sampling;
                 }
                 out << ")";