Import Tint changes from Dawn

Changes:
  - 5af9ce441b502808f35ff243fa62f29f62377265 tint/resolver: Clean up the resolver built-in unittests by Zhaoming Jiang <zhaoming.jiang@intel.com>
  - 634a2430d83f26970bef2cb381959f805f2d5241 tint: Use OverrideId for SubstituteOverride transform by Ben Clayton <bclayton@google.com>
GitOrigin-RevId: 5af9ce441b502808f35ff243fa62f29f62377265
Change-Id: I7b4fe7e33313e1f3824910d6c9581ba055d0849b
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/97602
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index b948e69..0c2bcdf 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -34,6 +34,7 @@
 
 #include "src/tint/utils/io/command.h"
 #include "src/tint/utils/string.h"
+#include "src/tint/utils/transform.h"
 #include "src/tint/val/val.h"
 #include "tint/tint.h"
 
@@ -990,30 +991,72 @@
 
     struct TransformFactory {
         const char* name;
-        std::function<void(tint::transform::Manager& manager, tint::transform::DataMap& inputs)>
+        /// Build and adds the transform to the transform manager.
+        /// Parameters:
+        ///   inspector - an inspector created from the parsed program
+        ///   manager   - the transform manager. Add transforms to this.
+        ///   inputs    - the input data to the transform manager. Add inputs to this.
+        /// Returns true on success, false on error (program will immediately exit)
+        std::function<bool(tint::inspector::Inspector& inspector,
+                           tint::transform::Manager& manager,
+                           tint::transform::DataMap& inputs)>
             make;
     };
     std::vector<TransformFactory> transforms = {
         {"first_index_offset",
-         [](tint::transform::Manager& m, tint::transform::DataMap& i) {
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap& i) {
              i.Add<tint::transform::FirstIndexOffset::BindingPoint>(0, 0);
              m.Add<tint::transform::FirstIndexOffset>();
+             return true;
          }},
         {"fold_trivial_single_use_lets",
-         [](tint::transform::Manager& m, tint::transform::DataMap&) {
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap&) {
              m.Add<tint::transform::FoldTrivialSingleUseLets>();
+             return true;
          }},
-        {"renamer", [](tint::transform::Manager& m,
-                       tint::transform::DataMap&) { m.Add<tint::transform::Renamer>(); }},
-        {"robustness", [](tint::transform::Manager& m,
-                          tint::transform::DataMap&) { m.Add<tint::transform::Robustness>(); }},
+        {"renamer",
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap&) {
+             m.Add<tint::transform::Renamer>();
+             return true;
+         }},
+        {"robustness",
+         [](tint::inspector::Inspector&, tint::transform::Manager& m, tint::transform::DataMap&) {
+             m.Add<tint::transform::Robustness>();
+             return true;
+         }},
         {"substitute_override",
-         [&](tint::transform::Manager& m, tint::transform::DataMap& i) {
+         [&](tint::inspector::Inspector& inspector, tint::transform::Manager& m,
+             tint::transform::DataMap& i) {
              tint::transform::SubstituteOverride::Config cfg;
-             cfg.map = options.overrides;
+
+             std::unordered_map<tint::OverrideId, double> values;
+             values.reserve(options.overrides.size());
+
+             for (const auto& [name, value] : options.overrides) {
+                 if (name.empty()) {
+                     std::cerr << "empty override name";
+                     return false;
+                 }
+                 if (isdigit(name[0])) {
+                     tint::OverrideId id{
+                         static_cast<decltype(tint::OverrideId::value)>(atoi(name.c_str()))};
+                     values.emplace(id, value);
+                 } else {
+                     auto override_names = inspector.GetNamedOverrideIds();
+                     auto it = override_names.find(name);
+                     if (it == override_names.end()) {
+                         std::cerr << "unknown override '" << name << "'";
+                         return false;
+                     }
+                     values.emplace(it->second, value);
+                 }
+             }
+
+             cfg.map = std::move(values);
 
              i.Add<tint::transform::SubstituteOverride::Config>(cfg);
              m.Add<tint::transform::SubstituteOverride>();
+             return true;
          }},
     };
     auto transform_names = [&] {
@@ -1144,6 +1187,40 @@
         return 1;
     }
 
+    tint::inspector::Inspector inspector(program.get());
+
+    if (options.dump_inspector_bindings) {
+        std::cout << std::string(80, '-') << std::endl;
+        auto entry_points = inspector.GetEntryPoints();
+        if (!inspector.error().empty()) {
+            std::cerr << "Failed to get entry points from Inspector: " << inspector.error()
+                      << std::endl;
+            return 1;
+        }
+
+        for (auto& entry_point : entry_points) {
+            auto bindings = inspector.GetResourceBindings(entry_point.name);
+            if (!inspector.error().empty()) {
+                std::cerr << "Failed to get bindings from Inspector: " << inspector.error()
+                          << std::endl;
+                return 1;
+            }
+            std::cout << "Entry Point = " << entry_point.name << std::endl;
+            for (auto& binding : bindings) {
+                std::cout << "\t[" << binding.bind_group << "][" << binding.binding
+                          << "]:" << std::endl;
+                std::cout << "\t\t resource_type = " << ResourceTypeToString(binding.resource_type)
+                          << std::endl;
+                std::cout << "\t\t dim = " << TextureDimensionToString(binding.dim) << std::endl;
+                std::cout << "\t\t sampled_kind = " << SampledKindToString(binding.sampled_kind)
+                          << std::endl;
+                std::cout << "\t\t image_format = " << TexelFormatToString(binding.image_format)
+                          << std::endl;
+            }
+        }
+        std::cout << std::string(80, '-') << std::endl;
+    }
+
     tint::transform::Manager transform_manager;
     tint::transform::DataMap transform_inputs;
 
@@ -1151,7 +1228,9 @@
     if (!options.overrides.empty()) {
         for (auto& t : transforms) {
             if (t.name == std::string("substitute_override")) {
-                t.make(transform_manager, transform_inputs);
+                if (!t.make(inspector, transform_manager, transform_inputs)) {
+                    return 1;
+                }
                 break;
             }
         }
@@ -1165,7 +1244,9 @@
         bool found = false;
         for (auto& t : transforms) {
             if (t.name == name) {
-                t.make(transform_manager, transform_inputs);
+                if (!t.make(inspector, transform_manager, transform_inputs)) {
+                    return 1;
+                }
                 found = true;
                 break;
             }
@@ -1219,39 +1300,6 @@
 
     *program = std::move(out.program);
 
-    if (options.dump_inspector_bindings) {
-        std::cout << std::string(80, '-') << std::endl;
-        tint::inspector::Inspector inspector(program.get());
-        auto entry_points = inspector.GetEntryPoints();
-        if (!inspector.error().empty()) {
-            std::cerr << "Failed to get entry points from Inspector: " << inspector.error()
-                      << std::endl;
-            return 1;
-        }
-
-        for (auto& entry_point : entry_points) {
-            auto bindings = inspector.GetResourceBindings(entry_point.name);
-            if (!inspector.error().empty()) {
-                std::cerr << "Failed to get bindings from Inspector: " << inspector.error()
-                          << std::endl;
-                return 1;
-            }
-            std::cout << "Entry Point = " << entry_point.name << std::endl;
-            for (auto& binding : bindings) {
-                std::cout << "\t[" << binding.bind_group << "][" << binding.binding
-                          << "]:" << std::endl;
-                std::cout << "\t\t resource_type = " << ResourceTypeToString(binding.resource_type)
-                          << std::endl;
-                std::cout << "\t\t dim = " << TextureDimensionToString(binding.dim) << std::endl;
-                std::cout << "\t\t sampled_kind = " << SampledKindToString(binding.sampled_kind)
-                          << std::endl;
-                std::cout << "\t\t image_format = " << TexelFormatToString(binding.image_format)
-                          << std::endl;
-            }
-        }
-        std::cout << std::string(80, '-') << std::endl;
-    }
-
     bool success = false;
     switch (options.format) {
         case Format::kSpirv:
diff --git a/src/tint/resolver/builtin_test.cc b/src/tint/resolver/builtin_test.cc
index 74367d1..eaaa9c2 100644
--- a/src/tint/resolver/builtin_test.cc
+++ b/src/tint/resolver/builtin_test.cc
@@ -48,7 +48,1510 @@
 
 using ResolverBuiltinTest = ResolverTest;
 
+struct BuiltinData {
+    const char* name;
+    BuiltinType builtin;
+};
+
+inline std::ostream& operator<<(std::ostream& out, BuiltinData data) {
+    out << data.name;
+    return out;
+}
+
+TEST_F(ResolverBuiltinTest, ModuleScopeUsage) {
+    GlobalConst("c", ty.f32(), Call(Source{{12, 34}}, "abs", 1._f));
+
+    EXPECT_FALSE(r()->Resolve());
+
+    // 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)");
+}
+
+// Tests for Logical builtins
+namespace logical_builtin_tests {
+
+using ResolverBuiltinTest_BoolMethod = ResolverTestWithParam<std::string>;
+
+TEST_P(ResolverBuiltinTest_BoolMethod, Scalar) {
+    auto name = GetParam();
+
+    GlobalVar("my_var", ty.bool_(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call(name, "my_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::Bool>());
+}
+TEST_P(ResolverBuiltinTest_BoolMethod, Vector) {
+    auto name = GetParam();
+
+    GlobalVar("my_var", ty.vec3<bool>(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call(name, "my_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::Bool>());
+}
+INSTANTIATE_TEST_SUITE_P(ResolverTest,
+                         ResolverBuiltinTest_BoolMethod,
+                         testing::Values("any", "all"));
+
+TEST_F(ResolverBuiltinTest, Select) {
+    GlobalVar("my_var", ty.vec3<f32>(), ast::StorageClass::kPrivate);
+
+    GlobalVar("bool_var", ty.vec3<bool>(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call("select", "my_var", "my_var", "bool_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::Vector>());
+    EXPECT_EQ(TypeOf(expr)->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(TypeOf(expr)->As<sem::Vector>()->type()->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinTest, Select_Error_NoParams) {
+    auto* expr = Call("select");
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to select()
+
+3 candidate functions:
+  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
+)");
+}
+
+TEST_F(ResolverBuiltinTest, Select_Error_SelectorInt) {
+    auto* expr = Call("select", 1_i, 1_i, 1_i);
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to select(i32, i32, i32)
+
+3 candidate functions:
+  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
+)");
+}
+
+TEST_F(ResolverBuiltinTest, Select_Error_Matrix) {
+    auto* expr = Call("select", mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)),
+                      mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)), Expr(true));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to select(mat2x2<f32>, mat2x2<f32>, bool)
+
+3 candidate functions:
+  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
+)");
+}
+
+TEST_F(ResolverBuiltinTest, Select_Error_MismatchTypes) {
+    auto* expr = Call("select", 1_f, vec2<f32>(2_f, 3_f), Expr(true));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to select(f32, vec2<f32>, bool)
+
+3 candidate functions:
+  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
+)");
+}
+
+TEST_F(ResolverBuiltinTest, Select_Error_MismatchVectorSize) {
+    auto* expr = Call("select", vec2<f32>(1_f, 2_f), vec3<f32>(3_f, 4_f, 5_f), Expr(true));
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to select(vec2<f32>, vec3<f32>, bool)
+
+3 candidate functions:
+  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
+)");
+}
+
+}  // namespace logical_builtin_tests
+
+// Tests for Array builtins
+namespace array_builtin_tests {
+
+using ResolverBuiltinArrayTest = ResolverTest;
+
+TEST_F(ResolverBuiltinArrayTest, ArrayLength_Vector) {
+    auto* ary = ty.array<i32>();
+    auto* str = Structure("S", {Member("x", ary)});
+    GlobalVar("a", ty.Of(str), ast::StorageClass::kStorage, ast::Access::kRead,
+              ast::AttributeList{
+                  create<ast::BindingAttribute>(0u),
+                  create<ast::GroupAttribute>(0u),
+              });
+
+    auto* call = Call("arrayLength", AddressOf(MemberAccessor("a", "x")));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+}
+
+TEST_F(ResolverBuiltinArrayTest, ArrayLength_Error_ArraySized) {
+    GlobalVar("arr", ty.array<i32, 4>(), ast::StorageClass::kPrivate);
+    auto* call = Call("arrayLength", AddressOf("arr"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to arrayLength(ptr<private, array<i32, 4>, read_write>)
+
+1 candidate function:
+  arrayLength(ptr<storage, array<T>, A>) -> u32
+)");
+}
+
+}  // namespace array_builtin_tests
+
+// Tests for Numeric builtins with float parameter
+namespace float_builtin_tests {
+
+// Testcase parameters for float built-in having signature of (T, ...) -> T and (vecN<T>, ...) ->
+// vecN<T>
+struct BuiltinDataWithParamNum {
+    uint32_t args_number;
+    const char* name;
+    BuiltinType builtin;
+};
+
+inline std::ostream& operator<<(std::ostream& out, BuiltinDataWithParamNum data) {
+    out << data.name;
+    return out;
+}
+
+// Tests for float built-ins that has signiture (T, ...) -> T and (vecN<T>, ...) -> vecN<T>
+using ResolverBuiltinTest_FloatBuiltin_IdenticalType =
+    ResolverTestWithParam<BuiltinDataWithParamNum>;
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, Error_NoParams) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(),
+                HasSubstr("error: no matching call to " + std::string(param.name) + "()"));
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, OneParam_Scalar_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_f);
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) + "(f32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, OneParam_Vector_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_float_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<f32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, TwoParams_Scalar_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_f, 1_f);
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(f32, f32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, TwoParams_Vector_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_float_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<f32>, vec3<f32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, ThreeParams_Scalar_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_f, 1_f, 1_f);
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(f32, f32, f32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, ThreeParams_Vector_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
+                      vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_float_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<f32>, vec3<f32>, vec3<f32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, FourParams_Scalar_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_f, 1_f, 1_f, 1_f);
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(f32, f32, f32, f32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_FloatBuiltin_IdenticalType, FourParams_Vector_f32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
+                      vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_float_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<f32>, vec3<f32>, vec3<f32>, vec3<f32>)"));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ResolverTest,
+    ResolverBuiltinTest_FloatBuiltin_IdenticalType,
+    testing::Values(BuiltinDataWithParamNum{1, "abs", BuiltinType::kAbs},
+                    BuiltinDataWithParamNum{1, "acos", BuiltinType::kAcos},
+                    BuiltinDataWithParamNum{1, "acosh", BuiltinType::kAcos},
+                    BuiltinDataWithParamNum{1, "asin", BuiltinType::kAsin},
+                    BuiltinDataWithParamNum{1, "asinh", BuiltinType::kAsin},
+                    BuiltinDataWithParamNum{1, "atan", BuiltinType::kAtan},
+                    BuiltinDataWithParamNum{1, "atanh", BuiltinType::kAtan},
+                    BuiltinDataWithParamNum{2, "atan2", BuiltinType::kAtan2},
+                    BuiltinDataWithParamNum{1, "ceil", BuiltinType::kCeil},
+                    BuiltinDataWithParamNum{3, "clamp", BuiltinType::kClamp},
+                    BuiltinDataWithParamNum{1, "cos", BuiltinType::kCos},
+                    BuiltinDataWithParamNum{1, "cosh", BuiltinType::kCosh},
+                    // cross: (vec3<T>, vec3<T>) -> vec3<T>
+                    BuiltinDataWithParamNum{1, "degrees", BuiltinType::kDegrees},
+                    // distance: (T, T) -> T, (vecN<T>, vecN<T>) -> T
+                    BuiltinDataWithParamNum{1, "exp", BuiltinType::kExp},
+                    BuiltinDataWithParamNum{1, "exp2", BuiltinType::kExp2},
+                    // faceForward: (vecN<T>, vecN<T>, vecN<T>) -> vecN<T>
+                    BuiltinDataWithParamNum{1, "floor", BuiltinType::kFloor},
+                    BuiltinDataWithParamNum{3, "fma", BuiltinType::kFma},
+                    BuiltinDataWithParamNum{1, "fract", BuiltinType::kFract},
+                    // frexp
+                    BuiltinDataWithParamNum{1, "inverseSqrt", BuiltinType::kInverseSqrt},
+                    // ldexp: (T, i32) -> T, (vecN<T>, vecN<i32>) -> vecN<T>
+                    // length: (vecN<T>) -> T
+                    BuiltinDataWithParamNum{1, "log", BuiltinType::kLog},
+                    BuiltinDataWithParamNum{1, "log2", BuiltinType::kLog2},
+                    BuiltinDataWithParamNum{2, "max", BuiltinType::kMax},
+                    BuiltinDataWithParamNum{2, "min", BuiltinType::kMin},
+                    // Note that `mix(vecN<f32>, vecN<f32>, f32) -> vecN<f32>` is not tested here.
+                    BuiltinDataWithParamNum{3, "mix", BuiltinType::kMix},
+                    // modf
+                    // normalize: (vecN<T>) -> vecN<T>
+                    BuiltinDataWithParamNum{2, "pow", BuiltinType::kPow},
+                    // quantizeToF16 is not implemented yet.
+                    BuiltinDataWithParamNum{1, "radians", BuiltinType::kRadians},
+                    // reflect: (vecN<T>, vecN<T>) -> vecN<T>
+                    // refract: (vecN<T>, vecN<T>, T) -> vecN<T>
+                    BuiltinDataWithParamNum{1, "round", BuiltinType::kRound},
+                    // saturate not implemented yet.
+                    BuiltinDataWithParamNum{1, "sign", BuiltinType::kSign},
+                    BuiltinDataWithParamNum{1, "sin", BuiltinType::kSin},
+                    BuiltinDataWithParamNum{1, "sinh", BuiltinType::kSinh},
+                    BuiltinDataWithParamNum{3, "smoothstep", BuiltinType::kSmoothstep},
+                    BuiltinDataWithParamNum{1, "sqrt", BuiltinType::kSqrt},
+                    BuiltinDataWithParamNum{2, "step", BuiltinType::kStep},
+                    BuiltinDataWithParamNum{1, "tan", BuiltinType::kTan},
+                    BuiltinDataWithParamNum{1, "tanh", BuiltinType::kTanh},
+                    BuiltinDataWithParamNum{1, "trunc", BuiltinType::kTrunc}));
+
+using ResolverBuiltinFloatTest = ResolverTest;
+
+// cross: (vec3<T>, vec3<T>) -> vec3<T>
+TEST_F(ResolverBuiltinFloatTest, Cross_f32) {
+    auto* call = Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->is_float_vector());
+    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Cross_Error_NoArgs) {
+    auto* call = Call("cross");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to cross()
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Cross_Error_Scalar) {
+    auto* call = Call("cross", 1_f, 1_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to cross(f32, f32)
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Cross_Error_Vec3Int) {
+    auto* call = Call("cross", vec3<i32>(1_i, 2_i, 3_i), vec3<i32>(1_i, 2_i, 3_i));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to cross(vec3<i32>, vec3<i32>)
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Cross_Error_Vec4) {
+    auto* call = Call("cross", vec4<f32>(1_f, 2_f, 3_f, 4_f), vec4<f32>(1_f, 2_f, 3_f, 4_f));
+
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to cross(vec4<f32>, vec4<f32>)
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Cross_Error_TooManyParams) {
+    auto* call =
+        Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
+
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to cross(vec3<f32>, vec3<f32>, vec3<f32>)
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+// distance: (T, T) -> T, (vecN<T>, vecN<T>) -> T
+TEST_F(ResolverBuiltinFloatTest, Distance_Scalar_f32) {
+    auto* call = Call("distance", 1_f, 1_f);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Distance_Vector_f32) {
+    auto* call = Call("distance", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Distance_TooManyParams) {
+    auto* call = Call("distance", 1_f, 1_f, 3_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to distance(f32, f32, f32)
+
+2 candidate functions:
+  distance(f32, f32) -> f32
+  distance(vecN<f32>, vecN<f32>) -> f32
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Distance_TooFewParams) {
+    auto* call = Call("distance", 1_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to distance(f32)
+
+2 candidate functions:
+  distance(f32, f32) -> f32
+  distance(vecN<f32>, vecN<f32>) -> f32
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Distance_NoParams) {
+    auto* call = Call("distance");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to distance()
+
+2 candidate functions:
+  distance(f32, f32) -> f32
+  distance(vecN<f32>, vecN<f32>) -> f32
+)");
+}
+
+// frexp: (f32) -> __frexp_result, (vecN<f32>) -> __frexp_result_vecN
+TEST_F(ResolverBuiltinFloatTest, FrexpScalar) {
+    auto* call = Call("frexp", 1_f);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    auto* ty = TypeOf(call)->As<sem::Struct>();
+    ASSERT_NE(ty, nullptr);
+    ASSERT_EQ(ty->Members().size(), 2u);
+
+    auto* sig = ty->Members()[0];
+    EXPECT_TRUE(sig->Type()->Is<sem::F32>());
+    EXPECT_EQ(sig->Offset(), 0u);
+    EXPECT_EQ(sig->Size(), 4u);
+    EXPECT_EQ(sig->Align(), 4u);
+    EXPECT_EQ(sig->Name(), Sym("sig"));
+
+    auto* exp = ty->Members()[1];
+    EXPECT_TRUE(exp->Type()->Is<sem::I32>());
+    EXPECT_EQ(exp->Offset(), 4u);
+    EXPECT_EQ(exp->Size(), 4u);
+    EXPECT_EQ(exp->Align(), 4u);
+    EXPECT_EQ(exp->Name(), Sym("exp"));
+
+    EXPECT_EQ(ty->Size(), 8u);
+    EXPECT_EQ(ty->SizeNoPadding(), 8u);
+}
+
+TEST_F(ResolverBuiltinFloatTest, FrexpVector) {
+    auto* call = Call("frexp", vec3<f32>());
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    auto* ty = TypeOf(call)->As<sem::Struct>();
+    ASSERT_NE(ty, nullptr);
+    ASSERT_EQ(ty->Members().size(), 2u);
+
+    auto* sig = ty->Members()[0];
+    ASSERT_TRUE(sig->Type()->Is<sem::Vector>());
+    EXPECT_EQ(sig->Type()->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(sig->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(sig->Offset(), 0u);
+    EXPECT_EQ(sig->Size(), 12u);
+    EXPECT_EQ(sig->Align(), 16u);
+    EXPECT_EQ(sig->Name(), Sym("sig"));
+
+    auto* exp = ty->Members()[1];
+    ASSERT_TRUE(exp->Type()->Is<sem::Vector>());
+    EXPECT_EQ(exp->Type()->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(exp->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
+    EXPECT_EQ(exp->Offset(), 16u);
+    EXPECT_EQ(exp->Size(), 12u);
+    EXPECT_EQ(exp->Align(), 16u);
+    EXPECT_EQ(exp->Name(), Sym("exp"));
+
+    EXPECT_EQ(ty->Size(), 32u);
+    EXPECT_EQ(ty->SizeNoPadding(), 28u);
+}
+
+TEST_F(ResolverBuiltinFloatTest, Frexp_Error_FirstParamInt) {
+    GlobalVar("v", ty.i32(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("frexp", 1_i, AddressOf("v"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to frexp(i32, ptr<workgroup, i32, read_write>)
+
+2 candidate functions:
+  frexp(f32) -> __frexp_result
+  frexp(vecN<f32>) -> __frexp_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Frexp_Error_SecondParamFloatPtr) {
+    GlobalVar("v", ty.f32(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("frexp", 1_f, AddressOf("v"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to frexp(f32, ptr<workgroup, f32, read_write>)
+
+2 candidate functions:
+  frexp(f32) -> __frexp_result
+  frexp(vecN<f32>) -> __frexp_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Frexp_Error_SecondParamNotAPointer) {
+    auto* call = Call("frexp", 1_f, 1_i);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to frexp(f32, i32)
+
+2 candidate functions:
+  frexp(f32) -> __frexp_result
+  frexp(vecN<f32>) -> __frexp_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Frexp_Error_VectorSizesDontMatch) {
+    GlobalVar("v", ty.vec4<i32>(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("frexp", vec2<f32>(1_f, 2_f), AddressOf("v"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to frexp(vec2<f32>, ptr<workgroup, vec4<i32>, read_write>)
+
+2 candidate functions:
+  frexp(vecN<f32>) -> __frexp_result_vecN
+  frexp(f32) -> __frexp_result
+)");
+}
+
+// length: (T) -> T, (vecN<T>) -> T
+TEST_F(ResolverBuiltinFloatTest, Length_Scalar_f32) {
+    auto* call = Call("length", 1_f);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Length_FloatVector_f32) {
+    auto* call = Call("length", vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Length_NoParams) {
+    auto* call = Call("length");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to length()
+
+2 candidate functions:
+  length(f32) -> f32
+  length(vecN<f32>) -> f32
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Length_TooManyParams) {
+    auto* call = Call("length", 1_f, 2_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to length(f32, f32)
+
+2 candidate functions:
+  length(f32) -> f32
+  length(vecN<f32>) -> f32
+)");
+}
+
+// mix(vecN<T>, vecN<T>, T) -> vecN<T>. Other overloads are tested in
+// ResolverBuiltinTest_FloatBuiltin_IdenticalType above.
+TEST_F(ResolverBuiltinFloatTest, Mix_VectorScalar_f32) {
+    auto* call = Call("mix", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f), 4_f);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->is_float_vector());
+    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+    ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+    EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+}
+
+// modf: (f32) -> __modf_result, (vecN<f32>) -> __modf_result_vecN
+TEST_F(ResolverBuiltinFloatTest, ModfScalar) {
+    auto* call = Call("modf", 1_f);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    auto* ty = TypeOf(call)->As<sem::Struct>();
+    ASSERT_NE(ty, nullptr);
+    ASSERT_EQ(ty->Members().size(), 2u);
+
+    auto* fract = ty->Members()[0];
+    EXPECT_TRUE(fract->Type()->Is<sem::F32>());
+    EXPECT_EQ(fract->Offset(), 0u);
+    EXPECT_EQ(fract->Size(), 4u);
+    EXPECT_EQ(fract->Align(), 4u);
+    EXPECT_EQ(fract->Name(), Sym("fract"));
+
+    auto* whole = ty->Members()[1];
+    EXPECT_TRUE(whole->Type()->Is<sem::F32>());
+    EXPECT_EQ(whole->Offset(), 4u);
+    EXPECT_EQ(whole->Size(), 4u);
+    EXPECT_EQ(whole->Align(), 4u);
+    EXPECT_EQ(whole->Name(), Sym("whole"));
+
+    EXPECT_EQ(ty->Size(), 8u);
+    EXPECT_EQ(ty->SizeNoPadding(), 8u);
+}
+
+TEST_F(ResolverBuiltinFloatTest, ModfVector) {
+    auto* call = Call("modf", vec3<f32>());
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    auto* ty = TypeOf(call)->As<sem::Struct>();
+    ASSERT_NE(ty, nullptr);
+    ASSERT_EQ(ty->Members().size(), 2u);
+
+    auto* fract = ty->Members()[0];
+    ASSERT_TRUE(fract->Type()->Is<sem::Vector>());
+    EXPECT_EQ(fract->Type()->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(fract->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(fract->Offset(), 0u);
+    EXPECT_EQ(fract->Size(), 12u);
+    EXPECT_EQ(fract->Align(), 16u);
+    EXPECT_EQ(fract->Name(), Sym("fract"));
+
+    auto* whole = ty->Members()[1];
+    ASSERT_TRUE(whole->Type()->Is<sem::Vector>());
+    EXPECT_EQ(whole->Type()->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(whole->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
+    EXPECT_EQ(whole->Offset(), 16u);
+    EXPECT_EQ(whole->Size(), 12u);
+    EXPECT_EQ(whole->Align(), 16u);
+    EXPECT_EQ(whole->Name(), Sym("whole"));
+
+    EXPECT_EQ(ty->Size(), 32u);
+    EXPECT_EQ(ty->SizeNoPadding(), 28u);
+}
+
+TEST_F(ResolverBuiltinFloatTest, Modf_Error_FirstParamInt) {
+    GlobalVar("whole", ty.f32(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("modf", 1_i, AddressOf("whole"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to modf(i32, ptr<workgroup, f32, read_write>)
+
+2 candidate functions:
+  modf(f32) -> __modf_result
+  modf(vecN<f32>) -> __modf_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Modf_Error_SecondParamIntPtr) {
+    GlobalVar("whole", ty.i32(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("modf", 1_f, AddressOf("whole"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to modf(f32, ptr<workgroup, i32, read_write>)
+
+2 candidate functions:
+  modf(f32) -> __modf_result
+  modf(vecN<f32>) -> __modf_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Modf_Error_SecondParamNotAPointer) {
+    auto* call = Call("modf", 1_f, 1_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to modf(f32, f32)
+
+2 candidate functions:
+  modf(f32) -> __modf_result
+  modf(vecN<f32>) -> __modf_result_vecN
+)");
+}
+
+TEST_F(ResolverBuiltinFloatTest, Modf_Error_VectorSizesDontMatch) {
+    GlobalVar("whole", ty.vec4<f32>(), ast::StorageClass::kWorkgroup);
+    auto* call = Call("modf", vec2<f32>(1_f, 2_f), AddressOf("whole"));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to modf(vec2<f32>, ptr<workgroup, vec4<f32>, read_write>)
+
+2 candidate functions:
+  modf(vecN<f32>) -> __modf_result_vecN
+  modf(f32) -> __modf_result
+)");
+}
+
+// normalize: (vecN<T>) -> vecN<T>
+TEST_F(ResolverBuiltinFloatTest, Normalize_Vector_f32) {
+    auto* call = Call("normalize", vec3<f32>(1_f, 1_f, 3_f));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->is_float_vector());
+    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+    EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinFloatTest, Normalize_Error_NoParams) {
+    auto* call = Call("normalize");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to normalize()
+
+1 candidate function:
+  normalize(vecN<f32>) -> vecN<f32>
+)");
+}
+
+}  // namespace float_builtin_tests
+
+// Tests for Numeric builtins with all integer parameter
+namespace integer_builtin_tests {
+
+// Testcase parameters for integer built-in having signature of (T, ...) -> T and (vecN<T>, ...) ->
+// vecN<T>, where T is i32 and u32
+struct BuiltinDataWithParamNum {
+    uint32_t args_number;
+    const char* name;
+    BuiltinType builtin;
+};
+
+inline std::ostream& operator<<(std::ostream& out, BuiltinDataWithParamNum data) {
+    out << data.name;
+    return out;
+}
+
+// Tests for integer built-ins that has signiture (T, ...) -> T and (vecN<T>, ...) -> vecN<T>
+using ResolverBuiltinTest_IntegerBuiltin_IdenticalType =
+    ResolverTestWithParam<BuiltinDataWithParamNum>;
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, Error_NoParams) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(),
+                HasSubstr("error: no matching call to " + std::string(param.name) + "()"));
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Scalar_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_i);
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) + "(i32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Vector_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i));
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<i32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Scalar_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_u);
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) + "(u32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, OneParams_Vector_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u));
+    WrapInFunction(call);
+
+    if (param.args_number == 1u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<u32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Scalar_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_i, 1_i);
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(i32, i32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Vector_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i));
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<i32>, vec3<i32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Scalar_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_u, 1_u);
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(u32, u32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, TwoParams_Vector_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u));
+    WrapInFunction(call);
+
+    if (param.args_number == 2u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(vec3<u32>, vec3<u32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Scalar_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_i, 1_i, 1_i);
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(i32, i32, i32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Vector_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i),
+                      vec3<i32>(1_i, 1_i, 3_i));
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<i32>, vec3<i32>, vec3<i32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Scalar_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_u, 1_u, 1_u);
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(u32, u32, u32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, ThreeParams_Vector_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u),
+                      vec3<u32>(1_u, 1_u, 3_u));
+    WrapInFunction(call);
+
+    if (param.args_number == 3u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<u32>, vec3<u32>, vec3<u32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Scalar_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_i, 1_i, 1_i, 1_i);
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(i32, i32, i32, i32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Vector_i32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i),
+                      vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i));
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::I32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<i32>, vec3<i32>, vec3<i32>, vec3<i32>)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Scalar_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, 1_u, 1_u, 1_u, 1_u);
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " +
+                                            std::string(param.name) + "(u32, u32, u32, u32)"));
+    }
+}
+
+TEST_P(ResolverBuiltinTest_IntegerBuiltin_IdenticalType, FourParams_Vector_u32) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u),
+                      vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u));
+    WrapInFunction(call);
+
+    if (param.args_number == 4u) {
+        // Parameter count matched.
+        EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+        ASSERT_NE(TypeOf(call), nullptr);
+        EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
+        ASSERT_NE(TypeOf(call)->As<sem::Vector>()->type(), nullptr);
+        EXPECT_TRUE(TypeOf(call)->As<sem::Vector>()->type()->Is<sem::U32>());
+    } else {
+        // Invalid parameter count.
+        EXPECT_FALSE(r()->Resolve());
+
+        EXPECT_THAT(r()->error(),
+                    HasSubstr("error: no matching call to " + std::string(param.name) +
+                              "(vec3<u32>, vec3<u32>, vec3<u32>, vec3<u32>)"));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ResolverTest,
+    ResolverBuiltinTest_IntegerBuiltin_IdenticalType,
+    testing::Values(
+        BuiltinDataWithParamNum{1, "abs", BuiltinType::kAbs},
+        BuiltinDataWithParamNum{3, "clamp", BuiltinType::kClamp},
+        BuiltinDataWithParamNum{1, "countLeadingZeros", BuiltinType::kCountLeadingZeros},
+        BuiltinDataWithParamNum{1, "countOneBits", BuiltinType::kCountOneBits},
+        BuiltinDataWithParamNum{1, "countTrailingZeros", BuiltinType::kCountTrailingZeros},
+        // extractBits: (T, u32, u32) -> T
+        BuiltinDataWithParamNum{1, "firstLeadingBit", BuiltinType::kFirstLeadingBit},
+        BuiltinDataWithParamNum{1, "firstTrailingBit", BuiltinType::kFirstTrailingBit},
+        // insertBits: (T, T, u32, u32) -> T
+        BuiltinDataWithParamNum{2, "max", BuiltinType::kMax},
+        BuiltinDataWithParamNum{2, "min", BuiltinType::kMin},
+        BuiltinDataWithParamNum{1, "reverseBits", BuiltinType::kReverseBits}));
+
+}  // namespace integer_builtin_tests
+
+// Tests for Numeric builtins with matrix parameter, i.e. "determinant" and "transpose"
+namespace matrix_builtin_tests {
+
+TEST_F(ResolverBuiltinTest, Determinant_2x2_f32) {
+    GlobalVar("var", ty.mat2x2<f32>(), ast::StorageClass::kPrivate);
+
+    auto* call = Call("determinant", "var");
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinTest, Determinant_3x3_f32) {
+    GlobalVar("var", ty.mat3x3<f32>(), ast::StorageClass::kPrivate);
+
+    auto* call = Call("determinant", "var");
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinTest, Determinant_4x4_f32) {
+    GlobalVar("var", ty.mat4x4<f32>(), ast::StorageClass::kPrivate);
+
+    auto* call = Call("determinant", "var");
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinTest, Determinant_NotSquare) {
+    GlobalVar("var", ty.mat2x3<f32>(), ast::StorageClass::kPrivate);
+
+    auto* call = Call("determinant", "var");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(mat2x3<f32>)
+
+1 candidate function:
+  determinant(matNxN<f32>) -> f32
+)");
+}
+
+TEST_F(ResolverBuiltinTest, Determinant_NotMatrix) {
+    GlobalVar("var", ty.f32(), ast::StorageClass::kPrivate);
+
+    auto* call = Call("determinant", "var");
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(f32)
+
+1 candidate function:
+  determinant(matNxN<f32>) -> f32
+)");
+}
+
+}  // namespace matrix_builtin_tests
+
+// Tests for Numeric builtins with float and integer vector parameter, i.e. "dot"
+namespace vector_builtin_tests {
+
+TEST_F(ResolverBuiltinTest, Dot_Vec2_f32) {
+    GlobalVar("my_var", ty.vec2<f32>(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call("dot", "my_var", "my_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::F32>());
+}
+
+TEST_F(ResolverBuiltinTest, Dot_Vec3_i32) {
+    GlobalVar("my_var", ty.vec3<i32>(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call("dot", "my_var", "my_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::I32>());
+}
+
+TEST_F(ResolverBuiltinTest, Dot_Vec4_u32) {
+    GlobalVar("my_var", ty.vec4<u32>(), ast::StorageClass::kPrivate);
+
+    auto* expr = Call("dot", "my_var", "my_var");
+    WrapInFunction(expr);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    ASSERT_NE(TypeOf(expr), nullptr);
+    EXPECT_TRUE(TypeOf(expr)->Is<sem::U32>());
+}
+
+TEST_F(ResolverBuiltinTest, Dot_Error_Scalar) {
+    auto* expr = Call("dot", 1_f, 1_f);
+    WrapInFunction(expr);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_EQ(r()->error(),
+              R"(error: no matching call to dot(f32, f32)
+
+1 candidate function:
+  dot(vecN<T>, vecN<T>) -> T  where: T is f32, i32 or u32
+)");
+}
+
+}  // namespace vector_builtin_tests
+
+// Tests for Derivative builtins
+namespace derivative_builtin_tests {
+
 using ResolverBuiltinDerivativeTest = ResolverTestWithParam<std::string>;
+
 TEST_P(ResolverBuiltinDerivativeTest, Scalar) {
     auto name = GetParam();
 
@@ -106,36 +1609,10 @@
                                          "fwidthCoarse",
                                          "fwidthFine"));
 
-using ResolverBuiltinTest_BoolMethod = ResolverTestWithParam<std::string>;
-TEST_P(ResolverBuiltinTest_BoolMethod, Scalar) {
-    auto name = GetParam();
+}  // namespace derivative_builtin_tests
 
-    GlobalVar("my_var", ty.bool_(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call(name, "my_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::Bool>());
-}
-TEST_P(ResolverBuiltinTest_BoolMethod, Vector) {
-    auto name = GetParam();
-
-    GlobalVar("my_var", ty.vec3<bool>(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call(name, "my_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::Bool>());
-}
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_BoolMethod,
-                         testing::Values("any", "all"));
+// Tests for Texture builtins
+namespace texture_builtin_tests {
 
 enum class Texture { kF32, kI32, kU32 };
 inline std::ostream& operator<<(std::ostream& out, Texture data) {
@@ -250,1416 +1727,14 @@
                                          TextureTestParams{ast::TextureDimension::k2dArray},
                                          TextureTestParams{ast::TextureDimension::k3d}));
 
-TEST_F(ResolverBuiltinTest, Dot_Vec2) {
-    GlobalVar("my_var", ty.vec2<f32>(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call("dot", "my_var", "my_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Dot_Vec3) {
-    GlobalVar("my_var", ty.vec3<i32>(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call("dot", "my_var", "my_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::I32>());
-}
-
-TEST_F(ResolverBuiltinTest, Dot_Vec4) {
-    GlobalVar("my_var", ty.vec4<u32>(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call("dot", "my_var", "my_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::U32>());
-}
-
-TEST_F(ResolverBuiltinTest, Dot_Error_Scalar) {
-    auto* expr = Call("dot", 1_f, 1_f);
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to dot(f32, f32)
-
-1 candidate function:
-  dot(vecN<T>, vecN<T>) -> T  where: T is f32, i32 or u32
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Select) {
-    GlobalVar("my_var", ty.vec3<f32>(), ast::StorageClass::kPrivate);
-
-    GlobalVar("bool_var", ty.vec3<bool>(), ast::StorageClass::kPrivate);
-
-    auto* expr = Call("select", "my_var", "my_var", "bool_var");
-    WrapInFunction(expr);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(expr), nullptr);
-    EXPECT_TRUE(TypeOf(expr)->Is<sem::Vector>());
-    EXPECT_EQ(TypeOf(expr)->As<sem::Vector>()->Width(), 3u);
-    EXPECT_TRUE(TypeOf(expr)->As<sem::Vector>()->type()->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Select_Error_NoParams) {
-    auto* expr = Call("select");
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select()
-
-3 candidate functions:
-  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Select_Error_SelectorInt) {
-    auto* expr = Call("select", 1_i, 1_i, 1_i);
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(i32, i32, i32)
-
-3 candidate functions:
-  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Select_Error_Matrix) {
-    auto* expr = Call("select", mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)),
-                      mat2x2<f32>(vec2<f32>(1_f, 1_f), vec2<f32>(1_f, 1_f)), Expr(true));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(mat2x2<f32>, mat2x2<f32>, bool)
-
-3 candidate functions:
-  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Select_Error_MismatchTypes) {
-    auto* expr = Call("select", 1_f, vec2<f32>(2_f, 3_f), Expr(true));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(f32, vec2<f32>, bool)
-
-3 candidate functions:
-  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Select_Error_MismatchVectorSize) {
-    auto* expr = Call("select", vec2<f32>(1_f, 2_f), vec3<f32>(3_f, 4_f, 5_f), Expr(true));
-    WrapInFunction(expr);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to select(vec2<f32>, vec3<f32>, bool)
-
-3 candidate functions:
-  select(T, T, bool) -> T  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, bool) -> vecN<T>  where: T is f32, i32, u32 or bool
-  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is f32, i32, u32 or bool
-)");
-}
-
-struct BuiltinData {
-    const char* name;
-    BuiltinType builtin;
-};
-
-inline std::ostream& operator<<(std::ostream& out, BuiltinData data) {
-    out << data.name;
-    return out;
-}
-
-using ResolverBuiltinTest_Barrier = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_Barrier, InferType) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(CallStmt(call));
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::Void>());
-}
-
-TEST_P(ResolverBuiltinTest_Barrier, Error_TooManyParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f);
-    WrapInFunction(CallStmt(call));
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    ResolverTest,
-    ResolverBuiltinTest_Barrier,
-    testing::Values(BuiltinData{"storageBarrier", BuiltinType::kStorageBarrier},
-                    BuiltinData{"workgroupBarrier", BuiltinType::kWorkgroupBarrier}));
-
-using ResolverBuiltinTest_DataPacking = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_DataPacking, InferType) {
-    auto param = GetParam();
-
-    bool pack4 =
-        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
-
-    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f))
-                       : Call(param.name, vec2<f32>(1_f, 2_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
-}
-
-TEST_P(ResolverBuiltinTest_DataPacking, Error_IncorrectParamType) {
-    auto param = GetParam();
-
-    bool pack4 =
-        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
-
-    auto* call = pack4 ? Call(param.name, vec4<i32>(1_i, 2_i, 3_i, 4_i))
-                       : Call(param.name, vec2<i32>(1_i, 2_i));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
-}
-
-TEST_P(ResolverBuiltinTest_DataPacking, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
-}
-
-TEST_P(ResolverBuiltinTest_DataPacking, Error_TooManyParams) {
-    auto param = GetParam();
-
-    bool pack4 =
-        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
-
-    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f)
-                       : Call(param.name, vec2<f32>(1_f, 2_f), 1_f);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_DataPacking,
-                         testing::Values(BuiltinData{"pack4x8snorm", BuiltinType::kPack4x8snorm},
-                                         BuiltinData{"pack4x8unorm", BuiltinType::kPack4x8unorm},
-                                         BuiltinData{"pack2x16snorm", BuiltinType::kPack2x16snorm},
-                                         BuiltinData{"pack2x16unorm", BuiltinType::kPack2x16unorm},
-                                         BuiltinData{"pack2x16float",
-                                                     BuiltinType::kPack2x16float}));
-
-using ResolverBuiltinTest_DataUnpacking = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_DataUnpacking, InferType) {
-    auto param = GetParam();
-
-    bool pack4 = param.builtin == BuiltinType::kUnpack4x8snorm ||
-                 param.builtin == BuiltinType::kUnpack4x8unorm;
-
-    auto* call = Call(param.name, 1_u);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    if (pack4) {
-        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 4u);
-    } else {
-        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 2u);
-    }
-}
-
-INSTANTIATE_TEST_SUITE_P(
-    ResolverTest,
-    ResolverBuiltinTest_DataUnpacking,
-    testing::Values(BuiltinData{"unpack4x8snorm", BuiltinType::kUnpack4x8snorm},
-                    BuiltinData{"unpack4x8unorm", BuiltinType::kUnpack4x8unorm},
-                    BuiltinData{"unpack2x16snorm", BuiltinType::kUnpack2x16snorm},
-                    BuiltinData{"unpack2x16unorm", BuiltinType::kUnpack2x16unorm},
-                    BuiltinData{"unpack2x16float", BuiltinType::kUnpack2x16float}));
-
-using ResolverBuiltinTest_SingleParam = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_SingleParam, Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam, Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "()\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) + "(f32) -> f32\n  " +
-                                std::string(param.name) + "(vecN<f32>) -> vecN<f32>\n");
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam, Error_TooManyParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i, 2_i, 3_i);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "(i32, i32, i32)\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) + "(f32) -> f32\n  " +
-                                std::string(param.name) + "(vecN<f32>) -> vecN<f32>\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_SingleParam,
-                         testing::Values(BuiltinData{"acos", BuiltinType::kAcos},
-                                         BuiltinData{"asin", BuiltinType::kAsin},
-                                         BuiltinData{"atan", BuiltinType::kAtan},
-                                         BuiltinData{"ceil", BuiltinType::kCeil},
-                                         BuiltinData{"cos", BuiltinType::kCos},
-                                         BuiltinData{"cosh", BuiltinType::kCosh},
-                                         BuiltinData{"exp", BuiltinType::kExp},
-                                         BuiltinData{"exp2", BuiltinType::kExp2},
-                                         BuiltinData{"floor", BuiltinType::kFloor},
-                                         BuiltinData{"fract", BuiltinType::kFract},
-                                         BuiltinData{"inverseSqrt", BuiltinType::kInverseSqrt},
-                                         BuiltinData{"log", BuiltinType::kLog},
-                                         BuiltinData{"log2", BuiltinType::kLog2},
-                                         BuiltinData{"round", BuiltinType::kRound},
-                                         BuiltinData{"sign", BuiltinType::kSign},
-                                         BuiltinData{"sin", BuiltinType::kSin},
-                                         BuiltinData{"sinh", BuiltinType::kSinh},
-                                         BuiltinData{"sqrt", BuiltinType::kSqrt},
-                                         BuiltinData{"tan", BuiltinType::kTan},
-                                         BuiltinData{"tanh", BuiltinType::kTanh},
-                                         BuiltinData{"trunc", BuiltinType::kTrunc}));
-
-using ResolverBuiltinDataTest = ResolverTest;
-
-TEST_F(ResolverBuiltinDataTest, ArrayLength_Vector) {
-    auto* ary = ty.array<i32>();
-    auto* str = Structure("S", {Member("x", ary)});
-    GlobalVar("a", ty.Of(str), ast::StorageClass::kStorage, ast::Access::kRead,
-              ast::AttributeList{
-                  create<ast::BindingAttribute>(0u),
-                  create<ast::GroupAttribute>(0u),
-              });
-
-    auto* call = Call("arrayLength", AddressOf(MemberAccessor("a", "x")));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
-}
-
-TEST_F(ResolverBuiltinDataTest, ArrayLength_Error_ArraySized) {
-    GlobalVar("arr", ty.array<i32, 4>(), ast::StorageClass::kPrivate);
-    auto* call = Call("arrayLength", AddressOf("arr"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to arrayLength(ptr<private, array<i32, 4>, read_write>)
-
-1 candidate function:
-  arrayLength(ptr<storage, array<T>, A>) -> u32
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Normalize_Vector) {
-    auto* call = Call("normalize", vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_F(ResolverBuiltinDataTest, Normalize_Error_NoParams) {
-    auto* call = Call("normalize");
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to normalize()
-
-1 candidate function:
-  normalize(vecN<f32>) -> vecN<f32>
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, FrexpScalar) {
-    auto* call = Call("frexp", 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
-    ASSERT_NE(ty, nullptr);
-    ASSERT_EQ(ty->Members().size(), 2u);
-
-    auto* sig = ty->Members()[0];
-    EXPECT_TRUE(sig->Type()->Is<sem::F32>());
-    EXPECT_EQ(sig->Offset(), 0u);
-    EXPECT_EQ(sig->Size(), 4u);
-    EXPECT_EQ(sig->Align(), 4u);
-    EXPECT_EQ(sig->Name(), Sym("sig"));
-
-    auto* exp = ty->Members()[1];
-    EXPECT_TRUE(exp->Type()->Is<sem::I32>());
-    EXPECT_EQ(exp->Offset(), 4u);
-    EXPECT_EQ(exp->Size(), 4u);
-    EXPECT_EQ(exp->Align(), 4u);
-    EXPECT_EQ(exp->Name(), Sym("exp"));
-
-    EXPECT_EQ(ty->Size(), 8u);
-    EXPECT_EQ(ty->SizeNoPadding(), 8u);
-}
-
-TEST_F(ResolverBuiltinDataTest, FrexpVector) {
-    auto* call = Call("frexp", vec3<f32>());
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
-    ASSERT_NE(ty, nullptr);
-    ASSERT_EQ(ty->Members().size(), 2u);
-
-    auto* sig = ty->Members()[0];
-    ASSERT_TRUE(sig->Type()->Is<sem::Vector>());
-    EXPECT_EQ(sig->Type()->As<sem::Vector>()->Width(), 3u);
-    EXPECT_TRUE(sig->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(sig->Offset(), 0u);
-    EXPECT_EQ(sig->Size(), 12u);
-    EXPECT_EQ(sig->Align(), 16u);
-    EXPECT_EQ(sig->Name(), Sym("sig"));
-
-    auto* exp = ty->Members()[1];
-    ASSERT_TRUE(exp->Type()->Is<sem::Vector>());
-    EXPECT_EQ(exp->Type()->As<sem::Vector>()->Width(), 3u);
-    EXPECT_TRUE(exp->Type()->As<sem::Vector>()->type()->Is<sem::I32>());
-    EXPECT_EQ(exp->Offset(), 16u);
-    EXPECT_EQ(exp->Size(), 12u);
-    EXPECT_EQ(exp->Align(), 16u);
-    EXPECT_EQ(exp->Name(), Sym("exp"));
-
-    EXPECT_EQ(ty->Size(), 32u);
-    EXPECT_EQ(ty->SizeNoPadding(), 28u);
-}
-
-TEST_F(ResolverBuiltinDataTest, Frexp_Error_FirstParamInt) {
-    GlobalVar("v", ty.i32(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("frexp", 1_i, AddressOf("v"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to frexp(i32, ptr<workgroup, i32, read_write>)
-
-2 candidate functions:
-  frexp(f32) -> __frexp_result
-  frexp(vecN<f32>) -> __frexp_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Frexp_Error_SecondParamFloatPtr) {
-    GlobalVar("v", ty.f32(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("frexp", 1_f, AddressOf("v"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to frexp(f32, ptr<workgroup, f32, read_write>)
-
-2 candidate functions:
-  frexp(f32) -> __frexp_result
-  frexp(vecN<f32>) -> __frexp_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Frexp_Error_SecondParamNotAPointer) {
-    auto* call = Call("frexp", 1_f, 1_i);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to frexp(f32, i32)
-
-2 candidate functions:
-  frexp(f32) -> __frexp_result
-  frexp(vecN<f32>) -> __frexp_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Frexp_Error_VectorSizesDontMatch) {
-    GlobalVar("v", ty.vec4<i32>(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("frexp", vec2<f32>(1_f, 2_f), AddressOf("v"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to frexp(vec2<f32>, ptr<workgroup, vec4<i32>, read_write>)
-
-2 candidate functions:
-  frexp(vecN<f32>) -> __frexp_result_vecN
-  frexp(f32) -> __frexp_result
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, ModfScalar) {
-    auto* call = Call("modf", 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
-    ASSERT_NE(ty, nullptr);
-    ASSERT_EQ(ty->Members().size(), 2u);
-
-    auto* fract = ty->Members()[0];
-    EXPECT_TRUE(fract->Type()->Is<sem::F32>());
-    EXPECT_EQ(fract->Offset(), 0u);
-    EXPECT_EQ(fract->Size(), 4u);
-    EXPECT_EQ(fract->Align(), 4u);
-    EXPECT_EQ(fract->Name(), Sym("fract"));
-
-    auto* whole = ty->Members()[1];
-    EXPECT_TRUE(whole->Type()->Is<sem::F32>());
-    EXPECT_EQ(whole->Offset(), 4u);
-    EXPECT_EQ(whole->Size(), 4u);
-    EXPECT_EQ(whole->Align(), 4u);
-    EXPECT_EQ(whole->Name(), Sym("whole"));
-
-    EXPECT_EQ(ty->Size(), 8u);
-    EXPECT_EQ(ty->SizeNoPadding(), 8u);
-}
-
-TEST_F(ResolverBuiltinDataTest, ModfVector) {
-    auto* call = Call("modf", vec3<f32>());
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    auto* ty = TypeOf(call)->As<sem::Struct>();
-    ASSERT_NE(ty, nullptr);
-    ASSERT_EQ(ty->Members().size(), 2u);
-
-    auto* fract = ty->Members()[0];
-    ASSERT_TRUE(fract->Type()->Is<sem::Vector>());
-    EXPECT_EQ(fract->Type()->As<sem::Vector>()->Width(), 3u);
-    EXPECT_TRUE(fract->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(fract->Offset(), 0u);
-    EXPECT_EQ(fract->Size(), 12u);
-    EXPECT_EQ(fract->Align(), 16u);
-    EXPECT_EQ(fract->Name(), Sym("fract"));
-
-    auto* whole = ty->Members()[1];
-    ASSERT_TRUE(whole->Type()->Is<sem::Vector>());
-    EXPECT_EQ(whole->Type()->As<sem::Vector>()->Width(), 3u);
-    EXPECT_TRUE(whole->Type()->As<sem::Vector>()->type()->Is<sem::F32>());
-    EXPECT_EQ(whole->Offset(), 16u);
-    EXPECT_EQ(whole->Size(), 12u);
-    EXPECT_EQ(whole->Align(), 16u);
-    EXPECT_EQ(whole->Name(), Sym("whole"));
-
-    EXPECT_EQ(ty->Size(), 32u);
-    EXPECT_EQ(ty->SizeNoPadding(), 28u);
-}
-
-TEST_F(ResolverBuiltinDataTest, Modf_Error_FirstParamInt) {
-    GlobalVar("whole", ty.f32(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("modf", 1_i, AddressOf("whole"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(i32, ptr<workgroup, f32, read_write>)
-
-2 candidate functions:
-  modf(f32) -> __modf_result
-  modf(vecN<f32>) -> __modf_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Modf_Error_SecondParamIntPtr) {
-    GlobalVar("whole", ty.i32(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("modf", 1_f, AddressOf("whole"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(f32, ptr<workgroup, i32, read_write>)
-
-2 candidate functions:
-  modf(f32) -> __modf_result
-  modf(vecN<f32>) -> __modf_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Modf_Error_SecondParamNotAPointer) {
-    auto* call = Call("modf", 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to modf(f32, f32)
-
-2 candidate functions:
-  modf(f32) -> __modf_result
-  modf(vecN<f32>) -> __modf_result_vecN
-)");
-}
-
-TEST_F(ResolverBuiltinDataTest, Modf_Error_VectorSizesDontMatch) {
-    GlobalVar("whole", ty.vec4<f32>(), ast::StorageClass::kWorkgroup);
-    auto* call = Call("modf", vec2<f32>(1_f, 2_f), AddressOf("whole"));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to modf(vec2<f32>, ptr<workgroup, vec4<f32>, read_write>)
-
-2 candidate functions:
-  modf(vecN<f32>) -> __modf_result_vecN
-  modf(f32) -> __modf_result
-)");
-}
-
-using ResolverBuiltinTest_SingleParam_FloatOrInt = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Float_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Float_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Sint_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, -1_i);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Sint_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Uint_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_u);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Uint_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_SingleParam_FloatOrInt, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              "error: no matching call to " + std::string(param.name) +
-                  "()\n\n"
-                  "2 candidate functions:\n  " +
-                  std::string(param.name) + "(T) -> T  where: T is f32, i32 or u32\n  " +
-                  std::string(param.name) + "(vecN<T>) -> vecN<T>  where: T is f32, i32 or u32\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_SingleParam_FloatOrInt,
-                         testing::Values(BuiltinData{"abs", BuiltinType::kAbs}));
-
-TEST_F(ResolverBuiltinTest, Length_Scalar) {
-    auto* call = Call("length", 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_F(ResolverBuiltinTest, Length_FloatVector) {
-    auto* call = Call("length", vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-using ResolverBuiltinTest_TwoParam = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_TwoParam, Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam, Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam, Error_NoTooManyParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i, 2_i, 3_i);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              "error: no matching call to " + std::string(param.name) +
-                  "(i32, i32, i32)\n\n"
-                  "2 candidate functions:\n  " +
-                  std::string(param.name) + "(T, T) -> T  where: T is abstract-float or f32\n  " +
-                  std::string(param.name) +
-                  "(vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float or f32\n");
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              "error: no matching call to " + std::string(param.name) +
-                  "()\n\n"
-                  "2 candidate functions:\n  " +
-                  std::string(param.name) + "(T, T) -> T  where: T is abstract-float or f32\n  " +
-                  std::string(param.name) +
-                  "(vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float or f32\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_TwoParam,
-                         testing::Values(BuiltinData{"atan2", BuiltinType::kAtan2}));
-
-using ResolverBuiltinTest_TwoParam_NoConstEval = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_TwoParam_NoConstEval, Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam_NoConstEval, Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam_NoConstEval, Error_NoTooManyParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i, 2_i, 3_i);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "(i32, i32, i32)\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) + "(f32, f32) -> f32\n  " +
-                                std::string(param.name) + "(vecN<f32>, vecN<f32>) -> vecN<f32>\n");
-}
-
-TEST_P(ResolverBuiltinTest_TwoParam_NoConstEval, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "()\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) + "(f32, f32) -> f32\n  " +
-                                std::string(param.name) + "(vecN<f32>, vecN<f32>) -> vecN<f32>\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_TwoParam_NoConstEval,
-                         testing::Values(BuiltinData{"pow", BuiltinType::kPow},
-                                         BuiltinData{"step", BuiltinType::kStep}));
-
-TEST_F(ResolverBuiltinTest, Distance_Scalar) {
-    auto* call = Call("distance", 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_F(ResolverBuiltinTest, Distance_Vector) {
-    auto* call = Call("distance", vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Cross) {
-    auto* call = Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_F(ResolverBuiltinTest, Cross_Error_NoArgs) {
-    auto* call = Call("cross");
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to cross()
-
-1 candidate function:
-  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Cross_Error_Scalar) {
-    auto* call = Call("cross", 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to cross(f32, f32)
-
-1 candidate function:
-  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Cross_Error_Vec3Int) {
-    auto* call = Call("cross", vec3<i32>(1_i, 2_i, 3_i), vec3<i32>(1_i, 2_i, 3_i));
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec3<i32>, vec3<i32>)
-
-1 candidate function:
-  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Cross_Error_Vec4) {
-    auto* call = Call("cross", vec4<f32>(1_f, 2_f, 3_f, 4_f), vec4<f32>(1_f, 2_f, 3_f, 4_f));
-
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec4<f32>, vec4<f32>)
-
-1 candidate function:
-  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Cross_Error_TooManyParams) {
-    auto* call =
-        Call("cross", vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f), vec3<f32>(1_f, 2_f, 3_f));
-
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(),
-              R"(error: no matching call to cross(vec3<f32>, vec3<f32>, vec3<f32>)
-
-1 candidate function:
-  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
-)");
-}
-TEST_F(ResolverBuiltinTest, Normalize) {
-    auto* call = Call("normalize", vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_F(ResolverBuiltinTest, Normalize_NoArgs) {
-    auto* call = Call("normalize");
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to normalize()
-
-1 candidate function:
-  normalize(vecN<f32>) -> vecN<f32>
-)");
-}
-
-using ResolverBuiltinTest_ThreeParam = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_ThreeParam, Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f, 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam, Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
-                      vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-TEST_P(ResolverBuiltinTest_ThreeParam, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_THAT(r()->error(),
-                HasSubstr("error: no matching call to " + std::string(param.name) + "()"));
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_ThreeParam,
-                         testing::Values(BuiltinData{"mix", BuiltinType::kMix},
-                                         BuiltinData{"smoothstep", BuiltinType::kSmoothstep},
-                                         BuiltinData{"fma", BuiltinType::kFma}));
-
-using ResolverBuiltinTest_ThreeParam_FloatOrInt = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Float_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f, 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Float_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f),
-                      vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Sint_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i, 1_i, 1_i);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Sint_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i),
-                      vec3<i32>(1_i, 1_i, 3_i));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Uint_Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_u, 1_u, 1_u);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Uint_Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u),
-                      vec3<u32>(1_u, 1_u, 3_u));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_ThreeParam_FloatOrInt, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(
-        r()->error(),
-        "error: no matching call to " + std::string(param.name) +
-            "()\n\n"
-            "2 candidate functions:\n  " +
-            std::string(param.name) +
-            "(T, T, T) -> T  where: T is abstract-float, abstract-int, f32, i32 or u32\n  " +
-            std::string(param.name) +
-            "(vecN<T>, vecN<T>, vecN<T>) -> vecN<T>  where: T is abstract-float, abstract-int, "
-            "f32, i32 or u32\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_ThreeParam_FloatOrInt,
-                         testing::Values(BuiltinData{"clamp", BuiltinType::kClamp}));
-
-using ResolverBuiltinTest_Int_SingleParam = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_Int_SingleParam, Scalar) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_integer_scalar());
-}
-
-TEST_P(ResolverBuiltinTest_Int_SingleParam, Vector) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_Int_SingleParam, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "()\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) + "(T) -> T  where: T is i32 or u32\n  " +
-                                std::string(param.name) +
-                                "(vecN<T>) -> vecN<T>  where: T is i32 or u32\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_Int_SingleParam,
-                         testing::Values(BuiltinData{"countOneBits", BuiltinType::kCountOneBits},
-                                         BuiltinData{"reverseBits", BuiltinType::kReverseBits}));
-
-using ResolverBuiltinTest_FloatOrInt_TwoParam = ResolverTestWithParam<BuiltinData>;
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Scalar_Signed) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_i, 1_i);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::I32>());
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Scalar_Unsigned) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_u, 1_u);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Scalar_Float) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, 1_f, 1_f);
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Vector_Signed) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<i32>(1_i, 1_i, 3_i), vec3<i32>(1_i, 1_i, 3_i));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_signed_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Vector_Unsigned) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<u32>(1_u, 1_u, 3_u), vec3<u32>(1_u, 1_u, 3_u));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_unsigned_integer_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Vector_Float) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name, vec3<f32>(1_f, 1_f, 3_f), vec3<f32>(1_f, 1_f, 3_f));
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->is_float_vector());
-    EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 3u);
-}
-
-TEST_P(ResolverBuiltinTest_FloatOrInt_TwoParam, Error_NoParams) {
-    auto param = GetParam();
-
-    auto* call = Call(param.name);
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), "error: no matching call to " + std::string(param.name) +
-                                "()\n\n"
-                                "2 candidate functions:\n  " +
-                                std::string(param.name) +
-                                "(T, T) -> T  where: T is f32, i32 or u32\n  " +
-                                std::string(param.name) +
-                                "(vecN<T>, vecN<T>) -> vecN<T>  where: T is f32, i32 or u32\n");
-}
-
-INSTANTIATE_TEST_SUITE_P(ResolverTest,
-                         ResolverBuiltinTest_FloatOrInt_TwoParam,
-                         testing::Values(BuiltinData{"min", BuiltinType::kMin},
-                                         BuiltinData{"max", BuiltinType::kMax}));
-
-TEST_F(ResolverBuiltinTest, Determinant_2x2) {
-    GlobalVar("var", ty.mat2x2<f32>(), ast::StorageClass::kPrivate);
-
-    auto* call = Call("determinant", "var");
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Determinant_3x3) {
-    GlobalVar("var", ty.mat3x3<f32>(), ast::StorageClass::kPrivate);
-
-    auto* call = Call("determinant", "var");
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Determinant_4x4) {
-    GlobalVar("var", ty.mat4x4<f32>(), ast::StorageClass::kPrivate);
-
-    auto* call = Call("determinant", "var");
-    WrapInFunction(call);
-
-    EXPECT_TRUE(r()->Resolve()) << r()->error();
-
-    ASSERT_NE(TypeOf(call), nullptr);
-    EXPECT_TRUE(TypeOf(call)->Is<sem::F32>());
-}
-
-TEST_F(ResolverBuiltinTest, Determinant_NotSquare) {
-    GlobalVar("var", ty.mat2x3<f32>(), ast::StorageClass::kPrivate);
-
-    auto* call = Call("determinant", "var");
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(mat2x3<f32>)
-
-1 candidate function:
-  determinant(matNxN<f32>) -> f32
-)");
-}
-
-TEST_F(ResolverBuiltinTest, Determinant_NotMatrix) {
-    GlobalVar("var", ty.f32(), ast::StorageClass::kPrivate);
-
-    auto* call = Call("determinant", "var");
-    WrapInFunction(call);
-
-    EXPECT_FALSE(r()->Resolve());
-
-    EXPECT_EQ(r()->error(), R"(error: no matching call to determinant(f32)
-
-1 candidate function:
-  determinant(matNxN<f32>) -> f32
-)");
-}
-
-TEST_F(ResolverBuiltinTest, ModuleScopeUsage) {
-    GlobalConst("c", ty.f32(), Call(Source{{12, 34}}, "abs", 1._f));
-
-    EXPECT_FALSE(r()->Resolve());
-
-    // 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)");
-}
-
 using ResolverBuiltinTest_Texture = ResolverTestWithParam<ast::builtin::test::TextureOverloadCase>;
 
 INSTANTIATE_TEST_SUITE_P(ResolverTest,
                          ResolverBuiltinTest_Texture,
                          testing::ValuesIn(ast::builtin::test::TextureOverloadCase::ValidCases()));
 
-std::string to_str(const std::string& function,
-                   utils::ConstVectorRef<const sem::Parameter*> params) {
+static std::string to_str(const std::string& function,
+                          utils::ConstVectorRef<const sem::Parameter*> params) {
     std::stringstream out;
     out << function << "(";
     bool first = true;
@@ -1674,7 +1749,7 @@
     return out.str();
 }
 
-const char* expected_texture_overload(ast::builtin::test::ValidTextureOverload overload) {
+static const char* expected_texture_overload(ast::builtin::test::ValidTextureOverload overload) {
     using ValidTextureOverload = ast::builtin::test::ValidTextureOverload;
     switch (overload) {
         case ValidTextureOverload::kDimensions1d:
@@ -2004,10 +2079,151 @@
     auto* target = call_sem->Target();
     ASSERT_NE(target, nullptr);
 
-    auto got = resolver::to_str(param.function, target->Parameters());
+    auto got = texture_builtin_tests::to_str(param.function, target->Parameters());
     auto* expected = expected_texture_overload(param.overload);
     EXPECT_EQ(got, expected);
 }
 
+}  // namespace texture_builtin_tests
+
+// Tests for Data Packing builtins
+namespace data_packing_builtin_tests {
+
+using ResolverBuiltinTest_DataPacking = ResolverTestWithParam<BuiltinData>;
+TEST_P(ResolverBuiltinTest_DataPacking, InferType) {
+    auto param = GetParam();
+
+    bool pack4 =
+        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
+
+    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f))
+                       : Call(param.name, vec2<f32>(1_f, 2_f));
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::U32>());
+}
+
+TEST_P(ResolverBuiltinTest_DataPacking, Error_IncorrectParamType) {
+    auto param = GetParam();
+
+    bool pack4 =
+        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
+
+    auto* call = pack4 ? Call(param.name, vec4<i32>(1_i, 2_i, 3_i, 4_i))
+                       : Call(param.name, vec2<i32>(1_i, 2_i));
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+}
+
+TEST_P(ResolverBuiltinTest_DataPacking, Error_NoParams) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+}
+
+TEST_P(ResolverBuiltinTest_DataPacking, Error_TooManyParams) {
+    auto param = GetParam();
+
+    bool pack4 =
+        param.builtin == BuiltinType::kPack4x8snorm || param.builtin == BuiltinType::kPack4x8unorm;
+
+    auto* call = pack4 ? Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f)
+                       : Call(param.name, vec2<f32>(1_f, 2_f), 1_f);
+    WrapInFunction(call);
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+}
+
+INSTANTIATE_TEST_SUITE_P(ResolverTest,
+                         ResolverBuiltinTest_DataPacking,
+                         testing::Values(BuiltinData{"pack4x8snorm", BuiltinType::kPack4x8snorm},
+                                         BuiltinData{"pack4x8unorm", BuiltinType::kPack4x8unorm},
+                                         BuiltinData{"pack2x16snorm", BuiltinType::kPack2x16snorm},
+                                         BuiltinData{"pack2x16unorm", BuiltinType::kPack2x16unorm},
+                                         BuiltinData{"pack2x16float",
+                                                     BuiltinType::kPack2x16float}));
+
+}  // namespace data_packing_builtin_tests
+
+// Tests for Data Unpacking builtins
+namespace data_unpacking_builtin_tests {
+
+using ResolverBuiltinTest_DataUnpacking = ResolverTestWithParam<BuiltinData>;
+TEST_P(ResolverBuiltinTest_DataUnpacking, InferType) {
+    auto param = GetParam();
+
+    bool pack4 = param.builtin == BuiltinType::kUnpack4x8snorm ||
+                 param.builtin == BuiltinType::kUnpack4x8unorm;
+
+    auto* call = Call(param.name, 1_u);
+    WrapInFunction(call);
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->is_float_vector());
+    if (pack4) {
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 4u);
+    } else {
+        EXPECT_EQ(TypeOf(call)->As<sem::Vector>()->Width(), 2u);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ResolverTest,
+    ResolverBuiltinTest_DataUnpacking,
+    testing::Values(BuiltinData{"unpack4x8snorm", BuiltinType::kUnpack4x8snorm},
+                    BuiltinData{"unpack4x8unorm", BuiltinType::kUnpack4x8unorm},
+                    BuiltinData{"unpack2x16snorm", BuiltinType::kUnpack2x16snorm},
+                    BuiltinData{"unpack2x16unorm", BuiltinType::kUnpack2x16unorm},
+                    BuiltinData{"unpack2x16float", BuiltinType::kUnpack2x16float}));
+
+}  // namespace data_unpacking_builtin_tests
+
+// Tests for Synchronization builtins
+namespace synchronization_builtin_tests {
+
+using ResolverBuiltinTest_Barrier = ResolverTestWithParam<BuiltinData>;
+TEST_P(ResolverBuiltinTest_Barrier, InferType) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name);
+    WrapInFunction(CallStmt(call));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    ASSERT_NE(TypeOf(call), nullptr);
+    EXPECT_TRUE(TypeOf(call)->Is<sem::Void>());
+}
+
+TEST_P(ResolverBuiltinTest_Barrier, Error_TooManyParams) {
+    auto param = GetParam();
+
+    auto* call = Call(param.name, vec4<f32>(1_f, 2_f, 3_f, 4_f), 1_f);
+    WrapInFunction(CallStmt(call));
+
+    EXPECT_FALSE(r()->Resolve());
+
+    EXPECT_THAT(r()->error(), HasSubstr("error: no matching call to " + std::string(param.name)));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ResolverTest,
+    ResolverBuiltinTest_Barrier,
+    testing::Values(BuiltinData{"storageBarrier", BuiltinType::kStorageBarrier},
+                    BuiltinData{"workgroupBarrier", BuiltinType::kWorkgroupBarrier}));
+
+}  // namespace synchronization_builtin_tests
+
 }  // namespace
 }  // namespace tint::resolver
diff --git a/src/tint/transform/substitute_override.cc b/src/tint/transform/substitute_override.cc
index aa7abff..de597c2 100644
--- a/src/tint/transform/substitute_override.cc
+++ b/src/tint/transform/substitute_override.cc
@@ -46,14 +46,14 @@
     }
 
     ctx.ReplaceAll([&](const ast::Override* w) -> const ast::Const* {
-        auto ident = w->Identifier(ctx.src->Symbols());
+        auto* sem = ctx.src->Sem().Get(w);
 
         auto src = ctx.Clone(w->source);
         auto sym = ctx.Clone(w->symbol);
         auto* ty = ctx.Clone(w->type);
 
         // No replacement provided, just clone the override node as a const.
-        auto iter = data->map.find(ident);
+        auto iter = data->map.find(sem->OverrideId());
         if (iter == data->map.end()) {
             if (!w->constructor) {
                 ctx.dst->Diagnostics().add_error(
@@ -66,7 +66,7 @@
 
         auto value = iter->second;
         auto* ctor = Switch(
-            ctx.src->Sem().Get(w)->Type(),
+            sem->Type(),
             [&](const sem::Bool*) { return ctx.dst->Expr(!std::equal_to<double>()(value, 0.0)); },
             [&](const sem::I32*) { return ctx.dst->Expr(i32(value)); },
             [&](const sem::U32*) { return ctx.dst->Expr(u32(value)); },
diff --git a/src/tint/transform/substitute_override.h b/src/tint/transform/substitute_override.h
index ab4b38a..9ea315d 100644
--- a/src/tint/transform/substitute_override.h
+++ b/src/tint/transform/substitute_override.h
@@ -18,6 +18,8 @@
 #include <string>
 #include <unordered_map>
 
+#include "tint/override_id.h"
+
 #include "src/tint/transform/transform.h"
 
 namespace tint::transform {
@@ -57,10 +59,10 @@
         /// @returns this Config
         Config& operator=(const Config&);
 
-        /// The map of override name (either the identifier or id) to value.
+        /// The map of override identifier to the override value.
         /// The value is always a double coming into the transform and will be
         /// converted to the correct type through and initializer.
-        std::unordered_map<std::string, double> map;
+        std::unordered_map<OverrideId, double> map;
     };
 
     /// Constructor
diff --git a/src/tint/transform/substitute_override_test.cc b/src/tint/transform/substitute_override_test.cc
index 70ac675..f84c32c 100644
--- a/src/tint/transform/substitute_override_test.cc
+++ b/src/tint/transform/substitute_override_test.cc
@@ -82,7 +82,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SubstituteOverrideTest, Identifier) {
+TEST_F(SubstituteOverrideTest, ImplicitId) {
     auto* src = R"(
 override i_width: i32;
 override i_height = 1i;
@@ -127,14 +127,14 @@
 )";
 
     SubstituteOverride::Config cfg;
-    cfg.map.insert({"i_width", 42.0});
-    cfg.map.insert({"i_height", 11.0});
-    cfg.map.insert({"f_width", 22.3});
-    cfg.map.insert({"f_height", 12.4});
-    cfg.map.insert({"h_width", 9.4});
-    cfg.map.insert({"h_height", 3.4});
-    cfg.map.insert({"b_width", 1.0});
-    cfg.map.insert({"b_height", 0.0});
+    cfg.map.insert({OverrideId{0}, 42.0});
+    cfg.map.insert({OverrideId{1}, 11.0});
+    cfg.map.insert({OverrideId{2}, 22.3});
+    cfg.map.insert({OverrideId{3}, 12.4});
+    // cfg.map.insert({OverrideId{4}, 9.4});
+    // cfg.map.insert({OverrideId{5}, 3.4});
+    cfg.map.insert({OverrideId{4}, 1.0});
+    cfg.map.insert({OverrideId{5}, 0.0});
 
     DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
@@ -143,7 +143,7 @@
     EXPECT_EQ(expect, str(got));
 }
 
-TEST_F(SubstituteOverrideTest, Id) {
+TEST_F(SubstituteOverrideTest, ExplicitId) {
     auto* src = R"(
 enable f16;
 
@@ -183,7 +183,7 @@
 
 const b_height = false;
 
-const o_width = 2i;
+const o_width = 13i;
 
 @vertex
 fn main() -> @builtin(position) vec4<f32> {
@@ -192,16 +192,15 @@
 )";
 
     SubstituteOverride::Config cfg;
-    cfg.map.insert({"0", 42.0});
-    cfg.map.insert({"10", 11.0});
-    cfg.map.insert({"1", 22.3});
-    cfg.map.insert({"9", 12.4});
-    cfg.map.insert({"2", 9.4});
-    cfg.map.insert({"8", 3.4});
-    cfg.map.insert({"3", 1.0});
-    cfg.map.insert({"7", 0.0});
-    // No effect because an @id is set for o_width
-    cfg.map.insert({"o_width", 13});
+    cfg.map.insert({OverrideId{0}, 42.0});
+    cfg.map.insert({OverrideId{10}, 11.0});
+    cfg.map.insert({OverrideId{1}, 22.3});
+    cfg.map.insert({OverrideId{9}, 12.4});
+    cfg.map.insert({OverrideId{2}, 9.4});
+    cfg.map.insert({OverrideId{8}, 3.4});
+    cfg.map.insert({OverrideId{3}, 1.0});
+    cfg.map.insert({OverrideId{7}, 0.0});
+    cfg.map.insert({OverrideId{5}, 13});
 
     DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);
@@ -230,7 +229,7 @@
 )";
 
     SubstituteOverride::Config cfg;
-    cfg.map.insert({"i_height", 11.0});
+    cfg.map.insert({OverrideId{0}, 11.0});
 
     DataMap data;
     data.Add<SubstituteOverride::Config>(cfg);