IntrinsicTable: Fix a number of TODOs

And add tests for IntrinsicTable.

Drop all the type unwrapping - be precise:
* Display the actual argument types in the signature mismatch message
* Only dereference pointer arguments if the parameter does not expect a pointer

Correctly match access control on storage types

Note that I was mistaken in tint:486 - the TypeDeterminer is resolving identifiers to variables correctly as pointer types. The confustion here was probably due to all the UnwrapAll() calls, which have now all gone.

Fixed: tint:486
Change-Id: I239eabd1fedfc082566c4af616ccfc58786cae25
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/41280
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 4147124..d2b2637 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -835,6 +835,7 @@
     "src/diagnostic/formatter_test.cc",
     "src/diagnostic/printer_test.cc",
     "src/inspector/inspector_test.cc",
+    "src/intrinsic_table_test.cc",
     "src/namer_test.cc",
     "src/program_builder_test.cc",
     "src/program_test.cc",
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4026aac..e4b81d4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -465,6 +465,7 @@
     diagnostic/formatter_test.cc
     diagnostic/printer_test.cc
     inspector/inspector_test.cc
+    intrinsic_table_test.cc
     namer_test.cc
     program_test.cc
     scope_stack_test.cc
diff --git a/src/intrinsic_table.cc b/src/intrinsic_table.cc
index a0f3cc2..74c2242 100644
--- a/src/intrinsic_table.cc
+++ b/src/intrinsic_table.cc
@@ -23,6 +23,7 @@
 #include "src/block_allocator.h"
 #include "src/program_builder.h"
 #include "src/semantic/intrinsic.h"
+#include "src/type/access_control_type.h"
 #include "src/type/depth_texture_type.h"
 #include "src/type/f32_type.h"
 #include "src/type/multisampled_texture_type.h"
@@ -91,15 +92,33 @@
   virtual ~Matcher() = default;
 
   /// Checks whether the given argument type matches.
+  /// Aliases are automatically unwrapped before matching.
   /// Match may add to, or compare against the open types and numbers in state.
   /// @returns true if the argument type is as expected.
-  virtual bool Match(MatchState& state, type::Type* argument_type) const = 0;
+  bool Match(MatchState& state, type::Type* argument_type) const {
+    auto* unwrapped = argument_type;
+    while (auto* alias = unwrapped->As<type::Alias>()) {
+      unwrapped = alias->type();
+    }
+    return MatchUnwrapped(state, unwrapped);
+  }
+
+  /// @return true if the matcher is expecting a pointer. If this method returns
+  /// false and the argument is a pointer type, then the argument should be
+  /// dereferenced before calling.
+  virtual bool ExpectsPointer() const { return false; }
 
   /// @return a string representation of the matcher. Used for printing error
   /// messages when no overload is found.
   virtual std::string str() const = 0;
 
  protected:
+  /// Checks whether the given alias-unwrapped argument type matches.
+  /// Match may add to, or compare against the open types and numbers in state.
+  /// @returns true if the argument type is as expected.
+  virtual bool MatchUnwrapped(MatchState& state,
+                              type::Type* argument_type) const = 0;
+
   /// Checks `state.open_type` to see if the OpenType `t` is equal to the type
   /// `ty`. If `state.open_type` does not contain an entry for `t`, then `ty`
   /// is added and returns true.
@@ -154,7 +173,7 @@
  public:
   explicit OpenTypeBuilder(OpenType open_type) : open_type_(open_type) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
     return MatchOpenType(state, open_type_, ty);
   }
 
@@ -171,7 +190,7 @@
 /// VoidBuilder is a Matcher / Builder for void types.
 class VoidBuilder : public Builder {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::Void>();
   }
   type::Type* Build(BuildState& state) const override {
@@ -183,7 +202,7 @@
 /// BoolBuilder is a Matcher / Builder for boolean types.
 class BoolBuilder : public Builder {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::Bool>();
   }
   type::Type* Build(BuildState& state) const override {
@@ -195,7 +214,7 @@
 /// F32Builder is a Matcher / Builder for f32 types.
 class F32Builder : public Builder {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::F32>();
   }
   type::Type* Build(BuildState& state) const override {
@@ -207,7 +226,7 @@
 /// U32Builder is a Matcher / Builder for u32 types.
 class U32Builder : public Builder {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::U32>();
   }
   type::Type* Build(BuildState& state) const override {
@@ -219,7 +238,7 @@
 /// I32Builder is a Matcher / Builder for i32 types.
 class I32Builder : public Builder {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::I32>();
   }
   type::Type* Build(BuildState& state) const override {
@@ -231,7 +250,7 @@
 /// IU32Matcher is a Matcher for i32 or u32 types.
 class IU32Matcher : public Matcher {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::I32>() || ty->Is<type::U32>();
   }
   std::string str() const override { return "i32 or u32"; }
@@ -240,7 +259,7 @@
 /// FIU32Matcher is a Matcher for f32, i32 or u32 types.
 class FIU32Matcher : public Matcher {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->Is<type::F32>() || ty->Is<type::I32>() || ty->Is<type::U32>();
   }
   std::string str() const override { return "f32, i32 or u32"; }
@@ -249,7 +268,7 @@
 /// ScalarMatcher is a Matcher for f32, i32, u32 or boolean types.
 class ScalarMatcher : public Matcher {
  public:
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     return ty->is_scalar();
   }
   std::string str() const override { return "scalar"; }
@@ -262,8 +281,8 @@
   OpenSizeVecBuilder(OpenNumber size, Builder* element_builder)
       : size_(size), element_builder_(element_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
-    if (auto* vec = ty->UnwrapAll()->As<type::Vector>()) {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
+    if (auto* vec = ty->As<type::Vector>()) {
       if (!MatchOpenNumber(state, size_, vec->size())) {
         return false;
       }
@@ -294,8 +313,8 @@
   VecBuilder(uint32_t size, Builder* element_builder)
       : size_(size), element_builder_(element_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
-    if (auto* vec = ty->UnwrapAll()->As<type::Vector>()) {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
+    if (auto* vec = ty->As<type::Vector>()) {
       if (vec->size() == size_) {
         return element_builder_->Match(state, vec->type());
       }
@@ -326,8 +345,8 @@
                      Builder* element_builder)
       : columns_(columns), rows_(rows), element_builder_(element_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
-    if (auto* mat = ty->UnwrapAll()->As<type::Matrix>()) {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
+    if (auto* mat = ty->As<type::Matrix>()) {
       if (!MatchOpenNumber(state, columns_, mat->columns())) {
         return false;
       }
@@ -347,7 +366,7 @@
   }
 
   std::string str() const override {
-    return "max" + std::string(tint::str(columns_)) + "x" +
+    return "mat" + std::string(tint::str(columns_)) + "x" +
            std::string(tint::str(rows_)) + "<" + element_builder_->str() + ">";
   }
 
@@ -363,15 +382,11 @@
   explicit PtrBuilder(Builder* element_builder)
       : element_builder_(element_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
     if (auto* ptr = ty->As<type::Pointer>()) {
       return element_builder_->Match(state, ptr->type());
     }
-    // TODO(bclayton): https://crbug.com/tint/486
-    // TypeDeterminer currently folds away the pointers on expressions.
-    // We'll need to fix this to ensure that pointer parameters are not fed
-    // non-pointer arguments, but for now just accept them.
-    return element_builder_->Match(state, ty);
+    return false;
   }
 
   type::Type* Build(BuildState& state) const override {
@@ -379,6 +394,8 @@
     return state.ty_mgr.Get<type::Pointer>(el, ast::StorageClass::kNone);
   }
 
+  bool ExpectsPointer() const override { return true; }
+
   std::string str() const override {
     return "ptr<" + element_builder_->str() + ">";
   }
@@ -393,7 +410,7 @@
   explicit ArrayBuilder(Builder* element_builder)
       : element_builder_(element_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
     if (auto* arr = ty->As<type::Array>()) {
       if (arr->size() == 0) {
         return element_builder_->Match(state, arr->type());
@@ -422,7 +439,7 @@
                                  Builder* type_builder)
       : dimensions_(dimensions), type_builder_(type_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
     if (auto* tex = ty->As<type::SampledTexture>()) {
       if (tex->dim() == dimensions_) {
         return type_builder_->Match(state, tex->type());
@@ -455,7 +472,7 @@
                                       Builder* type_builder)
       : dimensions_(dimensions), type_builder_(type_builder) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
     if (auto* tex = ty->As<type::MultisampledTexture>()) {
       if (tex->dim() == dimensions_) {
         return type_builder_->Match(state, tex->type());
@@ -487,7 +504,7 @@
   explicit DepthTextureBuilder(type::TextureDimension dimensions)
       : dimensions_(dimensions) {}
 
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     if (auto* tex = ty->As<type::DepthTexture>()) {
       return tex->dim() == dimensions_;
     }
@@ -520,7 +537,15 @@
         texel_format_(texel_format),
         channel_format_(channel_format) {}
 
-  bool Match(MatchState& state, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
+    if (auto* ac = ty->As<type::AccessControl>()) {
+      // If we have an storage texture argument that's got an access control
+      // type wrapped around it, accept it. Signatures that don't include an
+      // access control imply any access. Example:
+      //   textureDimensions(t : texture_storage_1d<F>) -> i32
+      ty = ac->type();
+    }
+
     if (auto* tex = ty->As<type::StorageTexture>()) {
       if (MatchOpenNumber(state, texel_format_,
                           static_cast<uint32_t>(tex->image_format()))) {
@@ -557,7 +582,7 @@
  public:
   explicit SamplerBuilder(type::SamplerKind kind) : kind_(kind) {}
 
-  bool Match(MatchState&, type::Type* ty) const override {
+  bool MatchUnwrapped(MatchState&, type::Type* ty) const override {
     if (auto* sampler = ty->As<type::Sampler>()) {
       return sampler->kind() == kind_;
     }
@@ -582,6 +607,38 @@
   type::SamplerKind const kind_;
 };
 
+/// AccessControlBuilder is a Matcher / Builder for AccessControl types
+class AccessControlBuilder : public Builder {
+ public:
+  explicit AccessControlBuilder(ast::AccessControl access_control,
+                                Builder* type)
+      : access_control_(access_control), type_(type) {}
+
+  bool MatchUnwrapped(MatchState& state, type::Type* ty) const override {
+    if (auto* ac = ty->As<type::AccessControl>()) {
+      if (ac->access_control() == access_control_) {
+        return type_->Match(state, ty);
+      }
+    }
+    return false;
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* ty = type_->Build(state);
+    return state.ty_mgr.Get<type::AccessControl>(access_control_, ty);
+  }
+
+  std::string str() const override {
+    std::stringstream ss;
+    ss << "[[access(" << access_control_ << ")]] " << type_->str();
+    return ss.str();
+  }
+
+ private:
+  ast::AccessControl const access_control_;
+  Builder* const type_;
+};
+
 /// Impl is the private implementation of the IntrinsicTable interface.
 class Impl : public IntrinsicTable {
  public:
@@ -717,6 +774,12 @@
     return matcher_allocator_.Create<SamplerBuilder>(kind);
   }
 
+  /// @returns a Matcher / Builder that matches an access control type
+  Builder* access_control(ast::AccessControl access_control, Builder* type) {
+    return matcher_allocator_.Create<AccessControlBuilder>(access_control,
+                                                           type);
+  }
+
   /// Registers an overload with the given intrinsic type, return type Matcher /
   /// Builder, and parameter Matcher / Builders.
   /// This overload of Register does not constrain any OpenTypes.
@@ -1044,6 +1107,26 @@
       storage_texture(Dim::k2dArray, OpenNumber::F, OpenType::T);
   auto* tex_storage_3d_FT =
       storage_texture(Dim::k3d, OpenNumber::F, OpenType::T);
+  auto* tex_storage_ro_1d_FT =
+      access_control(ast::AccessControl::kReadOnly, tex_storage_1d_FT);
+  auto* tex_storage_ro_1d_array_FT =
+      access_control(ast::AccessControl::kReadOnly, tex_storage_1d_array_FT);
+  auto* tex_storage_ro_2d_FT =
+      access_control(ast::AccessControl::kReadOnly, tex_storage_2d_FT);
+  auto* tex_storage_ro_2d_array_FT =
+      access_control(ast::AccessControl::kReadOnly, tex_storage_2d_array_FT);
+  auto* tex_storage_ro_3d_FT =
+      access_control(ast::AccessControl::kReadOnly, tex_storage_3d_FT);
+  auto* tex_storage_wo_1d_FT =
+      access_control(ast::AccessControl::kWriteOnly, tex_storage_1d_FT);
+  auto* tex_storage_wo_1d_array_FT =
+      access_control(ast::AccessControl::kWriteOnly, tex_storage_1d_array_FT);
+  auto* tex_storage_wo_2d_FT =
+      access_control(ast::AccessControl::kWriteOnly, tex_storage_2d_FT);
+  auto* tex_storage_wo_2d_array_FT =
+      access_control(ast::AccessControl::kWriteOnly, tex_storage_2d_array_FT);
+  auto* tex_storage_wo_3d_FT =
+      access_control(ast::AccessControl::kWriteOnly, tex_storage_3d_FT);
   auto* sampler = this->sampler(type::SamplerKind::kSampler);
   auto* sampler_comparison =
       this->sampler(type::SamplerKind::kComparisonSampler);
@@ -1170,14 +1253,12 @@
   Register(I::kTextureSampleLevel, f32,          {{t, tex_depth_cube},      {s, sampler}, {coords, vec3_f32},                     {level, i32},                     }); // NOLINT
   Register(I::kTextureSampleLevel, f32,          {{t, tex_depth_cube_array},{s, sampler}, {coords, vec3_f32}, {array_index, i32}, {level, i32},                     }); // NOLINT
 
-  // TODO(bclayton): Check for [[access(write)]]
-  Register(I::kTextureStore, void_, {{t, tex_storage_1d_FT},      {coords, i32},                          {value, vec4_T}, }); // NOLINT
-  Register(I::kTextureStore, void_, {{t, tex_storage_1d_array_FT},{coords, i32},      {array_index, i32}, {value, vec4_T}, }); // NOLINT
-  Register(I::kTextureStore, void_, {{t, tex_storage_2d_FT},      {coords, vec2_i32},                     {value, vec4_T}, }); // NOLINT
-  Register(I::kTextureStore, void_, {{t, tex_storage_2d_array_FT},{coords, vec2_i32}, {array_index, i32}, {value, vec4_T}, }); // NOLINT
-  Register(I::kTextureStore, void_, {{t, tex_storage_3d_FT},      {coords, vec3_i32},                     {value, vec4_T}, }); // NOLINT
+  Register(I::kTextureStore, void_, {{t, tex_storage_wo_1d_FT},      {coords, i32},                          {value, vec4_T}, }); // NOLINT
+  Register(I::kTextureStore, void_, {{t, tex_storage_wo_1d_array_FT},{coords, i32},      {array_index, i32}, {value, vec4_T}, }); // NOLINT
+  Register(I::kTextureStore, void_, {{t, tex_storage_wo_2d_FT},      {coords, vec2_i32},                     {value, vec4_T}, }); // NOLINT
+  Register(I::kTextureStore, void_, {{t, tex_storage_wo_2d_array_FT},{coords, vec2_i32}, {array_index, i32}, {value, vec4_T}, }); // NOLINT
+  Register(I::kTextureStore, void_, {{t, tex_storage_wo_3d_FT},      {coords, vec3_i32},                     {value, vec4_T}, }); // NOLINT
 
-  // TODO(bclayton): Check for [[access(read)]]
   Register(I::kTextureLoad, vec4_T, {{t, tex_2d_T},               {coords, vec2_i32},                      {level, i32},                      }); // NOLINT
   Register(I::kTextureLoad, vec4_T, {{t, tex_2d_array_T},         {coords, vec2_i32}, {array_index, i32},  {level, i32},                      }); // NOLINT
   Register(I::kTextureLoad, vec4_T, {{t, tex_3d_T},               {coords, vec3_i32},                      {level, i32},                      }); // NOLINT
@@ -1185,11 +1266,11 @@
   Register(I::kTextureLoad, vec4_T, {{t, tex_ms_2d_array_T},      {coords, vec2_i32}, {array_index, i32},                {sample_index, i32}, }); // NOLINT
   Register(I::kTextureLoad, f32,    {{t, tex_depth_2d},           {coords, vec2_i32},                      {level, i32},                      }); // NOLINT
   Register(I::kTextureLoad, f32,    {{t, tex_depth_2d_array},     {coords, vec2_i32}, {array_index, i32},  {level, i32},                      }); // NOLINT
-  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_1d_FT},      {coords, i32},                                                              }); // NOLINT
-  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_1d_array_FT},{coords, i32},      {array_index, i32},                                     }); // NOLINT
-  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_2d_FT},      {coords, vec2_i32},                                                         }); // NOLINT
-  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_2d_array_FT},{coords, vec2_i32}, {array_index, i32},                                     }); // NOLINT
-  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_3d_FT},      {coords, vec3_i32},                                                         }); // NOLINT
+  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_ro_1d_FT},      {coords, i32},                                                              }); // NOLINT
+  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_ro_1d_array_FT},{coords, i32},      {array_index, i32},                                     }); // NOLINT
+  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_ro_2d_FT},      {coords, vec2_i32},                                                         }); // NOLINT
+  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_ro_2d_array_FT},{coords, vec2_i32}, {array_index, i32},                                     }); // NOLINT
+  Register(I::kTextureLoad, vec4_T, {{t, tex_storage_ro_3d_FT},      {coords, vec3_i32},                                                         }); // NOLINT
 
   // TODO(bclayton): Update the rest of tint to reflect the spec changes made in
   // https://github.com/gpuweb/gpuweb/pull/1301:
@@ -1287,7 +1368,7 @@
         ss << ", ";
       }
       first = false;
-      ss << arg->UnwrapAll()->FriendlyName(builder.Symbols());
+      ss << arg->FriendlyName(builder.Symbols());
     }
   }
   ss << ")" << std::endl;
@@ -1327,13 +1408,21 @@
   auto count = std::min(parameters.size(), args.size());
   for (size_t i = 0; i < count; i++) {
     assert(args[i]);
-    auto* arg_ty = args[i]->UnwrapAll();
-    if (!parameters[i].matcher->Match(matcher_state, arg_ty)) {
-      matched = false;
-      continue;
+    auto* arg_ty = args[i];
+    if (auto* ptr = arg_ty->As<type::Pointer>()) {
+      if (!parameters[i].matcher->ExpectsPointer()) {
+        // Argument is a pointer, but the matcher isn't expecting one.
+        // Perform an implicit dereference.
+        arg_ty = ptr->type();
+      }
     }
-    // Weight correct parameter matches more than exact number of arguments
-    match_score += 2;
+    if (parameters[i].matcher->Match(matcher_state, arg_ty)) {
+      // A correct parameter match is scored higher than number of parameters to
+      // arguments.
+      match_score += 2;
+    } else {
+      matched = false;
+    }
   }
   if (!matched) {
     return nullptr;
diff --git a/src/intrinsic_table_test.cc b/src/intrinsic_table_test.cc
new file mode 100644
index 0000000..fc75e0c
--- /dev/null
+++ b/src/intrinsic_table_test.cc
@@ -0,0 +1,488 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/intrinsic_table.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "src/program_builder.h"
+#include "src/type/access_control_type.h"
+#include "src/type/depth_texture_type.h"
+#include "src/type/multisampled_texture_type.h"
+#include "src/type/sampled_texture_type.h"
+#include "src/type/storage_texture_type.h"
+
+namespace tint {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::HasSubstr;
+
+using IntrinsicType = semantic::IntrinsicType;
+using Parameter = semantic::Parameter;
+
+class IntrinsicTableTest : public testing::Test, public ProgramBuilder {
+ public:
+  std::unique_ptr<IntrinsicTable> table = IntrinsicTable::Create();
+};
+
+TEST_F(IntrinsicTableTest, MatchF32) {
+  auto result = table->Lookup(*this, IntrinsicType::kCos, {ty.f32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kCos);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.f32()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchF32) {
+  auto result = table->Lookup(*this, IntrinsicType::kCos, {ty.i32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchU32) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kUnpack2x16Float, {ty.u32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kUnpack2x16Float);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec2<f32>());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.u32()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchU32) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kUnpack2x16Float, {ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchI32) {
+  auto* tex =
+      create<type::SampledTexture>(type::TextureDimension::k1d, ty.f32());
+  auto result =
+      table->Lookup(*this, IntrinsicType::kTextureLoad, {tex, ty.i32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureLoad);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec4<f32>());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{tex, Parameter::Usage::kTexture},
+                          Parameter{ty.i32(), Parameter::Usage::kCoords}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchI32) {
+  auto* tex =
+      create<type::SampledTexture>(type::TextureDimension::k1d, ty.f32());
+  auto result =
+      table->Lookup(*this, IntrinsicType::kTextureLoad, {tex, ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchIU32AsI32) {
+  auto result = table->Lookup(*this, IntrinsicType::kCountOneBits, {ty.i32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kCountOneBits);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.i32());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.i32()}));
+}
+
+TEST_F(IntrinsicTableTest, MatchIU32AsU32) {
+  auto result = table->Lookup(*this, IntrinsicType::kCountOneBits, {ty.u32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kCountOneBits);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.u32());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.u32()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchIU32) {
+  auto result = table->Lookup(*this, IntrinsicType::kCountOneBits, {ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchFIU32AsI32) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.i32(), ty.i32(), ty.i32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kClamp);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.i32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.i32()}, Parameter{ty.i32()},
+                          Parameter{ty.i32()}));
+}
+
+TEST_F(IntrinsicTableTest, MatchFIU32AsU32) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.u32(), ty.u32(), ty.u32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kClamp);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.u32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.u32()}, Parameter{ty.u32()},
+                          Parameter{ty.u32()}));
+}
+
+TEST_F(IntrinsicTableTest, MatchFIU32AsF32) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.f32(), ty.f32(), ty.f32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kClamp);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.f32()}, Parameter{ty.f32()},
+                          Parameter{ty.f32()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchFIU32) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.bool_(), ty.bool_(), ty.bool_()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchBool) {
+  auto result = table->Lookup(*this, IntrinsicType::kSelect,
+                              {ty.f32(), ty.f32(), ty.bool_()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kSelect);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.f32()}, Parameter{ty.f32()},
+                          Parameter{ty.bool_()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchBool) {
+  auto result = table->Lookup(*this, IntrinsicType::kSelect,
+                              {ty.f32(), ty.f32(), ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchPointer) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kModf,
+                    {ty.f32(), ty.pointer<f32>(ast::StorageClass::kNone)});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kModf);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(
+      result.intrinsic->Parameters(),
+      ElementsAre(Parameter{ty.f32()},
+                  Parameter{ty.pointer<f32>(ast::StorageClass::kNone)}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchPointer) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kModf, {ty.f32(), ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchArray) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kArrayLength, {ty.array<f32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kArrayLength);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.u32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.array<f32>()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchArray) {
+  auto result = table->Lookup(*this, IntrinsicType::kArrayLength, {ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchSampler) {
+  auto* tex =
+      create<type::SampledTexture>(type::TextureDimension::k2d, ty.f32());
+  auto* sampler = create<type::Sampler>(type::SamplerKind::kSampler);
+  auto result = table->Lookup(*this, IntrinsicType::kTextureSample,
+                              {tex, sampler, ty.vec2<f32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureSample);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec4<f32>());
+  EXPECT_THAT(
+      result.intrinsic->Parameters(),
+      ElementsAre(Parameter{tex, Parameter::Usage::kTexture},
+                  Parameter{sampler, Parameter::Usage::kSampler},
+                  Parameter{ty.vec2<f32>(), Parameter::Usage::kCoords}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchSampler) {
+  auto* tex =
+      create<type::SampledTexture>(type::TextureDimension::k2d, ty.f32());
+  auto result = table->Lookup(*this, IntrinsicType::kTextureSample,
+                              {tex, ty.f32(), ty.vec2<f32>()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchSampledTexture) {
+  auto* tex =
+      create<type::SampledTexture>(type::TextureDimension::k2d, ty.f32());
+  auto result =
+      table->Lookup(*this, IntrinsicType::kTextureLoad, {tex, ty.vec2<i32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureLoad);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec4<f32>());
+  EXPECT_THAT(
+      result.intrinsic->Parameters(),
+      ElementsAre(Parameter{tex, Parameter::Usage::kTexture},
+                  Parameter{ty.vec2<i32>(), Parameter::Usage::kCoords}));
+}
+
+TEST_F(IntrinsicTableTest, MatchMultisampledTexture) {
+  auto* tex =
+      create<type::MultisampledTexture>(type::TextureDimension::k2d, ty.f32());
+  auto result = table->Lookup(*this, IntrinsicType::kTextureLoad,
+                              {tex, ty.vec2<i32>(), ty.i32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureLoad);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec4<f32>());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{tex, Parameter::Usage::kTexture},
+                          Parameter{ty.vec2<i32>(), Parameter::Usage::kCoords},
+                          Parameter{ty.i32(), Parameter::Usage::kSampleIndex}));
+}
+
+TEST_F(IntrinsicTableTest, MatchDepthTexture) {
+  auto* tex = create<type::DepthTexture>(type::TextureDimension::k2d);
+  auto result =
+      table->Lookup(*this, IntrinsicType::kTextureLoad, {tex, ty.vec2<i32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureLoad);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(
+      result.intrinsic->Parameters(),
+      ElementsAre(Parameter{tex, Parameter::Usage::kTexture},
+                  Parameter{ty.vec2<i32>(), Parameter::Usage::kCoords}));
+}
+
+TEST_F(IntrinsicTableTest, MatchROStorageTexture) {
+  auto* tex = create<type::StorageTexture>(
+      type::TextureDimension::k2d, type::ImageFormat::kR16Float,
+      type::StorageTexture::SubtypeFor(type::ImageFormat::kR16Float, Types()));
+  auto* tex_ac =
+      create<type::AccessControl>(ast::AccessControl::kReadOnly, tex);
+  auto result = table->Lookup(*this, IntrinsicType::kTextureLoad,
+                              {tex_ac, ty.vec2<i32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureLoad);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec4<f32>());
+  EXPECT_THAT(
+      result.intrinsic->Parameters(),
+      ElementsAre(Parameter{tex_ac, Parameter::Usage::kTexture},
+                  Parameter{ty.vec2<i32>(), Parameter::Usage::kCoords}));
+}
+
+TEST_F(IntrinsicTableTest, MatchWOStorageTexture) {
+  auto* tex = create<type::StorageTexture>(
+      type::TextureDimension::k2d, type::ImageFormat::kR16Float,
+      type::StorageTexture::SubtypeFor(type::ImageFormat::kR16Float, Types()));
+  auto* tex_ac =
+      create<type::AccessControl>(ast::AccessControl::kWriteOnly, tex);
+  auto result = table->Lookup(*this, IntrinsicType::kTextureStore,
+                              {tex_ac, ty.vec2<i32>(), ty.vec4<f32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kTextureStore);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.void_());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{tex_ac, Parameter::Usage::kTexture},
+                          Parameter{ty.vec2<i32>(), Parameter::Usage::kCoords},
+                          Parameter{ty.vec4<f32>(), Parameter::Usage::kValue}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchTexture) {
+  auto result = table->Lookup(*this, IntrinsicType::kTextureLoad,
+                              {ty.f32(), ty.vec2<i32>()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchAutoPointerDereference) {
+  auto result = table->Lookup(*this, IntrinsicType::kCos,
+                              {ty.pointer<f32>(ast::StorageClass::kNone)});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kCos);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.f32()}));
+}
+
+TEST_F(IntrinsicTableTest, MatchWithAliasUnwrapping) {
+  auto* alias_a = ty.alias("alias_a", ty.f32());
+  auto* alias_b = ty.alias("alias_b", alias_a);
+  auto* alias_c = ty.alias("alias_c", alias_b);
+  auto result = table->Lookup(*this, IntrinsicType::kCos, {alias_c});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kCos);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(), ElementsAre(Parameter{ty.f32()}));
+}
+
+TEST_F(IntrinsicTableTest, MatchOpenType) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.f32(), ty.f32(), ty.f32()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kClamp);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.f32()}, Parameter{ty.f32()},
+                          Parameter{ty.f32()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchOpenType) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.f32(), ty.u32(), ty.f32()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchOpenSizeVector) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.vec2<f32>(), ty.vec2<f32>(), ty.vec2<f32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kClamp);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.vec2<f32>());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.vec2<f32>()}, Parameter{ty.vec2<f32>()},
+                          Parameter{ty.vec2<f32>()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchOpenSizeVector) {
+  auto result = table->Lookup(*this, IntrinsicType::kClamp,
+                              {ty.vec2<f32>(), ty.vec2<u32>(), ty.vec2<f32>()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, MatchOpenSizeMatrix) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kDeterminant, {ty.mat3x3<f32>()});
+  ASSERT_NE(result.intrinsic, nullptr);
+  ASSERT_EQ(result.error, "");
+  EXPECT_THAT(result.intrinsic->Type(), IntrinsicType::kDeterminant);
+  EXPECT_THAT(result.intrinsic->ReturnType(), ty.f32());
+  EXPECT_THAT(result.intrinsic->Parameters(),
+              ElementsAre(Parameter{ty.mat3x3<f32>()}));
+}
+
+TEST_F(IntrinsicTableTest, MismatchOpenSizeMatrix) {
+  auto result =
+      table->Lookup(*this, IntrinsicType::kDeterminant, {ty.mat3x2<f32>()});
+  ASSERT_EQ(result.intrinsic, nullptr);
+  ASSERT_THAT(result.error, HasSubstr("no matching call"));
+}
+
+TEST_F(IntrinsicTableTest, OverloadOrderByNumberOfParameters) {
+  // None of the arguments match, so expect the overloads with 2 parameters to
+  // come first
+  auto result = table->Lookup(*this, IntrinsicType::kTextureDimensions,
+                              {ty.bool_(), ty.bool_()});
+  ASSERT_EQ(result.error,
+            R"(no matching call to textureDimensions(bool, bool)
+
+27 candidate functions:
+  textureDimensions(texture : texture_2d<T>, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_2d_array<T>, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_3d<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_cube<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_cube_array<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_depth_2d, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_depth_2d_array, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_depth_cube, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_depth_cube_array, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_1d<T>) -> i32
+  textureDimensions(texture : texture_1d_array<T>) -> i32
+  textureDimensions(texture : texture_2d<T>) -> vec2<i32>
+  textureDimensions(texture : texture_2d_array<T>) -> vec2<i32>
+  textureDimensions(texture : texture_3d<T>) -> vec3<i32>
+  textureDimensions(texture : texture_cube<T>) -> vec3<i32>
+  textureDimensions(texture : texture_cube_array<T>) -> vec3<i32>
+  textureDimensions(texture : texture_multisampled_2d<T>) -> vec2<i32>
+  textureDimensions(texture : texture_multisampled_2d_array<T>) -> vec2<i32>
+  textureDimensions(texture : texture_depth_2d) -> vec2<i32>
+  textureDimensions(texture : texture_depth_2d_array) -> vec2<i32>
+  textureDimensions(texture : texture_depth_cube) -> vec3<i32>
+  textureDimensions(texture : texture_depth_cube_array) -> vec3<i32>
+  textureDimensions(texture : texture_storage_1d<F>) -> i32
+  textureDimensions(texture : texture_storage_1d_array<F>) -> i32
+  textureDimensions(texture : texture_storage_2d<F>) -> vec2<i32>
+  textureDimensions(texture : texture_storage_2d_array<F>) -> vec2<i32>
+  textureDimensions(texture : texture_storage_3d<F>) -> vec3<i32>
+)");
+}
+
+TEST_F(IntrinsicTableTest, OverloadOrderByMatchingParameter) {
+  auto* tex = create<type::DepthTexture>(type::TextureDimension::k2d);
+  auto result = table->Lookup(*this, IntrinsicType::kTextureDimensions,
+                              {tex, ty.bool_()});
+  ASSERT_EQ(result.error,
+            R"(no matching call to textureDimensions(texture_depth_2d, bool)
+
+27 candidate functions:
+  textureDimensions(texture : texture_depth_2d, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_depth_2d) -> vec2<i32>
+  textureDimensions(texture : texture_2d<T>, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_2d_array<T>, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_3d<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_cube<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_cube_array<T>, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_depth_2d_array, level : i32) -> vec2<i32>
+  textureDimensions(texture : texture_depth_cube, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_depth_cube_array, level : i32) -> vec3<i32>
+  textureDimensions(texture : texture_1d<T>) -> i32
+  textureDimensions(texture : texture_1d_array<T>) -> i32
+  textureDimensions(texture : texture_2d<T>) -> vec2<i32>
+  textureDimensions(texture : texture_2d_array<T>) -> vec2<i32>
+  textureDimensions(texture : texture_3d<T>) -> vec3<i32>
+  textureDimensions(texture : texture_cube<T>) -> vec3<i32>
+  textureDimensions(texture : texture_cube_array<T>) -> vec3<i32>
+  textureDimensions(texture : texture_multisampled_2d<T>) -> vec2<i32>
+  textureDimensions(texture : texture_multisampled_2d_array<T>) -> vec2<i32>
+  textureDimensions(texture : texture_depth_2d_array) -> vec2<i32>
+  textureDimensions(texture : texture_depth_cube) -> vec3<i32>
+  textureDimensions(texture : texture_depth_cube_array) -> vec3<i32>
+  textureDimensions(texture : texture_storage_1d<F>) -> i32
+  textureDimensions(texture : texture_storage_1d_array<F>) -> i32
+  textureDimensions(texture : texture_storage_2d<F>) -> vec2<i32>
+  textureDimensions(texture : texture_storage_2d_array<F>) -> vec2<i32>
+  textureDimensions(texture : texture_storage_3d<F>) -> vec3<i32>
+)");
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/semantic/call_target.h b/src/semantic/call_target.h
index 16cae05..ce0587e 100644
--- a/src/semantic/call_target.h
+++ b/src/semantic/call_target.h
@@ -56,6 +56,13 @@
   Usage const usage = Usage::kNone;
 };
 
+std::ostream& operator<<(std::ostream& out, Parameter parameter);
+
+/// Comparison operator for Parameters
+static inline bool operator==(const Parameter& a, const Parameter& b) {
+  return a.type == b.type && a.usage == b.usage;
+}
+
 /// @returns a string representation of the given parameter usage.
 const char* str(Parameter::Usage usage);
 
diff --git a/src/semantic/sem_call_target.cc b/src/semantic/sem_call_target.cc
index 2ae9ad1..5c8011d 100644
--- a/src/semantic/sem_call_target.cc
+++ b/src/semantic/sem_call_target.cc
@@ -14,6 +14,7 @@
 
 #include "src/semantic/call_target.h"
 
+#include "src/symbol_table.h"
 #include "src/type/type.h"
 
 TINT_INSTANTIATE_CLASS_ID(tint::semantic::CallTarget);
@@ -65,5 +66,12 @@
       return "<unknown>";
   }
 }
+
+std::ostream& operator<<(std::ostream& out, Parameter parameter) {
+  out << "[type: " << parameter.type->FriendlyName(SymbolTable{})
+      << ", usage: " << str(parameter.usage) << "]";
+  return out;
+}
+
 }  // namespace semantic
 }  // namespace tint
diff --git a/src/type_determiner_test.cc b/src/type_determiner_test.cc
index fc46468..f6e8859 100644
--- a/src/type_determiner_test.cc
+++ b/src/type_determiner_test.cc
@@ -55,6 +55,7 @@
 #include "src/semantic/expression.h"
 #include "src/semantic/function.h"
 #include "src/semantic/variable.h"
+#include "src/type/access_control_type.h"
 #include "src/type/alias_type.h"
 #include "src/type/array_type.h"
 #include "src/type/bool_type.h"
@@ -1379,13 +1380,13 @@
 
   Global("my_var", ast::StorageClass::kNone, ty.f32());
 
-  auto* expr = Call(name, "my_var", "my_var");
+  auto* expr = Call(name, "my_var", 1.23f);
   WrapInFunction(expr);
 
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(), "no matching call to " + name +
-                               "(f32, f32)\n\n"
+                               "(ptr<f32>, f32)\n\n"
                                "2 candidate functions:\n  " +
                                name + "(f32) -> bool\n  " + name +
                                "(vecN<f32>) -> vecN<bool>\n");
@@ -1469,11 +1470,13 @@
   auto* coords_type = GetCoordsType(dim, ty.i32());
 
   auto* subtype = type::StorageTexture::SubtypeFor(format, Types());
-  type::Type* texture_type = create<type::StorageTexture>(dim, format, subtype);
+  auto* texture_type = create<type::StorageTexture>(dim, format, subtype);
+  auto* ro_texture_type =
+      create<type::AccessControl>(ast::AccessControl::kReadOnly, texture_type);
 
   ast::ExpressionList call_params;
 
-  add_call_param("texture", texture_type, &call_params);
+  add_call_param("texture", ro_texture_type, &call_params);
   add_call_param("coords", coords_type, &call_params);
 
   if (type::IsTextureArray(dim)) {
@@ -2504,7 +2507,7 @@
             "no matching call to " + std::string(param.name) + R"(()
 
 1 candidate function:
-  determinant(maxNxN<f32>) -> f32
+  determinant(matNxN<f32>) -> f32
 )");
 }