Add IntrinsicTable

Provides a centeralized table for all intrinsic overloads.

IntrinsicTable::Lookup() takes the intrinsic type and list of arguments, returning either the matched overload, or a sensible error message.

The validator has expectations that the TypeDeterminer resolves the return type of an intrinsic call, even when the signature doesn't match. To handle this, create semantic::Intrinsic nodes even when the overload fails to match. A significant portion of the Validator's logic for handling intrinsics can be removed (future change).

There are a number of benefits to migrating the TypeDeterminer and Validator over to the IntrinsicTable:
* There's far less intrininsic-bespoke code to maintain (no more duplicate `kIntrinsicData` tables in TypeDeterminer and Validator).
* Adding or adjusting an intrinsic overload involves adding or adjusting a single Register() line.
* Error messages give helpful suggestions for related overloads when given incorrect arguments.
* Error messages are consistent for all intrinsics.
* Error messages are far more understandable than those produced by the TypeDeterminer.
* Further improvements on the error messages produced by the IntrinsicTable will benefit _all_ the intrinsics and their overloads.
* The IntrinsicTable generates correct parameter information, including whether parameters are pointers or not.
* The IntrinsicTable will help with implementing autocomplete for a language server

Change-Id: I4bfa88533396b0b372aef41a62fe47b738531aed
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/40504
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 57d221a..699411a 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -355,6 +355,8 @@
     "src/clone_context.h",
     "src/demangler.cc",
     "src/demangler.h",
+    "src/intrinsic_table.cc",
+    "src/intrinsic_table.h",
     "src/diagnostic/diagnostic.cc",
     "src/diagnostic/diagnostic.h",
     "src/diagnostic/formatter.cc",
@@ -456,8 +458,6 @@
     "src/validator/validator.h",
     "src/validator/validator_impl.cc",
     "src/validator/validator_impl.h",
-    "src/validator/validator_test_helper.cc",
-    "src/validator/validator_test_helper.h",
     "src/writer/append_vector.cc",
     "src/writer/append_vector.h",
     "src/writer/float_to_string.cc",
@@ -864,6 +864,8 @@
     "src/validator/validator_control_block_test.cc",
     "src/validator/validator_function_test.cc",
     "src/validator/validator_test.cc",
+    "src/validator/validator_test_helper.cc",
+    "src/validator/validator_test_helper.h",
     "src/writer/float_to_string_test.cc",
   ]
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4ce8de2..876e851 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -169,6 +169,8 @@
   clone_context.h
   demangler.cc
   demangler.h;
+  intrinsic_table.cc
+  intrinsic_table.h
   diagnostic/diagnostic.cc
   diagnostic/diagnostic.h
   diagnostic/formatter.cc
@@ -270,8 +272,6 @@
   validator/validator.h
   validator/validator_impl.cc
   validator/validator_impl.h
-  validator/validator_test_helper.cc
-  validator/validator_test_helper.h
   writer/append_vector.cc
   writer/append_vector.h
   writer/float_to_string.cc
@@ -489,6 +489,8 @@
     validator/validator_function_test.cc
     validator/validator_test.cc
     validator/validator_type_test.cc
+    validator/validator_test_helper.cc
+    validator/validator_test_helper.h
     writer/float_to_string_test.cc
   )
 
diff --git a/src/intrinsic_table.cc b/src/intrinsic_table.cc
new file mode 100644
index 0000000..d550eec
--- /dev/null
+++ b/src/intrinsic_table.cc
@@ -0,0 +1,994 @@
+// 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 <algorithm>
+#include <limits>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+#include "src/block_allocator.h"
+#include "src/program_builder.h"
+#include "src/semantic/intrinsic.h"
+#include "src/type/f32_type.h"
+
+namespace tint {
+namespace {
+
+/// OpenTypes are the symbols used for templated types in overload signatures
+enum class OpenType {
+  T,
+  Count,  // Number of entries in the enum. Not a usable symbol.
+};
+
+/// OpenNumber are the symbols used for templated integers in overload
+/// signatures
+enum class OpenNumber {
+  N,  // Typically used for vecN
+  M,  // Typically used for matNxM
+  F,  // Typically used for texture_storage_2d<F>
+};
+
+/// @return a string of the OpenType symbol `ty`
+const char* str(OpenType ty) {
+  switch (ty) {
+    case OpenType::T:
+      return "T";
+
+    case OpenType::Count:
+      break;
+  }
+  return "";
+}
+
+/// @return a string of the OpenNumber symbol `num`
+const char* str(OpenNumber num) {
+  switch (num) {
+    case OpenNumber::N:
+      return "N";
+    case OpenNumber::M:
+      return "M";
+    case OpenNumber::F:
+      return "F";
+  }
+  return "";
+}
+
+/// A Matcher is an interface of a class used to match an overload parameter,
+/// return type, or open type.
+class Matcher {
+ public:
+  /// Current state passed to Match()
+  struct MatchState {
+    /// The map of open types. A new entry is assigned the first time an
+    /// OpenType is encountered. If the OpenType is encountered again, a
+    /// comparison is made to see if the type is consistent.
+    std::unordered_map<OpenType, type::Type*> open_types;
+    /// The map of open numbers. A new entry is assigned the first time an
+    /// OpenNumber is encountered. If the OpenNumber is encountered again, a
+    /// comparison is made to see if the number is consistent.
+    std::unordered_map<OpenNumber, uint32_t> open_numbers;
+  };
+
+  /// Destructor
+  virtual ~Matcher() = default;
+
+  /// Checks whether the given 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 Match(MatchState& state, type::Type* argument_type) const = 0;
+
+  /// @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 `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.
+  bool MatchOpenType(MatchState& state, OpenType t, type::Type* ty) const {
+    auto it = state.open_types.find(t);
+    if (it != state.open_types.end()) {
+      return it->second == ty;
+    }
+    state.open_types[t] = ty;
+    return true;
+  }
+
+  /// Checks `state.open_numbers` to see if the OpenNumber `n` is equal to
+  /// `val`. If `state.open_numbers` does not contain an entry for `n`, then
+  /// `val` is added and returns true.
+  bool MatchOpenNumber(MatchState& state, OpenNumber n, uint32_t val) const {
+    auto it = state.open_numbers.find(n);
+    if (it != state.open_numbers.end()) {
+      return it->second == val;
+    }
+    state.open_numbers[n] = val;
+    return true;
+  }
+};
+
+/// Builder is an extension of the Matcher interface that can also build the
+/// expected type. Builders are used to generate the parameter and return types
+/// on successful overload match.
+class Builder : public Matcher {
+ public:
+  /// Final matched state passed to Build()
+  struct BuildState {
+    /// The type manager used to construct new types
+    type::Manager& ty_mgr;
+    /// The final resolved list of open types
+    std::unordered_map<OpenType, type::Type*> const open_types;
+    /// The final resolved list of open numbers
+    std::unordered_map<OpenNumber, uint32_t> const open_numbers;
+  };
+
+  /// Destructor
+  ~Builder() override = default;
+
+  /// Constructs and returns the expected type
+  virtual type::Type* Build(BuildState& state) const = 0;
+};
+
+/// OpenTypeBuilder is a Matcher / Builder for an open type (T etc).
+/// The OpenTypeBuilder will match against any type (so long as it is consistent
+/// for the overload), and Build() will build the type it matched against.
+class OpenTypeBuilder : public Builder {
+ public:
+  explicit OpenTypeBuilder(OpenType open_type) : open_type_(open_type) {}
+
+  bool Match(MatchState& state, type::Type* ty) const override {
+    return MatchOpenType(state, open_type_, ty);
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    return state.open_types.at(open_type_);
+  }
+
+  std::string str() const override { return tint::str(open_type_); }
+
+ private:
+  OpenType open_type_;
+};
+
+/// BoolBuilder is a Matcher / Builder for boolean types.
+class BoolBuilder : public Builder {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->Is<type::Bool>();
+  }
+  type::Type* Build(BuildState& state) const override {
+    return state.ty_mgr.Get<type::Bool>();
+  }
+  std::string str() const override { return "bool"; }
+};
+
+/// F32Builder is a Matcher / Builder for f32 types.
+class F32Builder : public Builder {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->Is<type::F32>();
+  }
+  type::Type* Build(BuildState& state) const override {
+    return state.ty_mgr.Get<type::F32>();
+  }
+  std::string str() const override { return "f32"; }
+};
+
+/// U32Builder is a Matcher / Builder for u32 types.
+class U32Builder : public Builder {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->Is<type::U32>();
+  }
+  type::Type* Build(BuildState& state) const override {
+    return state.ty_mgr.Get<type::U32>();
+  }
+  std::string str() const override { return "u32"; }
+};
+
+/// I32Builder is a Matcher / Builder for i32 types.
+class I32Builder : public Builder {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->Is<type::I32>();
+  }
+  type::Type* Build(BuildState& state) const override {
+    return state.ty_mgr.Get<type::I32>();
+  }
+  std::string str() const override { return "i32"; }
+};
+
+/// IU32Matcher is a Matcher for i32 or u32 types.
+class IU32Matcher : public Matcher {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->Is<type::I32>() || ty->Is<type::U32>();
+  }
+  std::string str() const override { return "i32 or u32"; }
+};
+
+/// FIU32Matcher is a Matcher for f32, i32 or u32 types.
+class FIU32Matcher : public Matcher {
+ public:
+  bool Match(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"; }
+};
+
+/// ScalarMatcher is a Matcher for f32, i32, u32 or boolean types.
+class ScalarMatcher : public Matcher {
+ public:
+  bool Match(MatchState&, type::Type* ty) const override {
+    return ty->is_scalar();
+  }
+  std::string str() const override { return "scalar"; }
+};
+
+/// OpenSizeVecBuilder is a Matcher / Builder for vector types of an open number
+/// size.
+class OpenSizeVecBuilder : public Builder {
+ public:
+  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>()) {
+      if (!MatchOpenNumber(state, size_, vec->size())) {
+        return false;
+      }
+      return element_builder_->Match(state, vec->type());
+    }
+    return false;
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* el = element_builder_->Build(state);
+    auto n = state.open_numbers.at(size_);
+    return state.ty_mgr.Get<type::Vector>(el, n);
+  }
+
+  std::string str() const override {
+    return "vec" + std::string(tint::str(size_)) + "<" +
+           element_builder_->str() + ">";
+  }
+
+ protected:
+  OpenNumber const size_;
+  Builder* const element_builder_;
+};
+
+/// VecBuilder is a Matcher / Builder for vector types of a fixed size.
+class VecBuilder : public Builder {
+ public:
+  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>()) {
+      if (vec->size() == size_) {
+        return element_builder_->Match(state, vec->type());
+      }
+    }
+    return false;
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* el = element_builder_->Build(state);
+    return state.ty_mgr.Get<type::Vector>(el, size_);
+  }
+
+  std::string str() const override {
+    return "vec" + std::to_string(size_) + "<" + element_builder_->str() + ">";
+  }
+
+ protected:
+  const uint32_t size_;
+  Builder* element_builder_;
+};
+
+/// OpenSizeVecBuilder is a Matcher / Builder for matrix types of an open number
+/// column and row size.
+class OpenSizeMatBuilder : public Builder {
+ public:
+  OpenSizeMatBuilder(OpenNumber columns,
+                     OpenNumber rows,
+                     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>()) {
+      if (!MatchOpenNumber(state, columns_, mat->columns())) {
+        return false;
+      }
+      if (!MatchOpenNumber(state, rows_, mat->rows())) {
+        return false;
+      }
+      return element_builder_->Match(state, mat->type());
+    }
+    return false;
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* el = element_builder_->Build(state);
+    auto columns = state.open_numbers.at(columns_);
+    auto rows = state.open_numbers.at(rows_);
+    return state.ty_mgr.Get<type::Matrix>(el, rows, columns);
+  }
+
+  std::string str() const override {
+    return "max" + std::string(tint::str(columns_)) + "x" +
+           std::string(tint::str(rows_)) + "<" + element_builder_->str() + ">";
+  }
+
+ protected:
+  OpenNumber const columns_;
+  OpenNumber const rows_;
+  Builder* const element_builder_;
+};
+
+/// PtrBuilder is a Matcher / Builder for pointer types.
+class PtrBuilder : public Builder {
+ public:
+  explicit PtrBuilder(Builder* element_builder)
+      : element_builder_(element_builder) {}
+
+  bool Match(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);
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* el = element_builder_->Build(state);
+    return state.ty_mgr.Get<type::Pointer>(el, ast::StorageClass::kNone);
+  }
+
+  std::string str() const override {
+    return "ptr<" + element_builder_->str() + ">";
+  }
+
+ private:
+  Builder* const element_builder_;
+};
+
+/// ArrayBuilder is a Matcher / Builder for runtime sized array types.
+class ArrayBuilder : public Builder {
+ public:
+  explicit ArrayBuilder(Builder* element_builder)
+      : element_builder_(element_builder) {}
+
+  bool Match(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());
+      }
+    }
+    return false;
+  }
+
+  type::Type* Build(BuildState& state) const override {
+    auto* el = element_builder_->Build(state);
+    return state.ty_mgr.Get<type::Array>(el, 0, ast::ArrayDecorationList{});
+  }
+
+  std::string str() const override {
+    return "array<" + element_builder_->str() + ">";
+  }
+
+ private:
+  Builder* const element_builder_;
+};
+
+/// Impl is the private implementation of the IntrinsicTable interface.
+class Impl : public IntrinsicTable {
+ public:
+  Impl();
+
+  IntrinsicTable::Result Lookup(
+      ProgramBuilder& builder,
+      semantic::IntrinsicType type,
+      const std::vector<type::Type*>& args) const override;
+
+  /// A single overload definition.
+  struct Overload {
+    /// @returns a human readable string representation of the overload
+    std::string str() const;
+
+    /// Attempts to match this overload given the IntrinsicType and argument
+    /// types. If a match is made, the build intrinsic is returned, otherwise
+    /// `match_score` is assigned a score of how closely the overload matched
+    /// (positive representing a greater match), and nullptr is returned.
+    semantic::Intrinsic* Match(ProgramBuilder& builder,
+                               semantic::IntrinsicType type,
+                               const std::vector<type::Type*>& arg_types,
+                               int& match_score) const;
+
+    semantic::IntrinsicType type;
+    Builder* return_type;
+    std::vector<Builder*> parameters;
+    std::unordered_map<OpenType, Matcher*> open_type_matchers;
+  };
+
+ private:
+  /// Allocator for the built Matcher / Builders
+  BlockAllocator<Matcher> matcher_allocator_;
+
+  /// Commonly used Matcher / Builders
+  struct {
+    BoolBuilder bool_;
+    F32Builder f32;
+    I32Builder i32;
+    IU32Matcher iu32;
+    FIU32Matcher fiu32;
+    ScalarMatcher scalar;
+    U32Builder u32;
+    OpenTypeBuilder T{OpenType::T};
+  } matchers_;
+
+  // TODO(bclayton): Sort by type, or array these by IntrinsicType
+  std::vector<Overload> overloads_;
+
+  /// @returns a Matcher / Builder that matches a pointer with the given element
+  /// type
+  PtrBuilder* ptr(Builder* element_builder) {
+    return matcher_allocator_.Create<PtrBuilder>(element_builder);
+  }
+
+  /// @returns a Matcher / Builder that matches a vector of size OpenNumber::N
+  /// with the given element type
+  OpenSizeVecBuilder* vecN(Builder* element_builder) {
+    return matcher_allocator_.Create<OpenSizeVecBuilder>(OpenNumber::N,
+                                                         element_builder);
+  }
+
+  /// @returns a Matcher / Builder that matches a vector of the given size and
+  /// element type
+  VecBuilder* vec(uint32_t size, Builder* element_builder) {
+    return matcher_allocator_.Create<VecBuilder>(size, element_builder);
+  }
+
+  /// @returns a Matcher / Builder that matches a runtime sized array with the
+  /// given element type
+  ArrayBuilder* array(Builder* element_builder) {
+    return matcher_allocator_.Create<ArrayBuilder>(element_builder);
+  }
+
+  /// @returns a Matcher / Builder that matches a matrix with the given size and
+  /// element type
+  OpenSizeMatBuilder* mat(OpenNumber columns,
+                          OpenNumber rows,
+                          Builder* element_builder) {
+    return matcher_allocator_.Create<OpenSizeMatBuilder>(columns, rows,
+                                                         element_builder);
+  }
+
+  /// @returns a Matcher / Builder that matches a square matrix with the column
+  /// / row count of OpenNumber::N
+  template <typename T>
+  auto matNxN(T&& in) {
+    return mat(OpenNumber::N, OpenNumber::N, std::forward<T>(in));
+  }
+
+  /// 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.
+  void Register(semantic::IntrinsicType type,
+                Builder* return_type,
+                std::vector<Builder*> parameters) {
+    Overload overload{type, return_type, std::move(parameters), {}};
+    overloads_.emplace_back(overload);
+  }
+
+  /// Registers an overload with the given intrinsic type, return type Matcher /
+  /// Builder, and parameter Matcher / Builders.
+  /// A single OpenType is contained with the given Matcher in
+  /// open_type_matcher.
+  void Register(semantic::IntrinsicType type,
+                Builder* return_type,
+                std::vector<Builder*> parameters,
+                std::pair<OpenType, Matcher*> open_type_matcher) {
+    Overload overload{
+        type, return_type, std::move(parameters), {open_type_matcher}};
+    overloads_.emplace_back(overload);
+  }
+};
+
+Impl::Impl() {
+  using I = semantic::IntrinsicType;
+
+  auto* bool_ = &matchers_.bool_;      // bool
+  auto* f32 = &matchers_.f32;          // f32
+  auto* u32 = &matchers_.u32;          // u32
+  auto* iu32 = &matchers_.iu32;        // i32 or u32
+  auto* fiu32 = &matchers_.fiu32;      // f32, i32 or u32
+  auto* scalar = &matchers_.scalar;    // f32, i32, u32 or bool
+  auto* T = &matchers_.T;              // Any T type
+  auto* array_T = array(T);            // array<T>
+  auto* vec2_f32 = vec(2, f32);        // vec2<f32>
+  auto* vec3_f32 = vec(3, f32);        // vec3<f32>
+  auto* vec4_f32 = vec(4, f32);        // vec4<f32>
+  auto* vecN_f32 = vecN(f32);          // vecN<f32>
+  auto* vecN_T = vecN(T);              // vecN<T>
+  auto* vecN_bool = vecN(bool_);       // vecN<bool>
+  auto* matNxN_f32 = matNxN(f32);      // matNxN<f32>
+  auto* ptr_T = ptr(T);                // ptr<T>
+  auto* ptr_f32 = ptr(f32);            // ptr<f32>
+  auto* ptr_vecN_T = ptr(vecN_T);      // ptr<vecN<T>>
+  auto* ptr_vecN_f32 = ptr(vecN_f32);  // ptr<vecN<f32>>
+
+  // Intrinsic overloads are registered with a call to the Register().
+  //
+  // The best way to explain Register() and the lookup process is by example.
+  //
+  // Let's begin with a simple overload declaration:
+  //
+  //   Register(I::kIsInf, bool_, {f32});
+  //
+  //   I     - is an alias to semantic::IntrinsicType.
+  //           I::kIsInf is shorthand for semantic::IntrinsicType::kIsInf.
+  //   bool_ - is a pointer to a pre-constructed BoolBuilder which matches and
+  //           builds type::Bool types.
+  //   {f32} - is the list of parameter Builders for the overload.
+  //           Builders are a type of Matcher that can also build the the type.
+  //           All Builders are Matchers, not all Matchers are Builders.
+  //   f32     is a pointer to a pre-constructed F32Builder which matches and
+  //           builds type::F32 types.
+  //
+  // This call registers the overload for the `isInf(f32) -> bool` intrinsic.
+  //
+  // Let's now see the process of Overload::Match() when passed a single f32
+  // argument:
+  //
+  //   (1) Overload::Match() begins by attempting to match the argument types
+  //       from left to right.
+  //       F32Builder::Match() is called with the type::F32 argument type.
+  //       F32Builder (only) matches the type::F32 type, so F32Builder::Match()
+  //       returns true.
+  //   (2) All the parameters have had their Matcher::Match() methods return
+  //       true, there are no open-types (more about these later), so the
+  //       overload has matched.
+  //   (3) The semantic::Intrinsic now needs to be built, so we begin by
+  //       building the overload's parameter types (these may not exactly match
+  //       the argument types). Build() is called for each parameter Builder,
+  //       returning the parameter type.
+  //   (4) Finally, Builder::Build() is called for the return_type, and the
+  //       semantic::Intrinsic is constructed and returned.
+  //       Job done.
+  //
+  // Overload resolution also supports basic pattern matching through the use of
+  // open-types and open-numbers.
+  //
+  // OpenTypeBuilder is a Matcher that matches a single open-type.
+  //
+  // An 'open-type' can be thought as a template type that is determined by the
+  // arguments to the intrinsic.
+  //
+  // At the beginning of Overload::Match(), all open-types are undefined.
+  // Open-types are closed (pinned to a fixed type) on the first attempt to
+  // match against that open-type (e.g. via OpenTypeBuilder::Match()).
+  // Once open-types are closed, they remain that type, and
+  // OpenTypeBuilder::Match() will only ever return true if the queried type
+  // matches the closed type.
+  //
+  // To better understand, let's consider the following hypothetical overload
+  // declaration:
+  //
+  //    Register(I::kFoo, T, {T, T}, {OpenType::T, scalar});
+  //
+  //    T                  - is the matcher for the open-type OpenType::T.
+  //    scalar             - is a pointer to a pre-constructed ScalarMatcher
+  //                         which matches scalar types (f32, i32, u32, bool).
+  // {OpenType::T, scalar} - is a constraint on the open-type OpenType::T that
+  //                         it needs to resolve to a scalar.
+  //
+  // This call to Register() declares the foo intrinsic which accepts the
+  // identical scalar type for both arguments, and returns that scalar type.
+  //
+  // The process for resolving this overload is as follows:
+  //
+  //   (1) Overload::Match() begins by attempting to match the argument types
+  //       from left to right.
+  //       OpenTypeBuilder::Match() is called for the first parameter, being
+  //       passed the type of the first argument.
+  //       The OpenType::T has not been closed yet, so the OpenType::T is closed
+  //       as the type of the first argument.
+  //       There's no verification that the T type is a scalar at this stage.
+  //   (2) OpenTypeBuilder::Match() is called again for the second parameter
+  //       with the type of the second argument.
+  //       As the OpenType::T is now closed, the argument type is compared
+  //       against the value of the closed-type of OpenType::T.
+  //       OpenTypeBuilder::Match() returns true if these type match, otherwise
+  //       false and the overload match fails.
+  //   (3) If all the parameters have had their Matcher::Match() methods return
+  //       true, then the open-type constraints need to be checked next.
+  //       The Matcher::Match() is called for each closed type. If any return
+  //       false then the overload match fails.
+  //   (4) Overload::Match() now needs to build and return the output
+  //       semantic::Intrinsic holding the matched overload signature.
+  //   (5) The parameter types are built by calling OpenTypeBuilder::Build().
+  //       This simply returns the closed type.
+  //   (6) OpenTypeBuilder::Build() is called again for the return_type, and the
+  //       semantic::Intrinsic is constructed and returned.
+  //       Job done.
+  //
+  // Open-numbers are very similar to open-types, except they match against
+  // integers instead of types. The rules for open-numbers are almost identical
+  // to open-types, except open-numbers do not support constraints.
+  //
+  // vecN(f32) is an example of a Matcher that uses open-numbers.
+  // vecN() constructs a OpenSizeVecBuilder that will match a vector of size
+  // OpenNumber::N and of element type f32. As vecN() always uses the
+  // OpenNumber::N, using vecN() multiple times in the same overload signature
+  // will ensure that the vector size is identical for all vector types.
+  //
+  // Some Matcher implementations accept other Matchers for matching sub-types.
+  // Consider:
+  //
+  //   Register(I::kClamp, vecN(T), {vecN(T), vecN(T), vecN(T)},
+  //           {OpenType::T, fiu32});
+  //
+  // vecN(T) is a OpenSizeVecBuilder that matches a vector of size OpenNumber::N
+  // and of element type OpenType::T, where T must be either a f32, i32, or u32.
+
+  // clang-format off
+
+  //       name               return type  parameter types                    open type constraints    // NOLINT
+  Register(I::kAbs,           T,           {T},                               {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kAbs,           vecN_T,      {vecN_T},                          {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kAcos,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kAcos,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kAll,           bool_,       {vecN_bool}                                              ); // NOLINT
+  Register(I::kAny,           bool_,       {vecN_bool}                                              ); // NOLINT
+  Register(I::kArrayLength,   u32,         {array_T}                                                ); // NOLINT
+  Register(I::kAsin,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kAsin,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kAtan,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kAtan,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kAtan2,         f32,         {f32, f32}                                               ); // NOLINT
+  Register(I::kAtan2,         vecN_f32,    {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kCeil,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kCeil,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kClamp,         T,           {T, T, T},                         {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kClamp,         vecN_T,      {vecN_T, vecN_T, vecN_T},          {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kCos,           f32,         {f32}                                                    ); // NOLINT
+  Register(I::kCos,           vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kCosh,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kCosh,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kCountOneBits,  T,           {T},                               {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kCountOneBits,  vecN_T,      {vecN_T},                          {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kCross,         vec3_f32,    {vec3_f32, vec3_f32}                                     ); // NOLINT
+  Register(I::kDeterminant,   f32,         {matNxN_f32}                                             ); // NOLINT
+  Register(I::kDistance,      f32,         {f32, f32}                                               ); // NOLINT
+  Register(I::kDistance,      f32,         {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kDot,           f32,         {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kDpdx,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdx,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kDpdxCoarse,    f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdxCoarse,    vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kDpdxFine,      f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdxFine,      vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kDpdy,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdy,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kDpdyCoarse,    f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdyCoarse,    vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kDpdyFine,      f32,         {f32}                                                    ); // NOLINT
+  Register(I::kDpdyFine,      vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kExp,           f32,         {f32}                                                    ); // NOLINT
+  Register(I::kExp,           vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kExp2,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kExp2,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kFaceForward,   f32,         {f32, f32, f32}                                          ); // NOLINT
+  Register(I::kFaceForward,   vecN_f32,    {vecN_f32, vecN_f32, vecN_f32}                           ); // NOLINT
+  Register(I::kFloor,         f32,         {f32}                                                    ); // NOLINT
+  Register(I::kFloor,         vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kFma,           f32,         {f32, f32, f32}                                          ); // NOLINT
+  Register(I::kFma,           vecN_f32,    {vecN_f32, vecN_f32, vecN_f32}                           ); // NOLINT
+  Register(I::kFract,         f32,         {f32}                                                    ); // NOLINT
+  Register(I::kFract,         vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kFrexp,         f32,         {f32, ptr_T},                      {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kFrexp,         vecN_f32,    {vecN_f32, ptr_vecN_T},            {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kFwidth,        f32,         {f32}                                                    ); // NOLINT
+  Register(I::kFwidth,        vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kFwidthCoarse,  f32,         {f32}                                                    ); // NOLINT
+  Register(I::kFwidthCoarse,  vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kFwidthFine,    f32,         {f32}                                                    ); // NOLINT
+  Register(I::kFwidthFine,    vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kInverseSqrt,   f32,         {f32}                                                    ); // NOLINT
+  Register(I::kInverseSqrt,   vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kIsFinite,      bool_,       {f32}                                                    ); // NOLINT
+  Register(I::kIsFinite,      vecN_bool,   {vecN_f32}                                               ); // NOLINT
+  Register(I::kIsInf,         bool_,       {f32}                                                    ); // NOLINT
+  Register(I::kIsInf,         vecN_bool,   {vecN_f32}                                               ); // NOLINT
+  Register(I::kIsNan,         bool_,       {f32}                                                    ); // NOLINT
+  Register(I::kIsNan,         vecN_bool,   {vecN_f32}                                               ); // NOLINT
+  Register(I::kIsNormal,      bool_,       {f32}                                                    ); // NOLINT
+  Register(I::kIsNormal,      vecN_bool,   {vecN_f32}                                               ); // NOLINT
+  Register(I::kLdexp,         f32,         {f32, T},                          {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kLdexp,         vecN_f32,    {vecN_f32, vecN_T},                {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kLength,        f32,         {f32}                                                    ); // NOLINT
+  Register(I::kLength,        f32,         {vecN_f32}                                               ); // NOLINT
+  Register(I::kLog,           f32,         {f32}                                                    ); // NOLINT
+  Register(I::kLog,           vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kLog2,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kLog2,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kMax,           T,           {T, T},                            {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kMax,           vecN_T,      {vecN_T, vecN_T},                  {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kMin,           T,           {T, T},                            {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kMin,           vecN_T,      {vecN_T, vecN_T},                  {OpenType::T, fiu32}  ); // NOLINT
+  Register(I::kMix,           f32,         {f32, f32, f32}                                          ); // NOLINT
+  Register(I::kMix,           vecN_f32,    {vecN_f32, vecN_f32, vecN_f32}                           ); // NOLINT
+  Register(I::kModf,          f32,         {f32, ptr_f32}                                           ); // NOLINT
+  Register(I::kModf,          vecN_f32,    {vecN_f32, ptr_vecN_f32}                                 ); // NOLINT
+  Register(I::kNormalize,     vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kPack2x16Float, u32,         {vec2_f32}                                               ); // NOLINT
+  Register(I::kPack2x16Snorm, u32,         {vec2_f32}                                               ); // NOLINT
+  Register(I::kPack2x16Unorm, u32,         {vec2_f32}                                               ); // NOLINT
+  Register(I::kPack4x8Snorm,  u32,         {vec4_f32}                                               ); // NOLINT
+  Register(I::kPack4x8Unorm,  u32,         {vec4_f32}                                               ); // NOLINT
+  Register(I::kPow,           f32,         {f32, f32}                                               ); // NOLINT
+  Register(I::kPow,           vecN_f32,    {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kReflect,       f32,         {f32, f32}                                               ); // NOLINT
+  Register(I::kReflect,       vecN_f32,    {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kReverseBits,   T,           {T},                               {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kReverseBits,   vecN_T,      {vecN_T},                          {OpenType::T, iu32}   ); // NOLINT
+  Register(I::kRound,         f32,         {f32}                                                    ); // NOLINT
+  Register(I::kRound,         vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kSelect,        T,           {T, T, bool_},                     {OpenType::T, scalar} ); // NOLINT
+  Register(I::kSelect,        vecN_T,      {vecN_T, vecN_T, vecN_bool},       {OpenType::T, scalar} ); // NOLINT
+  Register(I::kSign,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kSign,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kSin,           f32,         {f32}                                                    ); // NOLINT
+  Register(I::kSin,           vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kSinh,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kSinh,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kSmoothStep,    f32,         {f32, f32, f32}                                          ); // NOLINT
+  Register(I::kSmoothStep,    vecN_f32,    {vecN_f32, vecN_f32, vecN_f32}                           ); // NOLINT
+  Register(I::kSqrt,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kSqrt,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kStep,          f32,         {f32, f32}                                               ); // NOLINT
+  Register(I::kStep,          vecN_f32,    {vecN_f32, vecN_f32}                                     ); // NOLINT
+  Register(I::kTan,           f32,         {f32}                                                    ); // NOLINT
+  Register(I::kTan,           vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kTanh,          f32,         {f32}                                                    ); // NOLINT
+  Register(I::kTanh,          vecN_f32,    {vecN_f32}                                               ); // NOLINT
+  Register(I::kTrunc,         f32,         {f32}                                                    ); // NOLINT
+  Register(I::kTrunc,         vecN_f32,    {vecN_f32}                                               ); // NOLINT
+
+  // clang-format on
+}
+
+std::string Impl::Overload::str() const {
+  std::stringstream ss;
+  ss << type << "(";
+  {
+    bool first = true;
+    for (auto* param : parameters) {
+      if (!first) {
+        ss << ", ";
+      }
+      first = false;
+      ss << param->str();
+    }
+  }
+  ss << ") -> ";
+  ss << return_type->str();
+
+  if (!open_type_matchers.empty()) {
+    ss << "  where: ";
+
+    for (uint32_t i = 0; i < static_cast<uint32_t>(OpenType::Count); i++) {
+      auto open_type = static_cast<OpenType>(i);
+      auto it = open_type_matchers.find(open_type);
+      if (it != open_type_matchers.end()) {
+        if (i > 0) {
+          ss << ", ";
+        }
+        ss << tint::str(open_type) << " is " << it->second->str();
+      }
+    }
+  }
+  return ss.str();
+}
+/// TODO(bclayton): This really does not belong here. It would be nice if
+/// type::Type::type_name() returned these strings.
+/// @returns a human readable string for the type `ty`.
+std::string TypeName(type::Type* ty) {
+  ty = ty->UnwrapAll();
+  if (ty->Is<type::F32>()) {
+    return "f32";
+  }
+  if (ty->Is<type::U32>()) {
+    return "u32";
+  }
+  if (ty->Is<type::I32>()) {
+    return "i32";
+  }
+  if (ty->Is<type::Bool>()) {
+    return "bool";
+  }
+  if (ty->Is<type::Void>()) {
+    return "void";
+  }
+  if (auto* ptr = ty->As<type::Pointer>()) {
+    return "ptr<" + TypeName(ptr->type()) + ">";
+  }
+  if (auto* vec = ty->As<type::Vector>()) {
+    return "vec" + std::to_string(vec->size()) + "<" + TypeName(vec->type()) +
+           ">";
+  }
+  if (auto* mat = ty->As<type::Matrix>()) {
+    return "mat" + std::to_string(mat->columns()) + "x" +
+           std::to_string(mat->rows()) + "<" + TypeName(mat->type()) + ">";
+  }
+  return ty->type_name();
+}
+
+IntrinsicTable::Result Impl::Lookup(
+    ProgramBuilder& builder,
+    semantic::IntrinsicType type,
+    const std::vector<type::Type*>& args) const {
+  // Candidate holds information about a mismatched overload that could be what
+  // the user intended to call.
+  struct Candidate {
+    const Overload* overload;
+    int score;
+  };
+
+  // The list of failed matches that had promise.
+  std::vector<Candidate> candidates;
+
+  // TODO(bclayton) Sort overloads_, or place them into a map keyed by intrinsic
+  // type. This is horribly inefficient.
+  for (auto& overload : overloads_) {
+    int match_score = 0;
+    if (auto* intrinsic = overload.Match(builder, type, args, match_score)) {
+      return Result{intrinsic, ""};  // Match found
+    }
+    if (match_score > 0) {
+      candidates.emplace_back(Candidate{&overload, match_score});
+    }
+  }
+
+  // Sort the candidates with the most promising first
+  std::stable_sort(
+      candidates.begin(), candidates.end(),
+      [](const Candidate& a, const Candidate& b) { return a.score > b.score; });
+
+  // Generate an error message
+  std::stringstream ss;
+  ss << "no matching call to " << semantic::str(type) << "(";
+  {
+    bool first = true;
+    for (auto* arg : args) {
+      if (!first) {
+        ss << ", ";
+      }
+      first = false;
+      ss << TypeName(arg);
+    }
+  }
+  ss << ")" << std::endl;
+
+  if (!candidates.empty()) {
+    ss << std::endl;
+    ss << candidates.size() << " candidate function"
+       << (candidates.size() > 1 ? "s:" : ":") << std::endl;
+    for (auto& candidate : candidates) {
+      ss << "  " << candidate.overload->str() << std::endl;
+    }
+  }
+
+  return Result{nullptr, ss.str()};
+}
+
+semantic::Intrinsic* Impl::Overload::Match(ProgramBuilder& builder,
+                                           semantic::IntrinsicType intrinsic,
+                                           const std::vector<type::Type*>& args,
+                                           int& match_score) const {
+  if (type != intrinsic) {
+    match_score = std::numeric_limits<int>::min();
+    return nullptr;  // Incorrect function
+  }
+
+  // Penalize argument <-> parameter count mismatches
+  match_score = 1000;
+  match_score -= std::max(parameters.size(), args.size()) -
+                 std::min(parameters.size(), args.size());
+
+  bool matched = parameters.size() == args.size();
+
+  Matcher::MatchState matcher_state;
+
+  // Check that each of the parameters match.
+  // This stage also populates the open_types and open_numbers.
+  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]->Match(matcher_state, arg_ty)) {
+      matched = false;
+      continue;
+    }
+    // Weight correct parameter matches more than exact number of arguments
+    match_score += 2;
+  }
+  if (!matched) {
+    return nullptr;
+  }
+
+  // If any of the open-types are constrained, check that they match.
+  for (auto matcher_it : open_type_matchers) {
+    OpenType open_type = matcher_it.first;
+    auto* matcher = matcher_it.second;
+    auto type_it = matcher_state.open_types.find(open_type);
+    if (type_it == matcher_state.open_types.end()) {
+      // We have an overload that claims to have matched, but didn't actually
+      // resolve the open type. This is a bug that needs fixing.
+      assert(false);
+      return nullptr;
+    }
+    auto* resolved_type = type_it->second;
+    if (resolved_type == nullptr) {
+      // We have an overload that claims to have matched, but has a nullptr
+      // resolved open type. This is a bug that needs fixing.
+      assert(false);
+      return nullptr;
+    }
+    if (!matcher->Match(matcher_state, resolved_type)) {
+      matched = false;
+      continue;
+    }
+    match_score++;
+  }
+  if (!matched) {
+    return nullptr;
+  }
+
+  // Overload matched!
+
+  // Build the return type
+  Builder::BuildState builder_state{builder.Types(), matcher_state.open_types,
+                                    matcher_state.open_numbers};
+  auto* ret = return_type->Build(builder_state);
+  assert(ret);  // Build() must return a type
+
+  // Build the semantic parameters
+  semantic::Parameters params;
+  params.reserve(parameters.size());
+  for (size_t i = 0; i < args.size(); i++) {
+    auto* ty = parameters[i]->Build(builder_state);
+    params.emplace_back(semantic::Parameter{ty});
+  }
+
+  return builder.create<semantic::Intrinsic>(intrinsic, ret, params);
+}
+
+}  // namespace
+
+std::unique_ptr<IntrinsicTable> IntrinsicTable::Create() {
+  return std::make_unique<Impl>();
+}
+
+IntrinsicTable::~IntrinsicTable() = default;
+
+}  // namespace tint
diff --git a/src/intrinsic_table.h b/src/intrinsic_table.h
new file mode 100644
index 0000000..2f58872
--- /dev/null
+++ b/src/intrinsic_table.h
@@ -0,0 +1,58 @@
+// 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.
+
+#ifndef SRC_INTRINSIC_TABLE_H_
+#define SRC_INTRINSIC_TABLE_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "src/semantic/intrinsic.h"
+
+namespace tint {
+
+// Forward declarations
+class ProgramBuilder;
+
+/// IntrinsicTable is a lookup table of all the WGSL intrinsic functions
+class IntrinsicTable {
+ public:
+  /// @return a pointer to a newly created IntrinsicTable
+  static std::unique_ptr<IntrinsicTable> Create();
+
+  /// Destructor
+  virtual ~IntrinsicTable();
+
+  /// Result is returned by Lookup
+  struct Result {
+    /// The intrinsic, if the lookup succeeded, otherwise nullptr
+    semantic::Intrinsic* intrinsic;
+    /// The error message, if the lookup failed, otherwise empty
+    std::string error;
+  };
+
+  /// Lookup looks for the intrinsic overload with the given signature.
+  /// @param builder the program builder
+  /// @param type the intrinsic type
+  /// @param args the argument types passed to the intrinsic function
+  /// @return the semantic intrinsic if found, otherwise nullptr
+  virtual Result Lookup(ProgramBuilder& builder,
+                        semantic::IntrinsicType type,
+                        const std::vector<type::Type*>& args) const = 0;
+};
+
+}  // namespace tint
+
+#endif  // SRC_INTRINSIC_TABLE_H_
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
index 7ea3c09..0353b66 100644
--- a/src/type_determiner.cc
+++ b/src/type_determiner.cc
@@ -71,7 +71,8 @@
 
 }  // namespace
 
-TypeDeterminer::TypeDeterminer(ProgramBuilder* builder) : builder_(builder) {}
+TypeDeterminer::TypeDeterminer(ProgramBuilder* builder)
+    : builder_(builder), intrinsic_table_(IntrinsicTable::Create()) {}
 
 TypeDeterminer::~TypeDeterminer() = default;
 
@@ -456,97 +457,9 @@
   return true;
 }
 
-namespace {
-
-enum class IntrinsicDataType {
-  kDependent,
-  kSignedInteger,
-  kUnsignedInteger,
-  kFloat,
-  kBool,
-};
-
-struct IntrinsicData {
-  IntrinsicType intrinsic;
-  IntrinsicDataType result_type;
-  uint8_t result_vector_width;
-  uint8_t param_for_result_type;
-};
-
-// Note, this isn't all the intrinsics. Some are handled specially before
-// we get to the generic code. See the DetermineIntrinsic code below.
-constexpr const IntrinsicData kIntrinsicData[] = {
-    {IntrinsicType::kAbs, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kAcos, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kAll, IntrinsicDataType::kBool, 1, 0},
-    {IntrinsicType::kAny, IntrinsicDataType::kBool, 1, 0},
-    {IntrinsicType::kArrayLength, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kAsin, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kAtan, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kAtan2, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kCeil, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kClamp, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kCos, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kCosh, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kCountOneBits, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kCross, IntrinsicDataType::kFloat, 3, 0},
-    {IntrinsicType::kDeterminant, IntrinsicDataType::kFloat, 1, 0},
-    {IntrinsicType::kDistance, IntrinsicDataType::kFloat, 1, 0},
-    {IntrinsicType::kDpdx, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDpdxCoarse, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDpdxFine, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDpdy, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDpdyCoarse, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDpdyFine, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kDot, IntrinsicDataType::kFloat, 1, 0},
-    {IntrinsicType::kExp, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kExp2, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFaceForward, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFloor, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFwidth, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFwidthCoarse, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFwidthFine, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFma, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFract, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kFrexp, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kInverseSqrt, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kLdexp, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kLength, IntrinsicDataType::kFloat, 1, 0},
-    {IntrinsicType::kLog, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kLog2, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kMax, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kMin, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kMix, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kModf, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kNormalize, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kPack4x8Snorm, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kPack4x8Unorm, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kPack2x16Snorm, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kPack2x16Unorm, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kPack2x16Float, IntrinsicDataType::kUnsignedInteger, 1, 0},
-    {IntrinsicType::kPow, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kReflect, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kReverseBits, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kRound, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSelect, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSign, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSin, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSinh, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSmoothStep, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kSqrt, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kStep, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kTan, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kTanh, IntrinsicDataType::kDependent, 0, 0},
-    {IntrinsicType::kTrunc, IntrinsicDataType::kDependent, 0, 0},
-};
-
-constexpr const uint32_t kIntrinsicDataCount =
-    sizeof(kIntrinsicData) / sizeof(IntrinsicData);
-
-}  // namespace
-
-bool TypeDeterminer::DetermineIntrinsicCall(ast::CallExpression* call,
-                                            IntrinsicType intrinsic_type) {
+bool TypeDeterminer::DetermineIntrinsicCall(
+    ast::CallExpression* call,
+    semantic::IntrinsicType intrinsic_type) {
   using Parameter = semantic::Parameter;
   using Parameters = semantic::Parameters;
   using Usage = Parameter::Usage;
@@ -557,30 +470,9 @@
     arg_tys.emplace_back(TypeOf(expr));
   }
 
-  auto create_sem = [&](type::Type* return_type) {
-    semantic::Parameters params;  // TODO(bclayton): Populate this
-    auto* intrinsic = builder_->create<semantic::Intrinsic>(
-        intrinsic_type, return_type, params);
-    builder_->Sem().Add(call, builder_->create<semantic::Call>(intrinsic));
-  };
-
   std::string name = semantic::str(intrinsic_type);
-  if (semantic::IsFloatClassificationIntrinsic(intrinsic_type)) {
-    if (call->params().size() != 1) {
-      set_error(call->source(), "incorrect number of parameters for " + name);
-      return false;
-    }
 
-    auto* bool_type = builder_->create<type::Bool>();
-
-    auto* param_type = TypeOf(call->params()[0])->UnwrapPtrIfNeeded();
-    if (auto* vec = param_type->As<type::Vector>()) {
-      create_sem(builder_->create<type::Vector>(bool_type, vec->size()));
-    } else {
-      create_sem(bool_type);
-    }
-    return true;
-  }
+  // TODO(bclayton): Add these to the IntrinsicTable
   if (semantic::IsTextureIntrinsic(intrinsic_type)) {
     Parameters params;
 
@@ -768,54 +660,45 @@
     return true;
   }
 
-  const IntrinsicData* data = nullptr;
-  for (uint32_t i = 0; i < kIntrinsicDataCount; ++i) {
-    if (intrinsic_type == kIntrinsicData[i].intrinsic) {
-      data = &kIntrinsicData[i];
-      break;
+  auto result = intrinsic_table_->Lookup(*builder_, intrinsic_type, arg_tys);
+  if (!result.intrinsic) {
+    // Intrinsic lookup failed.
+    set_error(call->source(), result.error);
+
+    // TODO(bclayton): https://crbug.com/tint/487
+    // The Validator expects intrinsic signature mismatches to still produce
+    // type information. The rules for what the Validator expects are rather
+    // bespoke. Try to match what the Validator expects. As the Validator's
+    // checks on intrinsics is now almost entirely covered by the
+    // IntrinsicTable, we should remove the Validator checks on intrinsic
+    // signatures and remove these hacks.
+    semantic::Parameters parameters;
+    parameters.reserve(arg_tys.size());
+    for (auto* arg : arg_tys) {
+      parameters.emplace_back(semantic::Parameter{arg});
     }
-  }
-  if (data == nullptr) {
-    error_ = "unable to find intrinsic " + name;
+    type::Type* ret_ty = nullptr;
+    switch (intrinsic_type) {
+      case IntrinsicType::kCross:
+        ret_ty = builder_->ty.vec3<ProgramBuilder::f32>();
+        break;
+      case IntrinsicType::kDeterminant:
+        ret_ty = builder_->create<type::F32>();
+        break;
+      case IntrinsicType::kArrayLength:
+        ret_ty = builder_->create<type::U32>();
+        break;
+      default:
+        ret_ty = arg_tys.empty() ? builder_->ty.void_() : arg_tys[0];
+        break;
+    }
+    auto* intrinsic = builder_->create<semantic::Intrinsic>(intrinsic_type,
+                                                            ret_ty, parameters);
+    builder_->Sem().Add(call, builder_->create<semantic::Call>(intrinsic));
     return false;
   }
 
-  if (data->result_type == IntrinsicDataType::kDependent) {
-    const auto param_idx = data->param_for_result_type;
-    if (call->params().size() <= param_idx) {
-      set_error(call->source(),
-                "missing parameter " + std::to_string(param_idx) +
-                    " required for type determination in builtin " + name);
-      return false;
-    }
-    create_sem(TypeOf(call->params()[param_idx])->UnwrapPtrIfNeeded());
-  } else {
-    // The result type is not dependent on the parameter types.
-    type::Type* type = nullptr;
-    switch (data->result_type) {
-      case IntrinsicDataType::kSignedInteger:
-        type = builder_->create<type::I32>();
-        break;
-      case IntrinsicDataType::kUnsignedInteger:
-        type = builder_->create<type::U32>();
-        break;
-      case IntrinsicDataType::kFloat:
-        type = builder_->create<type::F32>();
-        break;
-      case IntrinsicDataType::kBool:
-        type = builder_->create<type::Bool>();
-        break;
-      default:
-        error_ = "unhandled intrinsic data type for " + name;
-        return false;
-    }
-
-    if (data->result_vector_width > 1) {
-      type = builder_->create<type::Vector>(type, data->result_vector_width);
-    }
-    create_sem(type);
-  }
-
+  builder_->Sem().Add(call, builder_->create<semantic::Call>(result.intrinsic));
   return true;
 }
 
diff --git a/src/type_determiner.h b/src/type_determiner.h
index 9928b90..2a8b28e 100644
--- a/src/type_determiner.h
+++ b/src/type_determiner.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TYPE_DETERMINER_H_
 #define SRC_TYPE_DETERMINER_H_
 
+#include <memory>
 #include <string>
 #include <unordered_map>
 #include <unordered_set>
@@ -22,6 +23,7 @@
 
 #include "src/ast/module.h"
 #include "src/diagnostic/diagnostic.h"
+#include "src/intrinsic_table.h"
 #include "src/program_builder.h"
 #include "src/scope_stack.h"
 #include "src/semantic/intrinsic.h"
@@ -197,6 +199,7 @@
   void SetType(ast::Expression* expr, type::Type* type) const;
 
   ProgramBuilder* const builder_;
+  std::unique_ptr<IntrinsicTable> const intrinsic_table_;
   std::string error_;
   ScopeStack<VariableInfo*> variable_stack_;
   std::unordered_map<Symbol, FunctionInfo*> symbol_to_function_;
diff --git a/src/type_determiner_test.cc b/src/type_determiner_test.cc
index 4b43554..5ea317c 100644
--- a/src/type_determiner_test.cc
+++ b/src/type_determiner_test.cc
@@ -1256,9 +1256,11 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(name));
+  EXPECT_EQ(td()->error(), "no matching call to " + name +
+                               "()\n\n"
+                               "2 candidate functions:\n  " +
+                               name + "(f32) -> f32\n  " + name +
+                               "(vecN<f32>) -> vecN<f32>\n");
 }
 
 INSTANTIATE_TEST_SUITE_P(TypeDeterminerTest,
@@ -1332,7 +1334,11 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(td()->error(), "incorrect number of parameters for " + name);
+  EXPECT_EQ(td()->error(), "no matching call to " + name +
+                               "()\n\n"
+                               "2 candidate functions:\n  " +
+                               name + "(f32) -> bool\n  " + name +
+                               "(vecN<f32>) -> vecN<bool>\n");
 }
 
 TEST_P(Intrinsic_FloatMethod, TooManyParams) {
@@ -1345,7 +1351,11 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(td()->error(), "incorrect number of parameters for " + name);
+  EXPECT_EQ(td()->error(), "no matching call to " + name +
+                               "(f32, f32)\n\n"
+                               "2 candidate functions:\n  " +
+                               name + "(f32) -> bool\n  " + name +
+                               "(vecN<f32>) -> vecN<bool>\n");
 }
 INSTANTIATE_TEST_SUITE_P(
     TypeDeterminerTest,
@@ -1564,9 +1574,13 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(
-      td()->error(),
-      "missing parameter 0 required for type determination in builtin select");
+  EXPECT_EQ(td()->error(),
+            R"(no matching call to select()
+
+2 candidate functions:
+  select(T, T, bool) -> T  where: T is scalar
+  select(vecN<T>, vecN<T>, vecN<bool>) -> vecN<T>  where: T is scalar
+)");
 }
 
 using UnaryOpExpressionTest = TypeDeterminerTestWithParam<ast::UnaryOp>;
@@ -1781,9 +1795,12 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+  EXPECT_EQ(td()->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");
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -1829,8 +1846,9 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin "
-            "normalize");
+            "no matching call to normalize()\n\n"
+            "1 candidate function:\n  "
+            "normalize(vecN<f32>) -> vecN<f32>\n");
 }
 
 using ImportData_SingleParam_FloatOrInt_Test =
@@ -1930,8 +1948,13 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+            "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(TypeDeterminerTest,
@@ -1996,9 +2019,13 @@
 
   EXPECT_FALSE(td()->Determine());
 
-  EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+  EXPECT_EQ(td()->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(
     TypeDeterminerTest,
@@ -2041,10 +2068,23 @@
   EXPECT_EQ(TypeOf(call)->As<type::Vector>()->size(), 3u);
 }
 
-TEST_F(TypeDeterminerTest, ImportData_Cross_AutoType) {
+TEST_F(TypeDeterminerTest, ImportData_Cross_NoArgs) {
   auto* call = Call("cross");
   WrapInFunction(call);
 
+  EXPECT_FALSE(td()->Determine());
+
+  EXPECT_EQ(td()->error(), R"(no matching call to cross()
+
+1 candidate function:
+  cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+)");
+}
+
+TEST_F(TypeDeterminerTest, ImportData_Normalize) {
+  auto* call = Call("normalize", vec3<f32>(1.0f, 1.0f, 3.0f));
+  WrapInFunction(call);
+
   EXPECT_TRUE(td()->Determine()) << td()->error();
 
   ASSERT_NE(TypeOf(call), nullptr);
@@ -2052,6 +2092,19 @@
   EXPECT_EQ(TypeOf(call)->As<type::Vector>()->size(), 3u);
 }
 
+TEST_F(TypeDeterminerTest, ImportData_Normalize_NoArgs) {
+  auto* call = Call("normalize");
+  WrapInFunction(call);
+
+  EXPECT_FALSE(td()->Determine());
+
+  EXPECT_EQ(td()->error(), R"(no matching call to normalize()
+
+1 candidate function:
+  normalize(vecN<f32>) -> vecN<f32>
+)");
+}
+
 using ImportData_ThreeParamTest = TypeDeterminerTestWithParam<IntrinsicData>;
 TEST_P(ImportData_ThreeParamTest, Scalar) {
   auto param = GetParam();
@@ -2087,8 +2140,12 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+            "no matching call to " + std::string(param.name) +
+                "()\n\n"
+                "2 candidate functions:\n  " +
+                std::string(param.name) + "(f32, f32, f32) -> f32\n  " +
+                std::string(param.name) +
+                "(vecN<f32>, vecN<f32>, vecN<f32>) -> vecN<f32>\n");
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -2188,8 +2245,14 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+            "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 f32, i32 or u32\n  " +
+                std::string(param.name) +
+                "(vecN<T>, vecN<T>, vecN<T>) -> vecN<T>  where: T is f32, i32 "
+                "or u32\n");
 }
 
 INSTANTIATE_TEST_SUITE_P(TypeDeterminerTest,
@@ -2233,8 +2296,13 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+            "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(
@@ -2330,8 +2398,13 @@
   EXPECT_FALSE(td()->Determine());
 
   EXPECT_EQ(td()->error(),
-            "missing parameter 0 required for type determination in builtin " +
-                std::string(param.name));
+            "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(
@@ -2361,9 +2434,15 @@
   auto* call = Call(param.name);
   WrapInFunction(call);
 
-  EXPECT_TRUE(td()->Determine()) << td()->error();
+  EXPECT_FALSE(td()->Determine());
 
   EXPECT_TRUE(TypeOf(call)->Is<type::F32>());
+  EXPECT_EQ(td()->error(),
+            "no matching call to " + std::string(param.name) + R"(()
+
+1 candidate function:
+  determinant(maxNxN<f32>) -> f32
+)");
 }
 
 INSTANTIATE_TEST_SUITE_P(TypeDeterminerTest,