Import Tint changes from Dawn

Changes:
  - aecf1a2ab5106c4a08f8e2ae79717483f3abf8ee tint/inspector: Fix GetOverrideDefaultValues() by Ben Clayton <bclayton@google.com>
  - 723da2aac5934a47fb85b3bae607eb52af9cee58 tint: split up const eval unit tests into multiple files by Antonio Maiorano <amaiorano@google.com>
  - c84d06e8603ce9c4b5c8d86e42e9ec0acf3bd689 tint/resolver: Improve errors for expr eval-stages by Ben Clayton <bclayton@google.com>
  - 559a2482339c2034ee85c6ba06bbd7ff70466c8c tint/resolver: Allow texture 'offset' to be const-expr by Ben Clayton <bclayton@google.com>
  - d5139b44631f52bbae71790fb237cd35bf1db83b [msl] Handle packed conversions in shift expressions. by dan sinclair <dsinclair@chromium.org>
  - 840e42477d9d8683ef1130dd51e0b71e23816f83 tint: update natvis by Antonio Maiorano <amaiorano@google.com>
  - f10a57908aeaccbe5f3348183752dc2519e40b39 tint: Use `const-expression` and `override-expression` te... by Ben Clayton <bclayton@google.com>
  - 92264f8bb26250ea201e454898ba4761e51fc410 tint/resolver: Fix NPE in CollectTextureSamplerPairs() by Ben Clayton <bclayton@google.com>
  - 4fe330fff4481164f4733a2d4431c890dbd6d5e9 tint: s/analysing/analyzing by Ben Clayton <bclayton@google.com>
  - 6988e894d2c033d1c0eaa9ddb78cb1746ccbb7ae spirv-reader: track access mode for ptr/ref by David Neto <dneto@google.com>
  - cc0c67bce8d8b782d3a83940a1e86d40a94d0d10 spirv-reader: Support GLSL.std.450 FindSMsb by David Neto <dneto@google.com>
  - 042711b2b1db99c9944199ba8f894fdc1ba344c2 Remove redundant file in tint/BUILD.gn by Corentin Wallez <cwallez@chromium.org>
  - ba384f0383502bb182f810c3007235a6d0aadc3f tint/transform: Fix array materialization when indexing w... by Ben Clayton <bclayton@google.com>
  - d6daefc3798532cdd31cbcc9d006e41dc0ca43ce wgsl: Print abstract-floats with full precision. by Ben Clayton <bclayton@google.com>
  - 2b8c9d7c2a1ce0463db6feef8fc57c3c866336cb spirv-reader: Support GLSL.std.450 FindUMsb by David Neto <dneto@google.com>
  - d2e0db3af2c61efa01254d895d4f335b50d80e6e tint: Rename kInvalid to kUndefined for enums by Ben Clayton <bclayton@google.com>
  - 78c839be976a332cd651bbfc651768d426ecfea0 tint/resolver: Ensure that total workgroup size fits in u32 by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: aecf1a2ab5106c4a08f8e2ae79717483f3abf8ee
Change-Id: I6865b5a6e90a527fe223fa4623a897f2cc2a3c90
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/105700
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Commit-Queue: Copybara Prod <copybara-worker-blackhole@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 9e39630..e034c34 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -482,7 +482,6 @@
     "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",
@@ -1116,7 +1115,14 @@
       "resolver/call_validation_test.cc",
       "resolver/compound_assignment_validation_test.cc",
       "resolver/compound_statement_test.cc",
-      "resolver/const_eval_test.cc",
+      "resolver/const_eval_binary_op_test.cc",
+      "resolver/const_eval_builtin_test.cc",
+      "resolver/const_eval_construction_test.cc",
+      "resolver/const_eval_conversion_test.cc",
+      "resolver/const_eval_indexing_test.cc",
+      "resolver/const_eval_member_access_test.cc",
+      "resolver/const_eval_test.h",
+      "resolver/const_eval_unary_op_test.cc",
       "resolver/control_block_validation_test.cc",
       "resolver/dependency_graph_test.cc",
       "resolver/entry_point_validation_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index c331ccf..fc62717 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -794,7 +794,14 @@
     resolver/call_validation_test.cc
     resolver/compound_assignment_validation_test.cc
     resolver/compound_statement_test.cc
-    resolver/const_eval_test.cc
+    resolver/const_eval_binary_op_test.cc
+    resolver/const_eval_builtin_test.cc
+    resolver/const_eval_construction_test.cc
+    resolver/const_eval_conversion_test.cc
+    resolver/const_eval_indexing_test.cc
+    resolver/const_eval_member_access_test.cc
+    resolver/const_eval_test.h
+    resolver/const_eval_unary_op_test.cc
     resolver/control_block_validation_test.cc
     resolver/dependency_graph_test.cc
     resolver/entry_point_validation_test.cc
diff --git a/src/tint/ast/access.cc b/src/tint/ast/access.cc
index 3997715..7ca9b52 100644
--- a/src/tint/ast/access.cc
+++ b/src/tint/ast/access.cc
@@ -26,7 +26,7 @@
 
 /// 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.
+/// @returns the parsed enum, or Access::kUndefined if the string could not be parsed.
 Access ParseAccess(std::string_view str) {
     if (str == "read") {
         return Access::kRead;
@@ -37,13 +37,13 @@
     if (str == "write") {
         return Access::kWrite;
     }
-    return Access::kInvalid;
+    return Access::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, Access value) {
     switch (value) {
-        case Access::kInvalid:
-            return out << "invalid";
+        case Access::kUndefined:
+            return out << "undefined";
         case Access::kRead:
             return out << "read";
         case Access::kReadWrite:
diff --git a/src/tint/ast/access.h b/src/tint/ast/access.h
index 12e1a20..e213691 100644
--- a/src/tint/ast/access.h
+++ b/src/tint/ast/access.h
@@ -29,7 +29,7 @@
 
 /// Address space of a given pointer.
 enum class Access {
-    kInvalid,
+    kUndefined,
     kRead,
     kReadWrite,
     kWrite,
@@ -42,7 +42,7 @@
 
 /// 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.
+/// @returns the parsed enum, or Access::kUndefined if the string could not be parsed.
 Access ParseAccess(std::string_view str);
 
 constexpr const char* kAccessStrings[] = {
diff --git a/src/tint/ast/address_space.cc b/src/tint/ast/address_space.cc
index d8174a2..260f86b 100644
--- a/src/tint/ast/address_space.cc
+++ b/src/tint/ast/address_space.cc
@@ -26,7 +26,7 @@
 
 /// ParseAddressSpace parses a AddressSpace from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or AddressSpace::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or AddressSpace::kUndefined if the string could not be parsed.
 AddressSpace ParseAddressSpace(std::string_view str) {
     if (str == "function") {
         return AddressSpace::kFunction;
@@ -46,13 +46,13 @@
     if (str == "workgroup") {
         return AddressSpace::kWorkgroup;
     }
-    return AddressSpace::kInvalid;
+    return AddressSpace::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, AddressSpace value) {
     switch (value) {
-        case AddressSpace::kInvalid:
-            return out << "invalid";
+        case AddressSpace::kUndefined:
+            return out << "undefined";
         case AddressSpace::kFunction:
             return out << "function";
         case AddressSpace::kHandle:
diff --git a/src/tint/ast/address_space.h b/src/tint/ast/address_space.h
index 8b96f0f..d599975 100644
--- a/src/tint/ast/address_space.h
+++ b/src/tint/ast/address_space.h
@@ -29,7 +29,7 @@
 
 /// Address space of a given pointer.
 enum class AddressSpace {
-    kInvalid,
+    kUndefined,
     kFunction,
     kHandle,  // Tint-internal enum entry - not parsed
     kIn,      // Tint-internal enum entry - not parsed
@@ -49,7 +49,7 @@
 
 /// ParseAddressSpace parses a AddressSpace from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or AddressSpace::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or AddressSpace::kUndefined if the string could not be parsed.
 AddressSpace ParseAddressSpace(std::string_view str);
 
 constexpr const char* kAddressSpaceStrings[] = {
diff --git a/src/tint/ast/address_space_test.cc b/src/tint/ast/address_space_test.cc
index 271cf33..83646be 100644
--- a/src/tint/ast/address_space_test.cc
+++ b/src/tint/ast/address_space_test.cc
@@ -51,15 +51,15 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"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},
+    {"fccnctin", AddressSpace::kUndefined},        {"ucti3", AddressSpace::kUndefined},
+    {"functVon", AddressSpace::kUndefined},        {"priv1te", AddressSpace::kUndefined},
+    {"pqiJate", AddressSpace::kUndefined},         {"privat7ll", AddressSpace::kUndefined},
+    {"pqqsh_pponstHnt", AddressSpace::kUndefined}, {"pus_cnstat", AddressSpace::kUndefined},
+    {"bus_Gonstant", AddressSpace::kUndefined},    {"storiive", AddressSpace::kUndefined},
+    {"8WWorage", AddressSpace::kUndefined},        {"sxxrage", AddressSpace::kUndefined},
+    {"uXforgg", AddressSpace::kUndefined},         {"nfoXm", AddressSpace::kUndefined},
+    {"unif3rm", AddressSpace::kUndefined},         {"workgroEp", AddressSpace::kUndefined},
+    {"woTTPkroup", AddressSpace::kUndefined},      {"ddorkroxxp", AddressSpace::kUndefined},
 };
 
 using AddressSpaceParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/builtin_texture_helper_test.h b/src/tint/ast/builtin_texture_helper_test.h
index bb61875..2c4515b 100644
--- a/src/tint/ast/builtin_texture_helper_test.h
+++ b/src/tint/ast/builtin_texture_helper_test.h
@@ -241,7 +241,7 @@
     Access const access = Access::kReadWrite;
     /// The image format for the storage texture
     /// Used only when texture_kind is kStorage
-    ast::TexelFormat const texel_format = ast::TexelFormat::kInvalid;
+    ast::TexelFormat const texel_format = ast::TexelFormat::kUndefined;
     /// The dimensions of the texture parameter
     ast::TextureDimension const texture_dimension;
     /// The data type of the texture parameter
diff --git a/src/tint/ast/builtin_value.cc b/src/tint/ast/builtin_value.cc
index d4eaec0..0a4439b 100644
--- a/src/tint/ast/builtin_value.cc
+++ b/src/tint/ast/builtin_value.cc
@@ -26,7 +26,7 @@
 
 /// ParseBuiltinValue parses a BuiltinValue from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or BuiltinValue::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or BuiltinValue::kUndefined if the string could not be parsed.
 BuiltinValue ParseBuiltinValue(std::string_view str) {
     if (str == "frag_depth") {
         return BuiltinValue::kFragDepth;
@@ -64,13 +64,13 @@
     if (str == "workgroup_id") {
         return BuiltinValue::kWorkgroupId;
     }
-    return BuiltinValue::kInvalid;
+    return BuiltinValue::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, BuiltinValue value) {
     switch (value) {
-        case BuiltinValue::kInvalid:
-            return out << "invalid";
+        case BuiltinValue::kUndefined:
+            return out << "undefined";
         case BuiltinValue::kFragDepth:
             return out << "frag_depth";
         case BuiltinValue::kFrontFacing:
diff --git a/src/tint/ast/builtin_value.h b/src/tint/ast/builtin_value.h
index f7dd660..3a3c786 100644
--- a/src/tint/ast/builtin_value.h
+++ b/src/tint/ast/builtin_value.h
@@ -29,7 +29,7 @@
 
 /// Builtin value defined with `@builtin(<name>)`.
 enum class BuiltinValue {
-    kInvalid,
+    kUndefined,
     kFragDepth,
     kFrontFacing,
     kGlobalInvocationId,
@@ -52,7 +52,7 @@
 
 /// ParseBuiltinValue parses a BuiltinValue from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or BuiltinValue::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or BuiltinValue::kUndefined if the string could not be parsed.
 BuiltinValue ParseBuiltinValue(std::string_view str);
 
 constexpr const char* kBuiltinValueStrings[] = {
diff --git a/src/tint/ast/builtin_value_test.cc b/src/tint/ast/builtin_value_test.cc
index 55f8e4d..0ee2206 100644
--- a/src/tint/ast/builtin_value_test.cc
+++ b/src/tint/ast/builtin_value_test.cc
@@ -57,42 +57,42 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"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},
+    {"fragdeccth", BuiltinValue::kUndefined},
+    {"flaget3", BuiltinValue::kUndefined},
+    {"fVag_depth", BuiltinValue::kUndefined},
+    {"1ront_facing", BuiltinValue::kUndefined},
+    {"front_fJcqng", BuiltinValue::kUndefined},
+    {"frllnt_facin77", BuiltinValue::kUndefined},
+    {"global_invoqqtionppHid", BuiltinValue::kUndefined},
+    {"clvbal_inocaionid", BuiltinValue::kUndefined},
+    {"global_Gvocation_id", BuiltinValue::kUndefined},
+    {"invtance_iniiex", BuiltinValue::kUndefined},
+    {"8nstanceWWindex", BuiltinValue::kUndefined},
+    {"insxxanceindex", BuiltinValue::kUndefined},
+    {"lXcal_invoation_igg", BuiltinValue::kUndefined},
+    {"Xocal_nvocatin_Vd", BuiltinValue::kUndefined},
+    {"local_invoca3ion_id", BuiltinValue::kUndefined},
+    {"local_invocation_indeE", BuiltinValue::kUndefined},
+    {"loTTal_invPPcatin_index", BuiltinValue::kUndefined},
+    {"loal_invocadxxion_index", BuiltinValue::kUndefined},
+    {"num_work44roups", BuiltinValue::kUndefined},
+    {"num_wVVrkgSSoups", BuiltinValue::kUndefined},
+    {"Rum_wokgrou2Rs", BuiltinValue::kUndefined},
+    {"oFi9ion", BuiltinValue::kUndefined},
+    {"postion", BuiltinValue::kUndefined},
+    {"ROOoHiiVn", BuiltinValue::kUndefined},
+    {"samply_inde", BuiltinValue::kUndefined},
+    {"snrrmpl77l_indGx", BuiltinValue::kUndefined},
+    {"00ample4index", BuiltinValue::kUndefined},
+    {"smoo_mask", BuiltinValue::kUndefined},
+    {"sampzemask", BuiltinValue::kUndefined},
+    {"ppaplii1_mas", BuiltinValue::kUndefined},
+    {"vertex_iXXdex", BuiltinValue::kUndefined},
+    {"5nnertex_99IIdex", BuiltinValue::kUndefined},
+    {"verYeaaHHrrndeSS", BuiltinValue::kUndefined},
+    {"workkgHo_i", BuiltinValue::kUndefined},
+    {"worRgoupjid", BuiltinValue::kUndefined},
+    {"wrkgrupbid", BuiltinValue::kUndefined},
 };
 
 using BuiltinValueParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/extension.cc b/src/tint/ast/extension.cc
index a4d823f..be16806 100644
--- a/src/tint/ast/extension.cc
+++ b/src/tint/ast/extension.cc
@@ -26,7 +26,7 @@
 
 /// ParseExtension parses a Extension from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or Extension::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or Extension::kUndefined if the string could not be parsed.
 Extension ParseExtension(std::string_view str) {
     if (str == "chromium_disable_uniformity_analysis") {
         return Extension::kChromiumDisableUniformityAnalysis;
@@ -40,13 +40,13 @@
     if (str == "f16") {
         return Extension::kF16;
     }
-    return Extension::kInvalid;
+    return Extension::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, Extension value) {
     switch (value) {
-        case Extension::kInvalid:
-            return out << "invalid";
+        case Extension::kUndefined:
+            return out << "undefined";
         case Extension::kChromiumDisableUniformityAnalysis:
             return out << "chromium_disable_uniformity_analysis";
         case Extension::kChromiumExperimentalDp4A:
diff --git a/src/tint/ast/extension.h b/src/tint/ast/extension.h
index 4d1a937..f6fa03f 100644
--- a/src/tint/ast/extension.h
+++ b/src/tint/ast/extension.h
@@ -32,7 +32,7 @@
 /// An enumerator of WGSL extensions
 /// @see src/tint/intrinsics.def for extension descriptions
 enum class Extension {
-    kInvalid,
+    kUndefined,
     kChromiumDisableUniformityAnalysis,
     kChromiumExperimentalDp4A,
     kChromiumExperimentalPushConstant,
@@ -46,7 +46,7 @@
 
 /// ParseExtension parses a Extension from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or Extension::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or Extension::kUndefined if the string could not be parsed.
 Extension ParseExtension(std::string_view str);
 
 constexpr const char* kExtensionStrings[] = {
diff --git a/src/tint/ast/extension_test.cc b/src/tint/ast/extension_test.cc
index e43266c..2879bf8 100644
--- a/src/tint/ast/extension_test.cc
+++ b/src/tint/ast/extension_test.cc
@@ -49,18 +49,18 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"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},
-    {"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},
+    {"chromium_disableuniformiccy_analysis", Extension::kUndefined},
+    {"chromil3_disable_unifority_analss", Extension::kUndefined},
+    {"chromium_disable_Vniformity_analysis", Extension::kUndefined},
+    {"chro1ium_experimental_dp4a", Extension::kUndefined},
+    {"chrJmium_experiqqetal_dp4a", Extension::kUndefined},
+    {"chromium_experimenll77l_dp4a", Extension::kUndefined},
+    {"cppromium_experiHHenal_qqush_constant", Extension::kUndefined},
+    {"chromium_xpericental_sh_vonstant", Extension::kUndefined},
+    {"chromium_experimental_Gsh_cbnstant", Extension::kUndefined},
+    {"f1vi", Extension::kUndefined},
+    {"f8WW", Extension::kUndefined},
+    {"fxx", Extension::kUndefined},
 };
 
 using ExtensionParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/ast/interpolate_attribute.cc b/src/tint/ast/interpolate_attribute.cc
index 7c89ee1..2e9ef2a 100644
--- a/src/tint/ast/interpolate_attribute.cc
+++ b/src/tint/ast/interpolate_attribute.cc
@@ -51,7 +51,7 @@
 
 /// 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.
+/// @returns the parsed enum, or InterpolationType::kUndefined if the string could not be parsed.
 InterpolationType ParseInterpolationType(std::string_view str) {
     if (str == "flat") {
         return InterpolationType::kFlat;
@@ -62,13 +62,13 @@
     if (str == "perspective") {
         return InterpolationType::kPerspective;
     }
-    return InterpolationType::kInvalid;
+    return InterpolationType::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, InterpolationType value) {
     switch (value) {
-        case InterpolationType::kInvalid:
-            return out << "invalid";
+        case InterpolationType::kUndefined:
+            return out << "undefined";
         case InterpolationType::kFlat:
             return out << "flat";
         case InterpolationType::kLinear:
@@ -81,7 +81,8 @@
 
 /// 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.
+/// @returns the parsed enum, or InterpolationSampling::kUndefined if the string could not be
+/// parsed.
 InterpolationSampling ParseInterpolationSampling(std::string_view str) {
     if (str == "center") {
         return InterpolationSampling::kCenter;
@@ -92,13 +93,13 @@
     if (str == "sample") {
         return InterpolationSampling::kSample;
     }
-    return InterpolationSampling::kInvalid;
+    return InterpolationSampling::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, InterpolationSampling value) {
     switch (value) {
-        case InterpolationSampling::kInvalid:
-            return out << "invalid";
+        case InterpolationSampling::kUndefined:
+            return out << "undefined";
         case InterpolationSampling::kCenter:
             return out << "center";
         case InterpolationSampling::kCentroid:
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
index c6b4af0..dd60156 100644
--- a/src/tint/ast/interpolate_attribute.h
+++ b/src/tint/ast/interpolate_attribute.h
@@ -32,7 +32,7 @@
 
 /// The interpolation type.
 enum class InterpolationType {
-    kInvalid,
+    kUndefined,
     kFlat,
     kLinear,
     kPerspective,
@@ -45,7 +45,7 @@
 
 /// 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.
+/// @returns the parsed enum, or InterpolationType::kUndefined if the string could not be parsed.
 InterpolationType ParseInterpolationType(std::string_view str);
 
 constexpr const char* kInterpolationTypeStrings[] = {
@@ -56,7 +56,7 @@
 
 /// The interpolation sampling.
 enum class InterpolationSampling {
-    kInvalid,
+    kUndefined,
     kCenter,
     kCentroid,
     kSample,
@@ -69,7 +69,8 @@
 
 /// 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.
+/// @returns the parsed enum, or InterpolationSampling::kUndefined if the string could not be
+/// parsed.
 InterpolationSampling ParseInterpolationSampling(std::string_view str);
 
 constexpr const char* kInterpolationSamplingStrings[] = {
diff --git a/src/tint/ast/pointer.cc b/src/tint/ast/pointer.cc
index 96bbdaf..71b7004 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::kInvalid) {
+    if (access != ast::Access::kUndefined) {
         out << ", " << access;
     }
     out << ">";
diff --git a/src/tint/ast/pointer_test.cc b/src/tint/ast/pointer_test.cc
index 56a04b0..aea260a 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::kInvalid);
+    auto* p = create<Pointer>(i32, ast::AddressSpace::kWorkgroup, Access::kUndefined);
     EXPECT_EQ(p->FriendlyName(Symbols()), "ptr<workgroup, i32>");
 }
 
diff --git a/src/tint/ast/storage_texture.cc b/src/tint/ast/storage_texture.cc
index 9c223c3..c9d26c2 100644
--- a/src/tint/ast/storage_texture.cc
+++ b/src/tint/ast/storage_texture.cc
@@ -76,7 +76,7 @@
             return builder.create<F32>();
         }
 
-        case TexelFormat::kInvalid:
+        case TexelFormat::kUndefined:
             break;
     }
 
diff --git a/src/tint/ast/texel_format.cc b/src/tint/ast/texel_format.cc
index 201cad3..587cf29 100644
--- a/src/tint/ast/texel_format.cc
+++ b/src/tint/ast/texel_format.cc
@@ -26,7 +26,7 @@
 
 /// ParseTexelFormat parses a TexelFormat from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or TexelFormat::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or TexelFormat::kUndefined if the string could not be parsed.
 TexelFormat ParseTexelFormat(std::string_view str) {
     if (str == "r32float") {
         return TexelFormat::kR32Float;
@@ -76,13 +76,13 @@
     if (str == "rgba8unorm") {
         return TexelFormat::kRgba8Unorm;
     }
-    return TexelFormat::kInvalid;
+    return TexelFormat::kUndefined;
 }
 
 std::ostream& operator<<(std::ostream& out, TexelFormat value) {
     switch (value) {
-        case TexelFormat::kInvalid:
-            return out << "invalid";
+        case TexelFormat::kUndefined:
+            return out << "undefined";
         case TexelFormat::kR32Float:
             return out << "r32float";
         case TexelFormat::kR32Sint:
diff --git a/src/tint/ast/texel_format.h b/src/tint/ast/texel_format.h
index 9c65fb6..fff88a3 100644
--- a/src/tint/ast/texel_format.h
+++ b/src/tint/ast/texel_format.h
@@ -29,7 +29,7 @@
 
 /// Enumerator of texel formats
 enum class TexelFormat {
-    kInvalid,
+    kUndefined,
     kR32Float,
     kR32Sint,
     kR32Uint,
@@ -55,7 +55,7 @@
 
 /// ParseTexelFormat parses a TexelFormat from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or TexelFormat::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or TexelFormat::kUndefined if the string could not be parsed.
 TexelFormat ParseTexelFormat(std::string_view str);
 
 constexpr const char* kTexelFormatStrings[] = {
diff --git a/src/tint/ast/texel_format_test.cc b/src/tint/ast/texel_format_test.cc
index d431918..d257d5e 100644
--- a/src/tint/ast/texel_format_test.cc
+++ b/src/tint/ast/texel_format_test.cc
@@ -53,30 +53,30 @@
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"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},
+    {"rcc2flot", TexelFormat::kUndefined},       {"3flo3", TexelFormat::kUndefined},
+    {"r32flVat", TexelFormat::kUndefined},       {"r32s1nt", TexelFormat::kUndefined},
+    {"rq2Jint", TexelFormat::kUndefined},        {"r32sin7ll", TexelFormat::kUndefined},
+    {"ppqq2unHH", TexelFormat::kUndefined},      {"r3cv", TexelFormat::kUndefined},
+    {"b2uiGt", TexelFormat::kUndefined},         {"rgvi2float", TexelFormat::kUndefined},
+    {"rg328WWoat", TexelFormat::kUndefined},     {"rg32lxxMt", TexelFormat::kUndefined},
+    {"rgXggsnt", TexelFormat::kUndefined},       {"rgXsnu", TexelFormat::kUndefined},
+    {"rg32s3nt", TexelFormat::kUndefined},       {"rg3Euint", TexelFormat::kUndefined},
+    {"PP32TTint", TexelFormat::kUndefined},      {"xxg32ddnt", TexelFormat::kUndefined},
+    {"rgba446float", TexelFormat::kUndefined},   {"SSVVba16float", TexelFormat::kUndefined},
+    {"rgbRR6float", TexelFormat::kUndefined},    {"rga16Fint", TexelFormat::kUndefined},
+    {"rgb16sint", TexelFormat::kUndefined},      {"ORVHa16sint", TexelFormat::kUndefined},
+    {"ryba1uint", TexelFormat::kUndefined},      {"r77ba1nnullrrt", TexelFormat::kUndefined},
+    {"rgb4006uint", TexelFormat::kUndefined},    {"rboofloat", TexelFormat::kUndefined},
+    {"rgbaz2loat", TexelFormat::kUndefined},     {"ppga3ii1floa", TexelFormat::kUndefined},
+    {"XXgba32sint", TexelFormat::kUndefined},    {"IIgb9932nni55t", TexelFormat::kUndefined},
+    {"rYbaSSrrsiHHat", TexelFormat::kUndefined}, {"rbkk2Hit", TexelFormat::kUndefined},
+    {"jgba3ugRR", TexelFormat::kUndefined},      {"rgbab2ui", TexelFormat::kUndefined},
+    {"rgba8sijt", TexelFormat::kUndefined},      {"rba8sint", TexelFormat::kUndefined},
+    {"rba8sqt", TexelFormat::kUndefined},        {"rgba8NNnom", TexelFormat::kUndefined},
+    {"rga8vvorm", TexelFormat::kUndefined},      {"rgba8snorQ", TexelFormat::kUndefined},
+    {"rgbauirf", TexelFormat::kUndefined},       {"rgbajuint", TexelFormat::kUndefined},
+    {"wNNgbauin2", TexelFormat::kUndefined},     {"rgba8unrm", TexelFormat::kUndefined},
+    {"rgba8urrorm", TexelFormat::kUndefined},    {"rgba8Gnorm", TexelFormat::kUndefined},
 };
 
 using TexelFormatParseTest = testing::TestWithParam<Case>;
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
index ddb5b11..687ac91 100644
--- a/src/tint/inspector/inspector.cc
+++ b/src/tint/inspector/inspector.cc
@@ -29,6 +29,7 @@
 #include "src/tint/ast/override.h"
 #include "src/tint/ast/var.h"
 #include "src/tint/sem/array.h"
+#include "src/tint/sem/bool.h"
 #include "src/tint/sem/call.h"
 #include "src/tint/sem/depth_multisampled_texture.h"
 #include "src/tint/sem/depth_texture.h"
@@ -120,7 +121,7 @@
     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::kUndefined) {
         ast_sampling_type = ast::InterpolationSampling::kCenter;
     }
 
@@ -135,13 +136,13 @@
         case ast::InterpolationType::kFlat:
             interpolation_type = InterpolationType::kFlat;
             break;
-        case ast::InterpolationType::kInvalid:
+        case ast::InterpolationType::kUndefined:
             break;
     }
 
     auto sampling_type = InterpolationSampling::kUnknown;
     switch (ast_sampling_type) {
-        case ast::InterpolationSampling::kInvalid:
+        case ast::InterpolationSampling::kUndefined:
             sampling_type = InterpolationSampling::kNone;
             break;
         case ast::InterpolationSampling::kCenter:
@@ -301,40 +302,19 @@
             continue;
         }
 
-        if (!var->constructor) {
-            result[override_id] = Scalar();
-            continue;
-        }
-
-        auto* literal = var->constructor->As<ast::LiteralExpression>();
-        if (!literal) {
-            // This is invalid WGSL, but handling gracefully.
-            result[override_id] = Scalar();
-            continue;
-        }
-
-        if (auto* l = literal->As<ast::BoolLiteralExpression>()) {
-            result[override_id] = Scalar(l->value);
-            continue;
-        }
-
-        if (auto* l = literal->As<ast::IntLiteralExpression>()) {
-            switch (l->suffix) {
-                case ast::IntLiteralExpression::Suffix::kNone:
-                case ast::IntLiteralExpression::Suffix::kI:
-                    result[override_id] = Scalar(static_cast<int32_t>(l->value));
-                    continue;
-                case ast::IntLiteralExpression::Suffix::kU:
-                    result[override_id] = Scalar(static_cast<uint32_t>(l->value));
-                    continue;
+        if (global->Constructor()) {
+            if (auto* value = global->Constructor()->ConstantValue()) {
+                result[override_id] = Switch(
+                    value->Type(),  //
+                    [&](const sem::I32*) { return Scalar(value->As<i32>()); },
+                    [&](const sem::U32*) { return Scalar(value->As<u32>()); },
+                    [&](const sem::F32*) { return Scalar(value->As<f32>()); },
+                    [&](const sem::Bool*) { return Scalar(value->As<bool>()); });
+                continue;
             }
         }
 
-        if (auto* l = literal->As<ast::FloatLiteralExpression>()) {
-            result[override_id] = Scalar(static_cast<float>(l->value));
-            continue;
-        }
-
+        // No const-expression initializer for the override
         result[override_id] = Scalar();
     }
 
diff --git a/src/tint/inspector/inspector_test.cc b/src/tint/inspector/inspector_test.cc
index 7aa017c..1daec37 100644
--- a/src/tint/inspector/inspector_test.cc
+++ b/src/tint/inspector/inspector_test.cc
@@ -1250,7 +1250,7 @@
             ast::InterpolationType::kPerspective, ast::InterpolationSampling::kSample,
             InterpolationType::kPerspective, InterpolationSampling::kSample},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kPerspective, ast::InterpolationSampling::kInvalid,
+            ast::InterpolationType::kPerspective, ast::InterpolationSampling::kUndefined,
             InterpolationType::kPerspective, InterpolationSampling::kCenter},
         InspectorGetEntryPointInterpolateTestParams{
             ast::InterpolationType::kLinear, ast::InterpolationSampling::kCenter,
@@ -1262,21 +1262,24 @@
             ast::InterpolationType::kLinear, ast::InterpolationSampling::kSample,
             InterpolationType::kLinear, InterpolationSampling::kSample},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kLinear, ast::InterpolationSampling::kInvalid,
+            ast::InterpolationType::kLinear, ast::InterpolationSampling::kUndefined,
             InterpolationType::kLinear, InterpolationSampling::kCenter},
         InspectorGetEntryPointInterpolateTestParams{
-            ast::InterpolationType::kFlat, ast::InterpolationSampling::kInvalid,
+            ast::InterpolationType::kFlat, ast::InterpolationSampling::kUndefined,
             InterpolationType::kFlat, InterpolationSampling::kNone}));
 
 TEST_F(InspectorGetOverrideDefaultValuesTest, Bool) {
-    Override("foo", ty.bool_(), Id(1_a));
-    Override("bar", ty.bool_(), Expr(true), Id(20_a));
-    Override("baz", ty.bool_(), Expr(false), Id(300_a));
+    GlobalConst("C", Expr(true));
+    Override("a", ty.bool_(), Id(1_a));
+    Override("b", ty.bool_(), Expr(true), Id(20_a));
+    Override("c", Expr(false), Id(300_a));
+    Override("d", Or(true, false), Id(400_a));
+    Override("e", Expr("C"), Id(500_a));
 
     Inspector& inspector = Build();
 
     auto result = inspector.GetOverrideDefaultValues();
-    ASSERT_EQ(3u, result.size());
+    ASSERT_EQ(5u, result.size());
 
     ASSERT_TRUE(result.find(OverrideId{1}) != result.end());
     EXPECT_TRUE(result[OverrideId{1}].IsNull());
@@ -1288,16 +1291,29 @@
     ASSERT_TRUE(result.find(OverrideId{300}) != result.end());
     EXPECT_TRUE(result[OverrideId{300}].IsBool());
     EXPECT_FALSE(result[OverrideId{300}].AsBool());
+
+    ASSERT_TRUE(result.find(OverrideId{400}) != result.end());
+    EXPECT_TRUE(result[OverrideId{400}].IsBool());
+    EXPECT_TRUE(result[OverrideId{400}].AsBool());
+
+    ASSERT_TRUE(result.find(OverrideId{500}) != result.end());
+    EXPECT_TRUE(result[OverrideId{500}].IsBool());
+    EXPECT_TRUE(result[OverrideId{500}].AsBool());
 }
 
 TEST_F(InspectorGetOverrideDefaultValuesTest, U32) {
-    Override("foo", ty.u32(), Id(1_a));
-    Override("bar", ty.u32(), Expr(42_u), Id(20_a));
+    GlobalConst("C", Expr(100_u));
+    Override("a", ty.u32(), Id(1_a));
+    Override("b", ty.u32(), Expr(42_u), Id(20_a));
+    Override("c", ty.u32(), Expr(42_a), Id(30_a));
+    Override("d", ty.u32(), Add(42_a, 10_a), Id(40_a));
+    Override("e", Add(42_a, 10_u), Id(50_a));
+    Override("f", Expr("C"), Id(60_a));
 
     Inspector& inspector = Build();
 
     auto result = inspector.GetOverrideDefaultValues();
-    ASSERT_EQ(2u, result.size());
+    ASSERT_EQ(6u, result.size());
 
     ASSERT_TRUE(result.find(OverrideId{1}) != result.end());
     EXPECT_TRUE(result[OverrideId{1}].IsNull());
@@ -1305,17 +1321,37 @@
     ASSERT_TRUE(result.find(OverrideId{20}) != result.end());
     EXPECT_TRUE(result[OverrideId{20}].IsU32());
     EXPECT_EQ(42u, result[OverrideId{20}].AsU32());
+
+    ASSERT_TRUE(result.find(OverrideId{30}) != result.end());
+    EXPECT_TRUE(result[OverrideId{30}].IsU32());
+    EXPECT_EQ(42u, result[OverrideId{30}].AsU32());
+
+    ASSERT_TRUE(result.find(OverrideId{40}) != result.end());
+    EXPECT_TRUE(result[OverrideId{40}].IsU32());
+    EXPECT_EQ(52u, result[OverrideId{40}].AsU32());
+
+    ASSERT_TRUE(result.find(OverrideId{50}) != result.end());
+    EXPECT_TRUE(result[OverrideId{50}].IsU32());
+    EXPECT_EQ(52u, result[OverrideId{50}].AsU32());
+
+    ASSERT_TRUE(result.find(OverrideId{60}) != result.end());
+    EXPECT_TRUE(result[OverrideId{60}].IsU32());
+    EXPECT_EQ(100u, result[OverrideId{60}].AsU32());
 }
 
 TEST_F(InspectorGetOverrideDefaultValuesTest, I32) {
-    Override("foo", ty.i32(), Id(1_a));
-    Override("bar", ty.i32(), Expr(-42_i), Id(20_a));
-    Override("baz", ty.i32(), Expr(42_i), Id(300_a));
+    GlobalConst("C", Expr(100_a));
+    Override("a", ty.i32(), Id(1_a));
+    Override("b", ty.i32(), Expr(-42_i), Id(20_a));
+    Override("c", ty.i32(), Expr(42_i), Id(300_a));
+    Override("d", Expr(42_a), Id(400_a));
+    Override("e", Add(42_a, 7_a), Id(500_a));
+    Override("f", Expr("C"), Id(6000_a));
 
     Inspector& inspector = Build();
 
     auto result = inspector.GetOverrideDefaultValues();
-    ASSERT_EQ(3u, result.size());
+    ASSERT_EQ(6u, result.size());
 
     ASSERT_TRUE(result.find(OverrideId{1}) != result.end());
     EXPECT_TRUE(result[OverrideId{1}].IsNull());
@@ -1327,18 +1363,32 @@
     ASSERT_TRUE(result.find(OverrideId{300}) != result.end());
     EXPECT_TRUE(result[OverrideId{300}].IsI32());
     EXPECT_EQ(42, result[OverrideId{300}].AsI32());
+
+    ASSERT_TRUE(result.find(OverrideId{400}) != result.end());
+    EXPECT_TRUE(result[OverrideId{400}].IsI32());
+    EXPECT_EQ(42, result[OverrideId{400}].AsI32());
+
+    ASSERT_TRUE(result.find(OverrideId{500}) != result.end());
+    EXPECT_TRUE(result[OverrideId{500}].IsI32());
+    EXPECT_EQ(49, result[OverrideId{500}].AsI32());
+
+    ASSERT_TRUE(result.find(OverrideId{6000}) != result.end());
+    EXPECT_TRUE(result[OverrideId{6000}].IsI32());
+    EXPECT_EQ(100, result[OverrideId{6000}].AsI32());
 }
 
 TEST_F(InspectorGetOverrideDefaultValuesTest, Float) {
-    Override("foo", ty.f32(), Id(1_a));
-    Override("bar", ty.f32(), Expr(0_f), Id(20_a));
-    Override("baz", ty.f32(), Expr(-10_f), Id(300_a));
-    Override("x", ty.f32(), Expr(15_f), Id(4000_a));
+    Override("a", ty.f32(), Id(1_a));
+    Override("b", ty.f32(), Expr(0_f), Id(20_a));
+    Override("c", ty.f32(), Expr(-10_f), Id(300_a));
+    Override("d", Expr(15_f), Id(4000_a));
+    Override("3", Expr(42.0_a), Id(5000_a));
+    Override("e", ty.f32(), Mul(15_f, 10_a), Id(6000_a));
 
     Inspector& inspector = Build();
 
     auto result = inspector.GetOverrideDefaultValues();
-    ASSERT_EQ(4u, result.size());
+    ASSERT_EQ(6u, result.size());
 
     ASSERT_TRUE(result.find(OverrideId{1}) != result.end());
     EXPECT_TRUE(result[OverrideId{1}].IsNull());
@@ -1354,6 +1404,14 @@
     ASSERT_TRUE(result.find(OverrideId{4000}) != result.end());
     EXPECT_TRUE(result[OverrideId{4000}].IsFloat());
     EXPECT_FLOAT_EQ(15.0f, result[OverrideId{4000}].AsFloat());
+
+    ASSERT_TRUE(result.find(OverrideId{5000}) != result.end());
+    EXPECT_TRUE(result[OverrideId{5000}].IsFloat());
+    EXPECT_FLOAT_EQ(42.0f, result[OverrideId{5000}].AsFloat());
+
+    ASSERT_TRUE(result.find(OverrideId{6000}) != result.end());
+    EXPECT_TRUE(result[OverrideId{6000}].IsFloat());
+    EXPECT_FLOAT_EQ(150.0f, result[OverrideId{6000}].AsFloat());
 }
 
 TEST_F(InspectorGetConstantNameToIdMapTest, WithAndWithoutIds) {
diff --git a/src/tint/inspector/resource_binding.cc b/src/tint/inspector/resource_binding.cc
index 6434ac2..a0e89e2 100644
--- a/src/tint/inspector/resource_binding.cc
+++ b/src/tint/inspector/resource_binding.cc
@@ -104,7 +104,7 @@
             return ResourceBinding::TexelFormat::kRgba32Sint;
         case ast::TexelFormat::kRgba32Float:
             return ResourceBinding::TexelFormat::kRgba32Float;
-        case ast::TexelFormat::kInvalid:
+        case ast::TexelFormat::kUndefined:
             return ResourceBinding::TexelFormat::kNone;
     }
     return ResourceBinding::TexelFormat::kNone;
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index 2c99b3b..8430698 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::kInvalid;
+        ast::Access access = ast::Access::kUndefined;
         const ast::Expression* constructor = nullptr;
         utils::Vector<const ast::Attribute*, 4> attributes;
 
@@ -902,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::kInvalid) const {
+                                    ast::Access access = ast::Access::kUndefined) const {
             return builder->create<ast::Pointer>(type, address_space, access);
         }
 
@@ -914,7 +914,7 @@
         const ast::Pointer* pointer(const Source& source,
                                     const ast::Type* type,
                                     ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kInvalid) const {
+                                    ast::Access access = ast::Access::kUndefined) const {
             return builder->create<ast::Pointer>(source, type, address_space, access);
         }
 
@@ -923,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::kInvalid) const {
+                                    ast::Access access = ast::Access::kUndefined) const {
             return pointer(Of<T>(), address_space, access);
         }
 
@@ -934,7 +934,7 @@
         template <typename T>
         const ast::Pointer* pointer(const Source& source,
                                     ast::AddressSpace address_space,
-                                    ast::Access access = ast::Access::kInvalid) const {
+                                    ast::Access access = ast::Access::kUndefined) const {
             return pointer(source, Of<T>(), address_space, access);
         }
 
@@ -2909,7 +2909,7 @@
     const ast::InterpolateAttribute* Interpolate(
         const Source& source,
         ast::InterpolationType type,
-        ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid) {
+        ast::InterpolationSampling sampling = ast::InterpolationSampling::kUndefined) {
         return create<ast::InterpolateAttribute>(source, type, sampling);
     }
 
@@ -2919,7 +2919,7 @@
     /// @returns the interpolate attribute pointer
     const ast::InterpolateAttribute* Interpolate(
         ast::InterpolationType type,
-        ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid) {
+        ast::InterpolationSampling sampling = ast::InterpolationSampling::kUndefined) {
         return create<ast::InterpolateAttribute>(source_, type, sampling);
     }
 
diff --git a/src/tint/reader/spirv/enum_converter.cc b/src/tint/reader/spirv/enum_converter.cc
index bc899d7..923f250 100644
--- a/src/tint/reader/spirv/enum_converter.cc
+++ b/src/tint/reader/spirv/enum_converter.cc
@@ -59,7 +59,7 @@
     }
 
     Fail() << "unknown SPIR-V storage class: " << uint32_t(sc);
-    return ast::AddressSpace::kInvalid;
+    return ast::AddressSpace::kUndefined;
 }
 
 ast::BuiltinValue EnumConverter::ToBuiltin(SpvBuiltIn b) {
@@ -93,7 +93,7 @@
     }
 
     Fail() << "unknown SPIR-V builtin: " << uint32_t(b);
-    return ast::BuiltinValue::kInvalid;
+    return ast::BuiltinValue::kUndefined;
 }
 
 ast::TextureDimension EnumConverter::ToDim(SpvDim dim, bool arrayed) {
@@ -129,7 +129,7 @@
 ast::TexelFormat EnumConverter::ToTexelFormat(SpvImageFormat fmt) {
     switch (fmt) {
         case SpvImageFormatUnknown:
-            return ast::TexelFormat::kInvalid;
+            return ast::TexelFormat::kUndefined;
 
         // 8 bit channels
         case SpvImageFormatRgba8:
@@ -172,7 +172,7 @@
             break;
     }
     Fail() << "invalid image format: " << int(fmt);
-    return ast::TexelFormat::kInvalid;
+    return ast::TexelFormat::kUndefined;
 }
 
 }  // namespace tint::reader::spirv
diff --git a/src/tint/reader/spirv/enum_converter_test.cc b/src/tint/reader/spirv/enum_converter_test.cc
index 623f6ea..112dc4a 100644
--- a/src/tint/reader/spirv/enum_converter_test.cc
+++ b/src/tint/reader/spirv/enum_converter_test.cc
@@ -137,7 +137,7 @@
 INSTANTIATE_TEST_SUITE_P(EnumConverterBad,
                          SpvStorageClassTest,
                          testing::Values(StorageClassCase{static_cast<SpvStorageClass>(9999), false,
-                                                          ast::AddressSpace::kInvalid}));
+                                                          ast::AddressSpace::kUndefined}));
 
 // Builtin
 
@@ -202,12 +202,14 @@
                     BuiltinCase{SpvBuiltInFragDepth, true, ast::BuiltinValue::kFragDepth},
                     BuiltinCase{SpvBuiltInSampleMask, true, ast::BuiltinValue::kSampleMask}));
 
-INSTANTIATE_TEST_SUITE_P(
-    EnumConverterBad,
-    SpvBuiltinTest,
-    testing::Values(BuiltinCase{static_cast<SpvBuiltIn>(9999), false, ast::BuiltinValue::kInvalid},
-                    BuiltinCase{static_cast<SpvBuiltIn>(9999), false, ast::BuiltinValue::kInvalid},
-                    BuiltinCase{SpvBuiltInNumWorkgroups, false, ast::BuiltinValue::kInvalid}));
+INSTANTIATE_TEST_SUITE_P(EnumConverterBad,
+                         SpvBuiltinTest,
+                         testing::Values(BuiltinCase{static_cast<SpvBuiltIn>(9999), false,
+                                                     ast::BuiltinValue::kUndefined},
+                                         BuiltinCase{static_cast<SpvBuiltIn>(9999), false,
+                                                     ast::BuiltinValue::kUndefined},
+                                         BuiltinCase{SpvBuiltInNumWorkgroups, false,
+                                                     ast::BuiltinValue::kUndefined}));
 
 // Dim
 
@@ -326,7 +328,7 @@
     SpvImageFormatTest,
     testing::Values(
         // Unknown.  This is used for sampled images.
-        TexelFormatCase{SpvImageFormatUnknown, true, ast::TexelFormat::kInvalid},
+        TexelFormatCase{SpvImageFormatUnknown, true, ast::TexelFormat::kUndefined},
         // 8 bit channels
         TexelFormatCase{SpvImageFormatRgba8, true, ast::TexelFormat::kRgba8Unorm},
         TexelFormatCase{SpvImageFormatRgba8Snorm, true, ast::TexelFormat::kRgba8Snorm},
@@ -355,23 +357,23 @@
     SpvImageFormatTest,
     testing::Values(
         // Scanning in order from the SPIR-V spec.
-        TexelFormatCase{SpvImageFormatRg16f, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatR11fG11fB10f, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatR16f, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRgb10A2, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg16, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg8, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatR16, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatR8, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRgba16Snorm, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg16Snorm, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg8Snorm, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg16i, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg8i, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatR8i, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRgb10a2ui, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg16ui, false, ast::TexelFormat::kInvalid},
-        TexelFormatCase{SpvImageFormatRg8ui, false, ast::TexelFormat::kInvalid}));
+        TexelFormatCase{SpvImageFormatRg16f, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatR11fG11fB10f, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatR16f, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRgb10A2, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg16, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg8, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatR16, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatR8, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRgba16Snorm, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg16Snorm, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg8Snorm, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg16i, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg8i, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatR8i, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRgb10a2ui, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg16ui, false, ast::TexelFormat::kUndefined},
+        TexelFormatCase{SpvImageFormatRg8ui, false, ast::TexelFormat::kUndefined}));
 
 }  // namespace
 }  // namespace tint::reader::spirv
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
index 7bbb5df..9c38818 100644
--- a/src/tint/reader/spirv/function.cc
+++ b/src/tint/reader/spirv/function.cc
@@ -329,6 +329,10 @@
             return "faceForward";
         case GLSLstd450FindILsb:
             return "firstTrailingBit";
+        case GLSLstd450FindSMsb:
+            return "firstLeadingBit";
+        case GLSLstd450FindUMsb:
+            return "firstLeadingBit";
         case GLSLstd450Floor:
             return "floor";
         case GLSLstd450Fma:
@@ -429,9 +433,6 @@
         case GLSLstd450PackDouble2x32:
         case GLSLstd450UnpackDouble2x32:
 
-        case GLSLstd450FindSMsb:
-        case GLSLstd450FindUMsb:
-
         case GLSLstd450InterpolateAtCentroid:
         case GLSLstd450InterpolateAtSample:
         case GLSLstd450InterpolateAtOffset:
@@ -2592,7 +2593,6 @@
             // 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))};
         }
@@ -4854,15 +4854,20 @@
                 }
                 // Local variables are always Function storage class, with default
                 // access mode.
-                return DefInfo::Pointer{ast::AddressSpace::kFunction, ast::Access::kInvalid};
+                return DefInfo::Pointer{ast::AddressSpace::kFunction, ast::Access::kUndefined};
             }
             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};
+                // For access mode, 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.
+                // If/when the SPIR-V path supports variable pointers, then we
+                // can pointers to read-only storage buffers passed as
+                // parameters.  In that case we need to do a global analysis to
+                // determine what the formal argument parameter type should be,
+                // whether it has read_only or read_write access mode.
+                return DefInfo::Pointer{type->address_space, ast::Access::kUndefined};
             }
             default:
                 break;
@@ -4898,13 +4903,11 @@
 const Type* FunctionEmitter::RemapPointerProperties(const Type* type, uint32_t result_id) {
     if (auto* ast_ptr_type = As<Pointer>(type)) {
         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);
+        return ty_.Pointer(ast_ptr_type->type, pi.address_space, pi.access);
     }
     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 ty_.Reference(ast_ptr_type->type, pi.address_space, pi.access);
     }
     return type;
 }
@@ -5086,7 +5089,7 @@
             // Avoid moving combinatorial values across constructs.  This is a
             // simple heuristic to avoid changing the cost of an operation
             // by moving it into or out of a loop, for example.
-            if ((def_info->pointer.address_space == ast::AddressSpace::kInvalid) &&
+            if ((def_info->pointer.address_space == ast::AddressSpace::kUndefined) &&
                 local_def.used_in_another_construct) {
                 should_hoist_to_let = true;
             }
diff --git a/src/tint/reader/spirv/function.h b/src/tint/reader/spirv/function.h
index b8240cf..9c9c832 100644
--- a/src/tint/reader/spirv/function.h
+++ b/src/tint/reader/spirv/function.h
@@ -332,10 +332,10 @@
         /// buffer expressed in the old style (with Uniform address space)
         /// that needs to be remapped to StorageBuffer address space.
         /// This is kInvalid for non-pointers.
-        ast::AddressSpace address_space = ast::AddressSpace::kInvalid;
+        ast::AddressSpace address_space = ast::AddressSpace::kUndefined;
 
         /// The declared access mode.
-        ast::Access access = ast::Access::kInvalid;
+        ast::Access access = ast::Access::kUndefined;
     };
 
     /// The expression to use when sinking pointers into their use.
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 7a4fd94..584ec2a 100644
--- a/src/tint/reader/spirv/function_glsl_std_450_test.cc
+++ b/src/tint/reader/spirv/function_glsl_std_450_test.cc
@@ -583,7 +583,8 @@
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Inting_Inting,
                          ::testing::Values(GlslStd450Case{"SAbs", "abs"},
-                                           GlslStd450Case{"FindILsb", "firstTrailingBit"}));
+                                           GlslStd450Case{"FindILsb", "firstTrailingBit"},
+                                           GlslStd450Case{"FindSMsb", "firstLeadingBit"}));
 
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Inting_IntingInting,
@@ -701,7 +702,8 @@
 
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Uinting_Uinting,
-                         ::testing::Values(GlslStd450Case{"FindILsb", "firstTrailingBit"}));
+                         ::testing::Values(GlslStd450Case{"FindILsb", "firstTrailingBit"},
+                                           GlslStd450Case{"FindUMsb", "firstLeadingBit"}));
 
 INSTANTIATE_TEST_SUITE_P(Samples,
                          SpvParserTest_GlslStd450_Uinting_UintingUinting,
@@ -977,6 +979,106 @@
         << body;
 }
 
+TEST_F(SpvParserTest, RectifyOperandsAndResult_FindSMsb) {
+    // Check signedness conversion of arguments and results.
+    //   SPIR-V signed arg -> keep it
+    //      signed result -> keep it
+    //      unsigned result -> cast result to unsigned
+    //
+    //   SPIR-V unsigned arg -> cast it to signed
+    //      signed result -> keept it
+    //      unsigned result -> cast result to unsigned
+    const auto assembly = Preamble() + R"(
+     ; signed arg
+     ;    signed result
+     %1 = OpExtInst %int %glsl FindSMsb %i1
+     %2 = OpExtInst %v2int %glsl FindSMsb %v2i1
+
+     ; signed arg
+     ;    unsigned result
+     %3 = OpExtInst %uint %glsl FindSMsb %i1
+     %4 = OpExtInst %v2uint %glsl FindSMsb %v2i1
+
+     ; unsigned arg
+     ;    signed result
+     %5 = OpExtInst %int %glsl FindSMsb %u1
+     %6 = OpExtInst %v2int %glsl FindSMsb %v2u1
+
+     ; unsigned arg
+     ;    unsigned result
+     %7 = OpExtInst %uint %glsl FindSMsb %u1
+     %8 = OpExtInst %v2uint %glsl FindSMsb %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 : i32 = firstLeadingBit(i1);
+let x_2 : vec2<i32> = firstLeadingBit(v2i1);
+let x_3 : u32 = bitcast<u32>(firstLeadingBit(i1));
+let x_4 : vec2<u32> = bitcast<vec2<u32>>(firstLeadingBit(v2i1));
+let x_5 : i32 = firstLeadingBit(bitcast<i32>(u1));
+let x_6 : vec2<i32> = firstLeadingBit(bitcast<vec2<i32>>(v2u1));
+let x_7 : u32 = bitcast<u32>(firstLeadingBit(bitcast<i32>(u1)));
+let x_8 : vec2<u32> = bitcast<vec2<u32>>(firstLeadingBit(bitcast<vec2<i32>>(v2u1)));
+)")) << body;
+}
+
+TEST_F(SpvParserTest, RectifyOperandsAndResult_FindUMsb) {
+    // Check signedness conversion of arguments and results.
+    //   SPIR-V signed arg -> cast arg to unsigned
+    //      signed result -> cast result to signed
+    //      unsigned result -> keep it
+    //
+    //   SPIR-V unsigned arg -> keep it
+    //      signed result -> cast result to signed
+    //      unsigned result -> keep it
+    const auto assembly = Preamble() + R"(
+     ; signed arg
+     ;    signed result
+     %1 = OpExtInst %int %glsl FindUMsb %i1
+     %2 = OpExtInst %v2int %glsl FindUMsb %v2i1
+
+     ; signed arg
+     ;    unsigned result
+     %3 = OpExtInst %uint %glsl FindUMsb %i1
+     %4 = OpExtInst %v2uint %glsl FindUMsb %v2i1
+
+     ; unsigned arg
+     ;    signed result
+     %5 = OpExtInst %int %glsl FindUMsb %u1
+     %6 = OpExtInst %v2int %glsl FindUMsb %v2u1
+
+     ; unsigned arg
+     ;    unsigned result
+     %7 = OpExtInst %uint %glsl FindUMsb %u1
+     %8 = OpExtInst %v2uint %glsl FindUMsb %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 : i32 = bitcast<i32>(firstLeadingBit(bitcast<u32>(i1)));
+let x_2 : vec2<i32> = bitcast<vec2<i32>>(firstLeadingBit(bitcast<vec2<u32>>(v2i1)));
+let x_3 : u32 = firstLeadingBit(bitcast<u32>(i1));
+let x_4 : vec2<u32> = firstLeadingBit(bitcast<vec2<u32>>(v2i1));
+let x_5 : i32 = bitcast<i32>(firstLeadingBit(u1));
+let x_6 : vec2<i32> = bitcast<vec2<i32>>(firstLeadingBit(v2u1));
+let x_7 : u32 = firstLeadingBit(u1);
+let x_8 : vec2<u32> = firstLeadingBit(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 a64aac5..66979ae 100644
--- a/src/tint/reader/spirv/function_memory_test.cc
+++ b/src/tint/reader/spirv/function_memory_test.cc
@@ -858,7 +858,52 @@
     EXPECT_EQ(got, expected) << got;
 }
 
-std::string OldStorageBufferPreamble() {
+std::string NewStorageBufferPreamble(bool nonwritable = false) {
+    // Declare a buffer with
+    //  StorageBuffer storage class
+    //  Block struct decoration
+    return std::string(R"(
+     OpCapability Shader
+     OpExtension "SPV_KHR_storage_buffer_storage_class"
+     OpMemoryModel Logical Simple
+     OpEntryPoint Fragment %100 "main"
+     OpExecutionMode %100 OriginUpperLeft
+     OpName %myvar "myvar"
+
+     OpDecorate %myvar DescriptorSet 0
+     OpDecorate %myvar Binding 0
+
+     OpDecorate %struct Block
+     OpMemberDecorate %struct 0 Offset 0
+     OpMemberDecorate %struct 1 Offset 4
+     OpDecorate %arr ArrayStride 4
+     )") +
+           (nonwritable ? R"(
+     OpMemberDecorate %struct 0 NonWritable
+     OpMemberDecorate %struct 1 NonWritable)"
+                        : "") +
+           R"(
+     %void = OpTypeVoid
+     %voidfn = OpTypeFunction %void
+     %uint = OpTypeInt 32 0
+
+     %uint_0 = OpConstant %uint 0
+     %uint_1 = OpConstant %uint 1
+
+     %arr = OpTypeRuntimeArray %uint
+     %struct = OpTypeStruct %uint %arr
+     %ptr_struct = OpTypePointer StorageBuffer %struct
+     %ptr_uint = OpTypePointer StorageBuffer %uint
+
+     %myvar = OpVariable %ptr_struct StorageBuffer
+  )";
+}
+
+std::string OldStorageBufferPreamble(bool nonwritable = false) {
+    // Declare a buffer with
+    //  Unifrom storage class
+    //  BufferBlock struct decoration
+    // This is the deprecated way to declare a storage buffer.
     return Preamble() + R"(
      OpName %myvar "myvar"
 
@@ -869,7 +914,12 @@
      OpMemberDecorate %struct 0 Offset 0
      OpMemberDecorate %struct 1 Offset 4
      OpDecorate %arr ArrayStride 4
-
+     )" +
+           (nonwritable ? R"(
+     OpMemberDecorate %struct 0 NonWritable
+     OpMemberDecorate %struct 1 NonWritable)"
+                        : "") +
+           R"(
      %void = OpTypeVoid
      %voidfn = OpTypeFunction %void
      %uint = OpTypeInt 32 0
@@ -938,7 +988,7 @@
 )"));
 }
 
-TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice) {
+TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice_ReadWrite) {
     // 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'.
@@ -966,15 +1016,130 @@
     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);
+    EXPECT_THAT(got, HasSubstr(R"(let x_1 : ptr<storage, u32, read_write> = &(myvar.field0);
 *(x_1) = 0u;
 *(x_1) = 0u;
-let x_2 : ptr<storage, u32> = &(myvar.field1[1u]);
+let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
 let x_3 : u32 = *(x_2);
 *(x_2) = 0u;
 )"));
 }
 
+TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice_ReadOnly) {
+    // Like the previous test, but make the storage buffer read_only.
+    // The pointer type must also be read-only.
+    const auto assembly = OldStorageBufferPreamble(true) + 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, read> = &(myvar.field0);
+*(x_1) = 0u;
+*(x_1) = 0u;
+let x_2 : ptr<storage, u32, read> = &(myvar.field1[1u]);
+let x_3 : u32 = *(x_2);
+*(x_2) = 0u;
+)")) << got
+     << assembly;
+}
+
+TEST_F(SpvParserMemoryTest, StorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice_ReadWrite) {
+    // Use new style storage buffer declaration:
+    //   StorageBuffer storage class,
+    //   Block decoration
+    // The pointer type must use 'storage' address space, and should use defaulted access
+    const auto assembly = NewStorageBufferPreamble() + 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, read_write> = &(myvar.field0);
+*(x_1) = 0u;
+*(x_1) = 0u;
+let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
+let x_3 : u32 = *(x_2);
+*(x_2) = 0u;
+)")) << got;
+}
+
+TEST_F(SpvParserMemoryTest, StorageBuffer_ThroughAccessChain_NonCascaded_UsedTwice_ReadOnly) {
+    // Like the previous test, but make the storage buffer read_only.
+    // Use new style storage buffer declaration:
+    //   StorageBuffer storage class,
+    //   Block decoration
+    // The pointer type must use 'storage' address space, and must use read_only
+    // access
+    const auto assembly = NewStorageBufferPreamble(true) + 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, read> = &(myvar.field0);
+*(x_1) = 0u;
+*(x_1) = 0u;
+let x_2 : ptr<storage, u32, read> = &(myvar.field1[1u]);
+let x_3 : u32 = *(x_2);
+*(x_2) = 0u;
+)")) << got;
+}
+
 TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded_InBoundsAccessChain) {
     // Like the previous test, but using OpInBoundsAccessChain.
     const auto assembly = OldStorageBufferPreamble() + R"(
@@ -1050,7 +1215,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     EXPECT_THAT(test::ToString(p->program(), ast_body),
-                HasSubstr(R"(let x_2 : ptr<storage, u32> = &(myvar.field1[1u]);
+                HasSubstr(R"(let x_2 : ptr<storage, u32, read_write> = &(myvar.field1[1u]);
 *(x_2) = 0u;
 )")) << p->error();
 
@@ -1128,7 +1293,7 @@
     EXPECT_TRUE(fe.EmitBody()) << p->error();
     auto ast_body = fe.ast_body();
     const auto body_str = test::ToString(p->program(), ast_body);
-    EXPECT_THAT(body_str, HasSubstr(R"(let x_2 : ptr<storage, S> = &(myvar);
+    EXPECT_THAT(body_str, HasSubstr(R"(let x_2 : ptr<storage, S, read_write> = &(myvar);
 let x_1 : u32 = arrayLength(&((*(x_2)).rtarr));
 )")) << body_str;
 
diff --git a/src/tint/reader/spirv/parser_impl.cc b/src/tint/reader/spirv/parser_impl.cc
index 63e5cb4..c64586a 100644
--- a/src/tint/reader/spirv/parser_impl.cc
+++ b/src/tint/reader/spirv/parser_impl.cc
@@ -117,6 +117,7 @@
         case GLSLstd450SMin:
         case GLSLstd450SMax:
         case GLSLstd450SClamp:
+        case GLSLstd450FindSMsb:
             return true;
         default:
             break;
@@ -150,6 +151,7 @@
         case GLSLstd450UMin:
         case GLSLstd450UMax:
         case GLSLstd450UClamp:
+        case GLSLstd450FindUMsb:
             return true;
         default:
             break;
@@ -222,8 +224,8 @@
         case GLSLstd450UMax:
         case GLSLstd450UClamp:
         case GLSLstd450FindILsb:
-            // TODO(dneto): FindSMsb?
-            // TODO(dneto): FindUMsb?
+        case GLSLstd450FindSMsb:
+        case GLSLstd450FindUMsb:
             return true;
         default:
             break;
@@ -1207,7 +1209,7 @@
     }
 
     auto ast_address_space = enum_converter_.ToAddressSpace(storage_class);
-    if (ast_address_space == ast::AddressSpace::kInvalid) {
+    if (ast_address_space == ast::AddressSpace::kUndefined) {
         Fail() << "SPIR-V pointer type with ID " << type_id << " has invalid storage class "
                << static_cast<uint32_t>(storage_class);
         return nullptr;
@@ -1567,7 +1569,7 @@
         return nullptr;
     }
 
-    ast::Access access = ast::Access::kInvalid;
+    ast::Access access = ast::Access::kUndefined;
     if (address_space == ast::AddressSpace::kStorage) {
         bool read_only = false;
         if (auto* tn = storage_type->As<Named>()) {
@@ -1678,7 +1680,7 @@
                     break;
             }
             auto ast_builtin = enum_converter_.ToBuiltin(spv_builtin);
-            if (ast_builtin == ast::BuiltinValue::kInvalid) {
+            if (ast_builtin == ast::BuiltinValue::kUndefined) {
                 // A diagnostic has already been emitted.
                 return false;
             }
@@ -1750,7 +1752,7 @@
                                             AttributeList* attributes) {
     // Vulkan defaults to perspective-correct interpolation.
     ast::InterpolationType type = ast::InterpolationType::kPerspective;
-    ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid;
+    ast::InterpolationSampling sampling = ast::InterpolationSampling::kUndefined;
 
     for (const auto& deco : decorations) {
         TINT_ASSERT(Reader, deco.size() > 0);
@@ -1805,7 +1807,7 @@
 
     // Apply interpolation.
     if (type == ast::InterpolationType::kPerspective &&
-        sampling == ast::InterpolationSampling::kInvalid) {
+        sampling == ast::InterpolationSampling::kUndefined) {
         // This is the default. Don't add a decoration.
     } else {
         attributes->Push(create<ast::InterpolateAttribute>(type, sampling));
@@ -2477,7 +2479,7 @@
         } else {
             const auto access = ast::Access::kWrite;
             const auto format = enum_converter_.ToTexelFormat(image_type->format());
-            if (format == ast::TexelFormat::kInvalid) {
+            if (format == ast::TexelFormat::kUndefined) {
                 return nullptr;
             }
             ast_store_type = ty_.StorageTexture(dim, format, access);
diff --git a/src/tint/reader/spirv/parser_type.cc b/src/tint/reader/spirv/parser_type.cc
index 256444a..ff4b96e 100644
--- a/src/tint/reader/spirv/parser_type.cc
+++ b/src/tint/reader/spirv/parser_type.cc
@@ -50,11 +50,15 @@
 
 namespace {
 struct PointerHasher {
-    size_t operator()(const Pointer& t) const { return utils::Hash(t.type, t.address_space); }
+    size_t operator()(const Pointer& t) const {
+        return utils::Hash(t.type, t.address_space, t.access);
+    }
 };
 
 struct ReferenceHasher {
-    size_t operator()(const Reference& t) const { return utils::Hash(t.type, t.address_space); }
+    size_t operator()(const Reference& t) const {
+        return utils::Hash(t.type, t.address_space, t.access);
+    }
 };
 
 struct VectorHasher {
@@ -107,10 +111,10 @@
 // Equality operators
 //! @cond Doxygen_Suppress
 static bool operator==(const Pointer& a, const Pointer& b) {
-    return a.type == b.type && a.address_space == b.address_space;
+    return a.type == b.type && a.address_space == b.address_space && a.access == b.access;
 }
 static bool operator==(const Reference& a, const Reference& b) {
-    return a.type == b.type && a.address_space == b.address_space;
+    return a.type == b.type && a.address_space == b.address_space && a.access == b.access;
 }
 static bool operator==(const Vector& a, const Vector& b) {
     return a.type == b.type && a.size == b.size;
@@ -170,14 +174,16 @@
 
 Texture::~Texture() = default;
 
-Pointer::Pointer(const Type* t, ast::AddressSpace s) : type(t), address_space(s) {}
+Pointer::Pointer(const Type* t, ast::AddressSpace s, ast::Access a)
+    : type(t), address_space(s), access(a) {}
 Pointer::Pointer(const Pointer&) = default;
 
 const ast::Type* Pointer::Build(ProgramBuilder& b) const {
-    return b.ty.pointer(type->Build(b), address_space);
+    return b.ty.pointer(type->Build(b), address_space, access);
 }
 
-Reference::Reference(const Type* t, ast::AddressSpace s) : type(t), address_space(s) {}
+Reference::Reference(const Type* t, ast::AddressSpace s, ast::Access a)
+    : type(t), address_space(s), access(a) {}
 Reference::Reference(const Reference&) = default;
 
 const ast::Type* Reference::Build(ProgramBuilder& b) const {
@@ -438,12 +444,16 @@
     return state->i32_;
 }
 
-const spirv::Pointer* TypeManager::Pointer(const Type* el, ast::AddressSpace address_space) {
-    return state->pointers_.Get(el, address_space);
+const spirv::Pointer* TypeManager::Pointer(const Type* el,
+                                           ast::AddressSpace address_space,
+                                           ast::Access access) {
+    return state->pointers_.Get(el, address_space, access);
 }
 
-const spirv::Reference* TypeManager::Reference(const Type* el, ast::AddressSpace address_space) {
-    return state->references_.Get(el, address_space);
+const spirv::Reference* TypeManager::Reference(const Type* el,
+                                               ast::AddressSpace address_space,
+                                               ast::Access access) {
+    return state->references_.Get(el, address_space, access);
 }
 
 const spirv::Vector* TypeManager::Vector(const Type* el, uint32_t size) {
diff --git a/src/tint/reader/spirv/parser_type.h b/src/tint/reader/spirv/parser_type.h
index 4c67f7a..48b6f3f 100644
--- a/src/tint/reader/spirv/parser_type.h
+++ b/src/tint/reader/spirv/parser_type.h
@@ -157,12 +157,13 @@
 #endif  // NDEBUG
 };
 
-/// `ptr<SC, T>` type
+/// `ptr<SC, T, AM>` type
 struct Pointer final : public Castable<Pointer, Type> {
     /// Constructor
     /// @param ty the store type
     /// @param sc the pointer address space
-    Pointer(const Type* ty, ast::AddressSpace sc);
+    /// @param access the declared access mode
+    Pointer(const Type* ty, ast::AddressSpace sc, ast::Access access);
 
     /// Copy constructor
     /// @param other the other type to copy
@@ -181,16 +182,19 @@
     Type const* const type;
     /// the pointer address space
     ast::AddressSpace const address_space;
+    /// the pointer declared access mode
+    ast::Access const access;
 };
 
-/// `ref<SC, T>` type
+/// `ref<SC, T, AM>` type
 /// Note this has no AST representation, but is used for type tracking in the
 /// reader.
 struct Reference final : public Castable<Reference, Type> {
     /// Constructor
     /// @param ty the referenced type
     /// @param sc the reference address space
-    Reference(const Type* ty, ast::AddressSpace sc);
+    /// @param access the reference declared access mode
+    Reference(const Type* ty, ast::AddressSpace sc, ast::Access access);
 
     /// Copy constructor
     /// @param other the other type to copy
@@ -209,6 +213,8 @@
     Type const* const type;
     /// the pointer address space
     ast::AddressSpace const address_space;
+    /// the pointer declared access mode
+    ast::Access const access;
 };
 
 /// `vecN<T>` type
@@ -535,14 +541,20 @@
     const spirv::I32* I32();
     /// @param ty the store type
     /// @param address_space the pointer address space
+    /// @param access the declared access mode
     /// @return a Pointer type. Repeated calls with the same arguments will return
     /// the same pointer.
-    const spirv::Pointer* Pointer(const Type* ty, ast::AddressSpace address_space);
+    const spirv::Pointer* Pointer(const Type* ty,
+                                  ast::AddressSpace address_space,
+                                  ast::Access access = ast::Access::kUndefined);
     /// @param ty the referenced type
     /// @param address_space the reference address space
+    /// @param access the declared access mode
     /// @return a Reference type. Repeated calls with the same arguments will
     /// return the same pointer.
-    const spirv::Reference* Reference(const Type* ty, ast::AddressSpace address_space);
+    const spirv::Reference* Reference(const Type* ty,
+                                      ast::AddressSpace address_space,
+                                      ast::Access access = ast::Access::kUndefined);
     /// @param ty the element type
     /// @param sz the number of elements in the vector
     /// @return a Vector type. Repeated calls with the same arguments will return
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index b042ac2..3dad333 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -383,7 +383,7 @@
             return add_error(t.source(), "enable directives don't take parenthesis");
         }
 
-        auto extension = ast::Extension::kInvalid;
+        auto extension = ast::Extension::kUndefined;
         if (t.Is(Token::Type::kF16)) {
             // `f16` is a valid extension name and also a keyword
             synchronized_ = true;
@@ -991,7 +991,7 @@
             }
             return VariableQualifier{sc.value, ac.value};
         }
-        return Expect<VariableQualifier>{VariableQualifier{sc.value, ast::Access::kInvalid},
+        return Expect<VariableQualifier>{VariableQualifier{sc.value, ast::Access::kUndefined},
                                          source};
     });
 
@@ -1179,7 +1179,7 @@
     auto& t = peek();
     if (t.IsIdentifier()) {
         auto val = parse(t.to_str());
-        if (val != ENUM::kInvalid) {
+        if (val != ENUM::kUndefined) {
             synchronized_ = true;
             next();
             return {val, t.source()};
@@ -1247,7 +1247,7 @@
     const char* use = "ptr declaration";
 
     auto address_space = ast::AddressSpace::kNone;
-    auto access = ast::Access::kInvalid;
+    auto access = ast::Access::kUndefined;
 
     auto subtype = expect_lt_gt_block(use, [&]() -> Expect<const ast::Type*> {
         auto sc = expect_address_space(use);
@@ -3512,7 +3512,7 @@
                 return Failure::kErrored;
             }
 
-            ast::InterpolationSampling sampling = ast::InterpolationSampling::kInvalid;
+            ast::InterpolationSampling sampling = ast::InterpolationSampling::kUndefined;
             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 77196a0..60b94de 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::kInvalid;
+        ast::Access access = ast::Access::kUndefined;
         /// 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::kInvalid;
+        ast::Access access = ast::Access::kUndefined;
     };
 
     /// MatrixDimensions contains the column and row information for a matrix
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 ce31276..a13998a 100644
--- a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
@@ -239,7 +239,8 @@
     EXPECT_EQ(t.value, nullptr);
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
-    EXPECT_EQ(p->error(), R"(1:30: expected access control for storage texture type. Did you mean 'read'?
+    EXPECT_EQ(p->error(),
+              R"(1:30: expected access control for storage texture type. Did you mean 'read'?
 Possible values: 'read', 'read_write', 'write')");
 }
 
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 70af3b2..4c68f9b 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
@@ -227,7 +227,7 @@
 
     auto* interp = var_attr->As<ast::InterpolateAttribute>();
     EXPECT_EQ(interp->type, ast::InterpolationType::kFlat);
-    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kInvalid);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kUndefined);
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Single_TrailingComma) {
@@ -243,7 +243,7 @@
 
     auto* interp = var_attr->As<ast::InterpolateAttribute>();
     EXPECT_EQ(interp->type, ast::InterpolationType::kFlat);
-    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kInvalid);
+    EXPECT_EQ(interp->sampling, ast::InterpolationSampling::kUndefined);
 }
 
 TEST_F(ParserImplTest, Attribute_Interpolate_Single_DoubleTrailingComma) {
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 32c1a54..5c0cd5e 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::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{"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{"storage, read", ast::AddressSpace::kStorage, ast::Access::kRead},
         VariableStorageData{"storage, write", ast::AddressSpace::kStorage, ast::Access::kWrite},
         VariableStorageData{"storage, read_write", ast::AddressSpace::kStorage,
diff --git a/src/tint/resolver/address_space_validation_test.cc b/src/tint/resolver/address_space_validation_test.cc
index ae68013..3ef19a6 100644
--- a/src/tint/resolver/address_space_validation_test.cc
+++ b/src/tint/resolver/address_space_validation_test.cc
@@ -61,7 +61,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: runtime-sized arrays can only be used in the <storage> address space
-note: while analysing structure member S.m
+note: while analyzing structure member S.m
 12:34 note: while instantiating 'var' v)");
 }
 
@@ -81,7 +81,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: runtime-sized arrays can only be used in the <storage> address space
-note: while analysing structure member S.m
+note: while analyzing structure member S.m
 12:34 note: while instantiating 'var' v)");
 }
 
@@ -317,7 +317,7 @@
     ASSERT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(56:78 error: runtime-sized arrays can only be used in the <storage> address space
-note: while analysing structure member S.m
+note: while analyzing structure member S.m
 56:78 note: while instantiating 'var' svar)");
 }
 
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index 018dbfd..30615bb 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::kInvalid,
+    GlobalVar(Source{{1, 2}}, "val", ty.f32(), ast::AddressSpace::kPrivate, ast::Access::kUndefined,
               Expr(1.23_f));
 
     Structure(Source{{6, 4}}, "mystruct",
@@ -738,7 +738,9 @@
     Structure("mystruct", utils::Vector{Member(
                               "a", ty.f32(), utils::Vector{MemberAlign(Source{{12, 34}}, "val")})});
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'align' must be an i32 or u32 value)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(error: @align requires a const-expression, but expression is an override-expression)");
 }
 
 }  // namespace StructAndStructMemberTests
@@ -1386,16 +1388,16 @@
     ResolverAttributeValidationTest,
     InterpolateParameterTest,
     testing::Values(
-        Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kInvalid, true},
+        Params{ast::InterpolationType::kPerspective, ast::InterpolationSampling::kUndefined, 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::kInvalid, true},
+        Params{ast::InterpolationType::kLinear, ast::InterpolationSampling::kUndefined, 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::kInvalid, true},
+        Params{ast::InterpolationType::kFlat, ast::InterpolationSampling::kUndefined, 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}));
@@ -1433,7 +1435,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(12:34 error: integral user-defined vertex outputs must have a flat interpolation attribute
-note: while analysing entry point 'main')");
+note: while analyzing entry point 'main')");
 }
 
 TEST_F(InterpolateTest, MissingLocationAttribute_Parameter) {
@@ -1443,7 +1445,7 @@
                    utils::Vector{
                        Builtin(ast::BuiltinValue::kPosition),
                        Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                                   ast::InterpolationSampling::kInvalid),
+                                   ast::InterpolationSampling::kUndefined),
                    }),
          },
          ty.void_(), utils::Empty,
@@ -1467,7 +1469,7 @@
          utils::Vector{
              Builtin(ast::BuiltinValue::kPosition),
              Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                         ast::InterpolationSampling::kInvalid),
+                         ast::InterpolationSampling::kUndefined),
          });
 
     EXPECT_FALSE(r()->Resolve());
@@ -1480,7 +1482,7 @@
               utils::Vector{
                   Member("a", ty.f32(),
                          utils::Vector{Interpolate(Source{{12, 34}}, ast::InterpolationType::kFlat,
-                                                   ast::InterpolationSampling::kInvalid)}),
+                                                   ast::InterpolationSampling::kUndefined)}),
               });
 
     EXPECT_FALSE(r()->Resolve());
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 0883aed..b28521d 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -67,7 +67,9 @@
 
     // TODO(crbug.com/tint/1581): Once 'abs' is implemented as @const, this will no longer be an
     // error.
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be constant expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
 }
 
 // Tests for Logical builtins
diff --git a/src/tint/resolver/builtin_validation_test.cc b/src/tint/resolver/builtin_validation_test.cc
index 6ba48de..e5ad2e2 100644
--- a/src/tint/resolver/builtin_validation_test.cc
+++ b/src/tint/resolver/builtin_validation_test.cc
@@ -260,8 +260,7 @@
     // a vector constructor.
     bool is_vector = arg_to_replace->Is<ast::CallExpression>();
 
-    // Make the expression to be replaced, reachable. This keeps the resolver
-    // happy.
+    // Make the expression to be replaced, reachable. This keeps the resolver happy.
     WrapInFunction(arg_to_replace);
 
     arg_to_replace = expr(Source{{12, 34}}, *this);
@@ -310,13 +309,65 @@
     auto args = overload.args(this);
     auto*& arg_to_replace = (param.position == Position::kFirst) ? args.Front() : args.Back();
 
-    // Make the expression to be replaced, reachable. This keeps the resolver
-    // happy.
+    // BuildTextureVariable() uses a Literal for scalars, and a CallExpression for
+    // a vector constructor.
+    bool is_vector = arg_to_replace->Is<ast::CallExpression>();
+
+    // Make the expression to be replaced, reachable. This keeps the resolver happy.
     WrapInFunction(arg_to_replace);
 
     arg_to_replace = Expr(Source{{12, 34}}, "G");
 
-    // Call the builtin with the constexpr argument replaced
+    // Call the builtin with the constant-expression argument replaced
+    Func("func", utils::Empty, ty.void_(),
+         utils::Vector{
+             CallStmt(Call(overload.function, args)),
+         },
+         utils::Vector{
+             Stage(ast::PipelineStage::kFragment),
+         });
+
+    if (expr.invalid_index == Constexpr::kValid) {
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+    } else {
+        EXPECT_FALSE(r()->Resolve());
+        std::stringstream err;
+        if (is_vector) {
+            err << "12:34 error: each component of the " << param.name
+                << " argument must be at least " << param.min << " and at most " << param.max
+                << ". " << param.name << " component " << expr.invalid_index << " is "
+                << std::to_string(expr.values[static_cast<size_t>(expr.invalid_index)]);
+        } else {
+            err << "12:34 error: the " << param.name << " argument must be at least " << param.min
+                << " and at most " << param.max << ". " << param.name << " is "
+                << std::to_string(expr.values[static_cast<size_t>(expr.invalid_index)]);
+        }
+        EXPECT_EQ(r()->error(), err.str());
+    }
+}
+
+TEST_P(BuiltinTextureConstExprArgValidationTest, GlobalVar) {
+    auto& p = GetParam();
+    auto overload = std::get<0>(p);
+    auto param = std::get<1>(p);
+    auto expr = std::get<2>(p);
+
+    // Build the global texture and sampler variables
+    overload.BuildTextureVariable(this);
+    overload.BuildSamplerVariable(this);
+
+    // Build the module-scope var 'G' with the offset value
+    GlobalVar("G", expr({}, *this), ast::AddressSpace::kPrivate);
+
+    auto args = overload.args(this);
+    auto*& arg_to_replace = (param.position == Position::kFirst) ? args.Front() : args.Back();
+
+    // Make the expression to be replaced, reachable. This keeps the resolver happy.
+    WrapInFunction(arg_to_replace);
+
+    arg_to_replace = Expr(Source{{12, 34}}, "G");
+
+    // Call the builtin with the constant-expression argument replaced
     Func("func", utils::Empty, ty.void_(),
          utils::Vector{
              CallStmt(Call(overload.function, args)),
@@ -327,7 +378,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     std::stringstream err;
-    err << "12:34 error: the " << param.name << " argument must be a const_expression";
+    err << "12:34 error: the " << param.name << " argument must be a const-expression";
     EXPECT_EQ(r()->error(), err.str());
 }
 INSTANTIATE_TEST_SUITE_P(
diff --git a/src/tint/resolver/builtins_validation_test.cc b/src/tint/resolver/builtins_validation_test.cc
index f2bf191..7190ef1 100644
--- a/src/tint/resolver/builtins_validation_test.cc
+++ b/src/tint/resolver/builtins_validation_test.cc
@@ -204,7 +204,7 @@
     EXPECT_EQ(r()->error(),
               "12:34 error: builtin(frag_depth) cannot be used in input of "
               "fragment pipeline stage\n"
-              "note: while analysing entry point 'fragShader'");
+              "note: while analyzing entry point 'fragShader'");
 }
 
 TEST_F(ResolverBuiltinsValidationTest, StructBuiltinInsideEntryPoint_Ignored) {
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index b58ed41..065cca5 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -38,7 +38,7 @@
 
 namespace tint::resolver {
 
-/// ConstEval performs shader creation-time (constant expression) expression evaluation.
+/// ConstEval performs shader creation-time (const-expression) expression evaluation.
 /// Methods are called from the resolver, either directly or via member-function pointers indexed by
 /// the IntrinsicTable. All child-expression nodes are guaranteed to have been already resolved
 /// before calling a method to evaluate an expression's value.
diff --git a/src/tint/resolver/const_eval_binary_op_test.cc b/src/tint/resolver/const_eval_binary_op_test.cc
new file mode 100644
index 0000000..18b28da
--- /dev/null
+++ b/src/tint/resolver/const_eval_binary_op_test.cc
@@ -0,0 +1,918 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+using ::testing::HasSubstr;
+
+namespace tint::resolver {
+namespace {
+
+// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
+using resolver::operator<<;
+
+struct Case {
+    Types lhs;
+    Types rhs;
+    Types expected;
+    bool overflow;
+};
+
+/// Creates a Case with Values of any type
+template <typename T, typename U, typename V>
+Case C(Value<T> lhs, Value<U> rhs, Value<V> expected, bool overflow = false) {
+    return Case{std::move(lhs), std::move(rhs), std::move(expected), overflow};
+}
+
+/// Convenience overload that creates a Case with just scalars
+template <typename T, typename U, typename V, typename = std::enable_if_t<!IsValue<T>>>
+Case C(T lhs, U rhs, V expected, bool overflow = false) {
+    return Case{Val(lhs), Val(rhs), Val(expected), overflow};
+}
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    o << "lhs: " << c.lhs << ", rhs: " << c.rhs << ", expected: " << c.expected
+      << ", overflow: " << c.overflow;
+    return o;
+}
+
+using ResolverConstEvalBinaryOpTest = ResolverTestWithParam<std::tuple<ast::BinaryOp, Case>>;
+TEST_P(ResolverConstEvalBinaryOpTest, Test) {
+    Enable(ast::Extension::kF16);
+    auto op = std::get<0>(GetParam());
+    auto& c = std::get<1>(GetParam());
+
+    std::visit(
+        [&](auto&& expected) {
+            using T = typename std::decay_t<decltype(expected)>::ElementType;
+            if constexpr (std::is_same_v<T, AInt> || std::is_same_v<T, AFloat>) {
+                if (c.overflow) {
+                    // Overflow is not allowed for abstract types. This is tested separately.
+                    return;
+                }
+            }
+
+            auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
+            auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
+            auto* expr = create<ast::BinaryExpression>(op, lhs_expr, rhs_expr);
+
+            GlobalConst("C", expr);
+            auto* expected_expr = expected.Expr(*this);
+            GlobalConst("E", expected_expr);
+            ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+            auto* sem = Sem().Get(expr);
+            const sem::Constant* value = sem->ConstantValue();
+            ASSERT_NE(value, nullptr);
+            EXPECT_TYPE(value->Type(), sem->Type());
+
+            auto* expected_sem = Sem().Get(expected_expr);
+            const sem::Constant* expected_value = expected_sem->ConstantValue();
+            ASSERT_NE(expected_value, nullptr);
+            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+
+            ForEachElemPair(value, expected_value,
+                            [&](const sem::Constant* a, const sem::Constant* b) {
+                                EXPECT_EQ(a->As<T>(), b->As<T>());
+                                if constexpr (IsIntegral<T>) {
+                                    // Check that the constant's integer doesn't contain unexpected
+                                    // data in the MSBs that are outside of the bit-width of T.
+                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
+                                }
+                                return HasFailure() ? Action::kStop : Action::kContinue;
+                            });
+        },
+        c.expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(MixedAbstractArgs,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(testing::Values(ast::BinaryOp::kAdd),
+                                          testing::ValuesIn(std::vector{
+                                              // Mixed abstract type args
+                                              C(1_a, 2.3_a, 3.3_a),
+                                              C(2.3_a, 1_a, 3.3_a),
+                                          })));
+
+template <typename T>
+std::vector<Case> OpAddIntCases() {
+    static_assert(IsIntegral<T>);
+    return {
+        C(T{0}, T{0}, T{0}),
+        C(T{1}, T{2}, T{3}),
+        C(T::Lowest(), T{1}, T{T::Lowest() + 1}),
+        C(T::Highest(), Negate(T{1}), T{T::Highest() - 1}),
+        C(T::Lowest(), T::Highest(), Negate(T{1})),
+        C(T::Highest(), T{1}, T::Lowest(), true),
+        C(T::Lowest(), Negate(T{1}), T::Highest(), true),
+    };
+}
+template <typename T>
+std::vector<Case> OpAddFloatCases() {
+    static_assert(IsFloatingPoint<T>);
+    return {
+        C(T{0}, T{0}, T{0}),
+        C(T{1}, T{2}, T{3}),
+        C(T::Lowest(), T{1}, T{T::Lowest() + 1}),
+        C(T::Highest(), Negate(T{1}), T{T::Highest() - 1}),
+        C(T::Lowest(), T::Highest(), T{0}),
+        C(T::Highest(), T::Highest(), T::Inf(), true),
+        C(T::Lowest(), Negate(T::Highest()), -T::Inf(), true),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Add,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(testing::Values(ast::BinaryOp::kAdd),
+                                          testing::ValuesIn(Concat(  //
+                                              OpAddIntCases<AInt>(),
+                                              OpAddIntCases<i32>(),
+                                              OpAddIntCases<u32>(),
+                                              OpAddFloatCases<AFloat>(),
+                                              OpAddFloatCases<f32>(),
+                                              OpAddFloatCases<f16>()))));
+
+template <typename T>
+std::vector<Case> OpSubIntCases() {
+    static_assert(IsIntegral<T>);
+    return {
+        C(T{0}, T{0}, T{0}),
+        C(T{3}, T{2}, T{1}),
+        C(T{T::Lowest() + 1}, T{1}, T::Lowest()),
+        C(T{T::Highest() - 1}, Negate(T{1}), T::Highest()),
+        C(Negate(T{1}), T::Highest(), T::Lowest()),
+        C(T::Lowest(), T{1}, T::Highest(), true),
+        C(T::Highest(), Negate(T{1}), T::Lowest(), true),
+    };
+}
+template <typename T>
+std::vector<Case> OpSubFloatCases() {
+    static_assert(IsFloatingPoint<T>);
+    return {
+        C(T{0}, T{0}, T{0}),
+        C(T{3}, T{2}, T{1}),
+        C(T::Highest(), T{1}, T{T::Highest() - 1}),
+        C(T::Lowest(), Negate(T{1}), T{T::Lowest() + 1}),
+        C(T{0}, T::Highest(), T::Lowest()),
+        C(T::Highest(), Negate(T::Highest()), T::Inf(), true),
+        C(T::Lowest(), T::Highest(), -T::Inf(), true),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Sub,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(testing::Values(ast::BinaryOp::kSubtract),
+                                          testing::ValuesIn(Concat(  //
+                                              OpSubIntCases<AInt>(),
+                                              OpSubIntCases<i32>(),
+                                              OpSubIntCases<u32>(),
+                                              OpSubFloatCases<AFloat>(),
+                                              OpSubFloatCases<f32>(),
+                                              OpSubFloatCases<f16>()))));
+
+template <typename T>
+std::vector<Case> OpMulScalarCases() {
+    return {
+        C(T{0}, T{0}, T{0}),
+        C(T{1}, T{2}, T{2}),
+        C(T{2}, T{3}, T{6}),
+        C(Negate(T{2}), T{3}, Negate(T{6})),
+        C(T::Highest(), T{1}, T::Highest()),
+        C(T::Lowest(), T{1}, T::Lowest()),
+        C(T::Highest(), T::Highest(), Mul(T::Highest(), T::Highest()), true),
+        C(T::Lowest(), T::Lowest(), Mul(T::Lowest(), T::Lowest()), true),
+    };
+}
+
+template <typename T>
+std::vector<Case> OpMulVecCases() {
+    return {
+        // s * vec3 = vec3
+        C(Val(T{2.0}), Vec(T{1.25}, T{2.25}, T{3.25}), Vec(T{2.5}, T{4.5}, T{6.5})),
+        // vec3 * s = vec3
+        C(Vec(T{1.25}, T{2.25}, T{3.25}), Val(T{2.0}), Vec(T{2.5}, T{4.5}, T{6.5})),
+        // vec3 * vec3 = vec3
+        C(Vec(T{1.25}, T{2.25}, T{3.25}), Vec(T{2.0}, T{2.0}, T{2.0}), Vec(T{2.5}, T{4.5}, T{6.5})),
+    };
+}
+
+template <typename T>
+std::vector<Case> OpMulMatCases() {
+    return {
+        // s * mat3x2 = mat3x2
+        C(Val(T{2.25}),
+          Mat({T{1.0}, T{4.0}},  //
+              {T{2.0}, T{5.0}},  //
+              {T{3.0}, T{6.0}}),
+          Mat({T{2.25}, T{9.0}},   //
+              {T{4.5}, T{11.25}},  //
+              {T{6.75}, T{13.5}})),
+        // mat3x2 * s = mat3x2
+        C(Mat({T{1.0}, T{4.0}},  //
+              {T{2.0}, T{5.0}},  //
+              {T{3.0}, T{6.0}}),
+          Val(T{2.25}),
+          Mat({T{2.25}, T{9.0}},   //
+              {T{4.5}, T{11.25}},  //
+              {T{6.75}, T{13.5}})),
+        // vec3 * mat2x3 = vec2
+        C(Vec(T{1.25}, T{2.25}, T{3.25}),  //
+          Mat({T{1.0}, T{2.0}, T{3.0}},    //
+              {T{4.0}, T{5.0}, T{6.0}}),   //
+          Vec(T{15.5}, T{35.75})),
+        // mat2x3 * vec2 = vec3
+        C(Mat({T{1.0}, T{2.0}, T{3.0}},   //
+              {T{4.0}, T{5.0}, T{6.0}}),  //
+          Vec(T{1.25}, T{2.25}),          //
+          Vec(T{10.25}, T{13.75}, T{17.25})),
+        // mat3x2 * mat2x3 = mat2x2
+        C(Mat({T{1.0}, T{2.0}},              //
+              {T{3.0}, T{4.0}},              //
+              {T{5.0}, T{6.0}}),             //
+          Mat({T{1.25}, T{2.25}, T{3.25}},   //
+              {T{4.25}, T{5.25}, T{6.25}}),  //
+          Mat({T{24.25}, T{31.0}},           //
+              {T{51.25}, T{67.0}})),         //
+    };
+}
+
+INSTANTIATE_TEST_SUITE_P(Mul,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kMultiply),
+                             testing::ValuesIn(Concat(  //
+                                 OpMulScalarCases<AInt>(),
+                                 OpMulScalarCases<i32>(),
+                                 OpMulScalarCases<u32>(),
+                                 OpMulScalarCases<AFloat>(),
+                                 OpMulScalarCases<f32>(),
+                                 OpMulScalarCases<f16>(),
+                                 OpMulVecCases<AInt>(),
+                                 OpMulVecCases<i32>(),
+                                 OpMulVecCases<u32>(),
+                                 OpMulVecCases<AFloat>(),
+                                 OpMulVecCases<f32>(),
+                                 OpMulVecCases<f16>(),
+                                 OpMulMatCases<AFloat>(),
+                                 OpMulMatCases<f32>(),
+                                 OpMulMatCases<f16>()))));
+
+template <typename T>
+std::vector<Case> OpDivIntCases() {
+    std::vector<Case> r = {
+        C(Val(T{0}), Val(T{1}), Val(T{0})),
+        C(Val(T{1}), Val(T{1}), Val(T{1})),
+        C(Val(T{1}), Val(T{1}), Val(T{1})),
+        C(Val(T{2}), Val(T{1}), Val(T{2})),
+        C(Val(T{4}), Val(T{2}), Val(T{2})),
+        C(Val(T::Highest()), Val(T{1}), Val(T::Highest())),
+        C(Val(T::Lowest()), Val(T{1}), Val(T::Lowest())),
+        C(Val(T::Highest()), Val(T::Highest()), Val(T{1})),
+        C(Val(T{0}), Val(T::Highest()), Val(T{0})),
+        C(Val(T{0}), Val(T::Lowest()), Val(T{0})),
+    };
+    ConcatIntoIf<IsIntegral<T>>(  //
+        r, std::vector<Case>{
+               // e1, when e2 is zero.
+               C(T{123}, T{0}, T{123}, true),
+           });
+    ConcatIntoIf<IsSignedIntegral<T>>(  //
+        r, std::vector<Case>{
+               // e1, when e1 is the most negative value in T, and e2 is -1.
+               C(T::Smallest(), T{-1}, T::Smallest(), true),
+           });
+    return r;
+}
+
+template <typename T>
+std::vector<Case> OpDivFloatCases() {
+    return {
+        C(Val(T{0}), Val(T{1}), Val(T{0})),
+        C(Val(T{1}), Val(T{1}), Val(T{1})),
+        C(Val(T{1}), Val(T{1}), Val(T{1})),
+        C(Val(T{2}), Val(T{1}), Val(T{2})),
+        C(Val(T{4}), Val(T{2}), Val(T{2})),
+        C(Val(T::Highest()), Val(T{1}), Val(T::Highest())),
+        C(Val(T::Lowest()), Val(T{1}), Val(T::Lowest())),
+        C(Val(T::Highest()), Val(T::Highest()), Val(T{1})),
+        C(Val(T{0}), Val(T::Highest()), Val(T{0})),
+        C(Val(T{0}), Val(T::Lowest()), Val(-T{0})),
+        C(T{123}, T{0}, T::Inf(), true),
+        C(T{-123}, -T{0}, T::Inf(), true),
+        C(T{-123}, T{0}, -T::Inf(), true),
+        C(T{123}, -T{0}, -T::Inf(), true),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Div,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kDivide),
+                             testing::ValuesIn(Concat(  //
+                                 OpDivIntCases<AInt>(),
+                                 OpDivIntCases<i32>(),
+                                 OpDivIntCases<u32>(),
+                                 OpDivFloatCases<AFloat>(),
+                                 OpDivFloatCases<f32>(),
+                                 OpDivFloatCases<f16>()))));
+
+template <typename T, bool equals>
+std::vector<Case> OpEqualCases() {
+    return {
+        C(Val(T{0}), Val(T{0}), Val(true == equals)),
+        C(Val(T{0}), Val(T{1}), Val(false == equals)),
+        C(Val(T{1}), Val(T{0}), Val(false == equals)),
+        C(Val(T{1}), Val(T{1}), Val(true == equals)),
+        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(true == equals, true == equals)),
+        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(false == equals, false == equals)),
+        C(Vec(T{1}, T{1}), Vec(T{0}, T{1}), Vec(false == equals, true == equals)),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Equal,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kEqual),
+                             testing::ValuesIn(Concat(  //
+                                 OpEqualCases<AInt, true>(),
+                                 OpEqualCases<i32, true>(),
+                                 OpEqualCases<u32, true>(),
+                                 OpEqualCases<AFloat, true>(),
+                                 OpEqualCases<f32, true>(),
+                                 OpEqualCases<f16, true>(),
+                                 OpEqualCases<bool, true>()))));
+INSTANTIATE_TEST_SUITE_P(NotEqual,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kNotEqual),
+                             testing::ValuesIn(Concat(  //
+                                 OpEqualCases<AInt, false>(),
+                                 OpEqualCases<i32, false>(),
+                                 OpEqualCases<u32, false>(),
+                                 OpEqualCases<AFloat, false>(),
+                                 OpEqualCases<f32, false>(),
+                                 OpEqualCases<f16, false>(),
+                                 OpEqualCases<bool, false>()))));
+
+template <typename T, bool less_than>
+std::vector<Case> OpLessThanCases() {
+    return {
+        C(Val(T{0}), Val(T{0}), Val(false == less_than)),
+        C(Val(T{0}), Val(T{1}), Val(true == less_than)),
+        C(Val(T{1}), Val(T{0}), Val(false == less_than)),
+        C(Val(T{1}), Val(T{1}), Val(false == less_than)),
+        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(false == less_than, false == less_than)),
+        C(Vec(T{0}, T{0}), Vec(T{1}, T{1}), Vec(true == less_than, true == less_than)),
+        C(Vec(T{1}, T{1}), Vec(T{0}, T{0}), Vec(false == less_than, false == less_than)),
+        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(false == less_than, true == less_than)),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(LessThan,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kLessThan),
+                             testing::ValuesIn(Concat(  //
+                                 OpLessThanCases<AInt, true>(),
+                                 OpLessThanCases<i32, true>(),
+                                 OpLessThanCases<u32, true>(),
+                                 OpLessThanCases<AFloat, true>(),
+                                 OpLessThanCases<f32, true>(),
+                                 OpLessThanCases<f16, true>()))));
+INSTANTIATE_TEST_SUITE_P(GreaterThanEqual,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kGreaterThanEqual),
+                             testing::ValuesIn(Concat(  //
+                                 OpLessThanCases<AInt, false>(),
+                                 OpLessThanCases<i32, false>(),
+                                 OpLessThanCases<u32, false>(),
+                                 OpLessThanCases<AFloat, false>(),
+                                 OpLessThanCases<f32, false>(),
+                                 OpLessThanCases<f16, false>()))));
+
+template <typename T, bool greater_than>
+std::vector<Case> OpGreaterThanCases() {
+    return {
+        C(Val(T{0}), Val(T{0}), Val(false == greater_than)),
+        C(Val(T{0}), Val(T{1}), Val(false == greater_than)),
+        C(Val(T{1}), Val(T{0}), Val(true == greater_than)),
+        C(Val(T{1}), Val(T{1}), Val(false == greater_than)),
+        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(false == greater_than, false == greater_than)),
+        C(Vec(T{1}, T{1}), Vec(T{0}, T{0}), Vec(true == greater_than, true == greater_than)),
+        C(Vec(T{0}, T{0}), Vec(T{1}, T{1}), Vec(false == greater_than, false == greater_than)),
+        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(true == greater_than, false == greater_than)),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(GreaterThan,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kGreaterThan),
+                             testing::ValuesIn(Concat(  //
+                                 OpGreaterThanCases<AInt, true>(),
+                                 OpGreaterThanCases<i32, true>(),
+                                 OpGreaterThanCases<u32, true>(),
+                                 OpGreaterThanCases<AFloat, true>(),
+                                 OpGreaterThanCases<f32, true>(),
+                                 OpGreaterThanCases<f16, true>()))));
+INSTANTIATE_TEST_SUITE_P(LessThanEqual,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kLessThanEqual),
+                             testing::ValuesIn(Concat(  //
+                                 OpGreaterThanCases<AInt, false>(),
+                                 OpGreaterThanCases<i32, false>(),
+                                 OpGreaterThanCases<u32, false>(),
+                                 OpGreaterThanCases<AFloat, false>(),
+                                 OpGreaterThanCases<f32, false>(),
+                                 OpGreaterThanCases<f16, false>()))));
+
+static std::vector<Case> OpAndBoolCases() {
+    return {
+        C(true, true, true),
+        C(true, false, false),
+        C(false, true, false),
+        C(false, false, false),
+        C(Vec(true, true), Vec(true, false), Vec(true, false)),
+        C(Vec(true, true), Vec(false, true), Vec(false, true)),
+        C(Vec(true, false), Vec(true, false), Vec(true, false)),
+        C(Vec(false, true), Vec(true, false), Vec(false, false)),
+        C(Vec(false, false), Vec(true, false), Vec(false, false)),
+    };
+}
+template <typename T>
+std::vector<Case> OpAndIntCases() {
+    using B = BitValues<T>;
+    return {
+        C(T{0b1010}, T{0b1111}, T{0b1010}),
+        C(T{0b1010}, T{0b0000}, T{0b0000}),
+        C(T{0b1010}, T{0b0011}, T{0b0010}),
+        C(T{0b1010}, T{0b1100}, T{0b1000}),
+        C(T{0b1010}, T{0b0101}, T{0b0000}),
+        C(B::All, B::All, B::All),
+        C(B::LeftMost, B::LeftMost, B::LeftMost),
+        C(B::RightMost, B::RightMost, B::RightMost),
+        C(B::All, T{0}, T{0}),
+        C(T{0}, B::All, T{0}),
+        C(B::LeftMost, B::AllButLeftMost, T{0}),
+        C(B::AllButLeftMost, B::LeftMost, T{0}),
+        C(B::RightMost, B::AllButRightMost, T{0}),
+        C(B::AllButRightMost, B::RightMost, T{0}),
+        C(Vec(B::All, B::LeftMost, B::RightMost),      //
+          Vec(B::All, B::All, B::All),                 //
+          Vec(B::All, B::LeftMost, B::RightMost)),     //
+        C(Vec(B::All, B::LeftMost, B::RightMost),      //
+          Vec(T{0}, T{0}, T{0}),                       //
+          Vec(T{0}, T{0}, T{0})),                      //
+        C(Vec(B::LeftMost, B::RightMost),              //
+          Vec(B::AllButLeftMost, B::AllButRightMost),  //
+          Vec(T{0}, T{0})),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(And,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kAnd),
+                             testing::ValuesIn(            //
+                                 Concat(OpAndBoolCases(),  //
+                                        OpAndIntCases<AInt>(),
+                                        OpAndIntCases<i32>(),
+                                        OpAndIntCases<u32>()))));
+
+static std::vector<Case> OpOrBoolCases() {
+    return {
+        C(true, true, true),
+        C(true, false, true),
+        C(false, true, true),
+        C(false, false, false),
+        C(Vec(true, true), Vec(true, false), Vec(true, true)),
+        C(Vec(true, true), Vec(false, true), Vec(true, true)),
+        C(Vec(true, false), Vec(true, false), Vec(true, false)),
+        C(Vec(false, true), Vec(true, false), Vec(true, true)),
+        C(Vec(false, false), Vec(true, false), Vec(true, false)),
+    };
+}
+template <typename T>
+std::vector<Case> OpOrIntCases() {
+    using B = BitValues<T>;
+    return {
+        C(T{0b1010}, T{0b1111}, T{0b1111}),
+        C(T{0b1010}, T{0b0000}, T{0b1010}),
+        C(T{0b1010}, T{0b0011}, T{0b1011}),
+        C(T{0b1010}, T{0b1100}, T{0b1110}),
+        C(T{0b1010}, T{0b0101}, T{0b1111}),
+        C(B::All, B::All, B::All),
+        C(B::LeftMost, B::LeftMost, B::LeftMost),
+        C(B::RightMost, B::RightMost, B::RightMost),
+        C(B::All, T{0}, B::All),
+        C(T{0}, B::All, B::All),
+        C(B::LeftMost, B::AllButLeftMost, B::All),
+        C(B::AllButLeftMost, B::LeftMost, B::All),
+        C(B::RightMost, B::AllButRightMost, B::All),
+        C(B::AllButRightMost, B::RightMost, B::All),
+        C(Vec(B::All, B::LeftMost, B::RightMost),      //
+          Vec(B::All, B::All, B::All),                 //
+          Vec(B::All, B::All, B::All)),                //
+        C(Vec(B::All, B::LeftMost, B::RightMost),      //
+          Vec(T{0}, T{0}, T{0}),                       //
+          Vec(B::All, B::LeftMost, B::RightMost)),     //
+        C(Vec(B::LeftMost, B::RightMost),              //
+          Vec(B::AllButLeftMost, B::AllButRightMost),  //
+          Vec(B::All, B::All)),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Or,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kOr),
+                             testing::ValuesIn(Concat(OpOrBoolCases(),
+                                                      OpOrIntCases<AInt>(),
+                                                      OpOrIntCases<i32>(),
+                                                      OpOrIntCases<u32>()))));
+
+TEST_F(ResolverConstEvalTest, NotAndOrOfVecs) {
+    // const C = !((vec2(true, true) & vec2(true, false)) | vec2(false, true));
+    auto v1 = Vec(true, true).Expr(*this);
+    auto v2 = Vec(true, false).Expr(*this);
+    auto v3 = Vec(false, true).Expr(*this);
+    auto expr = Not(Or(And(v1, v2), v3));
+    GlobalConst("C", expr);
+    auto expected_expr = Vec(false, false).Expr(*this);
+    GlobalConst("E", expected_expr);
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    const sem::Constant* value = sem->ConstantValue();
+    ASSERT_NE(value, nullptr);
+    EXPECT_TYPE(value->Type(), sem->Type());
+
+    auto* expected_sem = Sem().Get(expected_expr);
+    const sem::Constant* expected_value = expected_sem->ConstantValue();
+    ASSERT_NE(expected_value, nullptr);
+    EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+
+    ForEachElemPair(value, expected_value, [&](const sem::Constant* a, const sem::Constant* b) {
+        EXPECT_EQ(a->As<bool>(), b->As<bool>());
+        return HasFailure() ? Action::kStop : Action::kContinue;
+    });
+}
+
+template <typename T>
+std::vector<Case> XorCases() {
+    using B = BitValues<T>;
+    return {
+        C(T{0b1010}, T{0b1111}, T{0b0101}),
+        C(T{0b1010}, T{0b0000}, T{0b1010}),
+        C(T{0b1010}, T{0b0011}, T{0b1001}),
+        C(T{0b1010}, T{0b1100}, T{0b0110}),
+        C(T{0b1010}, T{0b0101}, T{0b1111}),
+        C(B::All, B::All, T{0}),
+        C(B::LeftMost, B::LeftMost, T{0}),
+        C(B::RightMost, B::RightMost, T{0}),
+        C(B::All, T{0}, B::All),
+        C(T{0}, B::All, B::All),
+        C(B::LeftMost, B::AllButLeftMost, B::All),
+        C(B::AllButLeftMost, B::LeftMost, B::All),
+        C(B::RightMost, B::AllButRightMost, B::All),
+        C(B::AllButRightMost, B::RightMost, B::All),
+        C(Vec(B::All, B::LeftMost, B::RightMost),             //
+          Vec(B::All, B::All, B::All),                        //
+          Vec(T{0}, B::AllButLeftMost, B::AllButRightMost)),  //
+        C(Vec(B::All, B::LeftMost, B::RightMost),             //
+          Vec(T{0}, T{0}, T{0}),                              //
+          Vec(B::All, B::LeftMost, B::RightMost)),            //
+        C(Vec(B::LeftMost, B::RightMost),                     //
+          Vec(B::AllButLeftMost, B::AllButRightMost),         //
+          Vec(B::All, B::All)),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Xor,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kXor),
+                             testing::ValuesIn(Concat(XorCases<AInt>(),  //
+                                                      XorCases<i32>(),   //
+                                                      XorCases<u32>()))));
+
+template <typename T>
+std::vector<Case> ShiftLeftCases() {
+    // Shift type is u32 for non-abstract
+    using ST = std::conditional_t<IsAbstract<T>, T, u32>;
+    using B = BitValues<T>;
+    return {
+        C(T{0b1010}, ST{0}, T{0b0000'0000'1010}),    //
+        C(T{0b1010}, ST{1}, T{0b0000'0001'0100}),    //
+        C(T{0b1010}, ST{2}, T{0b0000'0010'1000}),    //
+        C(T{0b1010}, ST{3}, T{0b0000'0101'0000}),    //
+        C(T{0b1010}, ST{4}, T{0b0000'1010'0000}),    //
+        C(T{0b1010}, ST{5}, T{0b0001'0100'0000}),    //
+        C(T{0b1010}, ST{6}, T{0b0010'1000'0000}),    //
+        C(T{0b1010}, ST{7}, T{0b0101'0000'0000}),    //
+        C(T{0b1010}, ST{8}, T{0b1010'0000'0000}),    //
+        C(B::LeftMost, ST{0}, B::LeftMost),          //
+        C(B::TwoLeftMost, ST{1}, B::LeftMost),       // No overflow
+        C(B::All, ST{1}, B::AllButRightMost),        // No overflow
+        C(B::All, ST{B::NumBits - 1}, B::LeftMost),  // No overflow
+
+        C(Vec(T{0b1010}, T{0b1010}),                                            //
+          Vec(ST{0}, ST{1}),                                                    //
+          Vec(T{0b0000'0000'1010}, T{0b0000'0001'0100})),                       //
+        C(Vec(T{0b1010}, T{0b1010}),                                            //
+          Vec(ST{2}, ST{3}),                                                    //
+          Vec(T{0b0000'0010'1000}, T{0b0000'0101'0000})),                       //
+        C(Vec(T{0b1010}, T{0b1010}),                                            //
+          Vec(ST{4}, ST{5}),                                                    //
+          Vec(T{0b0000'1010'0000}, T{0b0001'0100'0000})),                       //
+        C(Vec(T{0b1010}, T{0b1010}, T{0b1010}),                                 //
+          Vec(ST{6}, ST{7}, ST{8}),                                             //
+          Vec(T{0b0010'1000'0000}, T{0b0101'0000'0000}, T{0b1010'0000'0000})),  //
+    };
+}
+INSTANTIATE_TEST_SUITE_P(ShiftLeft,
+                         ResolverConstEvalBinaryOpTest,
+                         testing::Combine(  //
+                             testing::Values(ast::BinaryOp::kShiftLeft),
+                             testing::ValuesIn(Concat(ShiftLeftCases<AInt>(),  //
+                                                      ShiftLeftCases<i32>(),   //
+                                                      ShiftLeftCases<u32>()))));
+
+// Tests for errors on overflow/underflow of binary operations with abstract numbers
+struct OverflowCase {
+    ast::BinaryOp op;
+    Types lhs;
+    Types rhs;
+};
+
+static std::ostream& operator<<(std::ostream& o, const OverflowCase& c) {
+    o << ast::FriendlyName(c.op) << ", lhs: " << c.lhs << ", rhs: " << c.rhs;
+    return o;
+}
+using ResolverConstEvalBinaryOpTest_Overflow = ResolverTestWithParam<OverflowCase>;
+TEST_P(ResolverConstEvalBinaryOpTest_Overflow, Test) {
+    Enable(ast::Extension::kF16);
+    auto& c = GetParam();
+    auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
+    auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
+    auto* expr = create<ast::BinaryExpression>(Source{{1, 1}}, c.op, lhs_expr, rhs_expr);
+    GlobalConst("C", expr);
+    ASSERT_FALSE(r()->Resolve());
+
+    std::string type_name = std::visit(
+        [&](auto&& value) {
+            using ValueType = std::decay_t<decltype(value)>;
+            return builder::FriendlyName<ValueType>();
+        },
+        c.lhs);
+
+    EXPECT_THAT(r()->error(), HasSubstr("1:1 error: '"));
+    EXPECT_THAT(r()->error(), HasSubstr("' cannot be represented as '" + type_name + "'"));
+}
+INSTANTIATE_TEST_SUITE_P(
+    Test,
+    ResolverConstEvalBinaryOpTest_Overflow,
+    testing::Values(
+
+        // scalar-scalar add
+        OverflowCase{ast::BinaryOp::kAdd, Val(AInt::Highest()), Val(1_a)},
+        OverflowCase{ast::BinaryOp::kAdd, Val(AInt::Lowest()), Val(-1_a)},
+        OverflowCase{ast::BinaryOp::kAdd, Val(AFloat::Highest()), Val(AFloat::Highest())},
+        OverflowCase{ast::BinaryOp::kAdd, Val(AFloat::Lowest()), Val(AFloat::Lowest())},
+        // scalar-scalar subtract
+        OverflowCase{ast::BinaryOp::kSubtract, Val(AInt::Lowest()), Val(1_a)},
+        OverflowCase{ast::BinaryOp::kSubtract, Val(AInt::Highest()), Val(-1_a)},
+        OverflowCase{ast::BinaryOp::kSubtract, Val(AFloat::Highest()), Val(AFloat::Lowest())},
+        OverflowCase{ast::BinaryOp::kSubtract, Val(AFloat::Lowest()), Val(AFloat::Highest())},
+
+        // scalar-scalar multiply
+        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Highest()), Val(2_a)},
+        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Lowest()), Val(-2_a)},
+
+        // scalar-vector multiply
+        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Highest()), Vec(2_a, 1_a)},
+        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Lowest()), Vec(-2_a, 1_a)},
+
+        // vector-matrix multiply
+
+        // Overflow from first multiplication of dot product of vector and matrix column 0
+        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
+        //            ^
+        OverflowCase{ast::BinaryOp::kMultiply,       //
+                     Vec(AFloat::Highest(), 1.0_a),  //
+                     Mat({2.0_a, 1.0_a},             //
+                         {1.0_a, 1.0_a})},
+
+        // Overflow from second multiplication of dot product of vector and matrix column 0
+        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
+        //                             ^
+        OverflowCase{ast::BinaryOp::kMultiply,       //
+                     Vec(1.0_a, AFloat::Highest()),  //
+                     Mat({1.0_a, 2.0_a},             //
+                         {1.0_a, 1.0_a})},
+
+        // Overflow from addition of dot product of vector and matrix column 0
+        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
+        //                      ^
+        OverflowCase{ast::BinaryOp::kMultiply,                   //
+                     Vec(AFloat::Highest(), AFloat::Highest()),  //
+                     Mat({1.0_a, 1.0_a},                         //
+                         {1.0_a, 1.0_a})},
+
+        // matrix-matrix multiply
+
+        // Overflow from first multiplication of dot product of lhs row 0 and rhs column 0
+        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
+        //               ^
+        OverflowCase{ast::BinaryOp::kMultiply,        //
+                     Mat({AFloat::Highest(), 1.0_a},  //
+                         {1.0_a, 1.0_a}),             //
+                     Mat({2.0_a, 1.0_a},              //
+                         {1.0_a, 1.0_a})},
+
+        // Overflow from second multiplication of dot product of lhs row 0 and rhs column 0
+        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
+        //                                     ^
+        OverflowCase{ast::BinaryOp::kMultiply,        //
+                     Mat({1.0_a, AFloat::Highest()},  //
+                         {1.0_a, 1.0_a}),             //
+                     Mat({1.0_a, 1.0_a},              //
+                         {2.0_a, 1.0_a})},
+
+        // Overflow from addition of dot product of lhs row 0 and rhs column 0
+        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
+        //                          ^
+        OverflowCase{ast::BinaryOp::kMultiply,         //
+                     Mat({AFloat::Highest(), 1.0_a},   //
+                         {AFloat::Highest(), 1.0_a}),  //
+                     Mat({1.0_a, 1.0_a},               //
+                         {1.0_a, 1.0_a})},
+
+        // Divide by zero
+        OverflowCase{ast::BinaryOp::kDivide, Val(123_a), Val(0_a)},
+        OverflowCase{ast::BinaryOp::kDivide, Val(-123_a), Val(-0_a)},
+        OverflowCase{ast::BinaryOp::kDivide, Val(-123_a), Val(0_a)},
+        OverflowCase{ast::BinaryOp::kDivide, Val(123_a), Val(-0_a)},
+
+        // Most negative value divided by -1
+        OverflowCase{ast::BinaryOp::kDivide, Val(AInt::Lowest()), Val(-1_a)},
+
+        // ShiftLeft of AInts that result in values not representable as AInts.
+        // Note that for i32/u32, these would error because shift value is larger than 32.
+        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
+                     Val(AInt{BitValues<AInt>::All}),             //
+                     Val(AInt{BitValues<AInt>::NumBits})},        //
+        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
+                     Val(AInt{BitValues<AInt>::RightMost}),       //
+                     Val(AInt{BitValues<AInt>::NumBits})},        //
+        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
+                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
+                     Val(AInt{BitValues<AInt>::NumBits})},        //
+        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
+                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
+                     Val(AInt{BitValues<AInt>::NumBits + 1})},    //
+        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
+                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
+                     Val(AInt{BitValues<AInt>::NumBits + 1000})}
+
+        ));
+
+TEST_F(ResolverConstEvalTest, BinaryAbstractAddOverflow_AInt) {
+    GlobalConst("c", Add(Source{{1, 1}}, Expr(AInt::Highest()), 1_a));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "1:1 error: '9223372036854775807 + 1' cannot be represented as 'abstract-int'");
+}
+
+TEST_F(ResolverConstEvalTest, BinaryAbstractAddUnderflow_AInt) {
+    GlobalConst("c", Add(Source{{1, 1}}, Expr(AInt::Lowest()), -1_a));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "1:1 error: '-9223372036854775808 + -1' cannot be represented as 'abstract-int'");
+}
+
+TEST_F(ResolverConstEvalTest, BinaryAbstractAddOverflow_AFloat) {
+    GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Highest()), AFloat::Highest()));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(),
+              "1:1 error: '1.79769e+308 + 1.79769e+308' cannot be represented as 'abstract-float'");
+}
+
+TEST_F(ResolverConstEvalTest, BinaryAbstractAddUnderflow_AFloat) {
+    GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Lowest()), AFloat::Lowest()));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        "1:1 error: '-1.79769e+308 + -1.79769e+308' cannot be represented as 'abstract-float'");
+}
+
+// Mixed AInt and AFloat args to test implicit conversion to AFloat
+INSTANTIATE_TEST_SUITE_P(
+    AbstractMixed,
+    ResolverConstEvalBinaryOpTest,
+    testing::Combine(
+        testing::Values(ast::BinaryOp::kAdd),
+        testing::Values(C(Val(1_a), Val(2.3_a), Val(3.3_a)),
+                        C(Val(2.3_a), Val(1_a), Val(3.3_a)),
+                        C(Val(1_a), Vec(2.3_a, 2.3_a, 2.3_a), Vec(3.3_a, 3.3_a, 3.3_a)),
+                        C(Vec(2.3_a, 2.3_a, 2.3_a), Val(1_a), Vec(3.3_a, 3.3_a, 3.3_a)),
+                        C(Vec(2.3_a, 2.3_a, 2.3_a), Val(1_a), Vec(3.3_a, 3.3_a, 3.3_a)),
+                        C(Val(1_a), Vec(2.3_a, 2.3_a, 2.3_a), Vec(3.3_a, 3.3_a, 3.3_a)),
+                        C(Mat({1_a, 2_a},        //
+                              {1_a, 2_a},        //
+                              {1_a, 2_a}),       //
+                          Mat({1.2_a, 2.3_a},    //
+                              {1.2_a, 2.3_a},    //
+                              {1.2_a, 2.3_a}),   //
+                          Mat({2.2_a, 4.3_a},    //
+                              {2.2_a, 4.3_a},    //
+                              {2.2_a, 4.3_a})),  //
+                        C(Mat({1.2_a, 2.3_a},    //
+                              {1.2_a, 2.3_a},    //
+                              {1.2_a, 2.3_a}),   //
+                          Mat({1_a, 2_a},        //
+                              {1_a, 2_a},        //
+                              {1_a, 2_a}),       //
+                          Mat({2.2_a, 4.3_a},    //
+                              {2.2_a, 4.3_a},    //
+                              {2.2_a, 4.3_a}))   //
+                        )));
+
+// AInt left shift negative value -> error
+TEST_F(ResolverConstEvalTest, BinaryAbstractShiftLeftByNegativeValue_Error) {
+    GlobalConst("c", Shl(Source{{1, 1}}, Expr(1_a), Expr(-1_a)));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "1:1 error: cannot shift left by a negative value");
+}
+
+// i32/u32 left shift by >= 32 -> error
+using ResolverConstEvalShiftLeftConcreteGeqBitWidthError =
+    ResolverTestWithParam<std::tuple<Types, Types>>;
+TEST_P(ResolverConstEvalShiftLeftConcreteGeqBitWidthError, Test) {
+    auto* lhs_expr =
+        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
+    auto* rhs_expr =
+        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
+    GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        "1:1 error: shift left value must be less than the bit width of the lhs, which is 32");
+}
+INSTANTIATE_TEST_SUITE_P(Test,
+                         ResolverConstEvalShiftLeftConcreteGeqBitWidthError,
+                         testing::Values(                                 //
+                             std::make_tuple(Val(1_i), Val(32_u)),        //
+                             std::make_tuple(Val(1_i), Val(33_u)),        //
+                             std::make_tuple(Val(1_i), Val(34_u)),        //
+                             std::make_tuple(Val(1_i), Val(99999999_u)),  //
+                             std::make_tuple(Val(1_u), Val(32_u)),        //
+                             std::make_tuple(Val(1_u), Val(33_u)),        //
+                             std::make_tuple(Val(1_u), Val(34_u)),        //
+                             std::make_tuple(Val(1_u), Val(99999999_u))   //
+                             ));
+
+// AInt left shift results in sign change error
+using ResolverConstEvalShiftLeftSignChangeError = ResolverTestWithParam<std::tuple<Types, Types>>;
+TEST_P(ResolverConstEvalShiftLeftSignChangeError, Test) {
+    auto* lhs_expr =
+        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
+    auto* rhs_expr =
+        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
+    GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "1:1 error: shift left operation results in sign change");
+}
+template <typename T>
+std::vector<std::tuple<Types, Types>> ShiftLeftSignChangeErrorCases() {
+    // Shift type is u32 for non-abstract
+    using ST = std::conditional_t<IsAbstract<T>, T, u32>;
+    using B = BitValues<T>;
+    return {
+        {Val(T{0b0001}), Val(ST{B::NumBits - 1})},
+        {Val(T{0b0010}), Val(ST{B::NumBits - 2})},
+        {Val(T{0b0100}), Val(ST{B::NumBits - 3})},
+        {Val(T{0b1000}), Val(ST{B::NumBits - 4})},
+        {Val(T{0b0011}), Val(ST{B::NumBits - 2})},
+        {Val(T{0b0110}), Val(ST{B::NumBits - 3})},
+        {Val(T{0b1100}), Val(ST{B::NumBits - 4})},
+        {Val(B::AllButLeftMost), Val(ST{1})},
+        {Val(B::AllButLeftMost), Val(ST{B::NumBits - 1})},
+        {Val(B::LeftMost), Val(ST{1})},
+        {Val(B::LeftMost), Val(ST{B::NumBits - 1})},
+    };
+}
+INSTANTIATE_TEST_SUITE_P(Test,
+                         ResolverConstEvalShiftLeftSignChangeError,
+                         testing::ValuesIn(Concat(  //
+                             ShiftLeftSignChangeErrorCases<AInt>(),
+                             ShiftLeftSignChangeErrorCases<i32>(),
+                             ShiftLeftSignChangeErrorCases<u32>())));
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_builtin_test.cc b/src/tint/resolver/const_eval_builtin_test.cc
new file mode 100644
index 0000000..3936fba
--- /dev/null
+++ b/src/tint/resolver/const_eval_builtin_test.cc
@@ -0,0 +1,302 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
+using resolver::operator<<;
+
+struct Case {
+    Case(utils::VectorRef<Types> in_args, Types in_expected)
+        : args(std::move(in_args)), expected(std::move(in_expected)) {}
+
+    /// Expected value may be positive or negative
+    Case& PosOrNeg() {
+        expected_pos_or_neg = true;
+        return *this;
+    }
+
+    /// Expected value should be compared using FLOAT_EQ instead of EQ
+    Case& FloatComp() {
+        float_compare = true;
+        return *this;
+    }
+
+    utils::Vector<Types, 8> args;
+    Types expected;
+    bool expected_pos_or_neg = false;
+    bool float_compare = false;
+};
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    o << "args: ";
+    for (auto& a : c.args) {
+        o << a << ", ";
+    }
+    o << "expected: " << c.expected << ", expected_pos_or_neg: " << c.expected_pos_or_neg;
+    return o;
+}
+
+/// Creates a Case with Values for args and result
+static Case C(std::initializer_list<Types> args, Types result) {
+    return Case{utils::Vector<Types, 8>{args}, std::move(result)};
+}
+
+/// Convenience overload that creates a Case with just scalars
+using ScalarTypes = std::variant<AInt, AFloat, u32, i32, f32, f16>;
+static Case C(std::initializer_list<ScalarTypes> sargs, ScalarTypes sresult) {
+    utils::Vector<Types, 8> args;
+    for (auto& sa : sargs) {
+        std::visit([&](auto&& v) { return args.Push(Val(v)); }, sa);
+    }
+    Types result = Val(0_a);
+    std::visit([&](auto&& v) { result = Val(v); }, sresult);
+    return Case{std::move(args), std::move(result)};
+}
+
+using ResolverConstEvalBuiltinTest = ResolverTestWithParam<std::tuple<sem::BuiltinType, Case>>;
+
+TEST_P(ResolverConstEvalBuiltinTest, Test) {
+    Enable(ast::Extension::kF16);
+
+    auto builtin = std::get<0>(GetParam());
+    auto& c = std::get<1>(GetParam());
+
+    utils::Vector<const ast::Expression*, 8> args;
+    for (auto& a : c.args) {
+        std::visit([&](auto&& v) { args.Push(v.Expr(*this)); }, a);
+    }
+
+    std::visit(
+        [&](auto&& expected) {
+            using T = typename std::decay_t<decltype(expected)>::ElementType;
+            auto* expr = Call(sem::str(builtin), std::move(args));
+
+            GlobalConst("C", expr);
+            auto* expected_expr = expected.Expr(*this);
+            GlobalConst("E", expected_expr);
+
+            EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+            auto* sem = Sem().Get(expr);
+            const sem::Constant* value = sem->ConstantValue();
+            ASSERT_NE(value, nullptr);
+            EXPECT_TYPE(value->Type(), sem->Type());
+
+            auto* expected_sem = Sem().Get(expected_expr);
+            const sem::Constant* expected_value = expected_sem->ConstantValue();
+            ASSERT_NE(expected_value, nullptr);
+            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+
+            ForEachElemPair(value, expected_value,
+                            [&](const sem::Constant* a, const sem::Constant* b) {
+                                auto v = a->As<T>();
+                                auto e = b->As<T>();
+                                if constexpr (std::is_same_v<bool, T>) {
+                                    EXPECT_EQ(v, e);
+                                } else if constexpr (IsFloatingPoint<T>) {
+                                    if (std::isnan(e)) {
+                                        EXPECT_TRUE(std::isnan(v));
+                                    } else {
+                                        auto vf = (c.expected_pos_or_neg ? Abs(v) : v);
+                                        if (c.float_compare) {
+                                            EXPECT_FLOAT_EQ(vf, e);
+                                        } else {
+                                            EXPECT_EQ(vf, e);
+                                        }
+                                    }
+                                } else {
+                                    EXPECT_EQ((c.expected_pos_or_neg ? Abs(v) : v), e);
+                                    // Check that the constant's integer doesn't contain unexpected
+                                    // data in the MSBs that are outside of the bit-width of T.
+                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
+                                }
+                                return HasFailure() ? Action::kStop : Action::kContinue;
+                            });
+        },
+        c.expected);
+}
+
+INSTANTIATE_TEST_SUITE_P(  //
+    MixedAbstractArgs,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kAtan2),
+                     testing::ValuesIn(std::vector{
+                         C({0_a, -0.0_a}, kPi<AFloat>),
+                         C({1.0_a, 0_a}, kPiOver2<AFloat>),
+                     })));
+
+template <typename T, bool finite_only>
+std::vector<Case> Atan2Cases() {
+    std::vector<Case> cases = {
+        // If y is +/-0 and x is negative or -0, +/-PI is returned
+        C({T(0.0), -T(0.0)}, kPi<T>).PosOrNeg().FloatComp(),
+
+        // If y is +/-0 and x is positive or +0, +/-0 is returned
+        C({T(0.0), T(0.0)}, T(0.0)).PosOrNeg(),
+
+        // If x is +/-0 and y is negative, -PI/2 is returned
+        C({-T(1.0), T(0.0)}, -kPiOver2<T>).FloatComp(),  //
+        C({-T(1.0), -T(0.0)}, -kPiOver2<T>).FloatComp(),
+
+        // If x is +/-0 and y is positive, +PI/2 is returned
+        C({T(1.0), T(0.0)}, kPiOver2<T>).FloatComp(),  //
+        C({T(1.0), -T(0.0)}, kPiOver2<T>).FloatComp(),
+
+        // Vector tests
+        C({Vec(T(0.0), T(0.0)), Vec(-T(0.0), T(0.0))}, Vec(kPi<T>, T(0.0))).PosOrNeg().FloatComp(),
+        C({Vec(-T(1.0), -T(1.0)), Vec(T(0.0), -T(0.0))}, Vec(-kPiOver2<T>, -kPiOver2<T>))
+            .FloatComp(),
+        C({Vec(T(1.0), T(1.0)), Vec(T(0.0), -T(0.0))}, Vec(kPiOver2<T>, kPiOver2<T>)).FloatComp(),
+    };
+
+    if constexpr (!finite_only) {
+        std::vector<Case> non_finite_cases = {
+            // If y is +/-INF and x is finite, +/-PI/2 is returned
+            C({T::Inf(), T(0.0)}, kPiOver2<T>).PosOrNeg().FloatComp(),
+            C({-T::Inf(), T(0.0)}, kPiOver2<T>).PosOrNeg().FloatComp(),
+
+            // If y is +/-INF and x is -INF, +/-3PI/4 is returned
+            C({T::Inf(), -T::Inf()}, k3PiOver4<T>).PosOrNeg().FloatComp(),
+            C({-T::Inf(), -T::Inf()}, k3PiOver4<T>).PosOrNeg().FloatComp(),
+
+            // If y is +/-INF and x is +INF, +/-PI/4 is returned
+            C({T::Inf(), T::Inf()}, kPiOver4<T>).PosOrNeg().FloatComp(),
+            C({-T::Inf(), T::Inf()}, kPiOver4<T>).PosOrNeg().FloatComp(),
+
+            // If x is -INF and y is finite and positive, +PI is returned
+            C({T(0.0), -T::Inf()}, kPi<T>).FloatComp(),
+
+            // If x is -INF and y is finite and negative, -PI is returned
+            C({-T(0.0), -T::Inf()}, -kPi<T>).FloatComp(),
+
+            // If x is +INF and y is finite and positive, +0 is returned
+            C({T(0.0), T::Inf()}, T(0.0)),
+
+            // If x is +INF and y is finite and negative, -0 is returned
+            C({-T(0.0), T::Inf()}, -T(0.0)),
+
+            // If either x is NaN or y is NaN, NaN is returned
+            C({T::NaN(), T(0.0)}, T::NaN()),
+            C({T(0.0), T::NaN()}, T::NaN()),
+            C({T::NaN(), T::NaN()}, T::NaN()),
+
+            // Vector tests
+            C({Vec(T::Inf(), -T::Inf(), T::Inf(), -T::Inf()),  //
+               Vec(T(0.0), T(0.0), -T::Inf(), -T::Inf())},     //
+              Vec(kPiOver2<T>, kPiOver2<T>, k3PiOver4<T>, k3PiOver4<T>))
+                .PosOrNeg()
+                .FloatComp(),
+        };
+        cases = Concat(cases, non_finite_cases);
+    }
+
+    return cases;
+}
+INSTANTIATE_TEST_SUITE_P(  //
+    Atan2,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kAtan2),
+                     testing::ValuesIn(Concat(Atan2Cases<AFloat, true>(),  //
+                                              Atan2Cases<f32, false>(),
+                                              Atan2Cases<f16, false>()))));
+
+template <typename T>
+std::vector<Case> ClampCases() {
+    return {
+        C({T(0), T(0), T(0)}, T(0)),
+        C({T(0), T(42), T::Highest()}, T(42)),
+        C({T::Lowest(), T(0), T(42)}, T(0)),
+        C({T(0), T::Lowest(), T::Highest()}, T(0)),
+        C({T(0), T::Highest(), T::Lowest()}, T::Lowest()),
+        C({T::Highest(), T::Highest(), T::Highest()}, T::Highest()),
+        C({T::Lowest(), T::Lowest(), T::Lowest()}, T::Lowest()),
+        C({T::Highest(), T::Lowest(), T::Highest()}, T::Highest()),
+        C({T::Lowest(), T::Lowest(), T::Highest()}, T::Lowest()),
+
+        // Vector tests
+        C({Vec(T(0), T(0)),                         //
+           Vec(T(0), T(42)),                        //
+           Vec(T(0), T::Highest())},                //
+          Vec(T(0), T(42))),                        //
+        C({Vec(T::Lowest(), T(0), T(0)),            //
+           Vec(T(0), T::Lowest(), T::Highest()),    //
+           Vec(T(42), T::Highest(), T::Lowest())},  //
+          Vec(T(0), T(0), T::Lowest())),
+    };
+}
+INSTANTIATE_TEST_SUITE_P(  //
+    Clamp,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kClamp),
+                     testing::ValuesIn(Concat(ClampCases<AInt>(),  //
+                                              ClampCases<i32>(),
+                                              ClampCases<u32>(),
+                                              ClampCases<AFloat>(),
+                                              ClampCases<f32>(),
+                                              ClampCases<f16>()))));
+
+template <typename T>
+std::vector<Case> SelectCases() {
+    return {
+        C({Val(T{1}), Val(T{2}), Val(false)}, Val(T{1})),
+        C({Val(T{1}), Val(T{2}), Val(true)}, Val(T{2})),
+
+        C({Val(T{2}), Val(T{1}), Val(false)}, Val(T{2})),
+        C({Val(T{2}), Val(T{1}), Val(true)}, Val(T{1})),
+
+        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(false, false)}, Vec(T{1}, T{2})),
+        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(false, true)}, Vec(T{1}, T{4})),
+        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(true, false)}, Vec(T{3}, T{2})),
+        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(true, true)}, Vec(T{3}, T{4})),
+
+        C({Vec(T{1}, T{1}, T{2}, T{2}),     //
+           Vec(T{2}, T{2}, T{1}, T{1}),     //
+           Vec(false, true, false, true)},  //
+          Vec(T{1}, T{2}, T{2}, T{1})),     //
+    };
+}
+static std::vector<Case> SelectBoolCases() {
+    return {
+        C({Val(true), Val(false), Val(false)}, Val(true)),
+        C({Val(true), Val(false), Val(true)}, Val(false)),
+
+        C({Val(false), Val(true), Val(true)}, Val(true)),
+        C({Val(false), Val(true), Val(false)}, Val(false)),
+
+        C({Vec(true, true, false, false),   //
+           Vec(false, false, true, true),   //
+           Vec(false, true, true, false)},  //
+          Vec(true, false, true, false)),   //
+    };
+}
+INSTANTIATE_TEST_SUITE_P(  //
+    Select,
+    ResolverConstEvalBuiltinTest,
+    testing::Combine(testing::Values(sem::BuiltinType::kSelect),
+                     testing::ValuesIn(Concat(SelectCases<AInt>(),  //
+                                              SelectCases<i32>(),
+                                              SelectCases<u32>(),
+                                              SelectCases<AFloat>(),
+                                              SelectCases<f32>(),
+                                              SelectCases<f16>(),
+                                              SelectBoolCases()))));
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_construction_test.cc b/src/tint/resolver/const_eval_construction_test.cc
new file mode 100644
index 0000000..9df9807
--- /dev/null
+++ b/src/tint/resolver/const_eval_construction_test.cc
@@ -0,0 +1,2099 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+TEST_F(ResolverConstEvalTest, Scalar_i32) {
+    auto* expr = Expr(99_i);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::I32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 99);
+}
+
+TEST_F(ResolverConstEvalTest, Scalar_u32) {
+    auto* expr = Expr(99_u);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::U32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 99u);
+}
+
+TEST_F(ResolverConstEvalTest, Scalar_f32) {
+    auto* expr = Expr(9.9_f);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::F32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<AFloat>().value, 9.9f);
+}
+
+TEST_F(ResolverConstEvalTest, Scalar_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = Expr(9.9_h);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    EXPECT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::F16>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    // 9.9 is not exactly representable by f16, and should be quantized to 9.8984375
+    EXPECT_EQ(sem->ConstantValue()->As<AFloat>(), 9.8984375f);
+}
+
+TEST_F(ResolverConstEvalTest, Scalar_bool) {
+    auto* expr = Expr(true);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::Bool>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<bool>(), true);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_i32) {
+    auto* expr = vec3<i32>();
+    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_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<AInt>(), 0);
+
+    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>(), 0);
+
+    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<AInt>(), 0);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_u32) {
+    auto* expr = vec3<u32>();
+    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_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<AInt>(), 0u);
+
+    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>(), 0u);
+
+    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<AInt>(), 0u);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f32) {
+    auto* expr = vec3<f32>();
+    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_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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._a);
+
+    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<AFloat>(), 0._a);
+
+    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._a);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>();
+    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_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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._a);
+
+    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<AFloat>(), 0._a);
+
+    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._a);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_bool) {
+    auto* expr = vec3<bool>();
+    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::Bool>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<bool>(), false);
+
+    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<bool>(), false);
+
+    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<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Splat_i32) {
+    auto* expr = vec3<i32>(99_i);
+    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_TRUE(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>(), 99);
+
+    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>(), 99);
+
+    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>(), 99);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Splat_u32) {
+    auto* expr = vec3<u32>(99_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::U32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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>(), 99u);
+
+    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>(), 99u);
+
+    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>(), 99u);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Splat_f32) {
+    auto* expr = vec3<f32>(9.9_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::F32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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>(), 9.9f);
+
+    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>(), 9.9f);
+
+    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>(), 9.9f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Splat_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(9.9_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::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    // 9.9 is not exactly representable by f16, and should be quantized to 9.8984375
+
+    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>(), 9.8984375f);
+
+    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>(), 9.8984375f);
+
+    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>(), 9.8984375f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Splat_bool) {
+    auto* expr = vec3<bool>(true);
+    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::Bool>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<bool>(), true);
+
+    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<bool>(), true);
+
+    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<bool>(), true);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_i32) {
+    auto* expr = vec3<i32>(1_i, 2_i, 3_i);
+    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_FullConstruct_u32) {
+    auto* expr = vec3<u32>(1_u, 2_u, 3_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::U32>());
+    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_FullConstruct_f32) {
+    auto* expr = vec3<f32>(1_f, 2_f, 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::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>(), 1.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>(), 2.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>(), 3.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(1_h, 2_h, 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::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>(), 1.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>(), 2.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>(), 3.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_bool) {
+    auto* expr = vec3<bool>(true, false, true);
+    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::Bool>());
+    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<bool>(), true);
+
+    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<bool>(), false);
+
+    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<bool>(), true);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_i32) {
+    auto* expr = vec3<i32>(1_i, vec2<i32>(2_i, 3_i));
+    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_MixConstruct_u32) {
+    auto* expr = vec3<u32>(vec2<u32>(1_u, 2_u), 3_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::U32>());
+    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_MixConstruct_f32) {
+    auto* expr = vec3<f32>(1_f, vec2<f32>(2_f, 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::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>(), 1.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>(), 2.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>(), 3.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_10) {
+    auto* expr = vec3<f32>(10_f, vec2<f32>(10_f, 10_f));
+    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::F32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<f32>(), 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<f32>(), 10_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<f32>(), 10_f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_positive_0) {
+    auto* expr = vec3<f32>(0_f, vec2<f32>(0_f, 0_f));
+    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::F32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<f32>(), 0_f);
+
+    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<f32>(), 0_f);
+
+    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<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_negative_0) {
+    auto* expr = vec3<f32>(vec2<f32>(-0_f, -0_f), -0_f);
+    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::F32>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<f32>(), -0_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<f32>(), -0_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<f32>(), -0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_mixed_sign_0) {
+    auto* expr = vec3<f32>(0_f, vec2<f32>(-0_f, 0_f));
+    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::F32>());
+    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<f32>(), 0_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<f32>(), -0_f);
+
+    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<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(1_h, vec2<f16>(2_h, 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::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>(), 1.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>(), 2.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>(), 3.f);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_10) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(10_h, vec2<f16>(10_h, 10_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::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<f16>(), 10_h);
+
+    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<f16>(), 10_h);
+
+    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<f16>(), 10_h);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_positive_0) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(0_h, vec2<f16>(0_h, 0_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::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<f16>(), 0_h);
+
+    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<f16>(), 0_h);
+
+    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<f16>(), 0_h);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_negative_0) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(vec2<f16>(-0_h, -0_h), -0_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::F16>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<f16>(), -0_h);
+
+    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<f16>(), -0_h);
+
+    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<f16>(), -0_h);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_mixed_sign_0) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = vec3<f16>(0_h, vec2<f16>(-0_h, 0_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::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<f16>(), 0_h);
+
+    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<f16>(), -0_h);
+
+    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<f16>(), 0_h);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_bool) {
+    auto* expr = vec3<bool>(vec2<bool>(true, false), true);
+    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::Bool>());
+    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<bool>(), true);
+
+    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<bool>(), false);
+
+    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<bool>(), true);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_true) {
+    auto* expr = vec3<bool>(true, vec2<bool>(true, true));
+    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::Bool>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(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<bool>(), true);
+
+    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<bool>(), true);
+
+    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<bool>(), true);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_false) {
+    auto* expr = vec3<bool>(false, vec2<bool>(false, false));
+    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::Bool>());
+    EXPECT_EQ(vec->Width(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<bool>(), false);
+
+    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<bool>(), false);
+
+    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<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f32) {
+    auto* expr = mat2x3<f32>();
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* mat = sem->Type()->As<sem::Matrix>();
+    ASSERT_NE(mat, nullptr);
+    EXPECT_TRUE(mat->type()->Is<sem::F32>());
+    EXPECT_EQ(mat->columns(), 2u);
+    EXPECT_EQ(mat->rows(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0._f);
+}
+
+TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f16) {
+    Enable(ast::Extension::kF16);
+
+    auto* expr = mat2x3<f16>();
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    EXPECT_NE(sem, nullptr);
+    auto* mat = sem->Type()->As<sem::Matrix>();
+    ASSERT_NE(mat, nullptr);
+    EXPECT_TRUE(mat->type()->Is<sem::F16>());
+    EXPECT_EQ(mat->columns(), 2u);
+    EXPECT_EQ(mat->rows(), 3u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f16>(), 0._h);
+}
+
+TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Scalars_af) {
+    auto* expr = Construct(ty.mat(nullptr, 3, 2), 1.0_a, 2.0_a, 3.0_a, 4.0_a, 5.0_a, 6.0_a);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* mat = sem->Type()->As<sem::Matrix>();
+    ASSERT_NE(mat, nullptr);
+    EXPECT_TRUE(mat->type()->Is<sem::F32>());
+    EXPECT_EQ(mat->columns(), 3u);
+    EXPECT_EQ(mat->rows(), 2u);
+    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)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<AFloat>(), 1._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<AFloat>(), 2._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<AFloat>(), 3._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<AFloat>(), 4._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<AFloat>(), 5._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<AFloat>(), 6._a);
+}
+
+TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Columns_af) {
+    auto* expr = Construct(ty.mat(nullptr, 3, 2),           //
+                           vec(nullptr, 2u, 1.0_a, 2.0_a),  //
+                           vec(nullptr, 2u, 3.0_a, 4.0_a),  //
+                           vec(nullptr, 2u, 5.0_a, 6.0_a));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* mat = sem->Type()->As<sem::Matrix>();
+    ASSERT_NE(mat, nullptr);
+    EXPECT_TRUE(mat->type()->Is<sem::F32>());
+    EXPECT_EQ(mat->columns(), 3u);
+    EXPECT_EQ(mat->rows(), 2u);
+    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)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<AFloat>(), 1._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<AFloat>(), 2._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<AFloat>(), 3._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<AFloat>(), 4._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<AFloat>(), 5._a);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<AFloat>(), 6._a);
+}
+
+TEST_F(ResolverConstEvalTest, Array_i32_Zero) {
+    auto* expr = Construct(ty.array<i32, 4>());
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::I32>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<i32>(), 0_i);
+
+    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<i32>(), 0_i);
+
+    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<i32>(), 0_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<i32>(), 0_i);
+}
+
+TEST_F(ResolverConstEvalTest, Array_f32_Zero) {
+    auto* expr = Construct(ty.array<f32, 4>());
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::F32>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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<f32>(), 0_f);
+
+    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<f32>(), 0_f);
+
+    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<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_vec3_f32_Zero) {
+    auto* expr = Construct(ty.array(ty.vec3<f32>(), 2_u));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::Vector>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_Struct_f32_Zero) {
+    Structure("S", utils::Vector{
+                       Member("m1", ty.f32()),
+                       Member("m2", ty.f32()),
+                   });
+    auto* expr = Construct(ty.array(ty.type_name("S"), 2_u));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->AllZero());
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_i32_Elements) {
+    auto* expr = Construct(ty.array<i32, 4>(), 10_i, 20_i, 30_i, 40_i);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::I32>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
+    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<i32>(), 10_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<i32>(), 20_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<i32>(), 30_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<i32>(), 40_i);
+}
+
+TEST_F(ResolverConstEvalTest, Array_f32_Elements) {
+    auto* expr = Construct(ty.array<f32, 4>(), 10_f, 20_f, 30_f, 40_f);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::F32>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
+    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<f32>(), 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<f32>(), 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<f32>(), 30_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f32>(), 40_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_vec3_f32_Elements) {
+    auto* expr = Construct(ty.array(ty.vec3<f32>(), 2_u),  //
+                           vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::Vector>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 1_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 2_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 3_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 4_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 5_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 6_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_Struct_f32_Elements) {
+    Structure("S", utils::Vector{
+                       Member("m1", ty.f32()),
+                       Member("m2", ty.f32()),
+                   });
+    auto* expr = Construct(ty.array(ty.type_name("S"), 2_u),        //
+                           Construct(ty.type_name("S"), 1_f, 2_f),  //
+                           Construct(ty.type_name("S"), 3_f, 4_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* arr = sem->Type()->As<sem::Array>();
+    ASSERT_NE(arr, nullptr);
+    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
+    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
+    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)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 1_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 2_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 3_f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 4_f);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_I32s_ZeroInit) {
+    Structure(
+        "S", utils::Vector{Member("m1", ty.i32()), Member("m2", ty.i32()), Member("m3", ty.i32())});
+    auto* expr = Construct(ty.type_name("S"));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 3u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 0_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<i32>(), 0_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<i32>(), 0_i);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_MixedScalars_ZeroInit) {
+    Enable(ast::Extension::kF16);
+
+    Structure("S", utils::Vector{
+                       Member("m1", ty.i32()),
+                       Member("m2", ty.u32()),
+                       Member("m3", ty.f32()),
+                       Member("m4", ty.f16()),
+                       Member("m5", ty.bool_()),
+                   });
+    auto* expr = Construct(ty.type_name("S"));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 5u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 0_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::U32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<u32>(), 0_u);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::F16>());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Bool>());
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->As<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_VectorF32s_ZeroInit) {
+    Structure("S", utils::Vector{
+                       Member("m1", ty.vec3<f32>()),
+                       Member("m2", ty.vec3<f32>()),
+                       Member("m3", ty.vec3<f32>()),
+                   });
+    auto* expr = Construct(ty.type_name("S"));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 3u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 0._f);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_MixedVectors_ZeroInit) {
+    Enable(ast::Extension::kF16);
+
+    Structure("S", utils::Vector{
+                       Member("m1", ty.vec2<i32>()),
+                       Member("m2", ty.vec3<u32>()),
+                       Member("m3", ty.vec4<f32>()),
+                       Member("m4", ty.vec3<f16>()),
+                       Member("m5", ty.vec2<bool>()),
+                   });
+    auto* expr = Construct(ty.type_name("S"));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 5u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 0_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<i32>(), 0_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::U32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<u32>(), 0_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<u32>(), 0_u);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 0._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(3)->As<f32>(), 0._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->As<sem::Vector>()->type()->Is<sem::F16>());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(0)->As<f16>(), 0._h);
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(1)->As<f16>(), 0._h);
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(2)->As<f16>(), 0._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->As<sem::Vector>()->type()->Is<sem::Bool>());
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(0)->As<bool>(), false);
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(1)->As<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_Struct_ZeroInit) {
+    Structure("Inner", utils::Vector{
+                           Member("m1", ty.i32()),
+                           Member("m2", ty.u32()),
+                           Member("m3", ty.f32()),
+                       });
+
+    Structure("Outer", utils::Vector{
+                           Member("m1", ty.type_name("Inner")),
+                           Member("m2", ty.type_name("Inner")),
+                       });
+    auto* expr = Construct(ty.type_name("Outer"));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 2u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 0_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 0_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0_f);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 0_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0_f);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_MixedScalars_Construct) {
+    Enable(ast::Extension::kF16);
+
+    Structure("S", utils::Vector{
+                       Member("m1", ty.i32()),
+                       Member("m2", ty.u32()),
+                       Member("m3", ty.f32()),
+                       Member("m4", ty.f16()),
+                       Member("m5", ty.bool_()),
+                   });
+    auto* expr = Construct(ty.type_name("S"), 1_i, 2_u, 3_f, 4_h, false);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 5u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 1_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::U32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<u32>(), 2_u);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<f32>(), 3._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::F16>());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f16>(), 4._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Bool>());
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->As<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_MixedVectors_Construct) {
+    Enable(ast::Extension::kF16);
+
+    Structure("S", utils::Vector{
+                       Member("m1", ty.vec2<i32>()),
+                       Member("m2", ty.vec3<u32>()),
+                       Member("m3", ty.vec4<f32>()),
+                       Member("m4", ty.vec3<f16>()),
+                       Member("m5", ty.vec2<bool>()),
+                   });
+    auto* expr = Construct(ty.type_name("S"), vec2<i32>(1_i), vec3<u32>(2_u), vec4<f32>(3_f),
+                           vec3<f16>(4_h), vec2<bool>(false));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 5u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<i32>(), 1_i);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::U32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<u32>(), 2_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 2_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<u32>(), 2_u);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 3._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 3._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 3._f);
+    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(3)->As<f32>(), 3._f);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->As<sem::Vector>()->type()->Is<sem::F16>());
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(0)->As<f16>(), 4._h);
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(1)->As<f16>(), 4._h);
+    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(2)->As<f16>(), 4._h);
+
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->As<sem::Vector>()->type()->Is<sem::Bool>());
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(0)->As<bool>(), false);
+    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(1)->As<bool>(), false);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_Struct_Construct) {
+    Structure("Inner", utils::Vector{
+                           Member("m1", ty.i32()),
+                           Member("m2", ty.u32()),
+                           Member("m3", ty.f32()),
+                       });
+
+    Structure("Outer", utils::Vector{
+                           Member("m1", ty.type_name("Inner")),
+                           Member("m2", ty.type_name("Inner")),
+                       });
+    auto* expr = Construct(ty.type_name("Outer"),  //
+                           Construct(ty.type_name("Inner"), 1_i, 2_u, 3_f),
+                           Construct(ty.type_name("Inner"), 4_i, 0_u, 6_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 2u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 2_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 3_f);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 4_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 6_f);
+}
+
+TEST_F(ResolverConstEvalTest, Struct_Array_Construct) {
+    Structure("S", utils::Vector{
+                       Member("m1", ty.array<i32, 2>()),
+                       Member("m2", ty.array<f32, 3>()),
+                   });
+    auto* expr = Construct(ty.type_name("S"),  //
+                           Construct(ty.array<i32, 2>(), 1_i, 2_i),
+                           Construct(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    auto* str = sem->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 2u);
+    ASSERT_NE(sem->ConstantValue(), nullptr);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Array>());
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
+    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 2_i);
+
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
+    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Array>());
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 1_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 2_f);
+    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 3_f);
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_conversion_test.cc b/src/tint/resolver/const_eval_conversion_test.cc
new file mode 100644
index 0000000..35657ee
--- /dev/null
+++ b/src/tint/resolver/const_eval_conversion_test.cc
@@ -0,0 +1,529 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+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
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_indexing_test.cc b/src/tint/resolver/const_eval_indexing_test.cc
new file mode 100644
index 0000000..09447d5
--- /dev/null
+++ b/src/tint/resolver/const_eval_indexing_test.cc
@@ -0,0 +1,314 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+TEST_F(ResolverConstEvalTest, Vec3_Index) {
+    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), 2_i);
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 3_i);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_High) {
+    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, 3_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index 3 out of bounds [0..2]");
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_Low) {
+    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, -3_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index -3 out of bounds [0..2]");
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Scalar) {
+    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "y");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 2_i);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Vector) {
+    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "zx");
+    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_EQ(vec->Width(), 2u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+
+    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<f32>(), 3._a);
+
+    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<f32>(), 1._a);
+}
+
+TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Chain) {
+    auto* expr =  // (1, 2, 3) -> (2, 3, 1) -> (3, 2) -> 2
+        MemberAccessor(MemberAccessor(MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "gbr"), "yx"), "y");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(expr);
+    ASSERT_NE(sem, nullptr);
+    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
+    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
+    EXPECT_FALSE(sem->ConstantValue()->AllZero());
+    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 2_i);
+}
+
+TEST_F(ResolverConstEvalTest, Mat3x2_Index) {
+    auto* expr = IndexAccessor(
+        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)), 2_i);
+    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_EQ(vec->Width(), 2u);
+    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
+
+    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<f32>(), 5._a);
+
+    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<f32>(), 6._a);
+}
+
+TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_High) {
+    auto* expr = IndexAccessor(
+        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
+        Expr(Source{{12, 34}}, 3_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index 3 out of bounds [0..2]");
+}
+
+TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_Low) {
+    auto* expr = IndexAccessor(
+        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
+        Expr(Source{{12, 34}}, -3_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index -3 out of bounds [0..2]");
+}
+
+TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index) {
+    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
+                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+                               1_i);
+    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_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<f32>(), 4_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<f32>(), 5_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<f32>(), 6_f);
+}
+
+TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_High) {
+    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
+                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+                               Expr(Source{{12, 34}}, 2_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index 2 out of bounds [0..1]");
+}
+
+TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_Low) {
+    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
+                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
+                               Expr(Source{{12, 34}}, -2_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index -2 out of bounds [0..1]");
+}
+
+TEST_F(ResolverConstEvalTest, RuntimeArray_vec3_f32_Index_OOB_Low) {
+    auto* sb = GlobalVar("sb", ty.array(ty.vec3<f32>()), Group(0_a), Binding(0_a),
+                         ast::AddressSpace::kStorage);
+    auto* expr = IndexAccessor(sb, Expr(Source{{12, 34}}, -2_i));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "12:34 error: index -2 out of bounds");
+}
+
+TEST_F(ResolverConstEvalTest, ChainedIndex) {
+    auto* arr_expr = Construct(ty.array(ty.mat2x3<f32>(), 2_u),        // array<mat2x3<f32>, 2u>
+                               mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f),   //
+                                           vec3<f32>(4_f, 5_f, 6_f)),  //
+                               mat2x3<f32>(vec3<f32>(7_f, 0_f, 9_f),   //
+                                           vec3<f32>(10_f, 11_f, 12_f)));
+
+    auto* mat_expr = IndexAccessor(arr_expr, 1_i);  // arr[1]
+    auto* vec_expr = IndexAccessor(mat_expr, 0_i);  // arr[1][0]
+    auto* f32_expr = IndexAccessor(vec_expr, 2_i);  // arr[1][0][2]
+    WrapInFunction(f32_expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    {
+        auto* mat = Sem().Get(mat_expr);
+        EXPECT_NE(mat, nullptr);
+        auto* ty = mat->Type()->As<sem::Matrix>();
+        ASSERT_NE(mat->Type(), nullptr);
+        EXPECT_TRUE(ty->ColumnType()->Is<sem::Vector>());
+        EXPECT_EQ(ty->columns(), 2u);
+        EXPECT_EQ(ty->rows(), 3u);
+        EXPECT_EQ(mat->ConstantValue()->Type(), mat->Type());
+        EXPECT_FALSE(mat->ConstantValue()->AllEqual());
+        EXPECT_TRUE(mat->ConstantValue()->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->AllZero());
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(0)->AllEqual());
+        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(0)->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(0)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(0)->As<f32>(), 7_f);
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AllEqual());
+        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AnyZero());
+        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(2)->AllEqual());
+        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(2)->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(2)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(2)->As<f32>(), 9_f);
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(0)->AllEqual());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(0)->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(0)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(0)->As<f32>(), 10_f);
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(1)->AllEqual());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(1)->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(1)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(1)->As<f32>(), 11_f);
+
+        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(2)->AllEqual());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(2)->AnyZero());
+        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(2)->AllZero());
+        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(2)->As<f32>(), 12_f);
+    }
+    {
+        auto* vec = Sem().Get(vec_expr);
+        EXPECT_NE(vec, nullptr);
+        auto* ty = vec->Type()->As<sem::Vector>();
+        ASSERT_NE(vec->Type(), nullptr);
+        EXPECT_TRUE(ty->type()->Is<sem::F32>());
+        EXPECT_EQ(ty->Width(), 3u);
+        EXPECT_EQ(vec->ConstantValue()->Type(), vec->Type());
+        EXPECT_FALSE(vec->ConstantValue()->AllEqual());
+        EXPECT_TRUE(vec->ConstantValue()->AnyZero());
+        EXPECT_FALSE(vec->ConstantValue()->AllZero());
+
+        EXPECT_TRUE(vec->ConstantValue()->Index(0)->AllEqual());
+        EXPECT_FALSE(vec->ConstantValue()->Index(0)->AnyZero());
+        EXPECT_FALSE(vec->ConstantValue()->Index(0)->AllZero());
+        EXPECT_EQ(vec->ConstantValue()->Index(0)->As<f32>(), 7_f);
+
+        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AllEqual());
+        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AnyZero());
+        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AllZero());
+        EXPECT_EQ(vec->ConstantValue()->Index(1)->As<f32>(), 0_f);
+
+        EXPECT_TRUE(vec->ConstantValue()->Index(2)->AllEqual());
+        EXPECT_FALSE(vec->ConstantValue()->Index(2)->AnyZero());
+        EXPECT_FALSE(vec->ConstantValue()->Index(2)->AllZero());
+        EXPECT_EQ(vec->ConstantValue()->Index(2)->As<f32>(), 9_f);
+    }
+    {
+        auto* f = Sem().Get(f32_expr);
+        EXPECT_NE(f, nullptr);
+        EXPECT_TRUE(f->Type()->Is<sem::F32>());
+        EXPECT_EQ(f->ConstantValue()->Type(), f->Type());
+        EXPECT_TRUE(f->ConstantValue()->AllEqual());
+        EXPECT_FALSE(f->ConstantValue()->AnyZero());
+        EXPECT_FALSE(f->ConstantValue()->AllZero());
+        EXPECT_EQ(f->ConstantValue()->As<f32>(), 9_f);
+    }
+}
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_member_access_test.cc b/src/tint/resolver/const_eval_member_access_test.cc
new file mode 100644
index 0000000..25d1f26
--- /dev/null
+++ b/src/tint/resolver/const_eval_member_access_test.cc
@@ -0,0 +1,98 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+TEST_F(ResolverConstEvalTest, MemberAccess) {
+    Structure("Inner", utils::Vector{
+                           Member("i1", ty.i32()),
+                           Member("i2", ty.u32()),
+                           Member("i3", ty.f32()),
+                       });
+
+    Structure("Outer", utils::Vector{
+                           Member("o1", ty.type_name("Inner")),
+                           Member("o2", ty.type_name("Inner")),
+                       });
+    auto* outer_expr = Construct(ty.type_name("Outer"),  //
+                                 Construct(ty.type_name("Inner"), 1_i, 2_u, 3_f),
+                                 Construct(ty.type_name("Inner")));
+    auto* o1_expr = MemberAccessor(outer_expr, "o1");
+    auto* i2_expr = MemberAccessor(o1_expr, "i2");
+    WrapInFunction(i2_expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* outer = Sem().Get(outer_expr);
+    ASSERT_NE(outer, nullptr);
+    auto* str = outer->Type()->As<sem::Struct>();
+    ASSERT_NE(str, nullptr);
+    EXPECT_EQ(str->Members().size(), 2u);
+    ASSERT_NE(outer->ConstantValue(), nullptr);
+    EXPECT_TYPE(outer->ConstantValue()->Type(), outer->Type());
+    EXPECT_FALSE(outer->ConstantValue()->AllEqual());
+    EXPECT_TRUE(outer->ConstantValue()->AnyZero());
+    EXPECT_FALSE(outer->ConstantValue()->AllZero());
+
+    auto* o1 = Sem().Get(o1_expr);
+    ASSERT_NE(o1->ConstantValue(), nullptr);
+    EXPECT_FALSE(o1->ConstantValue()->AllEqual());
+    EXPECT_FALSE(o1->ConstantValue()->AnyZero());
+    EXPECT_FALSE(o1->ConstantValue()->AllZero());
+    EXPECT_TRUE(o1->ConstantValue()->Type()->Is<sem::Struct>());
+    EXPECT_EQ(o1->ConstantValue()->Index(0)->As<i32>(), 1_i);
+    EXPECT_EQ(o1->ConstantValue()->Index(1)->As<u32>(), 2_u);
+    EXPECT_EQ(o1->ConstantValue()->Index(2)->As<f32>(), 3_f);
+
+    auto* i2 = Sem().Get(i2_expr);
+    ASSERT_NE(i2->ConstantValue(), nullptr);
+    EXPECT_TRUE(i2->ConstantValue()->AllEqual());
+    EXPECT_FALSE(i2->ConstantValue()->AnyZero());
+    EXPECT_FALSE(i2->ConstantValue()->AllZero());
+    EXPECT_TRUE(i2->ConstantValue()->Type()->Is<sem::U32>());
+    EXPECT_EQ(i2->ConstantValue()->As<u32>(), 2_u);
+}
+
+TEST_F(ResolverConstEvalTest, Matrix_AFloat_Construct_From_AInt_Vectors) {
+    auto* c = Const("a", Construct(ty.mat(nullptr, 2, 2),  //
+                                   Construct(ty.vec(nullptr, 2), Expr(1_a), Expr(2_a)),
+                                   Construct(ty.vec(nullptr, 2), Expr(3_a), Expr(4_a))));
+    WrapInFunction(c);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    auto* sem = Sem().Get(c);
+    ASSERT_NE(sem, nullptr);
+    EXPECT_TRUE(sem->Type()->Is<sem::Matrix>());
+    auto* cv = sem->ConstantValue();
+    EXPECT_TYPE(cv->Type(), sem->Type());
+    EXPECT_TRUE(cv->Index(0)->Type()->Is<sem::Vector>());
+    EXPECT_TRUE(cv->Index(0)->Index(0)->Type()->Is<sem::AbstractFloat>());
+    EXPECT_FALSE(cv->AllEqual());
+    EXPECT_FALSE(cv->AnyZero());
+    EXPECT_FALSE(cv->AllZero());
+    auto* c0 = cv->Index(0);
+    auto* c1 = cv->Index(1);
+    EXPECT_EQ(std::get<AFloat>(c0->Index(0)->Value()), 1.0);
+    EXPECT_EQ(std::get<AFloat>(c0->Index(1)->Value()), 2.0);
+    EXPECT_EQ(std::get<AFloat>(c1->Index(0)->Value()), 3.0);
+    EXPECT_EQ(std::get<AFloat>(c1->Index(1)->Value()), 4.0);
+}
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_test.cc b/src/tint/resolver/const_eval_test.cc
deleted file mode 100644
index cf3cda0..0000000
--- a/src/tint/resolver/const_eval_test.cc
+++ /dev/null
@@ -1,4602 +0,0 @@
-// 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 <cmath>
-#include <type_traits>
-
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
-#include "src/tint/resolver/resolver_test_helper.h"
-#include "src/tint/sem/builtin_type.h"
-#include "src/tint/sem/expression.h"
-#include "src/tint/sem/index_accessor_expression.h"
-#include "src/tint/sem/member_accessor_expression.h"
-#include "src/tint/sem/test_helper.h"
-#include "src/tint/utils/transform.h"
-
-using ::testing::HasSubstr;
-
-using namespace tint::number_suffixes;  // NOLINT
-
-namespace tint::resolver {
-namespace {
-
-template <typename T>
-const auto kPi = T(UnwrapNumber<T>(3.14159265358979323846));
-
-template <typename T>
-const auto kPiOver2 = T(UnwrapNumber<T>(1.57079632679489661923));
-
-template <typename T>
-const auto kPiOver4 = T(UnwrapNumber<T>(0.785398163397448309616));
-
-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>) {
-        if constexpr (std::is_signed_v<T>) {
-            // For signed integrals, avoid C++ UB by not negating the smallest negative number. In
-            // WGSL, this operation is well defined to return the same value, see:
-            // https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr.
-            if (v == std::numeric_limits<T>::min()) {
-                return v;
-            }
-            return -v;
-
-        } else {
-            // Allow negating unsigned values
-            using ST = std::make_signed_t<T>;
-            auto as_signed = Number<ST>{static_cast<ST>(v)};
-            return Number<T>{static_cast<T>(Negate(as_signed))};
-        }
-    } else {
-        // float case
-        return -v;
-    }
-}
-
-template <typename T>
-auto Abs(const Number<T>& v) {
-    if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>) {
-        return v;
-    } else {
-        return Number<T>(std::abs(v));
-    }
-}
-
-TINT_BEGIN_DISABLE_WARNING(CONSTANT_OVERFLOW);
-template <typename T>
-constexpr Number<T> Mul(Number<T> v1, Number<T> v2) {
-    if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
-        // For signed integrals, avoid C++ UB by multiplying as unsigned
-        using UT = std::make_unsigned_t<T>;
-        return static_cast<Number<T>>(static_cast<UT>(v1) * static_cast<UT>(v2));
-    } else {
-        return static_cast<Number<T>>(v1 * v2);
-    }
-}
-TINT_END_DISABLE_WARNING(CONSTANT_OVERFLOW);
-
-// Concats any number of std::vectors
-template <typename Vec, typename... Vecs>
-[[nodiscard]] auto Concat(Vec&& v1, Vecs&&... vs) {
-    auto total_size = v1.size() + (vs.size() + ...);
-    v1.reserve(total_size);
-    (std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
-    return std::move(v1);
-}
-
-// Concats vectors `vs` into `v1`
-template <typename Vec, typename... Vecs>
-void ConcatInto(Vec& v1, Vecs&&... vs) {
-    auto total_size = v1.size() + (vs.size() + ...);
-    v1.reserve(total_size);
-    (std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
-}
-
-// Concats vectors `vs` into `v1` iff `condition` is true
-template <bool condition, typename Vec, typename... Vecs>
-void ConcatIntoIf([[maybe_unused]] Vec& v1, [[maybe_unused]] Vecs&&... vs) {
-    if constexpr (condition) {
-        ConcatInto(v1, std::forward<Vecs>(vs)...);
-    }
-}
-
-using ResolverConstEvalTest = ResolverTest;
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Construction
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-TEST_F(ResolverConstEvalTest, Scalar_i32) {
-    auto* expr = Expr(99_i);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::I32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 99);
-}
-
-TEST_F(ResolverConstEvalTest, Scalar_u32) {
-    auto* expr = Expr(99_u);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::U32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 99u);
-}
-
-TEST_F(ResolverConstEvalTest, Scalar_f32) {
-    auto* expr = Expr(9.9_f);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::F32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<AFloat>().value, 9.9f);
-}
-
-TEST_F(ResolverConstEvalTest, Scalar_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = Expr(9.9_h);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    EXPECT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::F16>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    // 9.9 is not exactly representable by f16, and should be quantized to 9.8984375
-    EXPECT_EQ(sem->ConstantValue()->As<AFloat>(), 9.8984375f);
-}
-
-TEST_F(ResolverConstEvalTest, Scalar_bool) {
-    auto* expr = Expr(true);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::Bool>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<bool>(), true);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_i32) {
-    auto* expr = vec3<i32>();
-    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_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<AInt>(), 0);
-
-    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>(), 0);
-
-    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<AInt>(), 0);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_u32) {
-    auto* expr = vec3<u32>();
-    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_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<AInt>(), 0u);
-
-    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>(), 0u);
-
-    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<AInt>(), 0u);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f32) {
-    auto* expr = vec3<f32>();
-    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_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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._a);
-
-    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<AFloat>(), 0._a);
-
-    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._a);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>();
-    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_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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._a);
-
-    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<AFloat>(), 0._a);
-
-    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._a);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_ZeroInit_bool) {
-    auto* expr = vec3<bool>();
-    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::Bool>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<bool>(), false);
-
-    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<bool>(), false);
-
-    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<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Splat_i32) {
-    auto* expr = vec3<i32>(99_i);
-    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_TRUE(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>(), 99);
-
-    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>(), 99);
-
-    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>(), 99);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Splat_u32) {
-    auto* expr = vec3<u32>(99_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::U32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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>(), 99u);
-
-    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>(), 99u);
-
-    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>(), 99u);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Splat_f32) {
-    auto* expr = vec3<f32>(9.9_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::F32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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>(), 9.9f);
-
-    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>(), 9.9f);
-
-    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>(), 9.9f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Splat_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(9.9_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::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    // 9.9 is not exactly representable by f16, and should be quantized to 9.8984375
-
-    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>(), 9.8984375f);
-
-    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>(), 9.8984375f);
-
-    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>(), 9.8984375f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Splat_bool) {
-    auto* expr = vec3<bool>(true);
-    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::Bool>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<bool>(), true);
-
-    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<bool>(), true);
-
-    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<bool>(), true);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_i32) {
-    auto* expr = vec3<i32>(1_i, 2_i, 3_i);
-    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_FullConstruct_u32) {
-    auto* expr = vec3<u32>(1_u, 2_u, 3_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::U32>());
-    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_FullConstruct_f32) {
-    auto* expr = vec3<f32>(1_f, 2_f, 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::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>(), 1.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>(), 2.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>(), 3.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(1_h, 2_h, 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::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>(), 1.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>(), 2.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>(), 3.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_FullConstruct_bool) {
-    auto* expr = vec3<bool>(true, false, true);
-    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::Bool>());
-    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<bool>(), true);
-
-    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<bool>(), false);
-
-    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<bool>(), true);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_i32) {
-    auto* expr = vec3<i32>(1_i, vec2<i32>(2_i, 3_i));
-    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_MixConstruct_u32) {
-    auto* expr = vec3<u32>(vec2<u32>(1_u, 2_u), 3_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::U32>());
-    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_MixConstruct_f32) {
-    auto* expr = vec3<f32>(1_f, vec2<f32>(2_f, 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::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>(), 1.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>(), 2.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>(), 3.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_10) {
-    auto* expr = vec3<f32>(10_f, vec2<f32>(10_f, 10_f));
-    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::F32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<f32>(), 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<f32>(), 10_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<f32>(), 10_f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_positive_0) {
-    auto* expr = vec3<f32>(0_f, vec2<f32>(0_f, 0_f));
-    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::F32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<f32>(), 0_f);
-
-    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<f32>(), 0_f);
-
-    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<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_all_negative_0) {
-    auto* expr = vec3<f32>(vec2<f32>(-0_f, -0_f), -0_f);
-    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::F32>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<f32>(), -0_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<f32>(), -0_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<f32>(), -0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f32_mixed_sign_0) {
-    auto* expr = vec3<f32>(0_f, vec2<f32>(-0_f, 0_f));
-    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::F32>());
-    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<f32>(), 0_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<f32>(), -0_f);
-
-    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<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(1_h, vec2<f16>(2_h, 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::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>(), 1.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>(), 2.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>(), 3.f);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_10) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(10_h, vec2<f16>(10_h, 10_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::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<f16>(), 10_h);
-
-    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<f16>(), 10_h);
-
-    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<f16>(), 10_h);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_positive_0) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(0_h, vec2<f16>(0_h, 0_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::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<f16>(), 0_h);
-
-    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<f16>(), 0_h);
-
-    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<f16>(), 0_h);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_all_negative_0) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(vec2<f16>(-0_h, -0_h), -0_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::F16>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<f16>(), -0_h);
-
-    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<f16>(), -0_h);
-
-    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<f16>(), -0_h);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_f16_mixed_sign_0) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = vec3<f16>(0_h, vec2<f16>(-0_h, 0_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::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<f16>(), 0_h);
-
-    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<f16>(), -0_h);
-
-    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<f16>(), 0_h);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_bool) {
-    auto* expr = vec3<bool>(vec2<bool>(true, false), true);
-    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::Bool>());
-    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<bool>(), true);
-
-    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<bool>(), false);
-
-    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<bool>(), true);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_true) {
-    auto* expr = vec3<bool>(true, vec2<bool>(true, true));
-    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::Bool>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(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<bool>(), true);
-
-    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<bool>(), true);
-
-    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<bool>(), true);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_MixConstruct_all_false) {
-    auto* expr = vec3<bool>(false, vec2<bool>(false, false));
-    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::Bool>());
-    EXPECT_EQ(vec->Width(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<bool>(), false);
-
-    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<bool>(), false);
-
-    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<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f32) {
-    auto* expr = mat2x3<f32>();
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* mat = sem->Type()->As<sem::Matrix>();
-    ASSERT_NE(mat, nullptr);
-    EXPECT_TRUE(mat->type()->Is<sem::F32>());
-    EXPECT_EQ(mat->columns(), 2u);
-    EXPECT_EQ(mat->rows(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0._f);
-}
-
-TEST_F(ResolverConstEvalTest, Mat2x3_ZeroInit_f16) {
-    Enable(ast::Extension::kF16);
-
-    auto* expr = mat2x3<f16>();
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    EXPECT_NE(sem, nullptr);
-    auto* mat = sem->Type()->As<sem::Matrix>();
-    ASSERT_NE(mat, nullptr);
-    EXPECT_TRUE(mat->type()->Is<sem::F16>());
-    EXPECT_EQ(mat->columns(), 2u);
-    EXPECT_EQ(mat->rows(), 3u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f16>(), 0._h);
-}
-
-TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Scalars_af) {
-    auto* expr = Construct(ty.mat(nullptr, 3, 2), 1.0_a, 2.0_a, 3.0_a, 4.0_a, 5.0_a, 6.0_a);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* mat = sem->Type()->As<sem::Matrix>();
-    ASSERT_NE(mat, nullptr);
-    EXPECT_TRUE(mat->type()->Is<sem::F32>());
-    EXPECT_EQ(mat->columns(), 3u);
-    EXPECT_EQ(mat->rows(), 2u);
-    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)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<AFloat>(), 1._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<AFloat>(), 2._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<AFloat>(), 3._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<AFloat>(), 4._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<AFloat>(), 5._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<AFloat>(), 6._a);
-}
-
-TEST_F(ResolverConstEvalTest, Mat3x2_Construct_Columns_af) {
-    auto* expr = Construct(ty.mat(nullptr, 3, 2),           //
-                           vec(nullptr, 2u, 1.0_a, 2.0_a),  //
-                           vec(nullptr, 2u, 3.0_a, 4.0_a),  //
-                           vec(nullptr, 2u, 5.0_a, 6.0_a));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* mat = sem->Type()->As<sem::Matrix>();
-    ASSERT_NE(mat, nullptr);
-    EXPECT_TRUE(mat->type()->Is<sem::F32>());
-    EXPECT_EQ(mat->columns(), 3u);
-    EXPECT_EQ(mat->rows(), 2u);
-    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)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<AFloat>(), 1._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<AFloat>(), 2._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<AFloat>(), 3._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<AFloat>(), 4._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<AFloat>(), 5._a);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<AFloat>(), 6._a);
-}
-
-TEST_F(ResolverConstEvalTest, Array_i32_Zero) {
-    auto* expr = Construct(ty.array<i32, 4>());
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::I32>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<i32>(), 0_i);
-
-    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<i32>(), 0_i);
-
-    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<i32>(), 0_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<i32>(), 0_i);
-}
-
-TEST_F(ResolverConstEvalTest, Array_f32_Zero) {
-    auto* expr = Construct(ty.array<f32, 4>());
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::F32>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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<f32>(), 0_f);
-
-    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<f32>(), 0_f);
-
-    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<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_vec3_f32_Zero) {
-    auto* expr = Construct(ty.array(ty.vec3<f32>(), 2_u));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Vector>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(2)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_Struct_f32_Zero) {
-    Structure("S", utils::Vector{
-                       Member("m1", ty.f32()),
-                       Member("m2", ty.f32()),
-                   });
-    auto* expr = Construct(ty.array(ty.type_name("S"), 2_u));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->AllZero());
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_i32_Elements) {
-    auto* expr = Construct(ty.array<i32, 4>(), 10_i, 20_i, 30_i, 40_i);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::I32>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
-    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<i32>(), 10_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<i32>(), 20_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<i32>(), 30_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<i32>(), 40_i);
-}
-
-TEST_F(ResolverConstEvalTest, Array_f32_Elements) {
-    auto* expr = Construct(ty.array<f32, 4>(), 10_f, 20_f, 30_f, 40_f);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::F32>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{4u});
-    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<f32>(), 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<f32>(), 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<f32>(), 30_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f32>(), 40_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_vec3_f32_Elements) {
-    auto* expr = Construct(ty.array(ty.vec3<f32>(), 2_u),  //
-                           vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Vector>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 1_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 2_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 3_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 4_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 5_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 6_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_Struct_f32_Elements) {
-    Structure("S", utils::Vector{
-                       Member("m1", ty.f32()),
-                       Member("m2", ty.f32()),
-                   });
-    auto* expr = Construct(ty.array(ty.type_name("S"), 2_u),        //
-                           Construct(ty.type_name("S"), 1_f, 2_f),  //
-                           Construct(ty.type_name("S"), 3_f, 4_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* arr = sem->Type()->As<sem::Array>();
-    ASSERT_NE(arr, nullptr);
-    EXPECT_TRUE(arr->ElemType()->Is<sem::Struct>());
-    EXPECT_EQ(arr->Count(), sem::ConstantArrayCount{2u});
-    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)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 1_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 2_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(0)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 3_f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->Index(1)->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 4_f);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_I32s_ZeroInit) {
-    Structure(
-        "S", utils::Vector{Member("m1", ty.i32()), Member("m2", ty.i32()), Member("m3", ty.i32())});
-    auto* expr = Construct(ty.type_name("S"));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 3u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 0_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<i32>(), 0_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<i32>(), 0_i);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_MixedScalars_ZeroInit) {
-    Enable(ast::Extension::kF16);
-
-    Structure("S", utils::Vector{
-                       Member("m1", ty.i32()),
-                       Member("m2", ty.u32()),
-                       Member("m3", ty.f32()),
-                       Member("m4", ty.f16()),
-                       Member("m5", ty.bool_()),
-                   });
-    auto* expr = Construct(ty.type_name("S"));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 5u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 0_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::U32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<u32>(), 0_u);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::F16>());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Bool>());
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->As<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_VectorF32s_ZeroInit) {
-    Structure("S", utils::Vector{
-                       Member("m1", ty.vec3<f32>()),
-                       Member("m2", ty.vec3<f32>()),
-                       Member("m3", ty.vec3<f32>()),
-                   });
-    auto* expr = Construct(ty.type_name("S"));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 3u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 0._f);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_MixedVectors_ZeroInit) {
-    Enable(ast::Extension::kF16);
-
-    Structure("S", utils::Vector{
-                       Member("m1", ty.vec2<i32>()),
-                       Member("m2", ty.vec3<u32>()),
-                       Member("m3", ty.vec4<f32>()),
-                       Member("m4", ty.vec3<f16>()),
-                       Member("m5", ty.vec2<bool>()),
-                   });
-    auto* expr = Construct(ty.type_name("S"));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 5u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 0_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<i32>(), 0_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::U32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<u32>(), 0_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<u32>(), 0_u);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 0._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(3)->As<f32>(), 0._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->As<sem::Vector>()->type()->Is<sem::F16>());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(0)->As<f16>(), 0._h);
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(1)->As<f16>(), 0._h);
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(2)->As<f16>(), 0._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->As<sem::Vector>()->type()->Is<sem::Bool>());
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(0)->As<bool>(), false);
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(1)->As<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_Struct_ZeroInit) {
-    Structure("Inner", utils::Vector{
-                           Member("m1", ty.i32()),
-                           Member("m2", ty.u32()),
-                           Member("m3", ty.f32()),
-                       });
-
-    Structure("Outer", utils::Vector{
-                           Member("m1", ty.type_name("Inner")),
-                           Member("m2", ty.type_name("Inner")),
-                       });
-    auto* expr = Construct(ty.type_name("Outer"));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 2u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->AllZero());
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 0_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 0_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 0_f);
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 0_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 0_f);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_MixedScalars_Construct) {
-    Enable(ast::Extension::kF16);
-
-    Structure("S", utils::Vector{
-                       Member("m1", ty.i32()),
-                       Member("m2", ty.u32()),
-                       Member("m3", ty.f32()),
-                       Member("m4", ty.f16()),
-                       Member("m5", ty.bool_()),
-                   });
-    auto* expr = Construct(ty.type_name("S"), 1_i, 2_u, 3_f, 4_h, false);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 5u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->As<i32>(), 1_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::U32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->As<u32>(), 2_u);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->As<f32>(), 3._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::F16>());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->As<f16>(), 4._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Bool>());
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->As<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_MixedVectors_Construct) {
-    Enable(ast::Extension::kF16);
-
-    Structure("S", utils::Vector{
-                       Member("m1", ty.vec2<i32>()),
-                       Member("m2", ty.vec3<u32>()),
-                       Member("m3", ty.vec4<f32>()),
-                       Member("m4", ty.vec3<f16>()),
-                       Member("m5", ty.vec2<bool>()),
-                   });
-    auto* expr = Construct(ty.type_name("S"), vec2<i32>(1_i), vec3<u32>(2_u), vec4<f32>(3_f),
-                           vec3<f16>(4_h), vec2<bool>(false));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 5u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    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_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<i32>(), 1_i);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->As<sem::Vector>()->type()->Is<sem::U32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<u32>(), 2_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 2_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<u32>(), 2_u);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(2)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(2)->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(0)->As<f32>(), 3._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(1)->As<f32>(), 3._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(2)->As<f32>(), 3._f);
-    EXPECT_EQ(sem->ConstantValue()->Index(2)->Index(3)->As<f32>(), 3._f);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(3)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(3)->Type()->As<sem::Vector>()->type()->Is<sem::F16>());
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(0)->As<f16>(), 4._h);
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(1)->As<f16>(), 4._h);
-    EXPECT_EQ(sem->ConstantValue()->Index(3)->Index(2)->As<f16>(), 4._h);
-
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AnyZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(sem->ConstantValue()->Index(4)->Type()->As<sem::Vector>()->type()->Is<sem::Bool>());
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(0)->As<bool>(), false);
-    EXPECT_EQ(sem->ConstantValue()->Index(4)->Index(1)->As<bool>(), false);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_Struct_Construct) {
-    Structure("Inner", utils::Vector{
-                           Member("m1", ty.i32()),
-                           Member("m2", ty.u32()),
-                           Member("m3", ty.f32()),
-                       });
-
-    Structure("Outer", utils::Vector{
-                           Member("m1", ty.type_name("Inner")),
-                           Member("m2", ty.type_name("Inner")),
-                       });
-    auto* expr = Construct(ty.type_name("Outer"),  //
-                           Construct(ty.type_name("Inner"), 1_i, 2_u, 3_f),
-                           Construct(ty.type_name("Inner"), 4_i, 0_u, 6_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 2u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Struct>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 2_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(2)->As<f32>(), 3_f);
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Struct>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 4_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 0_u);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 6_f);
-}
-
-TEST_F(ResolverConstEvalTest, Struct_Array_Construct) {
-    Structure("S", utils::Vector{
-                       Member("m1", ty.array<i32, 2>()),
-                       Member("m2", ty.array<f32, 3>()),
-                   });
-    auto* expr = Construct(ty.type_name("S"),  //
-                           Construct(ty.array<i32, 2>(), 1_i, 2_i),
-                           Construct(ty.array<f32, 3>(), 1_f, 2_f, 3_f));
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    auto* str = sem->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 2u);
-    ASSERT_NE(sem->ConstantValue(), nullptr);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_FALSE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(0)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(0)->Type()->Is<sem::Array>());
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(0)->As<i32>(), 1_i);
-    EXPECT_EQ(sem->ConstantValue()->Index(0)->Index(1)->As<u32>(), 2_i);
-
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->Index(1)->AllZero());
-    EXPECT_TRUE(sem->ConstantValue()->Index(1)->Type()->Is<sem::Array>());
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(0)->As<i32>(), 1_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(1)->As<u32>(), 2_f);
-    EXPECT_EQ(sem->ConstantValue()->Index(1)->Index(2)->As<f32>(), 3_f);
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// 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
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-TEST_F(ResolverConstEvalTest, Vec3_Index) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), 2_i);
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 3_i);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_High) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, 3_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index 3 out of bounds [0..2]");
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Index_OOB_Low) {
-    auto* expr = IndexAccessor(vec3<i32>(1_i, 2_i, 3_i), Expr(Source{{12, 34}}, -3_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index -3 out of bounds [0..2]");
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Scalar) {
-    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "y");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 2_i);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Vector) {
-    auto* expr = MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "zx");
-    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_EQ(vec->Width(), 2u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-
-    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<f32>(), 3._a);
-
-    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<f32>(), 1._a);
-}
-
-TEST_F(ResolverConstEvalTest, Vec3_Swizzle_Chain) {
-    auto* expr =  // (1, 2, 3) -> (2, 3, 1) -> (3, 2) -> 2
-        MemberAccessor(MemberAccessor(MemberAccessor(vec3<i32>(1_i, 2_i, 3_i), "gbr"), "yx"), "y");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    ASSERT_NE(sem, nullptr);
-    ASSERT_TRUE(sem->Type()->Is<sem::I32>());
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-    EXPECT_TRUE(sem->ConstantValue()->AllEqual());
-    EXPECT_FALSE(sem->ConstantValue()->AnyZero());
-    EXPECT_FALSE(sem->ConstantValue()->AllZero());
-    EXPECT_EQ(sem->ConstantValue()->As<i32>(), 2_i);
-}
-
-TEST_F(ResolverConstEvalTest, Mat3x2_Index) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)), 2_i);
-    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_EQ(vec->Width(), 2u);
-    EXPECT_TYPE(sem->ConstantValue()->Type(), sem->Type());
-
-    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<f32>(), 5._a);
-
-    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<f32>(), 6._a);
-}
-
-TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_High) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
-        Expr(Source{{12, 34}}, 3_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index 3 out of bounds [0..2]");
-}
-
-TEST_F(ResolverConstEvalTest, Mat3x2_Index_OOB_Low) {
-    auto* expr = IndexAccessor(
-        mat3x2<f32>(vec2<f32>(1._a, 2._a), vec2<f32>(3._a, 4._a), vec2<f32>(5._a, 6._a)),
-        Expr(Source{{12, 34}}, -3_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index -3 out of bounds [0..2]");
-}
-
-TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index) {
-    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
-                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
-                               1_i);
-    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_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<f32>(), 4_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<f32>(), 5_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<f32>(), 6_f);
-}
-
-TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_High) {
-    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
-                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
-                               Expr(Source{{12, 34}}, 2_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index 2 out of bounds [0..1]");
-}
-
-TEST_F(ResolverConstEvalTest, Array_vec3_f32_Index_OOB_Low) {
-    auto* expr = IndexAccessor(Construct(ty.array(ty.vec3<f32>(), 2_u),  //
-                                         vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(4_f, 5_f, 6_f)),
-                               Expr(Source{{12, 34}}, -2_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index -2 out of bounds [0..1]");
-}
-
-TEST_F(ResolverConstEvalTest, RuntimeArray_vec3_f32_Index_OOB_Low) {
-    auto* sb = GlobalVar("sb", ty.array(ty.vec3<f32>()), Group(0_a), Binding(0_a),
-                         ast::AddressSpace::kStorage);
-    auto* expr = IndexAccessor(sb, Expr(Source{{12, 34}}, -2_i));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve()) << r()->error();
-    EXPECT_EQ(r()->error(), "12:34 error: index -2 out of bounds");
-}
-
-TEST_F(ResolverConstEvalTest, ChainedIndex) {
-    auto* arr_expr = Construct(ty.array(ty.mat2x3<f32>(), 2_u),        // array<mat2x3<f32>, 2u>
-                               mat2x3<f32>(vec3<f32>(1_f, 2_f, 3_f),   //
-                                           vec3<f32>(4_f, 5_f, 6_f)),  //
-                               mat2x3<f32>(vec3<f32>(7_f, 0_f, 9_f),   //
-                                           vec3<f32>(10_f, 11_f, 12_f)));
-
-    auto* mat_expr = IndexAccessor(arr_expr, 1_i);  // arr[1]
-    auto* vec_expr = IndexAccessor(mat_expr, 0_i);  // arr[1][0]
-    auto* f32_expr = IndexAccessor(vec_expr, 2_i);  // arr[1][0][2]
-    WrapInFunction(f32_expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    {
-        auto* mat = Sem().Get(mat_expr);
-        EXPECT_NE(mat, nullptr);
-        auto* ty = mat->Type()->As<sem::Matrix>();
-        ASSERT_NE(mat->Type(), nullptr);
-        EXPECT_TRUE(ty->ColumnType()->Is<sem::Vector>());
-        EXPECT_EQ(ty->columns(), 2u);
-        EXPECT_EQ(ty->rows(), 3u);
-        EXPECT_EQ(mat->ConstantValue()->Type(), mat->Type());
-        EXPECT_FALSE(mat->ConstantValue()->AllEqual());
-        EXPECT_TRUE(mat->ConstantValue()->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->AllZero());
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(0)->AllEqual());
-        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(0)->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(0)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(0)->As<f32>(), 7_f);
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AllEqual());
-        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AnyZero());
-        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(1)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(1)->As<f32>(), 0_f);
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(0)->Index(2)->AllEqual());
-        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(2)->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->Index(0)->Index(2)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(0)->Index(2)->As<f32>(), 9_f);
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(0)->AllEqual());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(0)->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(0)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(0)->As<f32>(), 10_f);
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(1)->AllEqual());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(1)->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(1)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(1)->As<f32>(), 11_f);
-
-        EXPECT_TRUE(mat->ConstantValue()->Index(1)->Index(2)->AllEqual());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(2)->AnyZero());
-        EXPECT_FALSE(mat->ConstantValue()->Index(1)->Index(2)->AllZero());
-        EXPECT_EQ(mat->ConstantValue()->Index(1)->Index(2)->As<f32>(), 12_f);
-    }
-    {
-        auto* vec = Sem().Get(vec_expr);
-        EXPECT_NE(vec, nullptr);
-        auto* ty = vec->Type()->As<sem::Vector>();
-        ASSERT_NE(vec->Type(), nullptr);
-        EXPECT_TRUE(ty->type()->Is<sem::F32>());
-        EXPECT_EQ(ty->Width(), 3u);
-        EXPECT_EQ(vec->ConstantValue()->Type(), vec->Type());
-        EXPECT_FALSE(vec->ConstantValue()->AllEqual());
-        EXPECT_TRUE(vec->ConstantValue()->AnyZero());
-        EXPECT_FALSE(vec->ConstantValue()->AllZero());
-
-        EXPECT_TRUE(vec->ConstantValue()->Index(0)->AllEqual());
-        EXPECT_FALSE(vec->ConstantValue()->Index(0)->AnyZero());
-        EXPECT_FALSE(vec->ConstantValue()->Index(0)->AllZero());
-        EXPECT_EQ(vec->ConstantValue()->Index(0)->As<f32>(), 7_f);
-
-        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AllEqual());
-        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AnyZero());
-        EXPECT_TRUE(vec->ConstantValue()->Index(1)->AllZero());
-        EXPECT_EQ(vec->ConstantValue()->Index(1)->As<f32>(), 0_f);
-
-        EXPECT_TRUE(vec->ConstantValue()->Index(2)->AllEqual());
-        EXPECT_FALSE(vec->ConstantValue()->Index(2)->AnyZero());
-        EXPECT_FALSE(vec->ConstantValue()->Index(2)->AllZero());
-        EXPECT_EQ(vec->ConstantValue()->Index(2)->As<f32>(), 9_f);
-    }
-    {
-        auto* f = Sem().Get(f32_expr);
-        EXPECT_NE(f, nullptr);
-        EXPECT_TRUE(f->Type()->Is<sem::F32>());
-        EXPECT_EQ(f->ConstantValue()->Type(), f->Type());
-        EXPECT_TRUE(f->ConstantValue()->AllEqual());
-        EXPECT_FALSE(f->ConstantValue()->AnyZero());
-        EXPECT_FALSE(f->ConstantValue()->AllZero());
-        EXPECT_EQ(f->ConstantValue()->As<f32>(), 9_f);
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Member accessing
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-TEST_F(ResolverConstEvalTest, MemberAccess) {
-    Structure("Inner", utils::Vector{
-                           Member("i1", ty.i32()),
-                           Member("i2", ty.u32()),
-                           Member("i3", ty.f32()),
-                       });
-
-    Structure("Outer", utils::Vector{
-                           Member("o1", ty.type_name("Inner")),
-                           Member("o2", ty.type_name("Inner")),
-                       });
-    auto* outer_expr = Construct(ty.type_name("Outer"),  //
-                                 Construct(ty.type_name("Inner"), 1_i, 2_u, 3_f),
-                                 Construct(ty.type_name("Inner")));
-    auto* o1_expr = MemberAccessor(outer_expr, "o1");
-    auto* i2_expr = MemberAccessor(o1_expr, "i2");
-    WrapInFunction(i2_expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* outer = Sem().Get(outer_expr);
-    ASSERT_NE(outer, nullptr);
-    auto* str = outer->Type()->As<sem::Struct>();
-    ASSERT_NE(str, nullptr);
-    EXPECT_EQ(str->Members().size(), 2u);
-    ASSERT_NE(outer->ConstantValue(), nullptr);
-    EXPECT_TYPE(outer->ConstantValue()->Type(), outer->Type());
-    EXPECT_FALSE(outer->ConstantValue()->AllEqual());
-    EXPECT_TRUE(outer->ConstantValue()->AnyZero());
-    EXPECT_FALSE(outer->ConstantValue()->AllZero());
-
-    auto* o1 = Sem().Get(o1_expr);
-    ASSERT_NE(o1->ConstantValue(), nullptr);
-    EXPECT_FALSE(o1->ConstantValue()->AllEqual());
-    EXPECT_FALSE(o1->ConstantValue()->AnyZero());
-    EXPECT_FALSE(o1->ConstantValue()->AllZero());
-    EXPECT_TRUE(o1->ConstantValue()->Type()->Is<sem::Struct>());
-    EXPECT_EQ(o1->ConstantValue()->Index(0)->As<i32>(), 1_i);
-    EXPECT_EQ(o1->ConstantValue()->Index(1)->As<u32>(), 2_u);
-    EXPECT_EQ(o1->ConstantValue()->Index(2)->As<f32>(), 3_f);
-
-    auto* i2 = Sem().Get(i2_expr);
-    ASSERT_NE(i2->ConstantValue(), nullptr);
-    EXPECT_TRUE(i2->ConstantValue()->AllEqual());
-    EXPECT_FALSE(i2->ConstantValue()->AnyZero());
-    EXPECT_FALSE(i2->ConstantValue()->AllZero());
-    EXPECT_TRUE(i2->ConstantValue()->Type()->Is<sem::U32>());
-    EXPECT_EQ(i2->ConstantValue()->As<u32>(), 2_u);
-}
-
-TEST_F(ResolverConstEvalTest, Matrix_AFloat_Construct_From_AInt_Vectors) {
-    auto* c = Const("a", Construct(ty.mat(nullptr, 2, 2),  //
-                                   Construct(ty.vec(nullptr, 2), Expr(1_a), Expr(2_a)),
-                                   Construct(ty.vec(nullptr, 2), Expr(3_a), Expr(4_a))));
-    WrapInFunction(c);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(c);
-    ASSERT_NE(sem, nullptr);
-    EXPECT_TRUE(sem->Type()->Is<sem::Matrix>());
-    auto* cv = sem->ConstantValue();
-    EXPECT_TYPE(cv->Type(), sem->Type());
-    EXPECT_TRUE(cv->Index(0)->Type()->Is<sem::Vector>());
-    EXPECT_TRUE(cv->Index(0)->Index(0)->Type()->Is<sem::AbstractFloat>());
-    EXPECT_FALSE(cv->AllEqual());
-    EXPECT_FALSE(cv->AnyZero());
-    EXPECT_FALSE(cv->AllZero());
-    auto* c0 = cv->Index(0);
-    auto* c1 = cv->Index(1);
-    EXPECT_EQ(std::get<AFloat>(c0->Index(0)->Value()), 1.0);
-    EXPECT_EQ(std::get<AFloat>(c0->Index(1)->Value()), 2.0);
-    EXPECT_EQ(std::get<AFloat>(c1->Index(0)->Value()), 3.0);
-    EXPECT_EQ(std::get<AFloat>(c1->Index(1)->Value()), 4.0);
-}
-
-using builder::IsValue;
-using builder::Mat;
-using builder::Val;
-using builder::Value;
-using builder::Vec;
-
-using Types = std::variant<  //
-    Value<AInt>,
-    Value<AFloat>,
-    Value<u32>,
-    Value<i32>,
-    Value<f32>,
-    Value<f16>,
-    Value<bool>,
-
-    Value<builder::vec2<AInt>>,
-    Value<builder::vec2<AFloat>>,
-    Value<builder::vec2<u32>>,
-    Value<builder::vec2<i32>>,
-    Value<builder::vec2<f32>>,
-    Value<builder::vec2<f16>>,
-    Value<builder::vec2<bool>>,
-
-    Value<builder::vec3<AInt>>,
-    Value<builder::vec3<AFloat>>,
-    Value<builder::vec3<u32>>,
-    Value<builder::vec3<i32>>,
-    Value<builder::vec3<f32>>,
-    Value<builder::vec3<f16>>,
-    Value<builder::vec3<bool>>,
-
-    Value<builder::vec4<AInt>>,
-    Value<builder::vec4<AFloat>>,
-    Value<builder::vec4<u32>>,
-    Value<builder::vec4<i32>>,
-    Value<builder::vec4<f32>>,
-    Value<builder::vec4<f16>>,
-    Value<builder::vec4<bool>>,
-
-    Value<builder::mat2x2<AInt>>,
-    Value<builder::mat2x2<AFloat>>,
-    Value<builder::mat2x2<f32>>,
-    Value<builder::mat2x2<f16>>,
-
-    Value<builder::mat2x3<AInt>>,
-    Value<builder::mat2x3<AFloat>>,
-    Value<builder::mat2x3<f32>>,
-    Value<builder::mat2x3<f16>>,
-
-    Value<builder::mat3x2<AInt>>,
-    Value<builder::mat3x2<AFloat>>,
-    Value<builder::mat3x2<f32>>,
-    Value<builder::mat3x2<f16>>
-    //
-    >;
-
-std::ostream& operator<<(std::ostream& o, const Types& types) {
-    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 << ")";
-        },
-        types);
-    return o;
-}
-
-// Calls `f` on deepest elements of both `a` and `b`. If function returns Action::kStop, it stops
-// traversing, and return Action::kStop; if the function returns Action::kContinue, it continues and
-// returns Action::kContinue when done.
-// TODO(amaiorano): Move to Constant.h?
-enum class Action { kStop, kContinue };
-template <typename Func>
-Action ForEachElemPair(const sem::Constant* a, const sem::Constant* b, Func&& f) {
-    EXPECT_EQ(a->Type(), b->Type());
-    size_t i = 0;
-    while (true) {
-        auto* a_elem = a->Index(i);
-        if (!a_elem) {
-            break;
-        }
-        auto* b_elem = b->Index(i);
-        if (ForEachElemPair(a_elem, b_elem, f) == Action::kStop) {
-            return Action::kStop;
-        }
-        i++;
-    }
-    if (i == 0) {
-        return f(a, b);
-    }
-    return Action::kContinue;
-}
-
-template <typename NumberT>
-struct BitValues {
-    using T = UnwrapNumber<NumberT>;
-    struct detail {
-        using UT = std::make_unsigned_t<T>;
-        static constexpr size_t NumBits = sizeof(T) * 8;
-        static constexpr T All = T{~T{0}};
-        static constexpr T LeftMost = static_cast<T>(UT{1} << (NumBits - 1u));
-        static constexpr T AllButLeftMost = T{~LeftMost};
-        static constexpr T TwoLeftMost = static_cast<T>(UT{0b11} << (NumBits - 2u));
-        static constexpr T AllButTwoLeftMost = T{~TwoLeftMost};
-        static constexpr T RightMost = T{1};
-        static constexpr T AllButRightMost = T{~RightMost};
-    };
-
-    static inline const size_t NumBits = detail::NumBits;
-    static inline const NumberT All = NumberT{detail::All};
-    static inline const NumberT LeftMost = NumberT{detail::LeftMost};
-    static inline const NumberT AllButLeftMost = NumberT{detail::AllButLeftMost};
-    static inline const NumberT TwoLeftMost = NumberT{detail::TwoLeftMost};
-    static inline const NumberT AllButTwoLeftMost = NumberT{detail::AllButTwoLeftMost};
-    static inline const NumberT RightMost = NumberT{detail::RightMost};
-    static inline const NumberT AllButRightMost = NumberT{detail::AllButRightMost};
-
-    template <typename U, typename V>
-    static constexpr NumberT Lsh(U val, V shiftBy) {
-        return NumberT{T{val} << T{shiftBy}};
-    }
-};
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Unary op
-////////////////////////////////////////////////////////////////////////////////////////////////////
-namespace unary_op {
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
-struct Case {
-    Types input;
-    Types expected;
-};
-
-static std::ostream& operator<<(std::ostream& o, const Case& c) {
-    o << "input: " << c.input << ", expected: " << c.expected;
-    return o;
-}
-
-/// Creates a Case with Values of any type
-template <typename T, typename U>
-Case C(Value<T> input, Value<U> expected) {
-    return Case{std::move(input), std::move(expected)};
-}
-
-/// Convenience overload to creates a Case with just scalars
-template <typename T, typename U, typename = std::enable_if_t<!IsValue<T>>>
-Case C(T input, U expected) {
-    return Case{Val(input), Val(expected)};
-}
-
-using ResolverConstEvalUnaryOpTest = ResolverTestWithParam<std::tuple<ast::UnaryOp, Case>>;
-
-TEST_P(ResolverConstEvalUnaryOpTest, Test) {
-    Enable(ast::Extension::kF16);
-
-    auto op = std::get<0>(GetParam());
-    auto& c = std::get<1>(GetParam());
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
-
-            auto* input_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.input);
-            auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
-
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
-            ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
-
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
-
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                EXPECT_EQ(a->As<T>(), b->As<T>());
-                                if constexpr (IsIntegral<T>) {
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
-}
-INSTANTIATE_TEST_SUITE_P(Complement,
-                         ResolverConstEvalUnaryOpTest,
-                         testing::Combine(testing::Values(ast::UnaryOp::kComplement),
-                                          testing::ValuesIn({
-                                              // AInt
-                                              C(0_a, 0xffffffffffffffff_a),
-                                              C(0xffffffffffffffff_a, 0_a),
-                                              C(0xf0f0f0f0f0f0f0f0_a, 0x0f0f0f0f0f0f0f0f_a),
-                                              C(0xaaaaaaaaaaaaaaaa_a, 0x5555555555555555_a),
-                                              C(0x5555555555555555_a, 0xaaaaaaaaaaaaaaaa_a),
-                                              // u32
-                                              C(0_u, 0xffffffff_u),
-                                              C(0xffffffff_u, 0_u),
-                                              C(0xf0f0f0f0_u, 0x0f0f0f0f_u),
-                                              C(0xaaaaaaaa_u, 0x55555555_u),
-                                              C(0x55555555_u, 0xaaaaaaaa_u),
-                                              // i32
-                                              C(0_i, -1_i),
-                                              C(-1_i, 0_i),
-                                              C(1_i, -2_i),
-                                              C(-2_i, 1_i),
-                                              C(2_i, -3_i),
-                                              C(-3_i, 2_i),
-                                          })));
-
-INSTANTIATE_TEST_SUITE_P(Negation,
-                         ResolverConstEvalUnaryOpTest,
-                         testing::Combine(testing::Values(ast::UnaryOp::kNegation),
-                                          testing::ValuesIn({
-                                              // AInt
-                                              C(0_a, -0_a),
-                                              C(-0_a, 0_a),
-                                              C(1_a, -1_a),
-                                              C(-1_a, 1_a),
-                                              C(AInt::Highest(), -AInt::Highest()),
-                                              C(-AInt::Highest(), AInt::Highest()),
-                                              C(AInt::Lowest(), Negate(AInt::Lowest())),
-                                              C(Negate(AInt::Lowest()), AInt::Lowest()),
-                                              // i32
-                                              C(0_i, -0_i),
-                                              C(-0_i, 0_i),
-                                              C(1_i, -1_i),
-                                              C(-1_i, 1_i),
-                                              C(i32::Highest(), -i32::Highest()),
-                                              C(-i32::Highest(), i32::Highest()),
-                                              C(i32::Lowest(), Negate(i32::Lowest())),
-                                              C(Negate(i32::Lowest()), i32::Lowest()),
-                                              // AFloat
-                                              C(0.0_a, -0.0_a),
-                                              C(-0.0_a, 0.0_a),
-                                              C(1.0_a, -1.0_a),
-                                              C(-1.0_a, 1.0_a),
-                                              C(AFloat::Highest(), -AFloat::Highest()),
-                                              C(-AFloat::Highest(), AFloat::Highest()),
-                                              C(AFloat::Lowest(), Negate(AFloat::Lowest())),
-                                              C(Negate(AFloat::Lowest()), AFloat::Lowest()),
-                                              // f32
-                                              C(0.0_f, -0.0_f),
-                                              C(-0.0_f, 0.0_f),
-                                              C(1.0_f, -1.0_f),
-                                              C(-1.0_f, 1.0_f),
-                                              C(f32::Highest(), -f32::Highest()),
-                                              C(-f32::Highest(), f32::Highest()),
-                                              C(f32::Lowest(), Negate(f32::Lowest())),
-                                              C(Negate(f32::Lowest()), f32::Lowest()),
-                                              // f16
-                                              C(0.0_h, -0.0_h),
-                                              C(-0.0_h, 0.0_h),
-                                              C(1.0_h, -1.0_h),
-                                              C(-1.0_h, 1.0_h),
-                                              C(f16::Highest(), -f16::Highest()),
-                                              C(-f16::Highest(), f16::Highest()),
-                                              C(f16::Lowest(), Negate(f16::Lowest())),
-                                              C(Negate(f16::Lowest()), f16::Lowest()),
-                                          })));
-
-// Make sure UBSan doesn't trip on C++'s undefined behaviour of negating the smallest negative
-// number.
-TEST_F(ResolverConstEvalTest, UnaryNegateLowestAbstract) {
-    // const break_me = -(-9223372036854775808);
-    auto* c = GlobalConst("break_me", Negation(Negation(Expr(9223372036854775808_a))));
-    (void)c;
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-    auto* sem = Sem().Get(c);
-    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 9223372036854775808_a);
-}
-
-INSTANTIATE_TEST_SUITE_P(Not,
-                         ResolverConstEvalUnaryOpTest,
-                         testing::Combine(testing::Values(ast::UnaryOp::kNot),
-                                          testing::ValuesIn({
-                                              C(true, false),
-                                              C(false, true),
-                                              C(Vec(true, true), Vec(false, false)),
-                                              C(Vec(true, false), Vec(false, true)),
-                                              C(Vec(false, true), Vec(true, false)),
-                                              C(Vec(false, false), Vec(true, true)),
-                                          })));
-
-}  // namespace unary_op
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Binary op
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-namespace binary_op {
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
-struct Case {
-    Types lhs;
-    Types rhs;
-    Types expected;
-    bool overflow;
-};
-
-/// Creates a Case with Values of any type
-template <typename T, typename U, typename V>
-Case C(Value<T> lhs, Value<U> rhs, Value<V> expected, bool overflow = false) {
-    return Case{std::move(lhs), std::move(rhs), std::move(expected), overflow};
-}
-
-/// Convenience overload that creates a Case with just scalars
-template <typename T, typename U, typename V, typename = std::enable_if_t<!IsValue<T>>>
-Case C(T lhs, U rhs, V expected, bool overflow = false) {
-    return Case{Val(lhs), Val(rhs), Val(expected), overflow};
-}
-
-static std::ostream& operator<<(std::ostream& o, const Case& c) {
-    o << "lhs: " << c.lhs << ", rhs: " << c.rhs << ", expected: " << c.expected
-      << ", overflow: " << c.overflow;
-    return o;
-}
-
-using ResolverConstEvalBinaryOpTest = ResolverTestWithParam<std::tuple<ast::BinaryOp, Case>>;
-TEST_P(ResolverConstEvalBinaryOpTest, Test) {
-    Enable(ast::Extension::kF16);
-    auto op = std::get<0>(GetParam());
-    auto& c = std::get<1>(GetParam());
-
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
-            if constexpr (std::is_same_v<T, AInt> || std::is_same_v<T, AFloat>) {
-                if (c.overflow) {
-                    // Overflow is not allowed for abstract types. This is tested separately.
-                    return;
-                }
-            }
-
-            auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
-            auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
-            auto* expr = create<ast::BinaryExpression>(op, lhs_expr, rhs_expr);
-
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
-            ASSERT_TRUE(r()->Resolve()) << r()->error();
-
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
-
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
-
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                EXPECT_EQ(a->As<T>(), b->As<T>());
-                                if constexpr (IsIntegral<T>) {
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
-}
-
-INSTANTIATE_TEST_SUITE_P(MixedAbstractArgs,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(testing::Values(ast::BinaryOp::kAdd),
-                                          testing::ValuesIn(std::vector{
-                                              // Mixed abstract type args
-                                              C(1_a, 2.3_a, 3.3_a),
-                                              C(2.3_a, 1_a, 3.3_a),
-                                          })));
-
-template <typename T>
-std::vector<Case> OpAddIntCases() {
-    static_assert(IsIntegral<T>);
-    return {
-        C(T{0}, T{0}, T{0}),
-        C(T{1}, T{2}, T{3}),
-        C(T::Lowest(), T{1}, T{T::Lowest() + 1}),
-        C(T::Highest(), Negate(T{1}), T{T::Highest() - 1}),
-        C(T::Lowest(), T::Highest(), Negate(T{1})),
-        C(T::Highest(), T{1}, T::Lowest(), true),
-        C(T::Lowest(), Negate(T{1}), T::Highest(), true),
-    };
-}
-template <typename T>
-std::vector<Case> OpAddFloatCases() {
-    static_assert(IsFloatingPoint<T>);
-    return {
-        C(T{0}, T{0}, T{0}),
-        C(T{1}, T{2}, T{3}),
-        C(T::Lowest(), T{1}, T{T::Lowest() + 1}),
-        C(T::Highest(), Negate(T{1}), T{T::Highest() - 1}),
-        C(T::Lowest(), T::Highest(), T{0}),
-        C(T::Highest(), T::Highest(), T::Inf(), true),
-        C(T::Lowest(), Negate(T::Highest()), -T::Inf(), true),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Add,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(testing::Values(ast::BinaryOp::kAdd),
-                                          testing::ValuesIn(Concat(  //
-                                              OpAddIntCases<AInt>(),
-                                              OpAddIntCases<i32>(),
-                                              OpAddIntCases<u32>(),
-                                              OpAddFloatCases<AFloat>(),
-                                              OpAddFloatCases<f32>(),
-                                              OpAddFloatCases<f16>()))));
-
-template <typename T>
-std::vector<Case> OpSubIntCases() {
-    static_assert(IsIntegral<T>);
-    return {
-        C(T{0}, T{0}, T{0}),
-        C(T{3}, T{2}, T{1}),
-        C(T{T::Lowest() + 1}, T{1}, T::Lowest()),
-        C(T{T::Highest() - 1}, Negate(T{1}), T::Highest()),
-        C(Negate(T{1}), T::Highest(), T::Lowest()),
-        C(T::Lowest(), T{1}, T::Highest(), true),
-        C(T::Highest(), Negate(T{1}), T::Lowest(), true),
-    };
-}
-template <typename T>
-std::vector<Case> OpSubFloatCases() {
-    static_assert(IsFloatingPoint<T>);
-    return {
-        C(T{0}, T{0}, T{0}),
-        C(T{3}, T{2}, T{1}),
-        C(T::Highest(), T{1}, T{T::Highest() - 1}),
-        C(T::Lowest(), Negate(T{1}), T{T::Lowest() + 1}),
-        C(T{0}, T::Highest(), T::Lowest()),
-        C(T::Highest(), Negate(T::Highest()), T::Inf(), true),
-        C(T::Lowest(), T::Highest(), -T::Inf(), true),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Sub,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(testing::Values(ast::BinaryOp::kSubtract),
-                                          testing::ValuesIn(Concat(  //
-                                              OpSubIntCases<AInt>(),
-                                              OpSubIntCases<i32>(),
-                                              OpSubIntCases<u32>(),
-                                              OpSubFloatCases<AFloat>(),
-                                              OpSubFloatCases<f32>(),
-                                              OpSubFloatCases<f16>()))));
-
-template <typename T>
-std::vector<Case> OpMulScalarCases() {
-    return {
-        C(T{0}, T{0}, T{0}),
-        C(T{1}, T{2}, T{2}),
-        C(T{2}, T{3}, T{6}),
-        C(Negate(T{2}), T{3}, Negate(T{6})),
-        C(T::Highest(), T{1}, T::Highest()),
-        C(T::Lowest(), T{1}, T::Lowest()),
-        C(T::Highest(), T::Highest(), Mul(T::Highest(), T::Highest()), true),
-        C(T::Lowest(), T::Lowest(), Mul(T::Lowest(), T::Lowest()), true),
-    };
-}
-
-template <typename T>
-std::vector<Case> OpMulVecCases() {
-    return {
-        // s * vec3 = vec3
-        C(Val(T{2.0}), Vec(T{1.25}, T{2.25}, T{3.25}), Vec(T{2.5}, T{4.5}, T{6.5})),
-        // vec3 * s = vec3
-        C(Vec(T{1.25}, T{2.25}, T{3.25}), Val(T{2.0}), Vec(T{2.5}, T{4.5}, T{6.5})),
-        // vec3 * vec3 = vec3
-        C(Vec(T{1.25}, T{2.25}, T{3.25}), Vec(T{2.0}, T{2.0}, T{2.0}), Vec(T{2.5}, T{4.5}, T{6.5})),
-    };
-}
-
-template <typename T>
-std::vector<Case> OpMulMatCases() {
-    return {
-        // s * mat3x2 = mat3x2
-        C(Val(T{2.25}),
-          Mat({T{1.0}, T{4.0}},  //
-              {T{2.0}, T{5.0}},  //
-              {T{3.0}, T{6.0}}),
-          Mat({T{2.25}, T{9.0}},   //
-              {T{4.5}, T{11.25}},  //
-              {T{6.75}, T{13.5}})),
-        // mat3x2 * s = mat3x2
-        C(Mat({T{1.0}, T{4.0}},  //
-              {T{2.0}, T{5.0}},  //
-              {T{3.0}, T{6.0}}),
-          Val(T{2.25}),
-          Mat({T{2.25}, T{9.0}},   //
-              {T{4.5}, T{11.25}},  //
-              {T{6.75}, T{13.5}})),
-        // vec3 * mat2x3 = vec2
-        C(Vec(T{1.25}, T{2.25}, T{3.25}),  //
-          Mat({T{1.0}, T{2.0}, T{3.0}},    //
-              {T{4.0}, T{5.0}, T{6.0}}),   //
-          Vec(T{15.5}, T{35.75})),
-        // mat2x3 * vec2 = vec3
-        C(Mat({T{1.0}, T{2.0}, T{3.0}},   //
-              {T{4.0}, T{5.0}, T{6.0}}),  //
-          Vec(T{1.25}, T{2.25}),          //
-          Vec(T{10.25}, T{13.75}, T{17.25})),
-        // mat3x2 * mat2x3 = mat2x2
-        C(Mat({T{1.0}, T{2.0}},              //
-              {T{3.0}, T{4.0}},              //
-              {T{5.0}, T{6.0}}),             //
-          Mat({T{1.25}, T{2.25}, T{3.25}},   //
-              {T{4.25}, T{5.25}, T{6.25}}),  //
-          Mat({T{24.25}, T{31.0}},           //
-              {T{51.25}, T{67.0}})),         //
-    };
-}
-
-INSTANTIATE_TEST_SUITE_P(Mul,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kMultiply),
-                             testing::ValuesIn(Concat(  //
-                                 OpMulScalarCases<AInt>(),
-                                 OpMulScalarCases<i32>(),
-                                 OpMulScalarCases<u32>(),
-                                 OpMulScalarCases<AFloat>(),
-                                 OpMulScalarCases<f32>(),
-                                 OpMulScalarCases<f16>(),
-                                 OpMulVecCases<AInt>(),
-                                 OpMulVecCases<i32>(),
-                                 OpMulVecCases<u32>(),
-                                 OpMulVecCases<AFloat>(),
-                                 OpMulVecCases<f32>(),
-                                 OpMulVecCases<f16>(),
-                                 OpMulMatCases<AFloat>(),
-                                 OpMulMatCases<f32>(),
-                                 OpMulMatCases<f16>()))));
-
-template <typename T>
-std::vector<Case> OpDivIntCases() {
-    std::vector<Case> r = {
-        C(Val(T{0}), Val(T{1}), Val(T{0})),
-        C(Val(T{1}), Val(T{1}), Val(T{1})),
-        C(Val(T{1}), Val(T{1}), Val(T{1})),
-        C(Val(T{2}), Val(T{1}), Val(T{2})),
-        C(Val(T{4}), Val(T{2}), Val(T{2})),
-        C(Val(T::Highest()), Val(T{1}), Val(T::Highest())),
-        C(Val(T::Lowest()), Val(T{1}), Val(T::Lowest())),
-        C(Val(T::Highest()), Val(T::Highest()), Val(T{1})),
-        C(Val(T{0}), Val(T::Highest()), Val(T{0})),
-        C(Val(T{0}), Val(T::Lowest()), Val(T{0})),
-    };
-    ConcatIntoIf<IsIntegral<T>>(  //
-        r, std::vector<Case>{
-               // e1, when e2 is zero.
-               C(T{123}, T{0}, T{123}, true),
-           });
-    ConcatIntoIf<IsSignedIntegral<T>>(  //
-        r, std::vector<Case>{
-               // e1, when e1 is the most negative value in T, and e2 is -1.
-               C(T::Smallest(), T{-1}, T::Smallest(), true),
-           });
-    return r;
-}
-
-template <typename T>
-std::vector<Case> OpDivFloatCases() {
-    return {
-        C(Val(T{0}), Val(T{1}), Val(T{0})),
-        C(Val(T{1}), Val(T{1}), Val(T{1})),
-        C(Val(T{1}), Val(T{1}), Val(T{1})),
-        C(Val(T{2}), Val(T{1}), Val(T{2})),
-        C(Val(T{4}), Val(T{2}), Val(T{2})),
-        C(Val(T::Highest()), Val(T{1}), Val(T::Highest())),
-        C(Val(T::Lowest()), Val(T{1}), Val(T::Lowest())),
-        C(Val(T::Highest()), Val(T::Highest()), Val(T{1})),
-        C(Val(T{0}), Val(T::Highest()), Val(T{0})),
-        C(Val(T{0}), Val(T::Lowest()), Val(-T{0})),
-        C(T{123}, T{0}, T::Inf(), true),
-        C(T{-123}, -T{0}, T::Inf(), true),
-        C(T{-123}, T{0}, -T::Inf(), true),
-        C(T{123}, -T{0}, -T::Inf(), true),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Div,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kDivide),
-                             testing::ValuesIn(Concat(  //
-                                 OpDivIntCases<AInt>(),
-                                 OpDivIntCases<i32>(),
-                                 OpDivIntCases<u32>(),
-                                 OpDivFloatCases<AFloat>(),
-                                 OpDivFloatCases<f32>(),
-                                 OpDivFloatCases<f16>()))));
-
-template <typename T, bool equals>
-std::vector<Case> OpEqualCases() {
-    return {
-        C(Val(T{0}), Val(T{0}), Val(true == equals)),
-        C(Val(T{0}), Val(T{1}), Val(false == equals)),
-        C(Val(T{1}), Val(T{0}), Val(false == equals)),
-        C(Val(T{1}), Val(T{1}), Val(true == equals)),
-        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(true == equals, true == equals)),
-        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(false == equals, false == equals)),
-        C(Vec(T{1}, T{1}), Vec(T{0}, T{1}), Vec(false == equals, true == equals)),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Equal,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kEqual),
-                             testing::ValuesIn(Concat(  //
-                                 OpEqualCases<AInt, true>(),
-                                 OpEqualCases<i32, true>(),
-                                 OpEqualCases<u32, true>(),
-                                 OpEqualCases<AFloat, true>(),
-                                 OpEqualCases<f32, true>(),
-                                 OpEqualCases<f16, true>(),
-                                 OpEqualCases<bool, true>()))));
-INSTANTIATE_TEST_SUITE_P(NotEqual,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kNotEqual),
-                             testing::ValuesIn(Concat(  //
-                                 OpEqualCases<AInt, false>(),
-                                 OpEqualCases<i32, false>(),
-                                 OpEqualCases<u32, false>(),
-                                 OpEqualCases<AFloat, false>(),
-                                 OpEqualCases<f32, false>(),
-                                 OpEqualCases<f16, false>(),
-                                 OpEqualCases<bool, false>()))));
-
-template <typename T, bool less_than>
-std::vector<Case> OpLessThanCases() {
-    return {
-        C(Val(T{0}), Val(T{0}), Val(false == less_than)),
-        C(Val(T{0}), Val(T{1}), Val(true == less_than)),
-        C(Val(T{1}), Val(T{0}), Val(false == less_than)),
-        C(Val(T{1}), Val(T{1}), Val(false == less_than)),
-        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(false == less_than, false == less_than)),
-        C(Vec(T{0}, T{0}), Vec(T{1}, T{1}), Vec(true == less_than, true == less_than)),
-        C(Vec(T{1}, T{1}), Vec(T{0}, T{0}), Vec(false == less_than, false == less_than)),
-        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(false == less_than, true == less_than)),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(LessThan,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kLessThan),
-                             testing::ValuesIn(Concat(  //
-                                 OpLessThanCases<AInt, true>(),
-                                 OpLessThanCases<i32, true>(),
-                                 OpLessThanCases<u32, true>(),
-                                 OpLessThanCases<AFloat, true>(),
-                                 OpLessThanCases<f32, true>(),
-                                 OpLessThanCases<f16, true>()))));
-INSTANTIATE_TEST_SUITE_P(GreaterThanEqual,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kGreaterThanEqual),
-                             testing::ValuesIn(Concat(  //
-                                 OpLessThanCases<AInt, false>(),
-                                 OpLessThanCases<i32, false>(),
-                                 OpLessThanCases<u32, false>(),
-                                 OpLessThanCases<AFloat, false>(),
-                                 OpLessThanCases<f32, false>(),
-                                 OpLessThanCases<f16, false>()))));
-
-template <typename T, bool greater_than>
-std::vector<Case> OpGreaterThanCases() {
-    return {
-        C(Val(T{0}), Val(T{0}), Val(false == greater_than)),
-        C(Val(T{0}), Val(T{1}), Val(false == greater_than)),
-        C(Val(T{1}), Val(T{0}), Val(true == greater_than)),
-        C(Val(T{1}), Val(T{1}), Val(false == greater_than)),
-        C(Vec(T{0}, T{0}), Vec(T{0}, T{0}), Vec(false == greater_than, false == greater_than)),
-        C(Vec(T{1}, T{1}), Vec(T{0}, T{0}), Vec(true == greater_than, true == greater_than)),
-        C(Vec(T{0}, T{0}), Vec(T{1}, T{1}), Vec(false == greater_than, false == greater_than)),
-        C(Vec(T{1}, T{0}), Vec(T{0}, T{1}), Vec(true == greater_than, false == greater_than)),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(GreaterThan,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kGreaterThan),
-                             testing::ValuesIn(Concat(  //
-                                 OpGreaterThanCases<AInt, true>(),
-                                 OpGreaterThanCases<i32, true>(),
-                                 OpGreaterThanCases<u32, true>(),
-                                 OpGreaterThanCases<AFloat, true>(),
-                                 OpGreaterThanCases<f32, true>(),
-                                 OpGreaterThanCases<f16, true>()))));
-INSTANTIATE_TEST_SUITE_P(LessThanEqual,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kLessThanEqual),
-                             testing::ValuesIn(Concat(  //
-                                 OpGreaterThanCases<AInt, false>(),
-                                 OpGreaterThanCases<i32, false>(),
-                                 OpGreaterThanCases<u32, false>(),
-                                 OpGreaterThanCases<AFloat, false>(),
-                                 OpGreaterThanCases<f32, false>(),
-                                 OpGreaterThanCases<f16, false>()))));
-
-static std::vector<Case> OpAndBoolCases() {
-    return {
-        C(true, true, true),
-        C(true, false, false),
-        C(false, true, false),
-        C(false, false, false),
-        C(Vec(true, true), Vec(true, false), Vec(true, false)),
-        C(Vec(true, true), Vec(false, true), Vec(false, true)),
-        C(Vec(true, false), Vec(true, false), Vec(true, false)),
-        C(Vec(false, true), Vec(true, false), Vec(false, false)),
-        C(Vec(false, false), Vec(true, false), Vec(false, false)),
-    };
-}
-template <typename T>
-std::vector<Case> OpAndIntCases() {
-    using B = BitValues<T>;
-    return {
-        C(T{0b1010}, T{0b1111}, T{0b1010}),
-        C(T{0b1010}, T{0b0000}, T{0b0000}),
-        C(T{0b1010}, T{0b0011}, T{0b0010}),
-        C(T{0b1010}, T{0b1100}, T{0b1000}),
-        C(T{0b1010}, T{0b0101}, T{0b0000}),
-        C(B::All, B::All, B::All),
-        C(B::LeftMost, B::LeftMost, B::LeftMost),
-        C(B::RightMost, B::RightMost, B::RightMost),
-        C(B::All, T{0}, T{0}),
-        C(T{0}, B::All, T{0}),
-        C(B::LeftMost, B::AllButLeftMost, T{0}),
-        C(B::AllButLeftMost, B::LeftMost, T{0}),
-        C(B::RightMost, B::AllButRightMost, T{0}),
-        C(B::AllButRightMost, B::RightMost, T{0}),
-        C(Vec(B::All, B::LeftMost, B::RightMost),      //
-          Vec(B::All, B::All, B::All),                 //
-          Vec(B::All, B::LeftMost, B::RightMost)),     //
-        C(Vec(B::All, B::LeftMost, B::RightMost),      //
-          Vec(T{0}, T{0}, T{0}),                       //
-          Vec(T{0}, T{0}, T{0})),                      //
-        C(Vec(B::LeftMost, B::RightMost),              //
-          Vec(B::AllButLeftMost, B::AllButRightMost),  //
-          Vec(T{0}, T{0})),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(And,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kAnd),
-                             testing::ValuesIn(            //
-                                 Concat(OpAndBoolCases(),  //
-                                        OpAndIntCases<AInt>(),
-                                        OpAndIntCases<i32>(),
-                                        OpAndIntCases<u32>()))));
-
-static std::vector<Case> OpOrBoolCases() {
-    return {
-        C(true, true, true),
-        C(true, false, true),
-        C(false, true, true),
-        C(false, false, false),
-        C(Vec(true, true), Vec(true, false), Vec(true, true)),
-        C(Vec(true, true), Vec(false, true), Vec(true, true)),
-        C(Vec(true, false), Vec(true, false), Vec(true, false)),
-        C(Vec(false, true), Vec(true, false), Vec(true, true)),
-        C(Vec(false, false), Vec(true, false), Vec(true, false)),
-    };
-}
-template <typename T>
-std::vector<Case> OpOrIntCases() {
-    using B = BitValues<T>;
-    return {
-        C(T{0b1010}, T{0b1111}, T{0b1111}),
-        C(T{0b1010}, T{0b0000}, T{0b1010}),
-        C(T{0b1010}, T{0b0011}, T{0b1011}),
-        C(T{0b1010}, T{0b1100}, T{0b1110}),
-        C(T{0b1010}, T{0b0101}, T{0b1111}),
-        C(B::All, B::All, B::All),
-        C(B::LeftMost, B::LeftMost, B::LeftMost),
-        C(B::RightMost, B::RightMost, B::RightMost),
-        C(B::All, T{0}, B::All),
-        C(T{0}, B::All, B::All),
-        C(B::LeftMost, B::AllButLeftMost, B::All),
-        C(B::AllButLeftMost, B::LeftMost, B::All),
-        C(B::RightMost, B::AllButRightMost, B::All),
-        C(B::AllButRightMost, B::RightMost, B::All),
-        C(Vec(B::All, B::LeftMost, B::RightMost),      //
-          Vec(B::All, B::All, B::All),                 //
-          Vec(B::All, B::All, B::All)),                //
-        C(Vec(B::All, B::LeftMost, B::RightMost),      //
-          Vec(T{0}, T{0}, T{0}),                       //
-          Vec(B::All, B::LeftMost, B::RightMost)),     //
-        C(Vec(B::LeftMost, B::RightMost),              //
-          Vec(B::AllButLeftMost, B::AllButRightMost),  //
-          Vec(B::All, B::All)),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Or,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kOr),
-                             testing::ValuesIn(Concat(OpOrBoolCases(),
-                                                      OpOrIntCases<AInt>(),
-                                                      OpOrIntCases<i32>(),
-                                                      OpOrIntCases<u32>()))));
-
-TEST_F(ResolverConstEvalTest, NotAndOrOfVecs) {
-    // const C = !((vec2(true, true) & vec2(true, false)) | vec2(false, true));
-    auto v1 = Vec(true, true).Expr(*this);
-    auto v2 = Vec(true, false).Expr(*this);
-    auto v3 = Vec(false, true).Expr(*this);
-    auto expr = Not(Or(And(v1, v2), v3));
-    GlobalConst("C", expr);
-    auto expected_expr = Vec(false, false).Expr(*this);
-    GlobalConst("E", expected_expr);
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    auto* sem = Sem().Get(expr);
-    const sem::Constant* value = sem->ConstantValue();
-    ASSERT_NE(value, nullptr);
-    EXPECT_TYPE(value->Type(), sem->Type());
-
-    auto* expected_sem = Sem().Get(expected_expr);
-    const sem::Constant* expected_value = expected_sem->ConstantValue();
-    ASSERT_NE(expected_value, nullptr);
-    EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
-
-    ForEachElemPair(value, expected_value, [&](const sem::Constant* a, const sem::Constant* b) {
-        EXPECT_EQ(a->As<bool>(), b->As<bool>());
-        return HasFailure() ? Action::kStop : Action::kContinue;
-    });
-}
-
-template <typename T>
-std::vector<Case> XorCases() {
-    using B = BitValues<T>;
-    return {
-        C(T{0b1010}, T{0b1111}, T{0b0101}),
-        C(T{0b1010}, T{0b0000}, T{0b1010}),
-        C(T{0b1010}, T{0b0011}, T{0b1001}),
-        C(T{0b1010}, T{0b1100}, T{0b0110}),
-        C(T{0b1010}, T{0b0101}, T{0b1111}),
-        C(B::All, B::All, T{0}),
-        C(B::LeftMost, B::LeftMost, T{0}),
-        C(B::RightMost, B::RightMost, T{0}),
-        C(B::All, T{0}, B::All),
-        C(T{0}, B::All, B::All),
-        C(B::LeftMost, B::AllButLeftMost, B::All),
-        C(B::AllButLeftMost, B::LeftMost, B::All),
-        C(B::RightMost, B::AllButRightMost, B::All),
-        C(B::AllButRightMost, B::RightMost, B::All),
-        C(Vec(B::All, B::LeftMost, B::RightMost),             //
-          Vec(B::All, B::All, B::All),                        //
-          Vec(T{0}, B::AllButLeftMost, B::AllButRightMost)),  //
-        C(Vec(B::All, B::LeftMost, B::RightMost),             //
-          Vec(T{0}, T{0}, T{0}),                              //
-          Vec(B::All, B::LeftMost, B::RightMost)),            //
-        C(Vec(B::LeftMost, B::RightMost),                     //
-          Vec(B::AllButLeftMost, B::AllButRightMost),         //
-          Vec(B::All, B::All)),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Xor,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kXor),
-                             testing::ValuesIn(Concat(XorCases<AInt>(),  //
-                                                      XorCases<i32>(),   //
-                                                      XorCases<u32>()))));
-
-template <typename T>
-std::vector<Case> ShiftLeftCases() {
-    // Shift type is u32 for non-abstract
-    using ST = std::conditional_t<IsAbstract<T>, T, u32>;
-    using B = BitValues<T>;
-    return {
-        C(T{0b1010}, ST{0}, T{0b0000'0000'1010}),    //
-        C(T{0b1010}, ST{1}, T{0b0000'0001'0100}),    //
-        C(T{0b1010}, ST{2}, T{0b0000'0010'1000}),    //
-        C(T{0b1010}, ST{3}, T{0b0000'0101'0000}),    //
-        C(T{0b1010}, ST{4}, T{0b0000'1010'0000}),    //
-        C(T{0b1010}, ST{5}, T{0b0001'0100'0000}),    //
-        C(T{0b1010}, ST{6}, T{0b0010'1000'0000}),    //
-        C(T{0b1010}, ST{7}, T{0b0101'0000'0000}),    //
-        C(T{0b1010}, ST{8}, T{0b1010'0000'0000}),    //
-        C(B::LeftMost, ST{0}, B::LeftMost),          //
-        C(B::TwoLeftMost, ST{1}, B::LeftMost),       // No overflow
-        C(B::All, ST{1}, B::AllButRightMost),        // No overflow
-        C(B::All, ST{B::NumBits - 1}, B::LeftMost),  // No overflow
-
-        C(Vec(T{0b1010}, T{0b1010}),                                            //
-          Vec(ST{0}, ST{1}),                                                    //
-          Vec(T{0b0000'0000'1010}, T{0b0000'0001'0100})),                       //
-        C(Vec(T{0b1010}, T{0b1010}),                                            //
-          Vec(ST{2}, ST{3}),                                                    //
-          Vec(T{0b0000'0010'1000}, T{0b0000'0101'0000})),                       //
-        C(Vec(T{0b1010}, T{0b1010}),                                            //
-          Vec(ST{4}, ST{5}),                                                    //
-          Vec(T{0b0000'1010'0000}, T{0b0001'0100'0000})),                       //
-        C(Vec(T{0b1010}, T{0b1010}, T{0b1010}),                                 //
-          Vec(ST{6}, ST{7}, ST{8}),                                             //
-          Vec(T{0b0010'1000'0000}, T{0b0101'0000'0000}, T{0b1010'0000'0000})),  //
-    };
-}
-INSTANTIATE_TEST_SUITE_P(ShiftLeft,
-                         ResolverConstEvalBinaryOpTest,
-                         testing::Combine(  //
-                             testing::Values(ast::BinaryOp::kShiftLeft),
-                             testing::ValuesIn(Concat(ShiftLeftCases<AInt>(),  //
-                                                      ShiftLeftCases<i32>(),   //
-                                                      ShiftLeftCases<u32>()))));
-
-// Tests for errors on overflow/underflow of binary operations with abstract numbers
-struct OverflowCase {
-    ast::BinaryOp op;
-    Types lhs;
-    Types rhs;
-};
-
-static std::ostream& operator<<(std::ostream& o, const OverflowCase& c) {
-    o << ast::FriendlyName(c.op) << ", lhs: " << c.lhs << ", rhs: " << c.rhs;
-    return o;
-}
-using ResolverConstEvalBinaryOpTest_Overflow = ResolverTestWithParam<OverflowCase>;
-TEST_P(ResolverConstEvalBinaryOpTest_Overflow, Test) {
-    Enable(ast::Extension::kF16);
-    auto& c = GetParam();
-    auto* lhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.lhs);
-    auto* rhs_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.rhs);
-    auto* expr = create<ast::BinaryExpression>(Source{{1, 1}}, c.op, lhs_expr, rhs_expr);
-    GlobalConst("C", expr);
-    ASSERT_FALSE(r()->Resolve());
-
-    std::string type_name = std::visit(
-        [&](auto&& value) {
-            using ValueType = std::decay_t<decltype(value)>;
-            return builder::FriendlyName<ValueType>();
-        },
-        c.lhs);
-
-    EXPECT_THAT(r()->error(), HasSubstr("1:1 error: '"));
-    EXPECT_THAT(r()->error(), HasSubstr("' cannot be represented as '" + type_name + "'"));
-}
-INSTANTIATE_TEST_SUITE_P(
-    Test,
-    ResolverConstEvalBinaryOpTest_Overflow,
-    testing::Values(
-
-        // scalar-scalar add
-        OverflowCase{ast::BinaryOp::kAdd, Val(AInt::Highest()), Val(1_a)},
-        OverflowCase{ast::BinaryOp::kAdd, Val(AInt::Lowest()), Val(-1_a)},
-        OverflowCase{ast::BinaryOp::kAdd, Val(AFloat::Highest()), Val(AFloat::Highest())},
-        OverflowCase{ast::BinaryOp::kAdd, Val(AFloat::Lowest()), Val(AFloat::Lowest())},
-        // scalar-scalar subtract
-        OverflowCase{ast::BinaryOp::kSubtract, Val(AInt::Lowest()), Val(1_a)},
-        OverflowCase{ast::BinaryOp::kSubtract, Val(AInt::Highest()), Val(-1_a)},
-        OverflowCase{ast::BinaryOp::kSubtract, Val(AFloat::Highest()), Val(AFloat::Lowest())},
-        OverflowCase{ast::BinaryOp::kSubtract, Val(AFloat::Lowest()), Val(AFloat::Highest())},
-
-        // scalar-scalar multiply
-        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Highest()), Val(2_a)},
-        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Lowest()), Val(-2_a)},
-
-        // scalar-vector multiply
-        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Highest()), Vec(2_a, 1_a)},
-        OverflowCase{ast::BinaryOp::kMultiply, Val(AInt::Lowest()), Vec(-2_a, 1_a)},
-
-        // vector-matrix multiply
-
-        // Overflow from first multiplication of dot product of vector and matrix column 0
-        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
-        //            ^
-        OverflowCase{ast::BinaryOp::kMultiply,       //
-                     Vec(AFloat::Highest(), 1.0_a),  //
-                     Mat({2.0_a, 1.0_a},             //
-                         {1.0_a, 1.0_a})},
-
-        // Overflow from second multiplication of dot product of vector and matrix column 0
-        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
-        //                             ^
-        OverflowCase{ast::BinaryOp::kMultiply,       //
-                     Vec(1.0_a, AFloat::Highest()),  //
-                     Mat({1.0_a, 2.0_a},             //
-                         {1.0_a, 1.0_a})},
-
-        // Overflow from addition of dot product of vector and matrix column 0
-        // i.e. (v[0] * m[0][0] + v[1] * m[0][1])
-        //                      ^
-        OverflowCase{ast::BinaryOp::kMultiply,                   //
-                     Vec(AFloat::Highest(), AFloat::Highest()),  //
-                     Mat({1.0_a, 1.0_a},                         //
-                         {1.0_a, 1.0_a})},
-
-        // matrix-matrix multiply
-
-        // Overflow from first multiplication of dot product of lhs row 0 and rhs column 0
-        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
-        //               ^
-        OverflowCase{ast::BinaryOp::kMultiply,        //
-                     Mat({AFloat::Highest(), 1.0_a},  //
-                         {1.0_a, 1.0_a}),             //
-                     Mat({2.0_a, 1.0_a},              //
-                         {1.0_a, 1.0_a})},
-
-        // Overflow from second multiplication of dot product of lhs row 0 and rhs column 0
-        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
-        //                                     ^
-        OverflowCase{ast::BinaryOp::kMultiply,        //
-                     Mat({1.0_a, AFloat::Highest()},  //
-                         {1.0_a, 1.0_a}),             //
-                     Mat({1.0_a, 1.0_a},              //
-                         {2.0_a, 1.0_a})},
-
-        // Overflow from addition of dot product of lhs row 0 and rhs column 0
-        // i.e. m1[0][0] * m2[0][0] + m1[0][1] * m[1][0]
-        //                          ^
-        OverflowCase{ast::BinaryOp::kMultiply,         //
-                     Mat({AFloat::Highest(), 1.0_a},   //
-                         {AFloat::Highest(), 1.0_a}),  //
-                     Mat({1.0_a, 1.0_a},               //
-                         {1.0_a, 1.0_a})},
-
-        // Divide by zero
-        OverflowCase{ast::BinaryOp::kDivide, Val(123_a), Val(0_a)},
-        OverflowCase{ast::BinaryOp::kDivide, Val(-123_a), Val(-0_a)},
-        OverflowCase{ast::BinaryOp::kDivide, Val(-123_a), Val(0_a)},
-        OverflowCase{ast::BinaryOp::kDivide, Val(123_a), Val(-0_a)},
-
-        // Most negative value divided by -1
-        OverflowCase{ast::BinaryOp::kDivide, Val(AInt::Lowest()), Val(-1_a)},
-
-        // ShiftLeft of AInts that result in values not representable as AInts.
-        // Note that for i32/u32, these would error because shift value is larger than 32.
-        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
-                     Val(AInt{BitValues<AInt>::All}),             //
-                     Val(AInt{BitValues<AInt>::NumBits})},        //
-        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
-                     Val(AInt{BitValues<AInt>::RightMost}),       //
-                     Val(AInt{BitValues<AInt>::NumBits})},        //
-        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
-                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
-                     Val(AInt{BitValues<AInt>::NumBits})},        //
-        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
-                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
-                     Val(AInt{BitValues<AInt>::NumBits + 1})},    //
-        OverflowCase{ast::BinaryOp::kShiftLeft,                   //
-                     Val(AInt{BitValues<AInt>::AllButLeftMost}),  //
-                     Val(AInt{BitValues<AInt>::NumBits + 1000})}
-
-        ));
-
-TEST_F(ResolverConstEvalTest, BinaryAbstractAddOverflow_AInt) {
-    GlobalConst("c", Add(Source{{1, 1}}, Expr(AInt::Highest()), 1_a));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "1:1 error: '9223372036854775807 + 1' cannot be represented as 'abstract-int'");
-}
-
-TEST_F(ResolverConstEvalTest, BinaryAbstractAddUnderflow_AInt) {
-    GlobalConst("c", Add(Source{{1, 1}}, Expr(AInt::Lowest()), -1_a));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "1:1 error: '-9223372036854775808 + -1' cannot be represented as 'abstract-int'");
-}
-
-TEST_F(ResolverConstEvalTest, BinaryAbstractAddOverflow_AFloat) {
-    GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Highest()), AFloat::Highest()));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(),
-              "1:1 error: '1.79769e+308 + 1.79769e+308' cannot be represented as 'abstract-float'");
-}
-
-TEST_F(ResolverConstEvalTest, BinaryAbstractAddUnderflow_AFloat) {
-    GlobalConst("c", Add(Source{{1, 1}}, Expr(AFloat::Lowest()), AFloat::Lowest()));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        "1:1 error: '-1.79769e+308 + -1.79769e+308' cannot be represented as 'abstract-float'");
-}
-
-// Mixed AInt and AFloat args to test implicit conversion to AFloat
-INSTANTIATE_TEST_SUITE_P(
-    AbstractMixed,
-    ResolverConstEvalBinaryOpTest,
-    testing::Combine(
-        testing::Values(ast::BinaryOp::kAdd),
-        testing::Values(C(Val(1_a), Val(2.3_a), Val(3.3_a)),
-                        C(Val(2.3_a), Val(1_a), Val(3.3_a)),
-                        C(Val(1_a), Vec(2.3_a, 2.3_a, 2.3_a), Vec(3.3_a, 3.3_a, 3.3_a)),
-                        C(Vec(2.3_a, 2.3_a, 2.3_a), Val(1_a), Vec(3.3_a, 3.3_a, 3.3_a)),
-                        C(Vec(2.3_a, 2.3_a, 2.3_a), Val(1_a), Vec(3.3_a, 3.3_a, 3.3_a)),
-                        C(Val(1_a), Vec(2.3_a, 2.3_a, 2.3_a), Vec(3.3_a, 3.3_a, 3.3_a)),
-                        C(Mat({1_a, 2_a},        //
-                              {1_a, 2_a},        //
-                              {1_a, 2_a}),       //
-                          Mat({1.2_a, 2.3_a},    //
-                              {1.2_a, 2.3_a},    //
-                              {1.2_a, 2.3_a}),   //
-                          Mat({2.2_a, 4.3_a},    //
-                              {2.2_a, 4.3_a},    //
-                              {2.2_a, 4.3_a})),  //
-                        C(Mat({1.2_a, 2.3_a},    //
-                              {1.2_a, 2.3_a},    //
-                              {1.2_a, 2.3_a}),   //
-                          Mat({1_a, 2_a},        //
-                              {1_a, 2_a},        //
-                              {1_a, 2_a}),       //
-                          Mat({2.2_a, 4.3_a},    //
-                              {2.2_a, 4.3_a},    //
-                              {2.2_a, 4.3_a}))   //
-                        )));
-
-// AInt left shift negative value -> error
-TEST_F(ResolverConstEvalTest, BinaryAbstractShiftLeftByNegativeValue_Error) {
-    GlobalConst("c", Shl(Source{{1, 1}}, Expr(1_a), Expr(-1_a)));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "1:1 error: cannot shift left by a negative value");
-}
-
-// i32/u32 left shift by >= 32 -> error
-using ResolverConstEvalShiftLeftConcreteGeqBitWidthError =
-    ResolverTestWithParam<std::tuple<Types, Types>>;
-TEST_P(ResolverConstEvalShiftLeftConcreteGeqBitWidthError, Test) {
-    auto* lhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
-    auto* rhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
-    GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(
-        r()->error(),
-        "1:1 error: shift left value must be less than the bit width of the lhs, which is 32");
-}
-INSTANTIATE_TEST_SUITE_P(Test,
-                         ResolverConstEvalShiftLeftConcreteGeqBitWidthError,
-                         testing::Values(                                 //
-                             std::make_tuple(Val(1_i), Val(32_u)),        //
-                             std::make_tuple(Val(1_i), Val(33_u)),        //
-                             std::make_tuple(Val(1_i), Val(34_u)),        //
-                             std::make_tuple(Val(1_i), Val(99999999_u)),  //
-                             std::make_tuple(Val(1_u), Val(32_u)),        //
-                             std::make_tuple(Val(1_u), Val(33_u)),        //
-                             std::make_tuple(Val(1_u), Val(34_u)),        //
-                             std::make_tuple(Val(1_u), Val(99999999_u))   //
-                             ));
-
-// AInt left shift results in sign change error
-using ResolverConstEvalShiftLeftSignChangeError = ResolverTestWithParam<std::tuple<Types, Types>>;
-TEST_P(ResolverConstEvalShiftLeftSignChangeError, Test) {
-    auto* lhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<0>(GetParam()));
-    auto* rhs_expr =
-        std::visit([&](auto&& value) { return value.Expr(*this); }, std::get<1>(GetParam()));
-    GlobalConst("c", Shl(Source{{1, 1}}, lhs_expr, rhs_expr));
-    EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), "1:1 error: shift left operation results in sign change");
-}
-template <typename T>
-std::vector<std::tuple<Types, Types>> ShiftLeftSignChangeErrorCases() {
-    // Shift type is u32 for non-abstract
-    using ST = std::conditional_t<IsAbstract<T>, T, u32>;
-    using B = BitValues<T>;
-    return {
-        {Val(T{0b0001}), Val(ST{B::NumBits - 1})},
-        {Val(T{0b0010}), Val(ST{B::NumBits - 2})},
-        {Val(T{0b0100}), Val(ST{B::NumBits - 3})},
-        {Val(T{0b1000}), Val(ST{B::NumBits - 4})},
-        {Val(T{0b0011}), Val(ST{B::NumBits - 2})},
-        {Val(T{0b0110}), Val(ST{B::NumBits - 3})},
-        {Val(T{0b1100}), Val(ST{B::NumBits - 4})},
-        {Val(B::AllButLeftMost), Val(ST{1})},
-        {Val(B::AllButLeftMost), Val(ST{B::NumBits - 1})},
-        {Val(B::LeftMost), Val(ST{1})},
-        {Val(B::LeftMost), Val(ST{B::NumBits - 1})},
-    };
-}
-INSTANTIATE_TEST_SUITE_P(Test,
-                         ResolverConstEvalShiftLeftSignChangeError,
-                         testing::ValuesIn(Concat(  //
-                             ShiftLeftSignChangeErrorCases<AInt>(),
-                             ShiftLeftSignChangeErrorCases<i32>(),
-                             ShiftLeftSignChangeErrorCases<u32>())));
-
-}  // namespace binary_op
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Builtin
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-namespace builtin {
-// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
-using resolver::operator<<;
-
-struct Case {
-    Case(utils::VectorRef<Types> in_args, Types in_expected)
-        : args(std::move(in_args)), expected(std::move(in_expected)) {}
-
-    /// Expected value may be positive or negative
-    Case& PosOrNeg() {
-        expected_pos_or_neg = true;
-        return *this;
-    }
-
-    /// Expected value should be compared using FLOAT_EQ instead of EQ
-    Case& FloatComp() {
-        float_compare = true;
-        return *this;
-    }
-
-    utils::Vector<Types, 8> args;
-    Types expected;
-    bool expected_pos_or_neg = false;
-    bool float_compare = false;
-};
-
-static std::ostream& operator<<(std::ostream& o, const Case& c) {
-    o << "args: ";
-    for (auto& a : c.args) {
-        o << a << ", ";
-    }
-    o << "expected: " << c.expected << ", expected_pos_or_neg: " << c.expected_pos_or_neg;
-    return o;
-}
-
-/// Creates a Case with Values for args and result
-static Case C(std::initializer_list<Types> args, Types result) {
-    return Case{utils::Vector<Types, 8>{args}, std::move(result)};
-}
-
-/// Convenience overload that creates a Case with just scalars
-using ScalarTypes = std::variant<AInt, AFloat, u32, i32, f32, f16>;
-static Case C(std::initializer_list<ScalarTypes> sargs, ScalarTypes sresult) {
-    utils::Vector<Types, 8> args;
-    for (auto& sa : sargs) {
-        std::visit([&](auto&& v) { return args.Push(Val(v)); }, sa);
-    }
-    Types result = Val(0_a);
-    std::visit([&](auto&& v) { result = Val(v); }, sresult);
-    return Case{std::move(args), std::move(result)};
-}
-
-using ResolverConstEvalBuiltinTest = ResolverTestWithParam<std::tuple<sem::BuiltinType, Case>>;
-
-TEST_P(ResolverConstEvalBuiltinTest, Test) {
-    Enable(ast::Extension::kF16);
-
-    auto builtin = std::get<0>(GetParam());
-    auto& c = std::get<1>(GetParam());
-
-    utils::Vector<const ast::Expression*, 8> args;
-    for (auto& a : c.args) {
-        std::visit([&](auto&& v) { args.Push(v.Expr(*this)); }, a);
-    }
-
-    std::visit(
-        [&](auto&& expected) {
-            using T = typename std::decay_t<decltype(expected)>::ElementType;
-            auto* expr = Call(sem::str(builtin), std::move(args));
-
-            GlobalConst("C", expr);
-            auto* expected_expr = expected.Expr(*this);
-            GlobalConst("E", expected_expr);
-
-            EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-            auto* sem = Sem().Get(expr);
-            const sem::Constant* value = sem->ConstantValue();
-            ASSERT_NE(value, nullptr);
-            EXPECT_TYPE(value->Type(), sem->Type());
-
-            auto* expected_sem = Sem().Get(expected_expr);
-            const sem::Constant* expected_value = expected_sem->ConstantValue();
-            ASSERT_NE(expected_value, nullptr);
-            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
-
-            ForEachElemPair(value, expected_value,
-                            [&](const sem::Constant* a, const sem::Constant* b) {
-                                auto v = a->As<T>();
-                                auto e = b->As<T>();
-                                if constexpr (std::is_same_v<bool, T>) {
-                                    EXPECT_EQ(v, e);
-                                } else if constexpr (IsFloatingPoint<T>) {
-                                    if (std::isnan(e)) {
-                                        EXPECT_TRUE(std::isnan(v));
-                                    } else {
-                                        auto vf = (c.expected_pos_or_neg ? Abs(v) : v);
-                                        if (c.float_compare) {
-                                            EXPECT_FLOAT_EQ(vf, e);
-                                        } else {
-                                            EXPECT_EQ(vf, e);
-                                        }
-                                    }
-                                } else {
-                                    EXPECT_EQ((c.expected_pos_or_neg ? Abs(v) : v), e);
-                                    // Check that the constant's integer doesn't contain unexpected
-                                    // data in the MSBs that are outside of the bit-width of T.
-                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
-                                }
-                                return HasFailure() ? Action::kStop : Action::kContinue;
-                            });
-        },
-        c.expected);
-}
-
-INSTANTIATE_TEST_SUITE_P(  //
-    MixedAbstractArgs,
-    ResolverConstEvalBuiltinTest,
-    testing::Combine(testing::Values(sem::BuiltinType::kAtan2),
-                     testing::ValuesIn(std::vector{
-                         C({0_a, -0.0_a}, kPi<AFloat>),
-                         C({1.0_a, 0_a}, kPiOver2<AFloat>),
-                     })));
-
-template <typename T, bool finite_only>
-std::vector<Case> Atan2Cases() {
-    std::vector<Case> cases = {
-        // If y is +/-0 and x is negative or -0, +/-PI is returned
-        C({T(0.0), -T(0.0)}, kPi<T>).PosOrNeg().FloatComp(),
-
-        // If y is +/-0 and x is positive or +0, +/-0 is returned
-        C({T(0.0), T(0.0)}, T(0.0)).PosOrNeg(),
-
-        // If x is +/-0 and y is negative, -PI/2 is returned
-        C({-T(1.0), T(0.0)}, -kPiOver2<T>).FloatComp(),  //
-        C({-T(1.0), -T(0.0)}, -kPiOver2<T>).FloatComp(),
-
-        // If x is +/-0 and y is positive, +PI/2 is returned
-        C({T(1.0), T(0.0)}, kPiOver2<T>).FloatComp(),  //
-        C({T(1.0), -T(0.0)}, kPiOver2<T>).FloatComp(),
-
-        // Vector tests
-        C({Vec(T(0.0), T(0.0)), Vec(-T(0.0), T(0.0))}, Vec(kPi<T>, T(0.0))).PosOrNeg().FloatComp(),
-        C({Vec(-T(1.0), -T(1.0)), Vec(T(0.0), -T(0.0))}, Vec(-kPiOver2<T>, -kPiOver2<T>))
-            .FloatComp(),
-        C({Vec(T(1.0), T(1.0)), Vec(T(0.0), -T(0.0))}, Vec(kPiOver2<T>, kPiOver2<T>)).FloatComp(),
-    };
-
-    if constexpr (!finite_only) {
-        std::vector<Case> non_finite_cases = {
-            // If y is +/-INF and x is finite, +/-PI/2 is returned
-            C({T::Inf(), T(0.0)}, kPiOver2<T>).PosOrNeg().FloatComp(),
-            C({-T::Inf(), T(0.0)}, kPiOver2<T>).PosOrNeg().FloatComp(),
-
-            // If y is +/-INF and x is -INF, +/-3PI/4 is returned
-            C({T::Inf(), -T::Inf()}, k3PiOver4<T>).PosOrNeg().FloatComp(),
-            C({-T::Inf(), -T::Inf()}, k3PiOver4<T>).PosOrNeg().FloatComp(),
-
-            // If y is +/-INF and x is +INF, +/-PI/4 is returned
-            C({T::Inf(), T::Inf()}, kPiOver4<T>).PosOrNeg().FloatComp(),
-            C({-T::Inf(), T::Inf()}, kPiOver4<T>).PosOrNeg().FloatComp(),
-
-            // If x is -INF and y is finite and positive, +PI is returned
-            C({T(0.0), -T::Inf()}, kPi<T>).FloatComp(),
-
-            // If x is -INF and y is finite and negative, -PI is returned
-            C({-T(0.0), -T::Inf()}, -kPi<T>).FloatComp(),
-
-            // If x is +INF and y is finite and positive, +0 is returned
-            C({T(0.0), T::Inf()}, T(0.0)),
-
-            // If x is +INF and y is finite and negative, -0 is returned
-            C({-T(0.0), T::Inf()}, -T(0.0)),
-
-            // If either x is NaN or y is NaN, NaN is returned
-            C({T::NaN(), T(0.0)}, T::NaN()),
-            C({T(0.0), T::NaN()}, T::NaN()),
-            C({T::NaN(), T::NaN()}, T::NaN()),
-
-            // Vector tests
-            C({Vec(T::Inf(), -T::Inf(), T::Inf(), -T::Inf()),  //
-               Vec(T(0.0), T(0.0), -T::Inf(), -T::Inf())},     //
-              Vec(kPiOver2<T>, kPiOver2<T>, k3PiOver4<T>, k3PiOver4<T>))
-                .PosOrNeg()
-                .FloatComp(),
-        };
-        cases = Concat(cases, non_finite_cases);
-    }
-
-    return cases;
-}
-INSTANTIATE_TEST_SUITE_P(  //
-    Atan2,
-    ResolverConstEvalBuiltinTest,
-    testing::Combine(testing::Values(sem::BuiltinType::kAtan2),
-                     testing::ValuesIn(Concat(Atan2Cases<AFloat, true>(),  //
-                                              Atan2Cases<f32, false>(),
-                                              Atan2Cases<f16, false>()))));
-
-template <typename T>
-std::vector<Case> ClampCases() {
-    return {
-        C({T(0), T(0), T(0)}, T(0)),
-        C({T(0), T(42), T::Highest()}, T(42)),
-        C({T::Lowest(), T(0), T(42)}, T(0)),
-        C({T(0), T::Lowest(), T::Highest()}, T(0)),
-        C({T(0), T::Highest(), T::Lowest()}, T::Lowest()),
-        C({T::Highest(), T::Highest(), T::Highest()}, T::Highest()),
-        C({T::Lowest(), T::Lowest(), T::Lowest()}, T::Lowest()),
-        C({T::Highest(), T::Lowest(), T::Highest()}, T::Highest()),
-        C({T::Lowest(), T::Lowest(), T::Highest()}, T::Lowest()),
-
-        // Vector tests
-        C({Vec(T(0), T(0)),                         //
-           Vec(T(0), T(42)),                        //
-           Vec(T(0), T::Highest())},                //
-          Vec(T(0), T(42))),                        //
-        C({Vec(T::Lowest(), T(0), T(0)),            //
-           Vec(T(0), T::Lowest(), T::Highest()),    //
-           Vec(T(42), T::Highest(), T::Lowest())},  //
-          Vec(T(0), T(0), T::Lowest())),
-    };
-}
-INSTANTIATE_TEST_SUITE_P(  //
-    Clamp,
-    ResolverConstEvalBuiltinTest,
-    testing::Combine(testing::Values(sem::BuiltinType::kClamp),
-                     testing::ValuesIn(Concat(ClampCases<AInt>(),  //
-                                              ClampCases<i32>(),
-                                              ClampCases<u32>(),
-                                              ClampCases<AFloat>(),
-                                              ClampCases<f32>(),
-                                              ClampCases<f16>()))));
-
-template <typename T>
-std::vector<Case> SelectCases() {
-    return {
-        C({Val(T{1}), Val(T{2}), Val(false)}, Val(T{1})),
-        C({Val(T{1}), Val(T{2}), Val(true)}, Val(T{2})),
-
-        C({Val(T{2}), Val(T{1}), Val(false)}, Val(T{2})),
-        C({Val(T{2}), Val(T{1}), Val(true)}, Val(T{1})),
-
-        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(false, false)}, Vec(T{1}, T{2})),
-        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(false, true)}, Vec(T{1}, T{4})),
-        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(true, false)}, Vec(T{3}, T{2})),
-        C({Vec(T{1}, T{2}), Vec(T{3}, T{4}), Vec(true, true)}, Vec(T{3}, T{4})),
-
-        C({Vec(T{1}, T{1}, T{2}, T{2}),     //
-           Vec(T{2}, T{2}, T{1}, T{1}),     //
-           Vec(false, true, false, true)},  //
-          Vec(T{1}, T{2}, T{2}, T{1})),     //
-    };
-}
-static std::vector<Case> SelectBoolCases() {
-    return {
-        C({Val(true), Val(false), Val(false)}, Val(true)),
-        C({Val(true), Val(false), Val(true)}, Val(false)),
-
-        C({Val(false), Val(true), Val(true)}, Val(true)),
-        C({Val(false), Val(true), Val(false)}, Val(false)),
-
-        C({Vec(true, true, false, false),   //
-           Vec(false, false, true, true),   //
-           Vec(false, true, true, false)},  //
-          Vec(true, false, true, false)),   //
-    };
-}
-INSTANTIATE_TEST_SUITE_P(  //
-    Select,
-    ResolverConstEvalBuiltinTest,
-    testing::Combine(testing::Values(sem::BuiltinType::kSelect),
-                     testing::ValuesIn(Concat(SelectCases<AInt>(),  //
-                                              SelectCases<i32>(),
-                                              SelectCases<u32>(),
-                                              SelectCases<AFloat>(),
-                                              SelectCases<f32>(),
-                                              SelectCases<f16>(),
-                                              SelectBoolCases()))));
-
-}  // namespace builtin
-
-}  // namespace
-}  // namespace tint::resolver
diff --git a/src/tint/resolver/const_eval_test.h b/src/tint/resolver/const_eval_test.h
new file mode 100644
index 0000000..3840540
--- /dev/null
+++ b/src/tint/resolver/const_eval_test.h
@@ -0,0 +1,292 @@
+// 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_RESOLVER_CONST_EVAL_TEST_H_
+#define SRC_TINT_RESOLVER_CONST_EVAL_TEST_H_
+
+#include <limits>
+#include <utility>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/tint/resolver/resolver_test_helper.h"
+#include "src/tint/sem/test_helper.h"
+
+namespace tint::resolver {
+
+template <typename T>
+inline const auto kPi = T(UnwrapNumber<T>(3.14159265358979323846));
+
+template <typename T>
+inline const auto kPiOver2 = T(UnwrapNumber<T>(1.57079632679489661923));
+
+template <typename T>
+inline const auto kPiOver4 = T(UnwrapNumber<T>(0.785398163397448309616));
+
+template <typename T>
+inline const auto k3PiOver4 = T(UnwrapNumber<T>(2.356194490192344928846));
+
+/// Walks the sem::Constant @p c, accumulating all the inner-most scalar values into @p args
+inline 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.
+inline builder::ScalarArgs ScalarArgsFrom(const sem::Constant* c) {
+    builder::ScalarArgs out;
+    CollectScalarArgs(c, out);
+    return out;
+}
+
+template <typename T>
+inline constexpr auto Negate(const Number<T>& v) {
+    if constexpr (std::is_integral_v<T>) {
+        if constexpr (std::is_signed_v<T>) {
+            // For signed integrals, avoid C++ UB by not negating the smallest negative number. In
+            // WGSL, this operation is well defined to return the same value, see:
+            // https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr.
+            if (v == std::numeric_limits<T>::min()) {
+                return v;
+            }
+            return -v;
+
+        } else {
+            // Allow negating unsigned values
+            using ST = std::make_signed_t<T>;
+            auto as_signed = Number<ST>{static_cast<ST>(v)};
+            return Number<T>{static_cast<T>(Negate(as_signed))};
+        }
+    } else {
+        // float case
+        return -v;
+    }
+}
+
+template <typename T>
+inline auto Abs(const Number<T>& v) {
+    if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>) {
+        return v;
+    } else {
+        return Number<T>(std::abs(v));
+    }
+}
+
+TINT_BEGIN_DISABLE_WARNING(CONSTANT_OVERFLOW);
+template <typename T>
+inline constexpr Number<T> Mul(Number<T> v1, Number<T> v2) {
+    if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
+        // For signed integrals, avoid C++ UB by multiplying as unsigned
+        using UT = std::make_unsigned_t<T>;
+        return static_cast<Number<T>>(static_cast<UT>(v1) * static_cast<UT>(v2));
+    } else {
+        return static_cast<Number<T>>(v1 * v2);
+    }
+}
+TINT_END_DISABLE_WARNING(CONSTANT_OVERFLOW);
+
+// Concats any number of std::vectors
+template <typename Vec, typename... Vecs>
+[[nodiscard]] inline auto Concat(Vec&& v1, Vecs&&... vs) {
+    auto total_size = v1.size() + (vs.size() + ...);
+    v1.reserve(total_size);
+    (std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
+    return std::move(v1);
+}
+
+// Concats vectors `vs` into `v1`
+template <typename Vec, typename... Vecs>
+inline void ConcatInto(Vec& v1, Vecs&&... vs) {
+    auto total_size = v1.size() + (vs.size() + ...);
+    v1.reserve(total_size);
+    (std::move(vs.begin(), vs.end(), std::back_inserter(v1)), ...);
+}
+
+// Concats vectors `vs` into `v1` iff `condition` is true
+template <bool condition, typename Vec, typename... Vecs>
+inline void ConcatIntoIf([[maybe_unused]] Vec& v1, [[maybe_unused]] Vecs&&... vs) {
+    if constexpr (condition) {
+        ConcatInto(v1, std::forward<Vecs>(vs)...);
+    }
+}
+
+using builder::IsValue;
+using builder::Mat;
+using builder::Val;
+using builder::Value;
+using builder::Vec;
+
+using Types = std::variant<  //
+    Value<AInt>,
+    Value<AFloat>,
+    Value<u32>,
+    Value<i32>,
+    Value<f32>,
+    Value<f16>,
+    Value<bool>,
+
+    Value<builder::vec2<AInt>>,
+    Value<builder::vec2<AFloat>>,
+    Value<builder::vec2<u32>>,
+    Value<builder::vec2<i32>>,
+    Value<builder::vec2<f32>>,
+    Value<builder::vec2<f16>>,
+    Value<builder::vec2<bool>>,
+
+    Value<builder::vec3<AInt>>,
+    Value<builder::vec3<AFloat>>,
+    Value<builder::vec3<u32>>,
+    Value<builder::vec3<i32>>,
+    Value<builder::vec3<f32>>,
+    Value<builder::vec3<f16>>,
+    Value<builder::vec3<bool>>,
+
+    Value<builder::vec4<AInt>>,
+    Value<builder::vec4<AFloat>>,
+    Value<builder::vec4<u32>>,
+    Value<builder::vec4<i32>>,
+    Value<builder::vec4<f32>>,
+    Value<builder::vec4<f16>>,
+    Value<builder::vec4<bool>>,
+
+    Value<builder::mat2x2<AInt>>,
+    Value<builder::mat2x2<AFloat>>,
+    Value<builder::mat2x2<f32>>,
+    Value<builder::mat2x2<f16>>,
+
+    Value<builder::mat2x3<AInt>>,
+    Value<builder::mat2x3<AFloat>>,
+    Value<builder::mat2x3<f32>>,
+    Value<builder::mat2x3<f16>>,
+
+    Value<builder::mat3x2<AInt>>,
+    Value<builder::mat3x2<AFloat>>,
+    Value<builder::mat3x2<f32>>,
+    Value<builder::mat3x2<f16>>
+    //
+    >;
+
+inline std::ostream& operator<<(std::ostream& o, const Types& types) {
+    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 << ")";
+        },
+        types);
+    return o;
+}
+
+// Calls `f` on deepest elements of both `a` and `b`. If function returns Action::kStop, it stops
+// traversing, and return Action::kStop; if the function returns Action::kContinue, it continues and
+// returns Action::kContinue when done.
+// TODO(amaiorano): Move to Constant.h?
+enum class Action { kStop, kContinue };
+template <typename Func>
+inline Action ForEachElemPair(const sem::Constant* a, const sem::Constant* b, Func&& f) {
+    EXPECT_EQ(a->Type(), b->Type());
+    size_t i = 0;
+    while (true) {
+        auto* a_elem = a->Index(i);
+        if (!a_elem) {
+            break;
+        }
+        auto* b_elem = b->Index(i);
+        if (ForEachElemPair(a_elem, b_elem, f) == Action::kStop) {
+            return Action::kStop;
+        }
+        i++;
+    }
+    if (i == 0) {
+        return f(a, b);
+    }
+    return Action::kContinue;
+}
+
+/// Defines common bit value patterns for the input `NumberT` type used for testing.
+template <typename NumberT>
+struct BitValues {
+    /// The unwrapped number type
+    using T = UnwrapNumber<NumberT>;
+    /// Details
+    struct detail {
+        /// Unsigned type of `T`
+        using UT = std::make_unsigned_t<T>;
+        /// Size in bits of type T
+        static constexpr size_t NumBits = sizeof(T) * 8;
+        /// All bits set 1
+        static constexpr T All = T{~T{0}};
+        /// Only left-most bits set to 1, rest set to 0
+        static constexpr T LeftMost = static_cast<T>(UT{1} << (NumBits - 1u));
+        /// Only left-most bits set to 0, rest set to 1
+        static constexpr T AllButLeftMost = T{~LeftMost};
+        /// Only two left-most bits set to 1, rest set to 0
+        static constexpr T TwoLeftMost = static_cast<T>(UT{0b11} << (NumBits - 2u));
+        /// Only two left-most bits set to 0, rest set to 1
+        static constexpr T AllButTwoLeftMost = T{~TwoLeftMost};
+        /// Only right-most bit set to 1, rest set to 0
+        static constexpr T RightMost = T{1};
+        /// Only right-most bit set to 0, rest set to 1
+        static constexpr T AllButRightMost = T{~RightMost};
+    };
+
+    /// Size in bits of type NumberT
+    static inline const size_t NumBits = detail::NumBits;
+    /// All bits set 1
+    static inline const NumberT All = NumberT{detail::All};
+    /// Only left-most bits set to 1, rest set to 0
+    static inline const NumberT LeftMost = NumberT{detail::LeftMost};
+    /// Only left-most bits set to 0, rest set to 1
+    static inline const NumberT AllButLeftMost = NumberT{detail::AllButLeftMost};
+    /// Only two left-most bits set to 1, rest set to 0
+    static inline const NumberT TwoLeftMost = NumberT{detail::TwoLeftMost};
+    /// Only two left-most bits set to 0, rest set to 1
+    static inline const NumberT AllButTwoLeftMost = NumberT{detail::AllButTwoLeftMost};
+    /// Only right-most bit set to 1, rest set to 0
+    static inline const NumberT RightMost = NumberT{detail::RightMost};
+    /// Only right-most bit set to 0, rest set to 1
+    static inline const NumberT AllButRightMost = NumberT{detail::AllButRightMost};
+
+    /// Performs a left-shift of `val` by `shiftBy`, both of varying type cast to `T`.
+    /// @param val value to shift left
+    /// @param shiftBy number of bits to shift left by
+    /// @returns the shifted value
+    template <typename U, typename V>
+    static constexpr NumberT Lsh(U val, V shiftBy) {
+        return NumberT{T{val} << T{shiftBy}};
+    }
+};
+
+using ResolverConstEvalTest = ResolverTest;
+
+}  // namespace tint::resolver
+
+#endif  // SRC_TINT_RESOLVER_CONST_EVAL_TEST_H_
diff --git a/src/tint/resolver/const_eval_unary_op_test.cc b/src/tint/resolver/const_eval_unary_op_test.cc
new file mode 100644
index 0000000..80b8caa
--- /dev/null
+++ b/src/tint/resolver/const_eval_unary_op_test.cc
@@ -0,0 +1,189 @@
+// 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/resolver/const_eval_test.h"
+
+using namespace tint::number_suffixes;  // NOLINT
+
+namespace tint::resolver {
+namespace {
+
+// Bring in std::ostream& operator<<(std::ostream& o, const Types& types)
+using resolver::operator<<;
+
+struct Case {
+    Types input;
+    Types expected;
+};
+
+static std::ostream& operator<<(std::ostream& o, const Case& c) {
+    o << "input: " << c.input << ", expected: " << c.expected;
+    return o;
+}
+
+/// Creates a Case with Values of any type
+template <typename T, typename U>
+Case C(Value<T> input, Value<U> expected) {
+    return Case{std::move(input), std::move(expected)};
+}
+
+/// Convenience overload to creates a Case with just scalars
+template <typename T, typename U, typename = std::enable_if_t<!IsValue<T>>>
+Case C(T input, U expected) {
+    return Case{Val(input), Val(expected)};
+}
+
+using ResolverConstEvalUnaryOpTest = ResolverTestWithParam<std::tuple<ast::UnaryOp, Case>>;
+
+TEST_P(ResolverConstEvalUnaryOpTest, Test) {
+    Enable(ast::Extension::kF16);
+
+    auto op = std::get<0>(GetParam());
+    auto& c = std::get<1>(GetParam());
+    std::visit(
+        [&](auto&& expected) {
+            using T = typename std::decay_t<decltype(expected)>::ElementType;
+
+            auto* input_expr = std::visit([&](auto&& value) { return value.Expr(*this); }, c.input);
+            auto* expr = create<ast::UnaryOpExpression>(op, input_expr);
+
+            GlobalConst("C", expr);
+            auto* expected_expr = expected.Expr(*this);
+            GlobalConst("E", expected_expr);
+            ASSERT_TRUE(r()->Resolve()) << r()->error();
+
+            auto* sem = Sem().Get(expr);
+            const sem::Constant* value = sem->ConstantValue();
+            ASSERT_NE(value, nullptr);
+            EXPECT_TYPE(value->Type(), sem->Type());
+
+            auto* expected_sem = Sem().Get(expected_expr);
+            const sem::Constant* expected_value = expected_sem->ConstantValue();
+            ASSERT_NE(expected_value, nullptr);
+            EXPECT_TYPE(expected_value->Type(), expected_sem->Type());
+
+            ForEachElemPair(value, expected_value,
+                            [&](const sem::Constant* a, const sem::Constant* b) {
+                                EXPECT_EQ(a->As<T>(), b->As<T>());
+                                if constexpr (IsIntegral<T>) {
+                                    // Check that the constant's integer doesn't contain unexpected
+                                    // data in the MSBs that are outside of the bit-width of T.
+                                    EXPECT_EQ(a->As<AInt>(), b->As<AInt>());
+                                }
+                                return HasFailure() ? Action::kStop : Action::kContinue;
+                            });
+        },
+        c.expected);
+}
+INSTANTIATE_TEST_SUITE_P(Complement,
+                         ResolverConstEvalUnaryOpTest,
+                         testing::Combine(testing::Values(ast::UnaryOp::kComplement),
+                                          testing::ValuesIn({
+                                              // AInt
+                                              C(0_a, 0xffffffffffffffff_a),
+                                              C(0xffffffffffffffff_a, 0_a),
+                                              C(0xf0f0f0f0f0f0f0f0_a, 0x0f0f0f0f0f0f0f0f_a),
+                                              C(0xaaaaaaaaaaaaaaaa_a, 0x5555555555555555_a),
+                                              C(0x5555555555555555_a, 0xaaaaaaaaaaaaaaaa_a),
+                                              // u32
+                                              C(0_u, 0xffffffff_u),
+                                              C(0xffffffff_u, 0_u),
+                                              C(0xf0f0f0f0_u, 0x0f0f0f0f_u),
+                                              C(0xaaaaaaaa_u, 0x55555555_u),
+                                              C(0x55555555_u, 0xaaaaaaaa_u),
+                                              // i32
+                                              C(0_i, -1_i),
+                                              C(-1_i, 0_i),
+                                              C(1_i, -2_i),
+                                              C(-2_i, 1_i),
+                                              C(2_i, -3_i),
+                                              C(-3_i, 2_i),
+                                          })));
+
+INSTANTIATE_TEST_SUITE_P(Negation,
+                         ResolverConstEvalUnaryOpTest,
+                         testing::Combine(testing::Values(ast::UnaryOp::kNegation),
+                                          testing::ValuesIn({
+                                              // AInt
+                                              C(0_a, -0_a),
+                                              C(-0_a, 0_a),
+                                              C(1_a, -1_a),
+                                              C(-1_a, 1_a),
+                                              C(AInt::Highest(), -AInt::Highest()),
+                                              C(-AInt::Highest(), AInt::Highest()),
+                                              C(AInt::Lowest(), Negate(AInt::Lowest())),
+                                              C(Negate(AInt::Lowest()), AInt::Lowest()),
+                                              // i32
+                                              C(0_i, -0_i),
+                                              C(-0_i, 0_i),
+                                              C(1_i, -1_i),
+                                              C(-1_i, 1_i),
+                                              C(i32::Highest(), -i32::Highest()),
+                                              C(-i32::Highest(), i32::Highest()),
+                                              C(i32::Lowest(), Negate(i32::Lowest())),
+                                              C(Negate(i32::Lowest()), i32::Lowest()),
+                                              // AFloat
+                                              C(0.0_a, -0.0_a),
+                                              C(-0.0_a, 0.0_a),
+                                              C(1.0_a, -1.0_a),
+                                              C(-1.0_a, 1.0_a),
+                                              C(AFloat::Highest(), -AFloat::Highest()),
+                                              C(-AFloat::Highest(), AFloat::Highest()),
+                                              C(AFloat::Lowest(), Negate(AFloat::Lowest())),
+                                              C(Negate(AFloat::Lowest()), AFloat::Lowest()),
+                                              // f32
+                                              C(0.0_f, -0.0_f),
+                                              C(-0.0_f, 0.0_f),
+                                              C(1.0_f, -1.0_f),
+                                              C(-1.0_f, 1.0_f),
+                                              C(f32::Highest(), -f32::Highest()),
+                                              C(-f32::Highest(), f32::Highest()),
+                                              C(f32::Lowest(), Negate(f32::Lowest())),
+                                              C(Negate(f32::Lowest()), f32::Lowest()),
+                                              // f16
+                                              C(0.0_h, -0.0_h),
+                                              C(-0.0_h, 0.0_h),
+                                              C(1.0_h, -1.0_h),
+                                              C(-1.0_h, 1.0_h),
+                                              C(f16::Highest(), -f16::Highest()),
+                                              C(-f16::Highest(), f16::Highest()),
+                                              C(f16::Lowest(), Negate(f16::Lowest())),
+                                              C(Negate(f16::Lowest()), f16::Lowest()),
+                                          })));
+
+// Make sure UBSan doesn't trip on C++'s undefined behaviour of negating the smallest negative
+// number.
+TEST_F(ResolverConstEvalTest, UnaryNegateLowestAbstract) {
+    // const break_me = -(-9223372036854775808);
+    auto* c = GlobalConst("break_me", Negation(Negation(Expr(9223372036854775808_a))));
+    (void)c;
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    auto* sem = Sem().Get(c);
+    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 9223372036854775808_a);
+}
+
+INSTANTIATE_TEST_SUITE_P(Not,
+                         ResolverConstEvalUnaryOpTest,
+                         testing::Combine(testing::Values(ast::UnaryOp::kNot),
+                                          testing::ValuesIn({
+                                              C(true, false),
+                                              C(false, true),
+                                              C(Vec(true, true), Vec(false, false)),
+                                              C(Vec(true, false), Vec(false, true)),
+                                              C(Vec(false, true), Vec(true, false)),
+                                              C(Vec(false, false), Vec(true, true)),
+                                          })));
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/resolver/entry_point_validation_test.cc b/src/tint/resolver/entry_point_validation_test.cc
index 2a9f25b..79b41d7 100644
--- a/src/tint/resolver/entry_point_validation_test.cc
+++ b/src/tint/resolver/entry_point_validation_test.cc
@@ -170,7 +170,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
 13:43 note: previously consumed location(0)
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_MemberMissingAttribute) {
@@ -198,7 +198,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(14:52 error: missing entry point IO attribute
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ReturnType_Struct_DuplicateBuiltins) {
@@ -227,7 +227,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(12:34 error: builtin(frag_depth) attribute appears multiple times as pipeline output
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, ParameterAttribute_Location) {
@@ -339,7 +339,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: multiple entry point IO attributes
 13:43 note: previously consumed location(0)
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_Struct_MemberMissingAttribute) {
@@ -366,7 +366,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(), R"(14:52 error: missing entry point IO attribute
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, Parameter_DuplicateBuiltins) {
@@ -432,7 +432,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(12:34 error: builtin(sample_index) attribute appears multiple times as pipeline input
-12:34 note: while analysing entry point 'main')");
+12:34 note: while analyzing entry point 'main')");
 }
 
 TEST_F(ResolverEntryPointValidationTest, VertexShaderMustReturnPosition) {
@@ -873,7 +873,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               "14:52 error: nested structures cannot be used for entry point IO\n"
-              "12:34 note: while analysing entry point 'main'");
+              "12:34 note: while analyzing entry point 'main'");
 }
 
 TEST_F(LocationAttributeTests, BadType_Input_Struct_RuntimeArray) {
@@ -1059,7 +1059,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               "14:52 error: nested structures cannot be used for entry point IO\n"
-              "12:34 note: while analysing entry point 'main'");
+              "12:34 note: while analyzing entry point 'main'");
 }
 
 TEST_F(LocationAttributeTests, ReturnType_Struct_RuntimeArray) {
@@ -1140,7 +1140,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               "12:34 error: attribute is not valid for compute shader output\n"
-              "56:78 note: while analysing entry point 'main'");
+              "56:78 note: while analyzing entry point 'main'");
 }
 
 TEST_F(LocationAttributeTests, ComputeShaderLocationStructMember_Input) {
@@ -1159,7 +1159,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               "12:34 error: attribute is not valid for compute shader inputs\n"
-              "56:78 note: while analysing entry point 'main'");
+              "56:78 note: while analyzing entry point 'main'");
 }
 
 TEST_F(LocationAttributeTests, Duplicate_input) {
@@ -1219,7 +1219,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               "34:56 error: location(1) attribute appears multiple times\n"
-              "12:34 note: while analysing entry point 'main'");
+              "12:34 note: while analyzing entry point 'main'");
 }
 
 }  // namespace
diff --git a/src/tint/resolver/function_validation_test.cc b/src/tint/resolver/function_validation_test.cc
index 2fddc09..9fb0da4 100644
--- a/src/tint/resolver/function_validation_test.cc
+++ b/src/tint/resolver/function_validation_test.cc
@@ -672,7 +672,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        "12:34 error: workgroup_size argument must be a constant or override expression of type "
+        "12:34 error: workgroup_size argument must be a constant or override-expression of type "
         "abstract-integer, i32 or u32");
 }
 
@@ -718,7 +718,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(
         r()->error(),
-        "12:34 error: workgroup_size argument must be a constant or override expression of type "
+        "12:34 error: workgroup_size argument must be a constant or override-expression of type "
         "abstract-integer, i32 or u32");
 }
 
@@ -767,6 +767,77 @@
     EXPECT_EQ(r()->error(), "12:34 error: workgroup_size argument must be at least 1");
 }
 
+TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_0x100_0x100) {
+    // @compute @workgroup_size(0x10000, 0x100, 0x100)
+    // fn main() {}
+    Func("main", utils::Empty, ty.void_(), utils::Empty,
+         utils::Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(0x10000_a, 0x100_a, Expr(Source{{12, 34}}, 0x100_a)),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+}
+
+TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_0x10000) {
+    // @compute @workgroup_size(0x10000, 0x10000)
+    // fn main() {}
+    Func("main", utils::Empty, ty.void_(), utils::Empty,
+         utils::Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(0x10000_a, Expr(Source{{12, 34}}, 0x10000_a)),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+}
+
+TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_C_0x10000) {
+    // const C = 1;
+    // @compute @workgroup_size(0x10000, C, 0x10000)
+    // fn main() {}
+    GlobalConst("C", ty.u32(), Expr(1_a));
+    Func("main", utils::Empty, ty.void_(), utils::Empty,
+         utils::Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(0x10000_a, "C", Expr(Source{{12, 34}}, 0x10000_a)),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+}
+
+TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_C) {
+    // const C = 0x10000;
+    // @compute @workgroup_size(0x10000, C)
+    // fn main() {}
+    GlobalConst("C", ty.u32(), Expr(0x10000_a));
+    Func("main", utils::Empty, ty.void_(), utils::Empty,
+         utils::Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(0x10000_a, Expr(Source{{12, 34}}, "C")),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+}
+
+TEST_F(ResolverFunctionValidationTest, WorkgroupSize_OverflowsU32_0x10000_O_0x10000) {
+    // override O = 0;
+    // @compute @workgroup_size(0x10000, O, 0x10000)
+    // fn main() {}
+    Override("O", ty.u32(), Expr(0_a));
+    Func("main", utils::Empty, ty.void_(), utils::Empty,
+         utils::Vector{
+             Stage(ast::PipelineStage::kCompute),
+             WorkgroupSize(0x10000_a, "O", Expr(Source{{12, 34}}, 0x10000_a)),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "12:34 error: total workgroup grid size cannot exceed 0xffffffff");
+}
+
 TEST_F(ResolverFunctionValidationTest, WorkgroupSize_NonConst) {
     // var<private> x = 64i;
     // @compute @workgroup_size(x)
@@ -780,7 +851,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override expression of "
+              "12:34 error: workgroup_size argument must be a constant or override-expression of "
               "type abstract-integer, i32 or u32");
 }
 
@@ -795,7 +866,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override expression of "
+              "12:34 error: workgroup_size argument must be a constant or override-expression of "
               "type abstract-integer, i32 or u32");
 }
 
@@ -810,7 +881,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override expression of "
+              "12:34 error: workgroup_size argument must be a constant or override-expression of "
               "type abstract-integer, i32 or u32");
 }
 
@@ -825,7 +896,7 @@
 
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: workgroup_size argument must be a constant or override expression of "
+              "12:34 error: workgroup_size argument must be a constant or override-expression of "
               "type abstract-integer, i32 or u32");
 }
 
diff --git a/src/tint/resolver/host_shareable_validation_test.cc b/src/tint/resolver/host_shareable_validation_test.cc
index bd8f3ca..97519cd 100644
--- a/src/tint/resolver/host_shareable_validation_test.cc
+++ b/src/tint/resolver/host_shareable_validation_test.cc
@@ -36,7 +36,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(56:78 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
-12:34 note: while analysing structure member S.x
+12:34 note: while analyzing structure member S.x
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -51,7 +51,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(56:78 error: Type 'vec3<bool>' cannot be used in address space 'storage' as it is non-host-shareable
-12:34 note: while analysing structure member S.x
+12:34 note: while analyzing structure member S.x
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -67,7 +67,7 @@
     EXPECT_EQ(
         r()->error(),
         R"(56:78 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
-12:34 note: while analysing structure member S.x
+12:34 note: while analyzing structure member S.x
 56:78 note: while instantiating 'var' g)");
 }
 
@@ -86,10 +86,10 @@
     EXPECT_EQ(
         r()->error(),
         R"(9:10 error: Type 'bool' cannot be used in address space 'storage' as it is non-host-shareable
-1:2 note: while analysing structure member I1.x
-3:4 note: while analysing structure member I2.y
-5:6 note: while analysing structure member I3.z
-7:8 note: while analysing structure member S.m
+1:2 note: while analyzing structure member I1.x
+3:4 note: while analyzing structure member I2.y
+5:6 note: while analyzing structure member I3.z
+7:8 note: while analyzing structure member S.m
 9:10 note: while instantiating 'var' g)");
 }
 
diff --git a/src/tint/resolver/intrinsic_table.cc b/src/tint/resolver/intrinsic_table.cc
index a82f9e8..6a190de 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::kInvalid, p.usage));
+                ast::Access::kUndefined, 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::kInvalid, p.usage));
+                ast::Access::kUndefined, 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::kInvalid,
-                                                    match.parameters[0].usage);
+        auto param = builder.create<sem::Parameter>(
+            nullptr, 0u, match.parameters[0].type, ast::AddressSpace::kNone,
+            ast::Access::kUndefined, 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);
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 0fe6691..b8ba0fe 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::Access::kInvalid) {
+                if (access == ast::Access::kUndefined) {
                     access = DefaultAccessForAddressSpace(t->address_space);
                 }
                 return builder_->create<sem::Pointer>(el, t->address_space, access);
@@ -386,12 +386,13 @@
     sem::Variable* sem = nullptr;
     if (is_global) {
         sem = builder_->create<sem::GlobalVariable>(
-            v, ty, sem::EvaluationStage::kRuntime, ast::AddressSpace::kNone, ast::Access::kInvalid,
+            v, ty, sem::EvaluationStage::kRuntime, ast::AddressSpace::kNone,
+            ast::Access::kUndefined,
             /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
     } else {
         sem = builder_->create<sem::LocalVariable>(v, ty, sem::EvaluationStage::kRuntime,
-                                                   ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                                   current_statement_,
+                                                   ast::AddressSpace::kNone,
+                                                   ast::Access::kUndefined, current_statement_,
                                                    /* constant_value */ nullptr);
     }
 
@@ -415,6 +416,8 @@
 
     // Does the variable have a constructor?
     if (v->constructor) {
+        ExprEvalStageConstraint constraint{sem::EvaluationStage::kOverride, "override initializer"};
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
         rhs = Materialize(Expression(v->constructor), ty);
         if (!rhs) {
             return nullptr;
@@ -441,7 +444,7 @@
     }
 
     auto* sem = builder_->create<sem::GlobalVariable>(
-        v, ty, sem::EvaluationStage::kOverride, ast::AddressSpace::kNone, ast::Access::kInvalid,
+        v, ty, sem::EvaluationStage::kOverride, ast::AddressSpace::kNone, ast::Access::kUndefined,
         /* constant_value */ nullptr, sem::BindingPoint{}, std::nullopt);
     sem->SetConstructor(rhs);
 
@@ -452,8 +455,8 @@
         }
         auto* c = materialize->ConstantValue();
         if (!c) {
-            // TODO(crbug.com/tint/1633): Handle invalid materialization when expressions
-            // are supported.
+            // TODO(crbug.com/tint/1633): Handle invalid materialization when expressions are
+            // supported.
             return nullptr;
         }
 
@@ -492,9 +495,14 @@
         return nullptr;
     }
 
-    const auto* rhs = Expression(c->constructor);
-    if (!rhs) {
-        return nullptr;
+    const sem::Expression* rhs = nullptr;
+    {
+        ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "const initializer"};
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+        rhs = Expression(c->constructor);
+        if (!rhs) {
+            return nullptr;
+        }
     }
 
     if (ty) {
@@ -508,12 +516,6 @@
         ty = rhs->Type();
     }
 
-    const auto value = rhs->ConstantValue();
-    if (!value) {
-        AddError("'const' initializer must be constant expression", c->constructor->source);
-        return nullptr;
-    }
-
     if (!validator_.VariableInitializer(c, ast::AddressSpace::kNone, ty, rhs)) {
         return nullptr;
     }
@@ -524,12 +526,13 @@
         return nullptr;
     }
 
+    const auto value = rhs->ConstantValue();
     auto* sem = is_global ? static_cast<sem::Variable*>(builder_->create<sem::GlobalVariable>(
                                 c, ty, sem::EvaluationStage::kConstant, ast::AddressSpace::kNone,
-                                ast::Access::kInvalid, value, sem::BindingPoint{}, std::nullopt))
+                                ast::Access::kUndefined, value, sem::BindingPoint{}, std::nullopt))
                           : static_cast<sem::Variable*>(builder_->create<sem::LocalVariable>(
                                 c, ty, sem::EvaluationStage::kConstant, ast::AddressSpace::kNone,
-                                ast::Access::kInvalid, current_statement_, value));
+                                ast::Access::kUndefined, current_statement_, value));
 
     sem->SetConstructor(rhs);
     builder_->Sem().Add(c, sem);
@@ -551,6 +554,12 @@
 
     // Does the variable have a constructor?
     if (var->constructor) {
+        ExprEvalStageConstraint constraint{
+            is_global ? sem::EvaluationStage::kOverride : sem::EvaluationStage::kRuntime,
+            "var initializer",
+        };
+        TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
         rhs = Materialize(Expression(var->constructor), storage_ty);
         if (!rhs) {
             return nullptr;
@@ -588,7 +597,7 @@
     }
 
     auto access = var->declared_access;
-    if (access == ast::Access::kInvalid) {
+    if (access == ast::Access::kUndefined) {
         access = DefaultAccessForAddressSpace(address_space);
     }
 
@@ -752,9 +761,9 @@
         location = c->As<uint32_t>();
     }
 
-    auto* sem = builder_->create<sem::Parameter>(param, index, ty, ast::AddressSpace::kNone,
-                                                 ast::Access::kInvalid, sem::ParameterUsage::kNone,
-                                                 binding_point, location);
+    auto* sem = builder_->create<sem::Parameter>(
+        param, index, ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
+        sem::ParameterUsage::kNone, binding_point, location);
     builder_->Sem().Add(param, sem);
     return sem;
 }
@@ -857,16 +866,13 @@
 }
 
 sem::Statement* Resolver::StaticAssert(const ast::StaticAssert* assertion) {
+    ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "static assertion"};
+    TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
     auto* expr = Expression(assertion->condition);
     if (!expr) {
         return nullptr;
     }
     auto* cond = expr->ConstantValue();
-    if (!cond) {
-        AddError("static assertion condition must be a constant expression",
-                 assertion->condition->source);
-        return nullptr;
-    }
     if (auto* ty = cond->Type(); !ty->Is<sem::Bool>()) {
         AddError(
             "static assertion condition must be a bool, got '" + builder_->FriendlyName(ty) + "'",
@@ -1067,12 +1073,12 @@
     utils::Vector<const sem::Type*, 3> arg_tys;
 
     constexpr const char* kErrBadExpr =
-        "workgroup_size argument must be a constant or override expression of type "
+        "workgroup_size argument must be a constant or override-expression of type "
         "abstract-integer, i32 or u32";
 
     for (size_t i = 0; i < 3; i++) {
         // Each argument to this attribute can either be a literal, an identifier for a module-scope
-        // constants, a constant expression, or nullptr if not specified.
+        // constants, a const-expression, or nullptr if not specified.
         auto* value = values[i];
         if (!value) {
             break;
@@ -1125,6 +1131,15 @@
         }
     }
 
+    uint64_t total_size = static_cast<uint64_t>(ws[0].value_or(1));
+    for (size_t i = 1; i < 3; i++) {
+        total_size *= static_cast<uint64_t>(ws[i].value_or(1));
+        if (total_size > 0xffffffff) {
+            AddError("total workgroup grid size cannot exceed 0xffffffff", values[i]->source);
+            return false;
+        }
+    }
+
     current_function_->SetWorkgroupSize(std::move(ws));
     return true;
 }
@@ -1448,6 +1463,13 @@
             return nullptr;
         }
 
+        if (auto* constraint = expr_eval_stage_constraint_.constraint) {
+            if (!validator_.EvaluationStage(sem_expr, expr_eval_stage_constraint_.stage,
+                                            constraint)) {
+                return nullptr;
+            }
+        }
+
         builder_->Sem().Add(expr, sem_expr);
         if (expr == root) {
             return sem_expr;
@@ -1788,7 +1810,7 @@
                                 static_cast<uint32_t>(i),  // index
                                 arr->ElemType(),           // type
                                 ast::AddressSpace::kNone,  // address_space
-                                ast::Access::kInvalid);
+                                ast::Access::kUndefined);
                         });
                         return builder_->create<sem::TypeConstructor>(arr, std::move(params),
                                                                       args_stage);
@@ -1817,7 +1839,7 @@
                                 static_cast<uint32_t>(i),   // index
                                 str->Members()[i]->Type(),  // type
                                 ast::AddressSpace::kNone,   // address_space
-                                ast::Access::kInvalid);     // access
+                                ast::Access::kUndefined);   // access
                         }
                         return builder_->create<sem::TypeConstructor>(str, std::move(params),
                                                                       args_stage);
@@ -2081,14 +2103,16 @@
     if (texture_index == -1) {
         TINT_ICE(Resolver, diagnostics_) << "texture builtin without texture parameter";
     }
-    auto* texture = args[static_cast<size_t>(texture_index)]->As<sem::VariableUser>()->Variable();
-    if (!texture->Type()->UnwrapRef()->Is<sem::StorageTexture>()) {
-        int sampler_index = signature.IndexOf(sem::ParameterUsage::kSampler);
-        const sem::Variable* sampler =
-            sampler_index != -1
-                ? args[static_cast<size_t>(sampler_index)]->As<sem::VariableUser>()->Variable()
-                : nullptr;
-        current_function_->AddTextureSamplerPair(texture, sampler);
+    if (auto* user = args[static_cast<size_t>(texture_index)]->As<sem::VariableUser>()) {
+        auto* texture = user->Variable();
+        if (!texture->Type()->UnwrapRef()->Is<sem::StorageTexture>()) {
+            int sampler_index = signature.IndexOf(sem::ParameterUsage::kSampler);
+            const sem::Variable* sampler =
+                sampler_index != -1
+                    ? args[static_cast<size_t>(sampler_index)]->As<sem::VariableUser>()->Variable()
+                    : nullptr;
+            current_function_->AddTextureSamplerPair(texture, sampler);
+        }
     }
 }
 
@@ -2758,7 +2782,7 @@
     // For alignment, use the alignment attribute if provided, otherwise use the
     // default alignment for the member type.
     // Diagnostic errors are raised if a basic rule is violated.
-    // Validation of storage-class rules requires analysing the actual variable
+    // Validation of storage-class rules requires analyzing the actual variable
     // usage of the structure, and so is performed as part of the variable
     // validation.
     uint64_t struct_size = 0;
@@ -2806,6 +2830,10 @@
                 // Offset attributes are not part of the WGSL spec, but are emitted
                 // by the SPIR-V reader.
 
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant,
+                                                   "@offset value"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(o->expr));
                 if (!materialized) {
                     return nullptr;
@@ -2824,6 +2852,9 @@
                 align = 1;
                 has_offset_attr = true;
             } else if (auto* a = attr->As<ast::StructMemberAlignAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@align"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(a->expr));
                 if (!materialized) {
                     return nullptr;
@@ -2844,9 +2875,12 @@
                     AddError("'align' value must be a positive, power-of-two integer", a->source);
                     return nullptr;
                 }
-                align = const_value->As<u32>();
+                align = u32(value);
                 has_align_attr = true;
             } else if (auto* s = attr->As<ast::StructMemberSizeAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@size"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialized = Materialize(Expression(s->expr));
                 if (!materialized) {
                     return nullptr;
@@ -2864,9 +2898,12 @@
                              s->source);
                     return nullptr;
                 }
-                size = const_value->As<u32>();
+                size = u32(value);
                 has_size_attr = true;
             } else if (auto* l = attr->As<ast::LocationAttribute>()) {
+                ExprEvalStageConstraint constraint{sem::EvaluationStage::kConstant, "@location"};
+                TINT_SCOPED_ASSIGNMENT(expr_eval_stage_constraint_, constraint);
+
                 auto* materialize = Materialize(Expression(l->expr));
                 if (!materialize) {
                     return nullptr;
@@ -3228,7 +3265,7 @@
             if (!ApplyAddressSpaceUsageToType(address_space, const_cast<sem::Type*>(member->Type()),
                                               usage)) {
                 std::stringstream err;
-                err << "while analysing structure member " << sem_.TypeNameOf(str) << "."
+                err << "while analyzing structure member " << sem_.TypeNameOf(str) << "."
                     << builder_->Symbols().NameFor(member->Declaration()->symbol);
                 AddNote(err.str(), member->Declaration()->source);
                 return false;
diff --git a/src/tint/resolver/resolver.h b/src/tint/resolver/resolver.h
index 5c413f9..f699996 100644
--- a/src/tint/resolver/resolver.h
+++ b/src/tint/resolver/resolver.h
@@ -414,6 +414,15 @@
     using StructConstructorSig =
         utils::UnorderedKeyWrapper<std::tuple<const sem::Struct*, size_t, sem::EvaluationStage>>;
 
+    /// ExprEvalStageConstraint describes a constraint on when expressions can be evaluated.
+    struct ExprEvalStageConstraint {
+        /// The latest stage that the expression can be evaluated
+        sem::EvaluationStage stage = sem::EvaluationStage::kRuntime;
+        /// The 'thing' that is imposing the contraint. e.g. "var declaration"
+        /// If nullptr, then there is no constraint
+        const char* constraint = nullptr;
+    };
+
     ProgramBuilder* const builder_;
     diag::List& diagnostics_;
     ConstEval const_eval_;
@@ -425,10 +434,10 @@
     std::vector<sem::Function*> entry_points_;
     std::unordered_map<const sem::Type*, const Source&> atomic_composite_info_;
     utils::Bitset<0> marked_;
+    ExprEvalStageConstraint expr_eval_stage_constraint_;
     std::unordered_map<OverrideId, const sem::Variable*> override_ids_;
     std::unordered_map<ArrayConstructorSig, sem::CallTarget*> array_ctors_;
     std::unordered_map<StructConstructorSig, sem::CallTarget*> struct_ctors_;
-
     sem::Function* current_function_ = nullptr;
     sem::Statement* current_statement_ = nullptr;
     sem::CompoundStatement* current_compound_statement_ = nullptr;
diff --git a/src/tint/resolver/resolver_test.cc b/src/tint/resolver/resolver_test.cc
index 631725a..5e4494c 100644
--- a/src/tint/resolver/resolver_test.cc
+++ b/src/tint/resolver/resolver_test.cc
@@ -2212,6 +2212,51 @@
     EXPECT_TRUE(pairs[0].second == nullptr);
 }
 
+TEST_F(ResolverTest, TextureSampler_Bug1715) {  // crbug.com/tint/1715
+    // @binding(0) @group(0) var s: sampler;
+    // @binding(1) @group(0) var t: texture_2d<f32>;
+    // @binding(2) @group(0) var<uniform> c: vec2<f32>;
+    //
+    // @fragment
+    // fn main() -> @location(0) vec4<f32> {
+    //     return helper(&s, &t);
+    // }
+    //
+    // fn helper(sl: ptr<function, sampler>, tl: ptr<function, texture_2d<f32>>) -> vec4<f32> {
+    //     return textureSampleLevel(*tl, *sl, c, 0.0);
+    // }
+    GlobalVar("s", ty.sampler(ast::SamplerKind::kSampler), Group(0_a), Binding(0_a));
+    GlobalVar("t", ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()), Group(0_a),
+              Binding(1_a));
+    GlobalVar("c", ty.vec2<f32>(), ast::AddressSpace::kUniform, Group(0_a), Binding(2_a));
+
+    Func("main", utils::Empty, ty.vec4<f32>(),
+         utils::Vector{
+             Return(Call("helper", AddressOf("s"), AddressOf("t"))),
+         },
+         utils::Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         utils::Vector{
+             Location(0_u),
+         });
+
+    Func("helper",
+         utils::Vector{
+             Param("sl", ty.pointer(ty.sampler(ast::SamplerKind::kSampler),
+                                    ast::AddressSpace::kFunction)),
+             Param("tl", ty.pointer(ty.sampled_texture(ast::TextureDimension::k2d, ty.f32()),
+                                    ast::AddressSpace::kFunction)),
+         },
+         ty.vec4<f32>(),
+         utils::Vector{
+             Return(Call("textureSampleLevel", Deref("tl"), Deref("sl"), "c", 0.0_a)),
+         });
+
+    ASSERT_FALSE(r()->Resolve());
+    EXPECT_EQ(r()->error(), "error: cannot take the address of expression in handle address space");
+}
+
 TEST_F(ResolverTest, ModuleDependencyOrderedDeclarations) {
     auto* f0 = Func("f0", utils::Empty, ty.void_(), utils::Empty);
     auto* v0 = GlobalVar("v0", ty.i32(), ast::AddressSpace::kPrivate);
diff --git a/src/tint/resolver/static_assert_test.cc b/src/tint/resolver/static_assert_test.cc
index ea7396d..6e8ecb7 100644
--- a/src/tint/resolver/static_assert_test.cc
+++ b/src/tint/resolver/static_assert_test.cc
@@ -88,7 +88,8 @@
     WrapInFunction(StaticAssert(Expr(Source{{12, 34}}, "V")));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              "12:34 error: static assertion condition must be a constant expression");
+              "12:34 error: static assertion requires a const-expression, but expression is a "
+              "runtime-expression");
 }
 
 TEST_F(ResolverStaticAssertTest, Local_LessThan_Pass) {
diff --git a/src/tint/resolver/type_validation_test.cc b/src/tint/resolver/type_validation_test.cc
index 1271f06..7cc5cef 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::kInvalid);
+                                  ast::TexelFormat::kR32Uint, ast::Access::kUndefined);
 
     GlobalVar("a", st, Group(0_a), Binding(0_a));
 
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 35ba7e9..3066315 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::kInvalid:
+        case ast::Access::kUndefined:
             AddError("storage texture missing access control", t->source);
             return false;
         default:
@@ -593,7 +593,7 @@
         [&](const ast::Var* var) {
             if (auto* init = global->Constructor();
                 init && init->Stage() > sem::EvaluationStage::kOverride) {
-                AddError("module-scope 'var' initializer must be a constant or override expression",
+                AddError("module-scope 'var' initializer must be a constant or override-expression",
                          init->Declaration()->source);
                 return false;
             }
@@ -621,7 +621,7 @@
             // 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::kInvalid) {
+            if (var->declared_access != ast::Access::kUndefined) {
                 if (global->AddressSpace() == ast::AddressSpace::kStorage) {
                     // The access mode for the storage address space can only be 'read' or
                     // 'read_write'.
@@ -795,7 +795,7 @@
     auto* storage_ty = v->Type()->UnwrapRef();
 
     if (auto* init = v->Constructor(); init && init->Stage() > sem::EvaluationStage::kOverride) {
-        AddError("'override' initializer must be an override expression",
+        AddError("'override' initializer must be an override-expression",
                  init->Declaration()->source);
         return false;
     }
@@ -1021,7 +1021,7 @@
     }
 
     if (attr->type == ast::InterpolationType::kFlat &&
-        attr->sampling != ast::InterpolationSampling::kInvalid) {
+        attr->sampling != ast::InterpolationSampling::kUndefined) {
         AddError("flat interpolation attribute must not have a sampling parameter", attr->source);
         return false;
     }
@@ -1299,7 +1299,7 @@
                         member->Declaration()->attributes, member->Type(),
                         member->Declaration()->source, param_or_ret,
                         /*is_struct_member*/ true, member->Location())) {
-                    AddNote("while analysing entry point '" + symbols_.NameFor(decl->symbol) + "'",
+                    AddNote("while analyzing entry point '" + symbols_.NameFor(decl->symbol) + "'",
                             decl->source);
                     return false;
                 }
@@ -1391,6 +1391,30 @@
     return true;
 }
 
+bool Validator::EvaluationStage(const sem::Expression* expr,
+                                sem::EvaluationStage latest_stage,
+                                std::string_view constraint) const {
+    if (expr->Stage() > latest_stage) {
+        auto stage_name = [](sem::EvaluationStage stage) -> std::string {
+            switch (stage) {
+                case sem::EvaluationStage::kRuntime:
+                    return "a runtime-expression";
+                case sem::EvaluationStage::kOverride:
+                    return "an override-expression";
+                case sem::EvaluationStage::kConstant:
+                    return "a const-expression";
+            }
+            return "<unknown>";
+        };
+
+        AddError(std::string(constraint) + " requires " + stage_name(latest_stage) +
+                     ", but expression is " + stage_name(expr->Stage()),
+                 expr->Declaration()->source);
+        return false;
+    }
+    return true;
+}
+
 bool Validator::Statements(utils::VectorRef<const ast::Statement*> stmts) const {
     for (auto* stmt : stmts) {
         if (!sem_.Get(stmt)->IsReachable()) {
@@ -1663,52 +1687,31 @@
         std::string name = sem::str(usage);
         auto* arg = call->Arguments()[index];
         if (auto values = arg->ConstantValue()) {
-            // Assert that the constant values are of the expected type.
-            if (!values->Type()->is_integer_scalar_or_vector()) {
-                TINT_ICE(Resolver, diagnostics_)
-                    << "failed to resolve '" + func_name + "' " << name << " parameter type";
-                return false;
-            }
-
-            // Currently const_expr is restricted to literals and type constructors.
-            // Check that that's all we have for the parameter.
-            bool is_const_expr = true;
-            ast::TraverseExpressions(
-                arg->Declaration(), diagnostics_, [&](const ast::Expression* e) {
-                    if (e->IsAnyOf<ast::LiteralExpression, ast::CallExpression>()) {
-                        return ast::TraverseAction::Descend;
-                    }
-                    is_const_expr = false;
-                    return ast::TraverseAction::Stop;
-                });
-            if (is_const_expr) {
-                if (auto* vector = builtin->Parameters()[index]->Type()->As<sem::Vector>()) {
-                    for (size_t i = 0; i < vector->Width(); i++) {
-                        auto value = values->Index(i)->As<AInt>();
-                        if (value < min || value > max) {
-                            AddError("each component of the " + name +
-                                         " argument must be at least " + std::to_string(min) +
-                                         " and at most " + std::to_string(max) + ". " + name +
-                                         " component " + std::to_string(i) + " is " +
-                                         std::to_string(value),
-                                     arg->Declaration()->source);
-                            return false;
-                        }
-                    }
-                } else {
-                    auto value = values->As<AInt>();
+            if (auto* vector = values->Type()->As<sem::Vector>()) {
+                for (size_t i = 0; i < vector->Width(); i++) {
+                    auto value = values->Index(i)->As<AInt>();
                     if (value < min || value > max) {
-                        AddError("the " + name + " argument must be at least " +
+                        AddError("each component of the " + name + " argument must be at least " +
                                      std::to_string(min) + " and at most " + std::to_string(max) +
-                                     ". " + name + " is " + std::to_string(value),
+                                     ". " + name + " component " + std::to_string(i) + " is " +
+                                     std::to_string(value),
                                  arg->Declaration()->source);
                         return false;
                     }
                 }
-                return true;
+            } else {
+                auto value = values->As<AInt>();
+                if (value < min || value > max) {
+                    AddError("the " + name + " argument must be at least " + std::to_string(min) +
+                                 " and at most " + std::to_string(max) + ". " + name + " is " +
+                                 std::to_string(value),
+                             arg->Declaration()->source);
+                    return false;
+                }
             }
+            return true;
         }
-        AddError("the " + name + " argument must be a const_expression",
+        AddError("the " + name + " argument must be a const-expression",
                  arg->Declaration()->source);
         return false;
     };
@@ -1726,7 +1729,7 @@
     }
 
     const auto extension = builtin->RequiredExtension();
-    if (extension == ast::Extension::kInvalid) {
+    if (extension == ast::Extension::kUndefined) {
         return true;
     }
 
@@ -1902,7 +1905,7 @@
     }
 
     if (array_type->IsOverrideSized()) {
-        AddError("cannot construct an array that has an override expression count", ctor->source);
+        AddError("cannot construct an array that has an override-expression count", ctor->source);
         return false;
     }
 
diff --git a/src/tint/resolver/validator.h b/src/tint/resolver/validator.h
index 2b84ad1..1297a9a 100644
--- a/src/tint/resolver/validator.h
+++ b/src/tint/resolver/validator.h
@@ -25,6 +25,7 @@
 #include "src/tint/ast/pipeline_stage.h"
 #include "src/tint/program_builder.h"
 #include "src/tint/resolver/sem_helper.h"
+#include "src/tint/sem/evaluation_stage.h"
 #include "src/tint/source.h"
 
 // Forward declarations
@@ -209,6 +210,15 @@
     /// @returns true on success, false otherwise
     bool EntryPoint(const sem::Function* func, ast::PipelineStage stage) const;
 
+    /// Validates that the expression must not be evaluated any later than @p latest_stage
+    /// @param expr the expression to check
+    /// @param latest_stage the latest evaluation stage that the expression can be evaluated
+    /// @param constraint the 'thing' that is imposing the contraint. e.g. "var declaration"
+    /// @returns true if @p expr is evaluated in or before @p latest_stage, false otherwise
+    bool EvaluationStage(const sem::Expression* expr,
+                         sem::EvaluationStage latest_stage,
+                         std::string_view constraint) const;
+
     /// Validates a for loop
     /// @param stmt the for loop statement to validate
     /// @returns true on success, false otherwise
diff --git a/src/tint/resolver/variable_validation_test.cc b/src/tint/resolver/variable_validation_test.cc
index 28f8dc3..65cfd51 100644
--- a/src/tint/resolver/variable_validation_test.cc
+++ b/src/tint/resolver/variable_validation_test.cc
@@ -329,7 +329,7 @@
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(12:34 error: runtime-sized arrays can only be used in the <storage> address space
-56:78 note: while analysing structure member S.m
+56:78 note: while analyzing structure member S.m
 12:34 note: while instantiating 'var' v)");
 }
 
@@ -423,7 +423,9 @@
     WrapInFunction(v, c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be constant expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
 }
 
 TEST_F(ResolverVariableValidationTest, ConstInitWithOverride) {
@@ -432,7 +434,9 @@
     WrapInFunction(c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be constant expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is an override-expression)");
 }
 
 TEST_F(ResolverVariableValidationTest, ConstInitWithLet) {
@@ -441,7 +445,30 @@
     WrapInFunction(l, c);
 
     EXPECT_FALSE(r()->Resolve());
-    EXPECT_EQ(r()->error(), R"(12:34 error: 'const' initializer must be constant expression)");
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
+}
+
+TEST_F(ResolverVariableValidationTest, ConstInitWithRuntimeExpr) {
+    // const c = clamp(2, dpdx(0.5), 3);
+    WrapInFunction(Const("c", Call("clamp", 2_a, Call(Source{{12, 34}}, "dpdx", 0.5_a), 3_a)));
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
+}
+
+TEST_F(ResolverVariableValidationTest, ConstInitWithOverrideExpr) {
+    auto* o = Override("v", Expr(1_i));
+    auto* c = Const("c", Add(10_a, Expr(Source{{12, 34}}, o)));
+    WrapInFunction(c);
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: const initializer requires a const-expression, but expression is an override-expression)");
 }
 
 }  // namespace
diff --git a/src/tint/sem/array.h b/src/tint/sem/array.h
index d41cd98..693549d 100644
--- a/src/tint/sem/array.h
+++ b/src/tint/sem/array.h
@@ -31,7 +31,7 @@
 
 namespace tint::sem {
 
-/// The variant of an ArrayCount when the array is a constant expression.
+/// The variant of an ArrayCount when the array is a const-expression.
 /// Example:
 /// ```
 /// const N = 123;
@@ -144,7 +144,7 @@
     /// @returns the number of elements in the array.
     const ArrayCount& Count() const { return count_; }
 
-    /// @returns the array count if the count is a constant expression, otherwise returns nullopt.
+    /// @returns the array count if the count is a const-expression, otherwise returns nullopt.
     inline std::optional<uint32_t> ConstantCount() const {
         if (auto* count = std::get_if<ConstantArrayCount>(&count_)) {
             return count->value;
@@ -175,7 +175,7 @@
     /// natural stride
     bool IsStrideImplicit() const { return stride_ == implicit_stride_; }
 
-    /// @returns true if this array is sized using an constant expression
+    /// @returns true if this array is sized using an const-expression
     bool IsConstantSized() const { return std::holds_alternative<ConstantArrayCount>(count_); }
 
     /// @returns true if this array is sized using an override variable
diff --git a/src/tint/sem/builtin.cc b/src/tint/sem/builtin.cc
index 5017352..68d8fc8 100644
--- a/src/tint/sem/builtin.cc
+++ b/src/tint/sem/builtin.cc
@@ -182,7 +182,7 @@
     if (IsDP4a()) {
         return ast::Extension::kChromiumExperimentalDp4A;
     }
-    return ast::Extension::kInvalid;
+    return ast::Extension::kUndefined;
 }
 
 }  // namespace tint::sem
diff --git a/src/tint/sem/function.h b/src/tint/sem/function.h
index 50d853c..9ef99ab 100644
--- a/src/tint/sem/function.h
+++ b/src/tint/sem/function.h
@@ -40,8 +40,9 @@
 namespace tint::sem {
 
 /// WorkgroupSize is a three-dimensional array of WorkgroupDimensions.
-/// Each dimension is a std::optional as a workgroup size can be a constant or override expression.
-/// Override expressions are not known at compilation time, so these will be std::nullopt.
+/// Each dimension is a std::optional as a workgroup size can be a const-expression or
+/// override-expression. Override expressions are not known at compilation time, so these will be
+/// std::nullopt.
 using WorkgroupSize = std::array<std::optional<uint32_t>, 3>;
 
 /// Function holds the semantic information for function nodes.
diff --git a/src/tint/sem/pointer.cc b/src/tint/sem/pointer.cc
index cb73724..3918233 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::kInvalid);
+    TINT_ASSERT(Semantic, access != ast::Access::kUndefined);
 }
 
 size_t Pointer::Hash() const {
diff --git a/src/tint/sem/reference.cc b/src/tint/sem/reference.cc
index 297b779..a3a9f24 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::kInvalid);
+    TINT_ASSERT(Semantic, access != ast::Access::kUndefined);
 }
 
 size_t Reference::Hash() const {
diff --git a/src/tint/sem/storage_texture.cc b/src/tint/sem/storage_texture.cc
index caa8a79..763eb21 100644
--- a/src/tint/sem/storage_texture.cc
+++ b/src/tint/sem/storage_texture.cc
@@ -75,7 +75,7 @@
             return type_mgr.Get<sem::F32>();
         }
 
-        case ast::TexelFormat::kInvalid:
+        case ast::TexelFormat::kUndefined:
             break;
     }
 
diff --git a/src/tint/symbol_table.h b/src/tint/symbol_table.h
index e07d4df..617266f 100644
--- a/src/tint/symbol_table.h
+++ b/src/tint/symbol_table.h
@@ -52,7 +52,7 @@
 
     /// Returns the symbol for the given `name`
     /// @param name the name to lookup
-    /// @returns the symbol for the name or symbol::kInvalid if not found.
+    /// @returns the symbol for the name or symbol::kUndefined if not found.
     Symbol Get(const std::string& name) const;
 
     /// Returns the name for the given symbol
diff --git a/src/tint/templates/enums.tmpl.inc b/src/tint/templates/enums.tmpl.inc
index 4726d80..d04140b 100644
--- a/src/tint/templates/enums.tmpl.inc
+++ b/src/tint/templates/enums.tmpl.inc
@@ -12,7 +12,7 @@
 {{- /* ------------------------------------------------------------------ */ -}}
 {{- $enum    := PascalCase $.Name -}}
 enum class {{$enum}} {
-    kInvalid,
+    kUndefined,
 {{-   range $entry := $.Entries }}
     k{{PascalCase $entry.Name}},{{if $entry.IsInternal}}  // Tint-internal enum entry - not parsed{{end}}
 {{-   end }}
@@ -25,7 +25,7 @@
 
 /// Parse{{$enum}} parses a {{$enum}} from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or {{$enum}}::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or {{$enum}}::kUndefined if the string could not be parsed.
 {{$enum}} Parse{{$enum}}(std::string_view str);
 
 constexpr const char* k{{$enum}}Strings[] = {
@@ -47,14 +47,14 @@
 {{- $enum    := PascalCase $.Name -}}
 /// Parse{{$enum}} parses a {{$enum}} from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or {{$enum}}::kInvalid if the string could not be parsed.
+/// @returns the parsed enum, or {{$enum}}::kUndefined if the string could not be parsed.
 {{$enum}} Parse{{$enum}}(std::string_view str) {
 {{-   range $entry := $.PublicEntries }}
     if (str == "{{$entry.Name}}") {
         return {{template "EnumCase" $entry}};
     }
 {{-   end }}
-    return {{$enum}}::kInvalid;
+    return {{$enum}}::kUndefined;
 }
 {{- end -}}
 
@@ -67,8 +67,8 @@
 {{- $enum    := PascalCase $.Name -}}
 std::ostream& operator<<(std::ostream& out, {{$enum}} value) {
     switch (value) {
-        case {{$enum}}::kInvalid:
-            return out << "invalid";
+        case {{$enum}}::kUndefined:
+            return out << "undefined";
 {{-   range $entry := $.Entries }}
         case {{template "EnumCase" $entry}}:
             return out << "{{$entry.Name}}";
@@ -105,9 +105,9 @@
 static constexpr Case kInvalidCases[] = {
 {{-   $exclude := $.NameSet -}}
 {{-   range $entry := $.PublicEntries }}
-    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kInvalid},
-    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kInvalid},
-    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kInvalid},
+    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kUndefined},
+    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kUndefined},
+    {"{{Scramble $entry.Name $exclude}}", {{$enum}}::kUndefined},
 {{-   end }}
 };
 
diff --git a/src/tint/tint.natvis b/src/tint/tint.natvis
index 45ecf93..aedd9be 100644
--- a/src/tint/tint.natvis
+++ b/src/tint/tint.natvis
@@ -16,6 +16,11 @@
 -->
 
 <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
+
+	<!--=================================================================-->
+	<!-- utils -->
+	<!--=================================================================-->
+
 	<Type Name="tint::utils::Slice&lt;*&gt;">
 		<DisplayString>{{ length={len}, capacity={cap} }}</DisplayString>
 		<Expand>
@@ -29,10 +34,15 @@
 	</Type>
 
 	<Type Name="tint::utils::Vector&lt;*,*&gt;">
+		<Intrinsic Name="Length" Expression="(size_t)(impl_.slice.len)" />
 		<Expand>
 			<Item Name="[heap]">impl_.slice.cap > (int)$T2</Item>
-			<ExpandedItem>impl_.slice</ExpandedItem>
+			<!--<ExpandedItem>impl_.slice</ExpandedItem>-->
 			<!--<Item Name="[slice]">impl_.slice</Item>-->
+			<ArrayItems>
+				<Size>Length()</Size>
+				<ValuePointer>impl_.slice.data</ValuePointer>
+			</ArrayItems>
 		</Expand>
 	</Type>
 
@@ -49,6 +59,10 @@
 		<DisplayString Optional="true">{debug_name_,sb}</DisplayString>
 	</Type>
 
+	<!--=================================================================-->
+	<!-- ast -->
+	<!--=================================================================-->
+	
 	<Type Name="tint::ast::AssignmentStatement">
 		<DisplayString>{*lhs} = {*rhs};</DisplayString>
 		<Expand>
@@ -58,11 +72,8 @@
 	</Type>
 
 	<Type Name="tint::ast::IfStatement">
-		<DisplayString Condition="else_statements.size() == 0">if ({*condition}) {*body}</DisplayString>
-		<DisplayString Condition="else_statements.size() == 1">if ({*condition}) {*body} {*else_statements[0]} </DisplayString>
-		<DisplayString Condition="else_statements.size() == 2">if ({*condition}) {*body} {*else_statements[0]} {*else_statements[1]} </DisplayString>
-		<DisplayString Condition="else_statements.size() == 3">if ({*condition}) {*body} {*else_statements[0]} {*else_statements[1]} {*else_statements[2]}</DisplayString>
-		<DisplayString Condition="else_statements.size()  > 3">if ({*condition}) {*body} {*else_statements[0]} {*else_statements[1]} {*else_statements[2]} else {else_statements.size()-3} more...</DisplayString>
+		<DisplayString Condition="!else_statement">if ({*condition}) {*body}</DisplayString>
+		<DisplayString Condition="else_statement">if ({*condition}) {*body} else { *else_statement }</DisplayString>
 	</Type>
 
 	<Type Name="tint::ast::ElseStatement">
@@ -71,10 +82,10 @@
 	</Type>
 
 	<Type Name="tint::ast::BlockStatement">
-		<DisplayString Condition="statements.size() == 1">{{ {*statements[0]} }} </DisplayString>
-		<DisplayString Condition="statements.size() == 2">{{ {*statements[0]} {*statements[1]} }} </DisplayString>
-		<DisplayString Condition="statements.size() == 3">{{ {*statements[0]} {*statements[1]} {*statements[2]} }} </DisplayString>
-		<DisplayString Condition="statements.size()  > 3">{{ {*statements[0]} {*statements[1]} {*statements[2]} {statements.size()-3} more... }} </DisplayString>
+		<DisplayString Condition="statements.Length() == 1">{{ {*statements[0]} }} </DisplayString>
+		<DisplayString Condition="statements.Length() == 2">{{ {*statements[0]} {*statements[1]} }} </DisplayString>
+		<DisplayString Condition="statements.Length() == 3">{{ {*statements[0]} {*statements[1]} {*statements[2]} }} </DisplayString>
+		<DisplayString Condition="statements.Length()  > 3">{{ {*statements[0]} {*statements[1]} {*statements[2]} {statements.Length()-3} more... }} </DisplayString>
 	</Type>
 
 	<Type Name="tint::ast::ReturnStatement">
@@ -114,15 +125,16 @@
 		</Expand>
 	</Type>
 
-	<Type Name="tint::ast::Variable">
-		<!-- Hack: we can deduce that this variable is a parameter if it's const with a type but no constructor, which is illegal for a let  -->
-		<DisplayString Condition=" is_const &amp; !!type &amp;  !constructor">{symbol} : {*type}</DisplayString>
+	<Type Name="tint::ast::Let">
+		<DisplayString Condition="!!type">let {symbol} : {*type} = {*constructor}</DisplayString>
+		<DisplayString Condition=" !type">let {symbol} = {*constructor}</DisplayString>
+	</Type>
 
-		<DisplayString Condition=" is_const &amp; !!type &amp; !!constructor">let {symbol} : {*type} = {*constructor}</DisplayString>
-		<DisplayString Condition=" is_const &amp;  !type &amp;  !constructor">let {symbol} = {*constructor}</DisplayString>
-		<DisplayString Condition="!is_const &amp; !!type &amp; !!constructor">var {symbol} : {*type} = {*constructor}</DisplayString>
-		<DisplayString Condition="!is_const &amp;  !type &amp; !!constructor">var {symbol} = {*constructor}</DisplayString>
-		<DisplayString Condition="!is_const &amp; !!type &amp;  !constructor">var {symbol}</DisplayString>
+	<Type Name="tint::ast::Variable">
+		<DisplayString Condition="!!type &amp; !!constructor">var {symbol} : {*type} = {*constructor}</DisplayString>
+		<DisplayString Condition="!!type &amp;  !constructor">var {symbol} : {*type}</DisplayString>
+		<DisplayString Condition=" !type &amp; !!constructor">var {symbol} = {*constructor}</DisplayString>
+		<DisplayString Condition=" !type &amp;  !constructor">var {symbol}</DisplayString>
 	</Type>
 
 	<Type Name="tint::ast::VariableDeclStatement">
@@ -142,11 +154,11 @@
 	</Type>
 
 	<Type Name="tint::ast::UnaryOpExpression">
-		<DisplayString Condition="op==tint::ast::UnaryOp::kAddressOf">&amp;({expr})</DisplayString>
-		<DisplayString Condition="op==tint::ast::UnaryOp::kComplement">~({expr})</DisplayString>
-		<DisplayString Condition="op==tint::ast::UnaryOp::kIndirection">*({expr})</DisplayString>
-		<DisplayString Condition="op==tint::ast::UnaryOp::kNegation">-({expr})</DisplayString>
-		<DisplayString Condition="op==tint::ast::UnaryOp::kNot">!({expr})</DisplayString>
+		<DisplayString Condition="op==tint::ast::UnaryOp::kAddressOf">&amp;({*expr})</DisplayString>
+		<DisplayString Condition="op==tint::ast::UnaryOp::kComplement">~({*expr})</DisplayString>
+		<DisplayString Condition="op==tint::ast::UnaryOp::kIndirection">*({*expr})</DisplayString>
+		<DisplayString Condition="op==tint::ast::UnaryOp::kNegation">-({*expr})</DisplayString>
+		<DisplayString Condition="op==tint::ast::UnaryOp::kNot">!({*expr})</DisplayString>
 	</Type>
 
 	<Type Name="tint::ast::BinaryExpression">
@@ -172,11 +184,12 @@
 	</Type>
 
 	<Type Name="tint::ast::CallExpression">
-		<DisplayString Condition="args.size() == 0">{target}()</DisplayString>
-		<DisplayString Condition="args.size() == 1">{target}({*args[0]})</DisplayString>
-		<DisplayString Condition="args.size() == 2">{target}({*args[0]}, {*args[1]})</DisplayString>
-		<DisplayString Condition="args.size() == 3">{target}({*args[0]}, {*args[1]}, {*args[2]})</DisplayString>
-		<DisplayString Condition="args.size()  > 3">{target}({*args[0]}, {*args[1]}, {*args[2]}, {args.size()-3} more...)</DisplayString>
+		<DisplayString Condition="args.Length() == 0">{target}()</DisplayString>
+		<DisplayString Condition="args.Length() == 1">{target}({*args[0]})</DisplayString>
+		<DisplayString Condition="args.Length() == 2">{target}({*args[0]}, {*args[1]})</DisplayString>
+		<DisplayString Condition="args.Length() == 3">{target}({*args[0]}, {*args[1]}, {*args[2]})</DisplayString>
+		<DisplayString Condition="args.Length() == 4">{target}({*args[0]}, {*args[1]}, {*args[2]}, {*args[3]})</DisplayString>
+		<DisplayString Condition="args.Length()  > 4">{target}({*args[0]}, {*args[1]}, {*args[2]}, {args.Length()-3} more...)</DisplayString>
 		<!-- TODO: add more overloads -->
 	</Type>
 
@@ -190,6 +203,15 @@
 		<DisplayString Condition="count == nullptr">array&lt;{*type}&gt;</DisplayString>
 	</Type>
 
+	<Type Name="tint::ast::Vector">
+		<DisplayString Condition="width==2 &amp; !type">vec2</DisplayString>
+		<DisplayString Condition="width==3 &amp; !type">vec3</DisplayString>
+		<DisplayString Condition="width==4 &amp; !type">vec4</DisplayString>
+		<DisplayString Condition="width==2 &amp; !!type">vec2&lt;{*type}&gt;</DisplayString>
+		<DisplayString Condition="width==3 &amp; !!type">vec3&lt;{*type}&gt;</DisplayString>
+		<DisplayString Condition="width==4 &amp; !!type">vec4&lt;{*type}&gt;</DisplayString>
+	</Type>
+
 	<Type Name="tint::ast::TypeName">
 		<DisplayString>{name}</DisplayString>
 	</Type>
@@ -210,4 +232,36 @@
 		<DisplayString>f32</DisplayString>
 	</Type>
 
+	<!--=================================================================-->
+	<!-- sem -->
+	<!--=================================================================-->
+
+	<Type Name="tint::sem::AbstractInt">
+		<DisplayString>AbstractInt</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::AbstractFloat">
+		<DisplayString>AbstractFloat</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::Bool">
+		<DisplayString>bool</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::F32">
+		<DisplayString>f32</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::Vector">
+		<DisplayString>vec{width_}&lt;{*subtype_}&gt;</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::Constant">
+		<DisplayString>Type={*Type()} Value={Value()}</DisplayString>
+	</Type>
+
+	<Type Name="tint::sem::Expression">
+		<DisplayString>Decl={*declaration_}</DisplayString>
+	</Type>
+
 </AutoVisualizer>
diff --git a/src/tint/transform/binding_remapper.cc b/src/tint/transform/binding_remapper.cc
index 66d8e99..102bb1e 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::kInvalid) {
+                if (ac == ast::Access::kUndefined) {
                     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 38a1b28..b990965 100644
--- a/src/tint/transform/canonicalize_entry_point_io.cc
+++ b/src/tint/transform/canonicalize_entry_point_io.cc
@@ -223,7 +223,7 @@
                 (ast::HasAttribute<ast::LocationAttribute>(attributes) ||
                  cfg.shader_style == ShaderStyle::kSpirv)) {
                 attributes.Push(ctx.dst->Interpolate(ast::InterpolationType::kFlat,
-                                                     ast::InterpolationSampling::kInvalid));
+                                                     ast::InterpolationSampling::kUndefined));
             }
 
             // Disable validation for use of the `input` address space.
@@ -292,7 +292,7 @@
             ast::HasAttribute<ast::LocationAttribute>(attributes) &&
             !ast::HasAttribute<ast::InterpolateAttribute>(attributes)) {
             attributes.Push(ctx.dst->Interpolate(ast::InterpolationType::kFlat,
-                                                 ast::InterpolationSampling::kInvalid));
+                                                 ast::InterpolationSampling::kUndefined));
         }
 
         // In GLSL, if it's a builtin, override the name with the
diff --git a/src/tint/transform/robustness_test.cc b/src/tint/transform/robustness_test.cc
index fa04e47..fe2f343 100644
--- a/src/tint/transform/robustness_test.cc
+++ b/src/tint/transform/robustness_test.cc
@@ -1311,7 +1311,8 @@
 }
 )";
 
-    auto* expect = R"(error: array size is an override-expression, when expected a constant-expression.
+    auto* expect =
+        R"(error: array size is an override-expression, when expected a constant-expression.
 Was the SubstituteOverride transform run?)";
 
     auto got = Run<Robustness>(src);
diff --git a/src/tint/transform/substitute_override.cc b/src/tint/transform/substitute_override.cc
index de597c2..bdaa45b 100644
--- a/src/tint/transform/substitute_override.cc
+++ b/src/tint/transform/substitute_override.cc
@@ -17,6 +17,8 @@
 #include <functional>
 
 #include "src/tint/program_builder.h"
+#include "src/tint/sem/builtin.h"
+#include "src/tint/sem/index_accessor_expression.h"
 #include "src/tint/sem/variable.h"
 
 TINT_INSTANTIATE_TYPEINFO(tint::transform::SubstituteOverride);
@@ -75,13 +77,32 @@
 
         if (!ctor) {
             ctx.dst->Diagnostics().add_error(diag::System::Transform,
-                                             "Failed to create override expression");
+                                             "Failed to create override-expression");
             return nullptr;
         }
 
         return ctx.dst->Const(src, sym, ty, ctor);
     });
 
+    // Ensure that objects that are indexed with an override-expression are materialized.
+    // If the object is not materialized, and the 'override' variable is turned to a 'const', the
+    // resulting type of the index may change. See: crbug.com/tint/1697.
+    ctx.ReplaceAll(
+        [&](const ast::IndexAccessorExpression* expr) -> const ast::IndexAccessorExpression* {
+            if (auto* sem = ctx.src->Sem().Get(expr)) {
+                if (auto* access = sem->UnwrapMaterialize()->As<sem::IndexAccessorExpression>()) {
+                    if (access->Object()->UnwrapMaterialize()->Type()->HoldsAbstract() &&
+                        access->Index()->Stage() == sem::EvaluationStage::kOverride) {
+                        auto& b = *ctx.dst;
+                        auto* obj = b.Call(sem::str(sem::BuiltinType::kTintMaterialize),
+                                           ctx.Clone(expr->object));
+                        return b.IndexAccessor(obj, ctx.Clone(expr->index));
+                    }
+                }
+            }
+            return nullptr;
+        });
+
     ctx.Clone();
 }
 
diff --git a/src/tint/transform/substitute_override_test.cc b/src/tint/transform/substitute_override_test.cc
index f84c32c..abc67f5 100644
--- a/src/tint/transform/substitute_override_test.cc
+++ b/src/tint/transform/substitute_override_test.cc
@@ -238,5 +238,46 @@
     EXPECT_EQ(expect, str(got));
 }
 
+TEST_F(SubstituteOverrideTest, IndexMaterialization) {
+    auto* src = R"(
+override O = 0; // Try switching to 'const'
+
+fn f() {
+  const smaller_than_any_f32 = 1e-50;
+  const large_float = 1e27;
+  // When O is an override, the outer index value is not constant, so the
+  // value is not calculated at shader-creation time, and does not error.
+  //
+  // When O is a const, and 'smaller_than_any_f32' *is not* materialized, the
+  // outer index value will evaluate to 10000, resulting in an out-of-bounds
+  // error.
+  //
+  // When O is a const, and 'smaller_than_any_f32' *is* materialized, the
+  // materialization of 'smaller_than_any_f32' to f32 will evaluate to zero,
+  // and so the outer index value will be zero, and we get no error.
+  _ = vec2(0)[i32(vec2(smaller_than_any_f32)[O]*large_float*large_float)];
+}
+)";
+
+    auto* expect = R"(
+const O = 0i;
+
+fn f() {
+  const smaller_than_any_f32 = 1e-50;
+  const large_float = 1000000000000000013287555072.0;
+  _ = _tint_materialize(vec2(0))[i32(((_tint_materialize(vec2(smaller_than_any_f32))[O] * large_float) * large_float))];
+}
+)";
+
+    SubstituteOverride::Config cfg;
+    cfg.map.insert({OverrideId{0}, 0.0});
+
+    DataMap data;
+    data.Add<SubstituteOverride::Config>(cfg);
+    auto got = Run<SubstituteOverride>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+}
+
 }  // namespace
 }  // namespace tint::transform
diff --git a/src/tint/writer/append_vector.cc b/src/tint/writer/append_vector.cc
index 98e9efc..edf9132 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::kInvalid),
+                                      ast::AddressSpace::kNone, ast::Access::kUndefined),
             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::kInvalid);
+                                 ast::AddressSpace::kNone, ast::Access::kUndefined);
                          }),
         sem::EvaluationStage::kRuntime);
     auto* constructor_sem =
diff --git a/src/tint/writer/float_to_string.cc b/src/tint/writer/float_to_string.cc
index 7494ace..3b4260e 100644
--- a/src/tint/writer/float_to_string.cc
+++ b/src/tint/writer/float_to_string.cc
@@ -25,9 +25,34 @@
 
 namespace tint::writer {
 
-std::string FloatToString(float f) {
-    // Try printing the float in fixed point, with a smallish limit on the
-    // precision
+namespace {
+
+template <typename T>
+struct Traits;
+
+template <>
+struct Traits<float> {
+    using uint_t = uint32_t;
+    static constexpr int kExponentBias = 127;
+    static constexpr uint_t kExponentMask = 0x7f800000;
+    static constexpr uint_t kMantissaMask = 0x007fffff;
+    static constexpr uint_t kSignMask = 0x80000000;
+    static constexpr int kMantissaBits = 23;
+};
+
+template <>
+struct Traits<double> {
+    using uint_t = uint64_t;
+    static constexpr int kExponentBias = 1023;
+    static constexpr uint_t kExponentMask = 0x7ff0000000000000;
+    static constexpr uint_t kMantissaMask = 0x000fffffffffffff;
+    static constexpr uint_t kSignMask = 0x8000000000000000;
+    static constexpr int kMantissaBits = 52;
+};
+
+template <typename F>
+std::string ToString(F f) {
+    // Try printing the float in fixed point, with a smallish limit on the precision
     std::stringstream fixed;
     fixed.flags(fixed.flags() | std::ios_base::showpoint | std::ios_base::fixed);
     fixed.imbue(std::locale::classic());
@@ -36,13 +61,13 @@
     std::string str = fixed.str();
 
     // If this string can be parsed without loss of information, use it.
-    // (Use double here to dodge a bug in older libc++ versions which
-    // would incorrectly read back FLT_MAX as INF.)
+    // (Use double here to dodge a bug in older libc++ versions which would incorrectly read back
+    // FLT_MAX as INF.)
     double roundtripped;
     fixed >> roundtripped;
 
-    auto float_equal_no_warning = std::equal_to<float>();
-    if (float_equal_no_warning(f, static_cast<float>(roundtripped))) {
+    auto float_equal_no_warning = std::equal_to<F>();
+    if (float_equal_no_warning(f, static_cast<F>(roundtripped))) {
         while (str.length() >= 2 && str[str.size() - 1] == '0' && str[str.size() - 2] != '.') {
             str.pop_back();
         }
@@ -50,38 +75,41 @@
         return str;
     }
 
-    // Resort to scientific, with the minimum precision needed to preserve the
-    // whole float
+    // Resort to scientific, with the minimum precision needed to preserve the whole float
     std::stringstream sci;
     sci.imbue(std::locale::classic());
-    sci.precision(std::numeric_limits<float>::max_digits10);
+    sci.precision(std::numeric_limits<F>::max_digits10);
     sci << f;
     return sci.str();
 }
 
-std::string FloatToBitPreservingString(float f) {
+template <typename F>
+std::string ToBitPreservingString(F f) {
+    using T = Traits<F>;
+    using uint_t = typename T::uint_t;
+
     // For the NaN case, avoid handling the number as a floating point value.
     // Some machines will modify the top bit in the mantissa of a NaN.
 
     std::stringstream ss;
 
-    uint32_t float_bits = 0u;
+    typename T::uint_t float_bits = 0u;
+    static_assert(sizeof(float_bits) == sizeof(f));
     std::memcpy(&float_bits, &f, sizeof(float_bits));
 
     // Handle the sign.
-    const uint32_t kSignMask = 1u << 31;
-    if (float_bits & kSignMask) {
+    if (float_bits & T::kSignMask) {
         // If `f` is -0.0 print -0.0.
         ss << '-';
         // Strip sign bit.
-        float_bits = float_bits & (~kSignMask);
+        float_bits = float_bits & (~T::kSignMask);
     }
 
     switch (std::fpclassify(f)) {
         case FP_ZERO:
         case FP_NORMAL:
             std::memcpy(&f, &float_bits, sizeof(float_bits));
-            ss << FloatToString(f);
+            ss << ToString(f);
             break;
 
         default: {
@@ -89,46 +117,39 @@
             // TODO(dneto): It's unclear how Infinity and NaN should be handled.
             // See https://github.com/gpuweb/gpuweb/issues/1769
 
-            // std::hexfloat prints 'nan' and 'inf' instead of an
-            // explicit representation like we want. Split it out
-            // manually.
-            const int kExponentBias = 127;
-            const int kExponentMask = 0x7f800000;
-            const int kMantissaMask = 0x007fffff;
-            const int kMantissaBits = 23;
-
-            int mantissaNibbles = (kMantissaBits + 3) / 4;
+            // std::hexfloat prints 'nan' and 'inf' instead of an explicit representation like we
+            // want. Split it out manually.
+            int mantissa_nibbles = (T::kMantissaBits + 3) / 4;
 
             const int biased_exponent =
-                static_cast<int>((float_bits & kExponentMask) >> kMantissaBits);
-            int exponent = biased_exponent - kExponentBias;
-            uint32_t mantissa = float_bits & kMantissaMask;
+                static_cast<int>((float_bits & T::kExponentMask) >> T::kMantissaBits);
+            int exponent = biased_exponent - T::kExponentBias;
+            uint_t mantissa = float_bits & T::kMantissaMask;
 
             ss << "0x";
 
-            if (exponent == 128) {
+            if (exponent == T::kExponentBias + 1) {
                 if (mantissa == 0) {
                     //  Infinity case.
-                    ss << "1p+128";
+                    ss << "1p+" << exponent;
                 } else {
-                    //  NaN case.
-                    //  Emit the mantissa bits as if they are left-justified after the
-                    //  binary point.  This is what SPIRV-Tools hex float emitter does,
-                    //  and it's a justifiable choice independent of the bit width
-                    //  of the mantissa.
-                    mantissa <<= (4 - (kMantissaBits % 4));
-                    // Remove trailing zeroes, for tidyness.
+                    // NaN case.
+                    // Emit the mantissa bits as if they are left-justified after the binary point.
+                    // This is what SPIRV-Tools hex float emitter does, and it's a justifiable
+                    // choice independent of the bit width of the mantissa.
+                    mantissa <<= (4 - (T::kMantissaBits % 4));
+                    // Remove trailing zeroes, for tidiness.
                     while (0 == (0xf & mantissa)) {
                         mantissa >>= 4;
-                        mantissaNibbles--;
+                        mantissa_nibbles--;
                     }
-                    ss << "1." << std::hex << std::setfill('0') << std::setw(mantissaNibbles)
-                       << mantissa << "p+128";
+                    ss << "1." << std::hex << std::setfill('0') << std::setw(mantissa_nibbles)
+                       << mantissa << "p+" << std::dec << exponent;
                 }
             } else {
                 // Subnormal, and not zero.
                 TINT_ASSERT(Writer, mantissa != 0);
-                const int kTopBit = (1 << kMantissaBits);
+                const auto kTopBit = static_cast<uint_t>(1u) << T::kMantissaBits;
 
                 // Shift left until we get 1.x
                 while (0 == (kTopBit & mantissa)) {
@@ -138,17 +159,19 @@
                 // Emit the leading 1, and remove it from the mantissa.
                 ss << "1";
                 mantissa = mantissa ^ kTopBit;
-                mantissa <<= 1;
                 exponent++;
 
+                // Left-justify mantissa to whole nibble.
+                mantissa <<= (4 - (T::kMantissaBits % 4));
+
                 // Emit the fractional part.
                 if (mantissa) {
-                    // Remove trailing zeroes, for tidyness
+                    // Remove trailing zeroes, for tidiness
                     while (0 == (0xf & mantissa)) {
                         mantissa >>= 4;
-                        mantissaNibbles--;
+                        mantissa_nibbles--;
                     }
-                    ss << "." << std::hex << std::setfill('0') << std::setw(mantissaNibbles)
+                    ss << "." << std::hex << std::setfill('0') << std::setw(mantissa_nibbles)
                        << mantissa;
                 }
                 // Emit the exponent
@@ -159,4 +182,22 @@
     return ss.str();
 }
 
+}  // namespace
+
+std::string FloatToString(float f) {
+    return ToString(f);
+}
+
+std::string FloatToBitPreservingString(float f) {
+    return ToBitPreservingString(f);
+}
+
+std::string DoubleToString(double f) {
+    return ToString(f);
+}
+
+std::string DoubleToBitPreservingString(double f) {
+    return ToBitPreservingString(f);
+}
+
 }  // namespace tint::writer
diff --git a/src/tint/writer/float_to_string.h b/src/tint/writer/float_to_string.h
index 4a5afd4..387c0a8 100644
--- a/src/tint/writer/float_to_string.h
+++ b/src/tint/writer/float_to_string.h
@@ -27,11 +27,24 @@
 /// @return the float f formatted to a string
 std::string FloatToString(float f);
 
+/// Converts the double `f` to a string using fixed-point notation (not
+/// scientific). The double will be printed with the full precision required to
+/// describe the double. All trailing `0`s will be omitted after the last
+/// non-zero fractional number, unless the fractional is zero, in which case the
+/// number will end with `.0`.
+/// @return the double f formatted to a string
+std::string DoubleToString(double f);
+
 /// Converts the float `f` to a string, using hex float notation for infinities,
 /// NaNs, or subnormal numbers. Otherwise behaves as FloatToString.
 /// @return the float f formatted to a string
 std::string FloatToBitPreservingString(float f);
 
+/// Converts the double `f` to a string, using hex double notation for infinities,
+/// NaNs, or subnormal numbers. Otherwise behaves as FloatToString.
+/// @return the double f formatted to a string
+std::string DoubleToBitPreservingString(double f);
+
 }  // namespace tint::writer
 
 #endif  // SRC_TINT_WRITER_FLOAT_TO_STRING_H_
diff --git a/src/tint/writer/float_to_string_test.cc b/src/tint/writer/float_to_string_test.cc
index 8e5f244..901334e 100644
--- a/src/tint/writer/float_to_string_test.cc
+++ b/src/tint/writer/float_to_string_test.cc
@@ -14,33 +14,19 @@
 
 #include "src/tint/writer/float_to_string.h"
 
-#include <cmath>
+#include <math.h>
 #include <cstring>
 #include <limits>
 
 #include "gtest/gtest.h"
+#include "src/tint/utils/bitcast.h"
 
 namespace tint::writer {
 namespace {
 
-// Makes an IEEE 754 binary32 floating point number with
-// - 0 sign if sign is 0, 1 otherwise
-// - 'exponent_bits' is placed in the exponent space.
-//   So, the exponent bias must already be included.
-float MakeFloat(uint32_t sign, uint32_t biased_exponent, uint32_t mantissa) {
-    const uint32_t sign_bit = sign ? 0x80000000u : 0u;
-    // The binary32 exponent is 8 bits, just below the sign.
-    const uint32_t exponent_bits = (biased_exponent & 0xffu) << 23;
-    // The mantissa is the bottom 23 bits.
-    const uint32_t mantissa_bits = (mantissa & 0x7fffffu);
-
-    uint32_t bits = sign_bit | exponent_bits | mantissa_bits;
-    float result = 0.0f;
-    static_assert(sizeof(result) == sizeof(bits),
-                  "expected float and uint32_t to be the same size");
-    std::memcpy(&result, &bits, sizeof(bits));
-    return result;
-}
+////////////////////////////////////////////////////////////////////////////////
+// FloatToString                                                              //
+////////////////////////////////////////////////////////////////////////////////
 
 TEST(FloatToStringTest, Zero) {
     EXPECT_EQ(FloatToString(0.0f), "0.0");
@@ -93,14 +79,18 @@
     EXPECT_EQ(FloatToString(1e-20f), "9.99999968e-21");
 }
 
-// FloatToBitPreservingString
-//
-// First replicate the tests for FloatToString
+////////////////////////////////////////////////////////////////////////////////
+// FloatToBitPreservingString                                                 //
+////////////////////////////////////////////////////////////////////////////////
 
 TEST(FloatToBitPreservingStringTest, Zero) {
     EXPECT_EQ(FloatToBitPreservingString(0.0f), "0.0");
 }
 
+TEST(FloatToBitPreservingStringTest, NegativeZero) {
+    EXPECT_EQ(FloatToBitPreservingString(-0.0f), "-0.0");
+}
+
 TEST(FloatToBitPreservingStringTest, One) {
     EXPECT_EQ(FloatToBitPreservingString(1.0f), "1.0");
 }
@@ -141,49 +131,204 @@
               "-340282346638528859811704183484516925440.0");
 }
 
-// Special cases for bit-preserving output.
-
-TEST(FloatToBitPreservingStringTest, NegativeZero) {
-    EXPECT_EQ(FloatToBitPreservingString(std::copysign(0.0f, -5.0f)), "-0.0");
-}
-
-TEST(FloatToBitPreservingStringTest, ZeroAsBits) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0)), "0.0");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0)), "-0.0");
-}
-
-TEST(FloatToBitPreservingStringTest, OneBits) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 127, 0)), "1.0");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 127, 0)), "-1.0");
-}
-
 TEST(FloatToBitPreservingStringTest, SmallestDenormal) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 1)), "0x1p-149");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 1)), "-0x1p-149");
+    EXPECT_EQ(FloatToBitPreservingString(0x1p-149f), "0x1p-149");
+    EXPECT_EQ(FloatToBitPreservingString(-0x1p-149f), "-0x1p-149");
 }
 
 TEST(FloatToBitPreservingStringTest, BiggerDenormal) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 2)), "0x1p-148");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 2)), "-0x1p-148");
+    EXPECT_EQ(FloatToBitPreservingString(0x1p-148f), "0x1p-148");
+    EXPECT_EQ(FloatToBitPreservingString(-0x1p-148f), "-0x1p-148");
 }
 
 TEST(FloatToBitPreservingStringTest, LargestDenormal) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0x7fffff)), "0x1.fffffcp-127");
+    static_assert(0x0.fffffep-126f == 0x1.fffffcp-127f);
+    EXPECT_EQ(FloatToBitPreservingString(0x0.fffffep-126f), "0x1.fffffcp-127");
 }
 
 TEST(FloatToBitPreservingStringTest, Subnormal_cafebe) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xcafebe)), "0x1.2bfaf8p-127");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xcafebe)), "-0x1.2bfaf8p-127");
+    EXPECT_EQ(FloatToBitPreservingString(0x1.2bfaf8p-127f), "0x1.2bfaf8p-127");
+    EXPECT_EQ(FloatToBitPreservingString(-0x1.2bfaf8p-127f), "-0x1.2bfaf8p-127");
 }
 
 TEST(FloatToBitPreservingStringTest, Subnormal_aaaaa) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 0, 0xaaaaa)), "0x1.55554p-130");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 0, 0xaaaaa)), "-0x1.55554p-130");
+    EXPECT_EQ(FloatToBitPreservingString(0x1.55554p-130f), "0x1.55554p-130");
+    EXPECT_EQ(FloatToBitPreservingString(-0x1.55554p-130f), "-0x1.55554p-130");
 }
 
 TEST(FloatToBitPreservingStringTest, Infinity) {
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(0, 255, 0)), "0x1p+128");
-    EXPECT_EQ(FloatToBitPreservingString(MakeFloat(1, 255, 0)), "-0x1p+128");
+    EXPECT_EQ(FloatToBitPreservingString(INFINITY), "0x1p+128");
+    EXPECT_EQ(FloatToBitPreservingString(-INFINITY), "-0x1p+128");
+}
+
+TEST(FloatToBitPreservingStringTest, NaN) {
+    // TODO(crbug.com/tint/1714): On x86, this bitcast will set bit 22 (the highest mantissa bit) to
+    // 1, regardless of the bit value in the integer. This is likely due to IEEE 754's
+    // recommendation that that the highest mantissa bit differentiates quiet NaNs from signalling
+    // NaNs. On x86, float return values usually go via the FPU which can transform the signalling
+    // NaN bit (0) to quiet NaN (1). As NaN floating point numbers can be silently modified by the
+    // architecture, and the signalling bit is architecture defined, this test may fail on other
+    // architectures.
+    auto nan = utils::Bitcast<float>(0x7fc0beef);
+    EXPECT_EQ(FloatToBitPreservingString(nan), "0x1.817ddep+128");
+    EXPECT_EQ(FloatToBitPreservingString(-nan), "-0x1.817ddep+128");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DoubleToString                                                              //
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(DoubleToStringTest, Zero) {
+    EXPECT_EQ(DoubleToString(0.0), "0.0");
+}
+
+TEST(DoubleToStringTest, One) {
+    EXPECT_EQ(DoubleToString(1.0), "1.0");
+}
+
+TEST(DoubleToStringTest, MinusOne) {
+    EXPECT_EQ(DoubleToString(-1.0), "-1.0");
+}
+
+TEST(DoubleToStringTest, Billion) {
+    EXPECT_EQ(DoubleToString(1e9), "1000000000.0");
+}
+
+TEST(DoubleToStringTest, Small) {
+    EXPECT_NE(DoubleToString(std::numeric_limits<double>::epsilon()), "0.0");
+}
+
+TEST(DoubleToStringTest, Highest) {
+    const auto highest = std::numeric_limits<double>::max();
+    const auto expected_highest = 1.797693134862315708e+308;
+    if (highest < expected_highest || highest > expected_highest) {
+        GTEST_SKIP() << "std::numeric_limits<double>::max() is not as expected for "
+                        "this target";
+    }
+    EXPECT_EQ(DoubleToString(std::numeric_limits<double>::max()),
+              "179769313486231570814527423731704356798070567525844996598917476803157260780028538760"
+              "589558632766878171540458953514382464234321326889464182768467546703537516986049910576"
+              "551282076245490090389328944075868508455133942304583236903222948165808559332123348274"
+              "797826204144723168738177180919299881250404026184124858368.0");
+}
+
+TEST(DoubleToStringTest, Lowest) {
+    // Some compilers complain if you test floating point numbers for equality.
+    // So say it via two inequalities.
+    const auto lowest = std::numeric_limits<double>::lowest();
+    const auto expected_lowest = -1.797693134862315708e+308;
+    if (lowest < expected_lowest || lowest > expected_lowest) {
+        GTEST_SKIP() << "std::numeric_limits<double>::lowest() is not as expected for "
+                        "this target";
+    }
+    EXPECT_EQ(DoubleToString(std::numeric_limits<double>::lowest()),
+              "-17976931348623157081452742373170435679807056752584499659891747680315726078002853876"
+              "058955863276687817154045895351438246423432132688946418276846754670353751698604991057"
+              "655128207624549009038932894407586850845513394230458323690322294816580855933212334827"
+              "4797826204144723168738177180919299881250404026184124858368.0");
+}
+
+TEST(DoubleToStringTest, Precision) {
+    EXPECT_EQ(DoubleToString(1e-8), "0.00000001");
+    EXPECT_EQ(DoubleToString(1e-9), "0.000000001");
+    EXPECT_EQ(DoubleToString(1e-10), "1e-10");
+    EXPECT_EQ(DoubleToString(1e-15), "1.0000000000000001e-15");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DoubleToBitPreservingString                                                 //
+////////////////////////////////////////////////////////////////////////////////
+
+TEST(DoubleToBitPreservingStringTest, Zero) {
+    EXPECT_EQ(DoubleToBitPreservingString(0.0), "0.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, NegativeZero) {
+    EXPECT_EQ(DoubleToBitPreservingString(-0.0), "-0.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, One) {
+    EXPECT_EQ(DoubleToBitPreservingString(1.0), "1.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, MinusOne) {
+    EXPECT_EQ(DoubleToBitPreservingString(-1.0), "-1.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, Billion) {
+    EXPECT_EQ(DoubleToBitPreservingString(1e9), "1000000000.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, Small) {
+    EXPECT_NE(DoubleToBitPreservingString(std::numeric_limits<double>::epsilon()), "0.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, Highest) {
+    const auto highest = std::numeric_limits<double>::max();
+    const auto expected_highest = 1.797693134862315708e+308;
+    if (highest < expected_highest || highest > expected_highest) {
+        GTEST_SKIP() << "std::numeric_limits<float>::max() is not as expected for "
+                        "this target";
+    }
+    EXPECT_EQ(DoubleToBitPreservingString(std::numeric_limits<double>::max()),
+              "179769313486231570814527423731704356798070567525844996598917476803157260780028538760"
+              "589558632766878171540458953514382464234321326889464182768467546703537516986049910576"
+              "551282076245490090389328944075868508455133942304583236903222948165808559332123348274"
+              "797826204144723168738177180919299881250404026184124858368.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, Lowest) {
+    // Some compilers complain if you test floating point numbers for equality.
+    // So say it via two inequalities.
+    const auto lowest = std::numeric_limits<double>::lowest();
+    const auto expected_lowest = -1.797693134862315708e+308;
+    if (lowest < expected_lowest || lowest > expected_lowest) {
+        GTEST_SKIP() << "std::numeric_limits<float>::lowest() is not as expected for "
+                        "this target";
+    }
+    EXPECT_EQ(DoubleToBitPreservingString(std::numeric_limits<double>::lowest()),
+              "-17976931348623157081452742373170435679807056752584499659891747680315726078002853876"
+              "058955863276687817154045895351438246423432132688946418276846754670353751698604991057"
+              "655128207624549009038932894407586850845513394230458323690322294816580855933212334827"
+              "4797826204144723168738177180919299881250404026184124858368.0");
+}
+
+TEST(DoubleToBitPreservingStringTest, SmallestDenormal) {
+    EXPECT_EQ(DoubleToBitPreservingString(0x1p-1074), "0x1p-1074");
+    EXPECT_EQ(DoubleToBitPreservingString(-0x1p-1074), "-0x1p-1074");
+}
+
+TEST(DoubleToBitPreservingStringTest, BiggerDenormal) {
+    EXPECT_EQ(DoubleToBitPreservingString(0x1p-1073), "0x1p-1073");
+    EXPECT_EQ(DoubleToBitPreservingString(-0x1p-1073), "-0x1p-1073");
+}
+
+TEST(DoubleToBitPreservingStringTest, LargestDenormal) {
+    static_assert(0x0.fffffffffffffp-1022 == 0x1.ffffffffffffep-1023);
+    EXPECT_EQ(DoubleToBitPreservingString(0x0.fffffffffffffp-1022), "0x1.ffffffffffffep-1023");
+    EXPECT_EQ(DoubleToBitPreservingString(-0x0.fffffffffffffp-1022), "-0x1.ffffffffffffep-1023");
+}
+
+TEST(DoubleToBitPreservingStringTest, Subnormal_cafef00dbeef) {
+    EXPECT_EQ(DoubleToBitPreservingString(0x1.cafef00dbeefp-1023), "0x1.cafef00dbeefp-1023");
+    EXPECT_EQ(DoubleToBitPreservingString(-0x1.cafef00dbeefp-1023), "-0x1.cafef00dbeefp-1023");
+}
+
+TEST(DoubleToBitPreservingStringTest, Subnormal_aaaaaaaaaaaaap) {
+    static_assert(0x0.aaaaaaaaaaaaap-1023 == 0x1.5555555555554p-1024);
+    EXPECT_EQ(DoubleToBitPreservingString(0x0.aaaaaaaaaaaaap-1023), "0x1.5555555555554p-1024");
+    EXPECT_EQ(DoubleToBitPreservingString(-0x0.aaaaaaaaaaaaap-1023), "-0x1.5555555555554p-1024");
+}
+
+TEST(DoubleToBitPreservingStringTest, Infinity) {
+    EXPECT_EQ(DoubleToBitPreservingString(static_cast<double>(INFINITY)), "0x1p+1024");
+    EXPECT_EQ(DoubleToBitPreservingString(static_cast<double>(-INFINITY)), "-0x1p+1024");
+}
+
+TEST(DoubleToBitPreservingStringTest, NaN) {
+    auto nan = utils::Bitcast<double>(0x7ff8cafef00dbeefull);
+    EXPECT_EQ(DoubleToBitPreservingString(static_cast<double>(nan)), "0x1.8cafef00dbeefp+1024");
+    EXPECT_EQ(DoubleToBitPreservingString(static_cast<double>(-nan)), "-0x1.8cafef00dbeefp+1024");
 }
 
 }  // namespace
diff --git a/src/tint/writer/glsl/generator_impl.cc b/src/tint/writer/glsl/generator_impl.cc
index da67b3d..49da7d6 100644
--- a/src/tint/writer/glsl/generator_impl.cc
+++ b/src/tint/writer/glsl/generator_impl.cc
@@ -141,7 +141,7 @@
             return "rgba32i";
         case ast::TexelFormat::kRgba32Float:
             return "rgba32f";
-        case ast::TexelFormat::kInvalid:
+        case ast::TexelFormat::kUndefined:
             return "unknown";
     }
     return "unknown";
@@ -509,20 +509,20 @@
                                 {
                                     auto decl = line(&b);
                                     if (!EmitTypeAndName(decl, ret_ty, ast::AddressSpace::kNone,
-                                                         ast::Access::kInvalid, fn_name)) {
+                                                         ast::Access::kUndefined, fn_name)) {
                                         return "";
                                     }
                                     {
                                         ScopedParen sp(decl);
                                         const auto* ty = TypeOf(expr->lhs)->UnwrapRef();
                                         if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                                             ast::Access::kInvalid, "lhs")) {
+                                                             ast::Access::kUndefined, "lhs")) {
                                             return "";
                                         }
                                         decl << ", ";
                                         ty = TypeOf(expr->rhs)->UnwrapRef();
                                         if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
-                                                             ast::Access::kInvalid, "rhs")) {
+                                                             ast::Access::kUndefined, "rhs")) {
                                             return "";
                                         }
                                     }
@@ -935,7 +935,7 @@
             {
                 auto pre = line();
                 if (!EmitTypeAndName(pre, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                     ast::Access::kInvalid, result)) {
+                                     ast::Access::kUndefined, result)) {
                     return false;
                 }
                 pre << ";";
@@ -1213,7 +1213,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kInvalid, "")) {
+                              ast::Access::kUndefined, "")) {
                     return false;
                 }
                 l << " result;";
@@ -1239,7 +1239,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kInvalid, "")) {
+                              ast::Access::kUndefined, "")) {
                     return false;
                 }
                 l << " result;";
@@ -1887,7 +1887,7 @@
         [&](const ast::Override*) {
             // Override is removed with SubstituteOverride
             diagnostics_.add_error(diag::System::Writer,
-                                   "override expressions should have been removed with the "
+                                   "override-expressions should have been removed with the "
                                    "SubstituteOverride transform");
             return false;
         },
@@ -2049,7 +2049,7 @@
             switch (interpolate->type) {
                 case ast::InterpolationType::kPerspective:
                 case ast::InterpolationType::kLinear:
-                case ast::InterpolationType::kInvalid:
+                case ast::InterpolationType::kUndefined:
                     break;
                 case ast::InterpolationType::kFlat:
                     out << "flat ";
@@ -2061,7 +2061,7 @@
                     break;
                 case ast::InterpolationSampling::kSample:
                 case ast::InterpolationSampling::kCenter:
-                case ast::InterpolationSampling::kInvalid:
+                case ast::InterpolationSampling::kUndefined:
                     break;
             }
         }
@@ -2109,7 +2109,7 @@
             if (!wgsize[i].has_value()) {
                 diagnostics_.add_error(
                     diag::System::Writer,
-                    "override expressions should have been removed with the SubstituteOverride "
+                    "override-expressions should have been removed with the SubstituteOverride "
                     "transform");
                 return false;
             }
@@ -2198,7 +2198,7 @@
             return true;
         },
         [&](const sem::Vector* v) {
-            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -2219,7 +2219,7 @@
             return true;
         },
         [&](const sem::Matrix* m) {
-            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -2236,7 +2236,7 @@
             return true;
         },
         [&](const sem::Array* a) {
-            if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -2260,7 +2260,7 @@
             return true;
         },
         [&](const sem::Struct* s) {
-            if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -2351,7 +2351,7 @@
             }
         }
     } else if (auto* str = type->As<sem::Struct>()) {
-        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
             return false;
         }
         bool first = true;
@@ -2365,7 +2365,7 @@
             EmitZeroValue(out, member->Type());
         }
     } else if (auto* arr = type->As<sem::Array>()) {
-        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+        if (!EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
             return false;
         }
         ScopedParen sp(out);
@@ -2972,7 +2972,7 @@
 
     auto out = line();
     // TODO(senorblanco): handle const
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
                          builder_.Symbols().NameFor(let->symbol))) {
         return false;
     }
@@ -2994,7 +2994,7 @@
 
     auto out = line();
     out << "const ";
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
                          builder_.Symbols().NameFor(var->symbol))) {
         return false;
     }
@@ -3022,7 +3022,7 @@
         {
             auto decl = line(&b);
             if (!EmitTypeAndName(decl, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                 ast::Access::kInvalid, fn_name)) {
+                                 ast::Access::kUndefined, fn_name)) {
                 return "";
             }
             {
@@ -3037,8 +3037,8 @@
                         decl << "inout ";
                         ty = ptr->StoreType();
                     }
-                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                         param_name)) {
+                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
+                                         ast::Access::kUndefined, param_name)) {
                         return "";
                     }
                     parameter_names.emplace_back(std::move(param_name));
diff --git a/src/tint/writer/glsl/generator_impl_function_test.cc b/src/tint/writer/glsl/generator_impl_function_test.cc
index 1357985..56beefc 100644
--- a/src/tint/writer/glsl/generator_impl_function_test.cc
+++ b/src/tint/writer/glsl/generator_impl_function_test.cc
@@ -799,7 +799,7 @@
     EXPECT_FALSE(gen.Generate()) << gen.error();
     EXPECT_EQ(
         gen.error(),
-        R"(error: override expressions should have been removed with the SubstituteOverride transform)");
+        R"(error: override-expressions should have been removed with the SubstituteOverride transform)");
 }
 
 TEST_F(GlslGeneratorImplTest_Function, Emit_Function_WithArrayParams) {
diff --git a/src/tint/writer/hlsl/generator_impl.cc b/src/tint/writer/hlsl/generator_impl.cc
index 94e80e8..a51b954 100644
--- a/src/tint/writer/hlsl/generator_impl.cc
+++ b/src/tint/writer/hlsl/generator_impl.cc
@@ -325,7 +325,8 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, vec, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
+            if (!EmitType(ss, vec, tint::ast::AddressSpace::kUndefined, ast::Access::kUndefined,
+                          "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_" + ss.str());
@@ -333,13 +334,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, vec, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
+            if (!EmitTypeAndName(out, vec, ast::AddressSpace::kUndefined, ast::Access::kUndefined,
                                  "vec")) {
                 return "";
             }
             out << ", int idx, ";
-            if (!EmitTypeAndName(out, vec->type(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kInvalid, "val")) {
+            if (!EmitTypeAndName(out, vec->type(), ast::AddressSpace::kUndefined,
+                                 ast::Access::kUndefined, "val")) {
                 return "";
             }
             out << ") {";
@@ -398,7 +399,8 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
+            if (!EmitType(ss, mat, tint::ast::AddressSpace::kUndefined, ast::Access::kUndefined,
+                          "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_vector_" + ss.str());
@@ -406,13 +408,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
+            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kUndefined, ast::Access::kUndefined,
                                  "mat")) {
                 return "";
             }
             out << ", int col, ";
-            if (!EmitTypeAndName(out, mat->ColumnType(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kInvalid, "val")) {
+            if (!EmitTypeAndName(out, mat->ColumnType(), ast::AddressSpace::kUndefined,
+                                 ast::Access::kUndefined, "val")) {
                 return "";
             }
             out << ") {";
@@ -466,7 +468,8 @@
         std::string fn;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, mat, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
+            if (!EmitType(ss, mat, tint::ast::AddressSpace::kUndefined, ast::Access::kUndefined,
+                          "")) {
                 return "";
             }
             fn = UniqueIdentifier("set_scalar_" + ss.str());
@@ -474,13 +477,13 @@
         {
             auto out = line(&helpers_);
             out << "void " << fn << "(inout ";
-            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kInvalid, ast::Access::kInvalid,
+            if (!EmitTypeAndName(out, mat, ast::AddressSpace::kUndefined, ast::Access::kUndefined,
                                  "mat")) {
                 return "";
             }
             out << ", int col, int row, ";
-            if (!EmitTypeAndName(out, mat->type(), ast::AddressSpace::kInvalid,
-                                 ast::Access::kInvalid, "val")) {
+            if (!EmitTypeAndName(out, mat->type(), ast::AddressSpace::kUndefined,
+                                 ast::Access::kUndefined, "val")) {
                 return "";
             }
             out << ") {";
@@ -655,7 +658,7 @@
         if (auto* vec = ty->As<sem::Vector>()) {
             auto* elem_ty = vec->type();
 
-            if (!EmitType(out, ty, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, ty, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -720,7 +723,8 @@
         std::string ty_name;
         {
             std::ostringstream ss;
-            if (!EmitType(ss, ty, tint::ast::AddressSpace::kInvalid, ast::Access::kInvalid, "")) {
+            if (!EmitType(ss, ty, tint::ast::AddressSpace::kUndefined, ast::Access::kUndefined,
+                          "")) {
                 return "";
             }
             ty_name = ss.str();
@@ -1428,12 +1432,12 @@
     auto rmw = [&](const char* hlsl) -> bool {
         {
             auto fn = line(&buf);
-            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
                                  name)) {
                 return false;
             }
             fn << "(RWByteAddressBuffer buffer, uint offset, ";
-            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+            if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
                                  "value")) {
                 return false;
             }
@@ -1449,7 +1453,7 @@
 
         {
             auto l = line(&buf);
-            if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+            if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
                                  "original_value")) {
                 return false;
             }
@@ -1498,8 +1502,8 @@
             // InterlockedOr using 0 as the OR value
             {
                 auto fn = line(&buf);
-                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     name)) {
+                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, name)) {
                     return false;
                 }
                 fn << "(RWByteAddressBuffer buffer, uint offset) {";
@@ -1514,8 +1518,8 @@
 
             {
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     "value")) {
+                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, "value")) {
                     return false;
                 }
                 l << " = 0;";
@@ -1532,8 +1536,8 @@
             {
                 auto fn = line(&buf);
                 fn << "void " << name << "(RWByteAddressBuffer buffer, uint offset, ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     "value")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, "value")) {
                     return false;
                 }
                 fn << ") {";
@@ -1548,7 +1552,7 @@
 
             {
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
+                if (!EmitTypeAndName(l, value_ty, ast::AddressSpace::kNone, ast::Access::kUndefined,
                                      "ignored")) {
                     return false;
                 }
@@ -1563,18 +1567,18 @@
             auto* value_ty = params[2]->Type()->UnwrapRef();
             {
                 auto fn = line(&buf);
-                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     name)) {
+                if (!EmitTypeAndName(fn, result_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, name)) {
                     return false;
                 }
                 fn << "(RWByteAddressBuffer buffer, uint offset, ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     "compare")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, "compare")) {
                     return false;
                 }
                 fn << ", ";
-                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     "value")) {
+                if (!EmitTypeAndName(fn, value_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, "value")) {
                     return false;
                 }
                 fn << ") {";
@@ -1589,8 +1593,8 @@
 
             {  // T result = {0};
                 auto l = line(&buf);
-                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                     "result")) {
+                if (!EmitTypeAndName(l, result_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, "result")) {
                     return false;
                 }
                 l << "=";
@@ -1625,7 +1629,7 @@
     if (!builtin->ReturnType()->Is<sem::Void>()) {
         auto pre = line();
         if (!EmitTypeAndName(pre, builtin->ReturnType(), ast::AddressSpace::kNone,
-                             ast::Access::kInvalid, result)) {
+                             ast::Access::kUndefined, result)) {
             return false;
         }
         pre << " = ";
@@ -1688,8 +1692,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::kInvalid,
-                                     result)) {
+                if (!EmitTypeAndName(pre, value_ty, ast::AddressSpace::kNone,
+                                     ast::Access::kUndefined, result)) {
                     return false;
                 }
                 pre << " = ";
@@ -1729,7 +1733,7 @@
             {  // T compare_value = <compare_value>;
                 auto pre = line();
                 if (!EmitTypeAndName(pre, TypeOf(compare_value)->UnwrapRef(),
-                                     ast::AddressSpace::kNone, ast::Access::kInvalid, compare)) {
+                                     ast::AddressSpace::kNone, ast::Access::kUndefined, compare)) {
                     return false;
                 }
                 pre << " = ";
@@ -1839,7 +1843,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kInvalid, "")) {
+                              ast::Access::kUndefined, "")) {
                     return false;
                 }
                 l << " result;";
@@ -1881,7 +1885,7 @@
             {
                 auto l = line(b);
                 if (!EmitType(l, builtin->ReturnType(), ast::AddressSpace::kNone,
-                              ast::Access::kInvalid, "")) {
+                              ast::Access::kUndefined, "")) {
                     return false;
                 }
                 l << " result = {sig, int" << width << "(exp)};";
@@ -2718,7 +2722,7 @@
 
             auto const* type = v->Type();
             auto address_space = ast::AddressSpace::kNone;
-            auto access = ast::Access::kInvalid;
+            auto access = ast::Access::kUndefined;
 
             if (auto* ptr = type->As<sem::Pointer>()) {
                 type = ptr->StoreType();
@@ -2833,7 +2837,7 @@
         [&](const ast::Override*) {
             // Override is removed with SubstituteOverride
             diagnostics_.add_error(diag::System::Writer,
-                                   "override expressions should have been removed with the "
+                                   "override-expressions should have been removed with the "
                                    "SubstituteOverride transform");
             return false;
         },
@@ -3007,7 +3011,7 @@
         case ast::InterpolationType::kFlat:
             modifiers += "nointerpolation ";
             break;
-        case ast::InterpolationType::kInvalid:
+        case ast::InterpolationType::kUndefined:
             break;
     }
     switch (sampling) {
@@ -3018,7 +3022,7 @@
             modifiers += "sample ";
             break;
         case ast::InterpolationSampling::kCenter:
-        case ast::InterpolationSampling::kInvalid:
+        case ast::InterpolationSampling::kUndefined:
             break;
     }
     return modifiers;
@@ -3040,7 +3044,7 @@
                 if (!wgsize[i].has_value()) {
                     diagnostics_.add_error(
                         diag::System::Writer,
-                        "override expressions should have been removed with the SubstituteOverride "
+                        "override-expressions should have been removed with the SubstituteOverride "
                         "transform");
                     return false;
                 }
@@ -3140,7 +3144,7 @@
                 return true;
             }
 
-            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, v, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -3157,7 +3161,7 @@
             return true;
         },
         [&](const sem::Matrix* m) {
-            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+            if (!EmitType(out, m, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                 return false;
             }
 
@@ -3176,7 +3180,7 @@
         [&](const sem::Array* a) {
             if (constant->AllZero()) {
                 out << "(";
-                if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+                if (!EmitType(out, a, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                     return false;
                 }
                 out << ")0";
@@ -3206,7 +3210,7 @@
         [&](const sem::Struct* s) {
             if (constant->AllZero()) {
                 out << "(";
-                if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kInvalid, "")) {
+                if (!EmitType(out, s, ast::AddressSpace::kNone, ast::Access::kUndefined, "")) {
                     return false;
                 }
                 out << ")0";
@@ -3327,12 +3331,12 @@
         [&](const sem::Struct*) {
             out << "(";
             TINT_DEFER(out << ")" << value);
-            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "");
+            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "");
         },
         [&](const sem::Array*) {
             out << "(";
             TINT_DEFER(out << ")" << value);
-            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid, "");
+            return EmitType(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined, "");
         },
         [&](Default) {
             diagnostics_.add_error(
@@ -4080,7 +4084,7 @@
 
     auto out = line();
     out << "const ";
-    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kInvalid,
+    if (!EmitTypeAndName(out, type, ast::AddressSpace::kNone, ast::Access::kUndefined,
                          builder_.Symbols().NameFor(let->symbol))) {
         return false;
     }
@@ -4108,7 +4112,7 @@
         {
             auto decl = line(&b);
             if (!EmitTypeAndName(decl, builtin->ReturnType(), ast::AddressSpace::kNone,
-                                 ast::Access::kInvalid, fn_name)) {
+                                 ast::Access::kUndefined, fn_name)) {
                 return "";
             }
             {
@@ -4123,8 +4127,8 @@
                         decl << "inout ";
                         ty = ptr->StoreType();
                     }
-                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone, ast::Access::kInvalid,
-                                         param_name)) {
+                    if (!EmitTypeAndName(decl, ty, ast::AddressSpace::kNone,
+                                         ast::Access::kUndefined, param_name)) {
                         return "";
                     }
                     parameter_names.emplace_back(std::move(param_name));
diff --git a/src/tint/writer/hlsl/generator_impl_function_test.cc b/src/tint/writer/hlsl/generator_impl_function_test.cc
index 1adb852..56465d3 100644
--- a/src/tint/writer/hlsl/generator_impl_function_test.cc
+++ b/src/tint/writer/hlsl/generator_impl_function_test.cc
@@ -728,7 +728,7 @@
     EXPECT_FALSE(gen.Generate()) << gen.error();
     EXPECT_EQ(
         gen.error(),
-        R"(error: override expressions should have been removed with the SubstituteOverride transform)");
+        R"(error: override-expressions should have been removed with the SubstituteOverride transform)");
 }
 
 TEST_F(HlslGeneratorImplTest_Function, Emit_Function_WithArrayParams) {
diff --git a/src/tint/writer/msl/generator_impl.cc b/src/tint/writer/msl/generator_impl.cc
index 5db1d8f..27a5191 100644
--- a/src/tint/writer/msl/generator_impl.cc
+++ b/src/tint/writer/msl/generator_impl.cc
@@ -301,7 +301,7 @@
             [&](const ast::Override*) {
                 // Override is removed with SubstituteOverride
                 diagnostics_.add_error(diag::System::Writer,
-                                       "override expressions should have been removed with the "
+                                       "override-expressions should have been removed with the "
                                        "SubstituteOverride transform.");
                 return false;
             },
@@ -590,8 +590,18 @@
         ScopedParen sp(out);
         {
             ScopedBitCast lhs_uint_cast(this, out, lhs_type, unsigned_type_of(lhs_type));
-            if (!EmitExpression(out, expr->lhs)) {
-                return false;
+
+            // In case the type is packed, cast to our own type in order to remove the packing.
+            // Otherwise, this just casts to itself.
+            if (lhs_type->is_signed_integer_vector()) {
+                ScopedCast lhs_self_cast(this, out, lhs_type, lhs_type);
+                if (!EmitExpression(out, expr->lhs)) {
+                    return false;
+                }
+            } else {
+                if (!EmitExpression(out, expr->lhs)) {
+                    return false;
+                }
             }
         }
         if (!emit_op()) {
@@ -1947,7 +1957,7 @@
         case ast::InterpolationSampling::kSample:
             attr = "sample_";
             break;
-        case ast::InterpolationSampling::kInvalid:
+        case ast::InterpolationSampling::kUndefined:
             break;
     }
     switch (type) {
@@ -1960,7 +1970,7 @@
         case ast::InterpolationType::kFlat:
             attr += "flat";
             break;
-        case ast::InterpolationType::kInvalid:
+        case ast::InterpolationType::kUndefined:
             break;
     }
     return attr;
diff --git a/src/tint/writer/spirv/builder.cc b/src/tint/writer/spirv/builder.cc
index 57a363d..83587f1 100644
--- a/src/tint/writer/spirv/builder.cc
+++ b/src/tint/writer/spirv/builder.cc
@@ -509,7 +509,7 @@
         // Check if the workgroup_size uses pipeline-overridable constants.
         if (!wgsize[0].has_value() || !wgsize[1].has_value() || !wgsize[2].has_value()) {
             error_ =
-                "override expressions should have been removed with the SubstituteOverride "
+                "override-expressions should have been removed with the SubstituteOverride "
                 "transform";
             return false;
         }
@@ -766,7 +766,7 @@
                     push_annot(spv::Op::OpDecorate,
                                {Operand(var_id), U32Operand(SpvDecorationNonWritable)});
                     break;
-                case ast::Access::kInvalid:
+                case ast::Access::kUndefined:
                 case ast::Access::kReadWrite:
                     break;
             }
@@ -4009,7 +4009,7 @@
 
 SpvStorageClass Builder::ConvertAddressSpace(ast::AddressSpace klass) const {
     switch (klass) {
-        case ast::AddressSpace::kInvalid:
+        case ast::AddressSpace::kUndefined:
             return SpvStorageClassMax;
         case ast::AddressSpace::kIn:
             return SpvStorageClassInput;
@@ -4071,7 +4071,7 @@
             return SpvBuiltInSampleId;
         case ast::BuiltinValue::kSampleMask:
             return SpvBuiltInSampleMask;
-        case ast::BuiltinValue::kInvalid:
+        case ast::BuiltinValue::kUndefined:
             break;
     }
     return SpvBuiltInMax;
@@ -4088,7 +4088,7 @@
             push_annot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationFlat)});
             break;
         case ast::InterpolationType::kPerspective:
-        case ast::InterpolationType::kInvalid:
+        case ast::InterpolationType::kUndefined:
             break;
     }
     switch (sampling) {
@@ -4100,7 +4100,7 @@
             push_annot(spv::Op::OpDecorate, {Operand(id), U32Operand(SpvDecorationSample)});
             break;
         case ast::InterpolationSampling::kCenter:
-        case ast::InterpolationSampling::kInvalid:
+        case ast::InterpolationSampling::kUndefined:
             break;
     }
 }
@@ -4142,7 +4142,7 @@
             return SpvImageFormatRgba32i;
         case ast::TexelFormat::kRgba32Float:
             return SpvImageFormatRgba32f;
-        case ast::TexelFormat::kInvalid:
+        case ast::TexelFormat::kUndefined:
             return SpvImageFormatUnknown;
     }
     return SpvImageFormatUnknown;
diff --git a/src/tint/writer/spirv/builder_function_attribute_test.cc b/src/tint/writer/spirv/builder_function_attribute_test.cc
index 60bf062..b905d8c 100644
--- a/src/tint/writer/spirv/builder_function_attribute_test.cc
+++ b/src/tint/writer/spirv/builder_function_attribute_test.cc
@@ -164,7 +164,7 @@
     EXPECT_FALSE(b.GenerateExecutionModes(func, 3)) << b.error();
     EXPECT_EQ(
         b.error(),
-        R"(override expressions should have been removed with the SubstituteOverride transform)");
+        R"(override-expressions should have been removed with the SubstituteOverride transform)");
 }
 
 TEST_F(BuilderTest, Decoration_ExecutionMode_WorkgroupSize_LiteralAndConst) {
@@ -181,7 +181,7 @@
     EXPECT_FALSE(b.GenerateExecutionModes(func, 3)) << b.error();
     EXPECT_EQ(
         b.error(),
-        R"(override expressions should have been removed with the SubstituteOverride transform)");
+        R"(override-expressions should have been removed with the SubstituteOverride transform)");
 }
 
 TEST_F(BuilderTest, Decoration_ExecutionMode_MultipleFragment) {
diff --git a/src/tint/writer/spirv/builder_global_variable_test.cc b/src/tint/writer/spirv/builder_global_variable_test.cc
index b0bbf3f..0d2ec9c 100644
--- a/src/tint/writer/spirv/builder_global_variable_test.cc
+++ b/src/tint/writer/spirv/builder_global_variable_test.cc
@@ -270,7 +270,7 @@
     BuilderTest_Type,
     BuiltinDataTest,
     testing::Values(
-        BuiltinData{ast::BuiltinValue::kInvalid, ast::AddressSpace::kNone, SpvBuiltInMax},
+        BuiltinData{ast::BuiltinValue::kUndefined, ast::AddressSpace::kNone, SpvBuiltInMax},
         BuiltinData{ast::BuiltinValue::kPosition, ast::AddressSpace::kIn, SpvBuiltInFragCoord},
         BuiltinData{ast::BuiltinValue::kPosition, ast::AddressSpace::kOut, SpvBuiltInPosition},
         BuiltinData{
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index cf43f23..7795455 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -263,7 +263,11 @@
             // Note that all normal and subnormal f16 values are normal f32 values, and since NaN
             // and Inf are not allowed to be spelled in literal, it should be fine to emit f16
             // literals in this way.
-            out << FloatToBitPreservingString(static_cast<float>(l->value)) << l->suffix;
+            if (l->suffix == ast::FloatLiteralExpression::Suffix::kNone) {
+                out << DoubleToBitPreservingString(l->value);
+            } else {
+                out << FloatToBitPreservingString(static_cast<float>(l->value)) << l->suffix;
+            }
             return true;
         },
         [&](const ast::IntLiteralExpression* l) {  //
@@ -346,7 +350,7 @@
 
 bool GeneratorImpl::EmitImageFormat(std::ostream& out, const ast::TexelFormat fmt) {
     switch (fmt) {
-        case ast::TexelFormat::kInvalid:
+        case ast::TexelFormat::kUndefined:
             diagnostics_.add_error(diag::System::Writer, "unknown image format");
             return false;
         default:
@@ -433,7 +437,7 @@
             if (!EmitType(out, ptr->type)) {
                 return false;
             }
-            if (ptr->access != ast::Access::kInvalid) {
+            if (ptr->access != ast::Access::kUndefined) {
                 out << ", ";
                 if (!EmitAccess(out, ptr->access)) {
                     return false;
@@ -656,9 +660,9 @@
             out << "var";
             auto address_space = var->declared_address_space;
             auto ac = var->declared_access;
-            if (address_space != ast::AddressSpace::kNone || ac != ast::Access::kInvalid) {
+            if (address_space != ast::AddressSpace::kNone || ac != ast::Access::kUndefined) {
                 out << "<" << address_space;
-                if (ac != ast::Access::kInvalid) {
+                if (ac != ast::Access::kUndefined) {
                     out << ", ";
                     if (!EmitAccess(out, ac)) {
                         return false;
@@ -769,7 +773,7 @@
             },
             [&](const ast::InterpolateAttribute* interpolate) {
                 out << "interpolate(" << interpolate->type;
-                if (interpolate->sampling != ast::InterpolationSampling::kInvalid) {
+                if (interpolate->sampling != ast::InterpolationSampling::kUndefined) {
                     out << ", " << interpolate->sampling;
                 }
                 out << ")";