[tint] Finish decoupling of intrinsic table from WGSL

The intrinsic table is now reusable for other langauges.

This required moving the WGSL-specifics into the WGSL resolver.

Change-Id: I0e625fabc83b3e47b32bfc5cd1fd8cd67afbd28c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/145262
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index b1e6daa..18e6532 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -289,12 +289,13 @@
   sources = [
     "lang/core/constant/eval.cc",
     "lang/core/constant/eval.h",
-    "lang/core/intrinsic/core_table_data.cc",
-    "lang/core/intrinsic/core_table_data.h",
     "lang/core/intrinsic/ctor_conv.cc",
     "lang/core/intrinsic/ctor_conv.h",
     "lang/core/intrinsic/table.cc",
     "lang/core/intrinsic/table.h",
+    "lang/core/intrinsic_data.cc",
+    "lang/core/intrinsic_data.h",
+    "lang/core/intrinsic_type_matchers.h",
     "lang/wgsl/program/clone_context.cc",
     "lang/wgsl/program/program.cc",
     "lang/wgsl/program/program_builder.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 157a51e..b1e2402 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -516,14 +516,15 @@
   lang/wgsl/sem/while_statement.h
   lang/core/constant/eval.cc
   lang/core/constant/eval.h
-  lang/wgsl/resolver/dependency_graph.cc
-  lang/wgsl/resolver/dependency_graph.h
-  lang/core/intrinsic/core_table_data.cc
-  lang/core/intrinsic/core_table_data.h
   lang/core/intrinsic/ctor_conv.cc
   lang/core/intrinsic/ctor_conv.h
   lang/core/intrinsic/table.cc
   lang/core/intrinsic/table.h
+  lang/core/intrinsic_data.cc
+  lang/core/intrinsic_data.h
+  lang/core/intrinsic_type_matchers.h
+  lang/wgsl/resolver/dependency_graph.cc
+  lang/wgsl/resolver/dependency_graph.h
   lang/wgsl/resolver/resolve.cc
   lang/wgsl/resolver/resolve.h
   lang/wgsl/resolver/resolver.cc
diff --git a/src/tint/lang/core/intrinsic/core_type_matchers.h b/src/tint/lang/core/intrinsic/core_type_matchers.h
deleted file mode 100644
index b061443..0000000
--- a/src/tint/lang/core/intrinsic/core_type_matchers.h
+++ /dev/null
@@ -1,570 +0,0 @@
-// Copyright 2023 The Tint Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SRC_TINT_LANG_CORE_INTRINSIC_CORE_TYPE_MATCHERS_H_
-#define SRC_TINT_LANG_CORE_INTRINSIC_CORE_TYPE_MATCHERS_H_
-
-#include "src/tint/lang/core/evaluation_stage.h"
-#include "src/tint/lang/core/intrinsic/table_data.h"
-#include "src/tint/lang/core/type/abstract_float.h"
-#include "src/tint/lang/core/type/abstract_int.h"
-#include "src/tint/lang/core/type/abstract_numeric.h"
-#include "src/tint/lang/core/type/array.h"
-#include "src/tint/lang/core/type/atomic.h"
-#include "src/tint/lang/core/type/bool.h"
-#include "src/tint/lang/core/type/builtin_structs.h"
-#include "src/tint/lang/core/type/depth_multisampled_texture.h"
-#include "src/tint/lang/core/type/depth_texture.h"
-#include "src/tint/lang/core/type/external_texture.h"
-#include "src/tint/lang/core/type/f16.h"
-#include "src/tint/lang/core/type/f32.h"
-#include "src/tint/lang/core/type/i32.h"
-#include "src/tint/lang/core/type/manager.h"
-#include "src/tint/lang/core/type/matrix.h"
-#include "src/tint/lang/core/type/multisampled_texture.h"
-#include "src/tint/lang/core/type/pointer.h"
-#include "src/tint/lang/core/type/sampled_texture.h"
-#include "src/tint/lang/core/type/storage_texture.h"
-#include "src/tint/lang/core/type/texture_dimension.h"
-#include "src/tint/lang/core/type/u32.h"
-#include "src/tint/lang/core/type/vector.h"
-
-namespace tint::core::intrinsic {
-
-inline bool match_bool(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::Bool>();
-}
-
-inline const type::AbstractFloat* build_fa(TableData::MatchState& state) {
-    return state.types.AFloat();
-}
-
-inline bool match_fa(TableData::MatchState& state, const type::Type* ty) {
-    return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
-           ty->IsAnyOf<TableData::Any, type::AbstractNumeric>();
-}
-
-inline const type::AbstractInt* build_ia(TableData::MatchState& state) {
-    return state.types.AInt();
-}
-
-inline bool match_ia(TableData::MatchState& state, const type::Type* ty) {
-    return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
-           ty->IsAnyOf<TableData::Any, type::AbstractInt>();
-}
-
-inline const type::Bool* build_bool(TableData::MatchState& state) {
-    return state.types.bool_();
-}
-
-inline const type::F16* build_f16(TableData::MatchState& state) {
-    return state.types.f16();
-}
-
-inline bool match_f16(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::F16, type::AbstractNumeric>();
-}
-
-inline const type::F32* build_f32(TableData::MatchState& state) {
-    return state.types.f32();
-}
-
-inline bool match_f32(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::F32, type::AbstractNumeric>();
-}
-
-inline const type::I32* build_i32(TableData::MatchState& state) {
-    return state.types.i32();
-}
-
-inline bool match_i32(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::I32, type::AbstractInt>();
-}
-
-inline const type::U32* build_u32(TableData::MatchState& state) {
-    return state.types.u32();
-}
-
-inline bool match_u32(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::U32, type::AbstractInt>();
-}
-
-inline bool match_vec(TableData::MatchState&,
-                      const type::Type* ty,
-                      TableData::Number& N,
-                      const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        N = TableData::Number::any;
-        T = ty;
-        return true;
-    }
-
-    if (auto* v = ty->As<type::Vector>()) {
-        N = v->Width();
-        T = v->type();
-        return true;
-    }
-    return false;
-}
-
-template <uint32_t N>
-inline bool match_vec(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-
-    if (auto* v = ty->As<type::Vector>()) {
-        if (v->Width() == N) {
-            T = v->type();
-            return true;
-        }
-    }
-    return false;
-}
-
-inline const type::Vector* build_vec(TableData::MatchState& state,
-                                     TableData::Number N,
-                                     const type::Type* el) {
-    return state.types.vec(el, N.Value());
-}
-
-template <uint32_t N>
-inline const type::Vector* build_vec(TableData::MatchState& state, const type::Type* el) {
-    return state.types.vec(el, N);
-}
-
-constexpr auto match_vec2 = match_vec<2>;
-constexpr auto match_vec3 = match_vec<3>;
-constexpr auto match_vec4 = match_vec<4>;
-
-constexpr auto build_vec2 = build_vec<2>;
-constexpr auto build_vec3 = build_vec<3>;
-constexpr auto build_vec4 = build_vec<4>;
-
-inline bool match_packedVec3(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-
-    if (auto* v = ty->As<type::Vector>()) {
-        if (v->Packed()) {
-            T = v->type();
-            return true;
-        }
-    }
-    return false;
-}
-
-inline const type::Vector* build_packedVec3(TableData::MatchState& state, const type::Type* el) {
-    return state.types.Get<type::Vector>(el, 3u, /* packed */ true);
-}
-
-inline bool match_mat(TableData::MatchState&,
-                      const type::Type* ty,
-                      TableData::Number& M,
-                      TableData::Number& N,
-                      const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        M = TableData::Number::any;
-        N = TableData::Number::any;
-        T = ty;
-        return true;
-    }
-    if (auto* m = ty->As<type::Matrix>()) {
-        M = m->columns();
-        N = m->ColumnType()->Width();
-        T = m->type();
-        return true;
-    }
-    return false;
-}
-
-template <uint32_t C, uint32_t R>
-inline bool match_mat(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-    if (auto* m = ty->As<type::Matrix>()) {
-        if (m->columns() == C && m->rows() == R) {
-            T = m->type();
-            return true;
-        }
-    }
-    return false;
-}
-
-inline const type::Matrix* build_mat(TableData::MatchState& state,
-                                     TableData::Number C,
-                                     TableData::Number R,
-                                     const type::Type* T) {
-    auto* column_type = state.types.vec(T, R.Value());
-    return state.types.mat(column_type, C.Value());
-}
-
-template <uint32_t C, uint32_t R>
-inline const type::Matrix* build_mat(TableData::MatchState& state, const type::Type* T) {
-    auto* column_type = state.types.vec(T, R);
-    return state.types.mat(column_type, C);
-}
-
-constexpr auto build_mat2x2 = build_mat<2, 2>;
-constexpr auto build_mat2x3 = build_mat<2, 3>;
-constexpr auto build_mat2x4 = build_mat<2, 4>;
-constexpr auto build_mat3x2 = build_mat<3, 2>;
-constexpr auto build_mat3x3 = build_mat<3, 3>;
-constexpr auto build_mat3x4 = build_mat<3, 4>;
-constexpr auto build_mat4x2 = build_mat<4, 2>;
-constexpr auto build_mat4x3 = build_mat<4, 3>;
-constexpr auto build_mat4x4 = build_mat<4, 4>;
-
-constexpr auto match_mat2x2 = match_mat<2, 2>;
-constexpr auto match_mat2x3 = match_mat<2, 3>;
-constexpr auto match_mat2x4 = match_mat<2, 4>;
-constexpr auto match_mat3x2 = match_mat<3, 2>;
-constexpr auto match_mat3x3 = match_mat<3, 3>;
-constexpr auto match_mat3x4 = match_mat<3, 4>;
-constexpr auto match_mat4x2 = match_mat<4, 2>;
-constexpr auto match_mat4x3 = match_mat<4, 3>;
-constexpr auto match_mat4x4 = match_mat<4, 4>;
-
-inline bool match_array(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-
-    if (auto* a = ty->As<type::Array>()) {
-        if (a->Count()->Is<type::RuntimeArrayCount>()) {
-            T = a->ElemType();
-            return true;
-        }
-    }
-    return false;
-}
-
-inline const type::Array* build_array(TableData::MatchState& state, const type::Type* el) {
-    return state.types.Get<type::Array>(el,
-                                        /* count */ state.types.Get<type::RuntimeArrayCount>(),
-                                        /* align */ 0u,
-                                        /* size */ 0u,
-                                        /* stride */ 0u,
-                                        /* stride_implicit */ 0u);
-}
-
-inline bool match_ptr(TableData::MatchState&,
-                      const type::Type* ty,
-                      TableData::Number& S,
-                      const type::Type*& T,
-                      TableData::Number& A) {
-    if (ty->Is<TableData::Any>()) {
-        S = TableData::Number::any;
-        T = ty;
-        A = TableData::Number::any;
-        return true;
-    }
-
-    if (auto* p = ty->As<type::Pointer>()) {
-        S = TableData::Number(static_cast<uint32_t>(p->AddressSpace()));
-        T = p->StoreType();
-        A = TableData::Number(static_cast<uint32_t>(p->Access()));
-        return true;
-    }
-    return false;
-}
-
-inline const type::Pointer* build_ptr(TableData::MatchState& state,
-                                      TableData::Number S,
-                                      const type::Type* T,
-                                      TableData::Number& A) {
-    return state.types.ptr(static_cast<core::AddressSpace>(S.Value()), T,
-                           static_cast<core::Access>(A.Value()));
-}
-
-inline bool match_atomic(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-
-    if (auto* a = ty->As<type::Atomic>()) {
-        T = a->Type();
-        return true;
-    }
-    return false;
-}
-
-inline const type::Atomic* build_atomic(TableData::MatchState& state, const type::Type* T) {
-    return state.types.atomic(T);
-}
-
-inline bool match_sampler(TableData::MatchState&, const type::Type* ty) {
-    if (ty->Is<TableData::Any>()) {
-        return true;
-    }
-    return ty->Is([](const type::Sampler* s) { return s->kind() == type::SamplerKind::kSampler; });
-}
-
-inline const type::Sampler* build_sampler(TableData::MatchState& state) {
-    return state.types.sampler();
-}
-
-inline bool match_sampler_comparison(TableData::MatchState&, const type::Type* ty) {
-    if (ty->Is<TableData::Any>()) {
-        return true;
-    }
-    return ty->Is(
-        [](const type::Sampler* s) { return s->kind() == type::SamplerKind::kComparisonSampler; });
-}
-
-inline const type::Sampler* build_sampler_comparison(TableData::MatchState& state) {
-    return state.types.comparison_sampler();
-}
-
-inline bool match_texture(TableData::MatchState&,
-                          const type::Type* ty,
-                          type::TextureDimension dim,
-                          const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-    if (auto* v = ty->As<type::SampledTexture>()) {
-        if (v->dim() == dim) {
-            T = v->type();
-            return true;
-        }
-    }
-    return false;
-}
-
-#define JOIN(a, b) a##b
-
-#define DECLARE_SAMPLED_TEXTURE(suffix, dim)                                                       \
-    inline bool JOIN(match_texture_, suffix)(TableData::MatchState & state, const type::Type* ty,  \
-                                             const type::Type*& T) {                               \
-        return match_texture(state, ty, dim, T);                                                   \
-    }                                                                                              \
-    inline const type::SampledTexture* JOIN(build_texture_, suffix)(TableData::MatchState & state, \
-                                                                    const type::Type* T) {         \
-        return state.types.Get<type::SampledTexture>(dim, T);                                      \
-    }
-
-DECLARE_SAMPLED_TEXTURE(1d, type::TextureDimension::k1d)
-DECLARE_SAMPLED_TEXTURE(2d, type::TextureDimension::k2d)
-DECLARE_SAMPLED_TEXTURE(2d_array, type::TextureDimension::k2dArray)
-DECLARE_SAMPLED_TEXTURE(3d, type::TextureDimension::k3d)
-DECLARE_SAMPLED_TEXTURE(cube, type::TextureDimension::kCube)
-DECLARE_SAMPLED_TEXTURE(cube_array, type::TextureDimension::kCubeArray)
-#undef DECLARE_SAMPLED_TEXTURE
-
-inline bool match_texture_multisampled(TableData::MatchState&,
-                                       const type::Type* ty,
-                                       type::TextureDimension dim,
-                                       const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-    if (auto* v = ty->As<type::MultisampledTexture>()) {
-        if (v->dim() == dim) {
-            T = v->type();
-            return true;
-        }
-    }
-    return false;
-}
-
-#define DECLARE_MULTISAMPLED_TEXTURE(suffix, dim)                                      \
-    inline bool JOIN(match_texture_multisampled_, suffix)(                             \
-        TableData::MatchState & state, const type::Type* ty, const type::Type*& T) {   \
-        return match_texture_multisampled(state, ty, dim, T);                          \
-    }                                                                                  \
-    inline const type::MultisampledTexture* JOIN(build_texture_multisampled_, suffix)( \
-        TableData::MatchState & state, const type::Type* T) {                          \
-        return state.types.Get<type::MultisampledTexture>(dim, T);                     \
-    }
-
-DECLARE_MULTISAMPLED_TEXTURE(2d, type::TextureDimension::k2d)
-#undef DECLARE_MULTISAMPLED_TEXTURE
-
-inline bool match_texture_depth(TableData::MatchState&,
-                                const type::Type* ty,
-                                type::TextureDimension dim) {
-    if (ty->Is<TableData::Any>()) {
-        return true;
-    }
-    return ty->Is([&](const type::DepthTexture* t) { return t->dim() == dim; });
-}
-
-#define DECLARE_DEPTH_TEXTURE(suffix, dim)                                         \
-    inline bool JOIN(match_texture_depth_, suffix)(TableData::MatchState & state,  \
-                                                   const type::Type* ty) {         \
-        return match_texture_depth(state, ty, dim);                                \
-    }                                                                              \
-    inline const type::DepthTexture* JOIN(build_texture_depth_,                    \
-                                          suffix)(TableData::MatchState & state) { \
-        return state.types.Get<type::DepthTexture>(dim);                           \
-    }
-
-DECLARE_DEPTH_TEXTURE(2d, type::TextureDimension::k2d)
-DECLARE_DEPTH_TEXTURE(2d_array, type::TextureDimension::k2dArray)
-DECLARE_DEPTH_TEXTURE(cube, type::TextureDimension::kCube)
-DECLARE_DEPTH_TEXTURE(cube_array, type::TextureDimension::kCubeArray)
-#undef DECLARE_DEPTH_TEXTURE
-
-inline bool match_texture_depth_multisampled_2d(TableData::MatchState&, const type::Type* ty) {
-    if (ty->Is<TableData::Any>()) {
-        return true;
-    }
-    return ty->Is([&](const type::DepthMultisampledTexture* t) {
-        return t->dim() == type::TextureDimension::k2d;
-    });
-}
-
-inline type::DepthMultisampledTexture* build_texture_depth_multisampled_2d(
-    TableData::MatchState& state) {
-    return state.types.Get<type::DepthMultisampledTexture>(type::TextureDimension::k2d);
-}
-
-inline bool match_texture_storage(TableData::MatchState&,
-                                  const type::Type* ty,
-                                  type::TextureDimension dim,
-                                  TableData::Number& F,
-                                  TableData::Number& A) {
-    if (ty->Is<TableData::Any>()) {
-        F = TableData::Number::any;
-        A = TableData::Number::any;
-        return true;
-    }
-    if (auto* v = ty->As<type::StorageTexture>()) {
-        if (v->dim() == dim) {
-            F = TableData::Number(static_cast<uint32_t>(v->texel_format()));
-            A = TableData::Number(static_cast<uint32_t>(v->access()));
-            return true;
-        }
-    }
-    return false;
-}
-
-#define DECLARE_STORAGE_TEXTURE(suffix, dim)                                                     \
-    inline bool JOIN(match_texture_storage_, suffix)(TableData::MatchState & state,              \
-                                                     const type::Type* ty, TableData::Number& F, \
-                                                     TableData::Number& A) {                     \
-        return match_texture_storage(state, ty, dim, F, A);                                      \
-    }                                                                                            \
-    inline const type::StorageTexture* JOIN(build_texture_storage_, suffix)(                     \
-        TableData::MatchState & state, TableData::Number F, TableData::Number A) {               \
-        auto format = static_cast<TexelFormat>(F.Value());                                       \
-        auto access = static_cast<Access>(A.Value());                                            \
-        auto* T = type::StorageTexture::SubtypeFor(format, state.types);                         \
-        return state.types.Get<type::StorageTexture>(dim, format, access, T);                    \
-    }
-
-DECLARE_STORAGE_TEXTURE(1d, type::TextureDimension::k1d)
-DECLARE_STORAGE_TEXTURE(2d, type::TextureDimension::k2d)
-DECLARE_STORAGE_TEXTURE(2d_array, type::TextureDimension::k2dArray)
-DECLARE_STORAGE_TEXTURE(3d, type::TextureDimension::k3d)
-#undef DECLARE_STORAGE_TEXTURE
-
-inline bool match_texture_external(TableData::MatchState&, const type::Type* ty) {
-    return ty->IsAnyOf<TableData::Any, type::ExternalTexture>();
-}
-
-inline const type::ExternalTexture* build_texture_external(TableData::MatchState& state) {
-    return state.types.Get<type::ExternalTexture>();
-}
-
-// Builtin types starting with a _ prefix cannot be declared in WGSL, so they
-// can only be used as return types. Because of this, they must only match Any,
-// which is used as the return type matcher.
-inline bool match_modf_result(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (!ty->Is<TableData::Any>()) {
-        return false;
-    }
-    T = ty;
-    return true;
-}
-inline bool match_modf_result_vec(TableData::MatchState&,
-                                  const type::Type* ty,
-                                  TableData::Number& N,
-                                  const type::Type*& T) {
-    if (!ty->Is<TableData::Any>()) {
-        return false;
-    }
-    N = TableData::Number::any;
-    T = ty;
-    return true;
-}
-inline bool match_frexp_result(TableData::MatchState&, const type::Type* ty, const type::Type*& T) {
-    if (!ty->Is<TableData::Any>()) {
-        return false;
-    }
-    T = ty;
-    return true;
-}
-inline bool match_frexp_result_vec(TableData::MatchState&,
-                                   const type::Type* ty,
-                                   TableData::Number& N,
-                                   const type::Type*& T) {
-    if (!ty->Is<TableData::Any>()) {
-        return false;
-    }
-    N = TableData::Number::any;
-    T = ty;
-    return true;
-}
-
-inline bool match_atomic_compare_exchange_result(TableData::MatchState&,
-                                                 const type::Type* ty,
-                                                 const type::Type*& T) {
-    if (ty->Is<TableData::Any>()) {
-        T = ty;
-        return true;
-    }
-    return false;
-}
-
-inline const type::Struct* build_modf_result(TableData::MatchState& state, const type::Type* el) {
-    return type::CreateModfResult(state.types, state.symbols, el);
-}
-
-inline const type::Struct* build_modf_result_vec(TableData::MatchState& state,
-                                                 TableData::Number& n,
-                                                 const type::Type* el) {
-    auto* vec = state.types.vec(el, n.Value());
-    return type::CreateModfResult(state.types, state.symbols, vec);
-}
-
-inline const type::Struct* build_frexp_result(TableData::MatchState& state, const type::Type* el) {
-    return type::CreateFrexpResult(state.types, state.symbols, el);
-}
-
-inline const type::Struct* build_frexp_result_vec(TableData::MatchState& state,
-                                                  TableData::Number& n,
-                                                  const type::Type* el) {
-    auto* vec = state.types.vec(el, n.Value());
-    return type::CreateFrexpResult(state.types, state.symbols, vec);
-}
-
-inline const type::Struct* build_atomic_compare_exchange_result(TableData::MatchState& state,
-                                                                const type::Type* ty) {
-    return type::CreateAtomicCompareExchangeResult(state.types, state.symbols, ty);
-}
-
-}  // namespace tint::core::intrinsic
-
-#endif  // SRC_TINT_LANG_CORE_INTRINSIC_CORE_TYPE_MATCHERS_H_
diff --git a/src/tint/lang/core/intrinsic/table.cc b/src/tint/lang/core/intrinsic/table.cc
index b9dbcc8..2fa6bb9 100644
--- a/src/tint/lang/core/intrinsic/table.cc
+++ b/src/tint/lang/core/intrinsic/table.cc
@@ -19,14 +19,11 @@
 #include <utility>
 
 #include "src/tint/lang/core/evaluation_stage.h"
-#include "src/tint/lang/core/intrinsic/core_table_data.h"
 #include "src/tint/lang/core/intrinsic/table_data.h"
-#include "src/tint/lang/wgsl/ast/binary_expression.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
-#include "src/tint/lang/wgsl/sem/pipeline_stage_set.h"
-#include "src/tint/lang/wgsl/sem/value_constructor.h"
-#include "src/tint/lang/wgsl/sem/value_conversion.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/void.h"
 #include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/diagnostic/diagnostic.h"
 #include "src/tint/utils/macros/scoped_assignment.h"
 #include "src/tint/utils/math/hash.h"
 #include "src/tint/utils/math/math.h"
@@ -73,89 +70,38 @@
 constexpr const auto kNoMatcher = TableData::kNoMatcher;
 
 /// The Vector `N` template argument value for arrays of parameters.
-constexpr const size_t kNumFixedParams = 8;
+constexpr const size_t kNumFixedParams = decltype(Table::Overload{}.parameters)::static_length;
 
 /// The Vector `N` template argument value for arrays of overload candidates.
 constexpr const size_t kNumFixedCandidates = 8;
 
-////////////////////////////////////////////////////////////////////////////////
-// Binding functions for use in the generated builtin_table.inl
-// TODO(bclayton): See if we can move more of this hand-rolled code to the
-// template
-////////////////////////////////////////////////////////////////////////////////
-using PipelineStage = ast::PipelineStage;
-
-/// IntrinsicPrototype describes a fully matched intrinsic.
-struct IntrinsicPrototype {
-    /// Parameter describes a single parameter
-    struct Parameter {
-        /// Parameter type
-        const type::Type* const type;
-        /// Parameter usage
-        ParameterUsage const usage = ParameterUsage::kNone;
-    };
-
-    /// Hasher provides a hash function for the IntrinsicPrototype
-    struct Hasher {
-        /// @param i the IntrinsicPrototype to create a hash for
-        /// @return the hash value
-        inline std::size_t operator()(const IntrinsicPrototype& i) const {
-            size_t hash = Hash(i.parameters.Length());
-            for (auto& p : i.parameters) {
-                hash = HashCombine(hash, p.type, p.usage);
-            }
-            return Hash(hash, i.overload, i.return_type);
-        }
-    };
-
-    const TableData::OverloadInfo* overload = nullptr;
-    type::Type const* return_type = nullptr;
-    Vector<Parameter, kNumFixedParams> parameters;
-};
-
-/// Equality operator for IntrinsicPrototype
-bool operator==(const IntrinsicPrototype& a, const IntrinsicPrototype& b) {
-    if (a.overload != b.overload || a.return_type != b.return_type ||
-        a.parameters.Length() != b.parameters.Length()) {
-        return false;
-    }
-    for (size_t i = 0; i < a.parameters.Length(); i++) {
-        auto& pa = a.parameters[i];
-        auto& pb = b.parameters[i];
-        if (pa.type != pb.type || pa.usage != pb.usage) {
-            return false;
-        }
-    }
-    return true;
-}
-
 /// Impl is the private implementation of the Table interface.
 class Impl : public Table {
   public:
-    Impl(ProgramBuilder& b, const TableData& d);
+    Impl(const TableData& td, type::Manager& tys, SymbolTable& syms, diag::List& d);
 
-    Builtin Lookup(core::Function builtin_type,
-                   VectorRef<const type::Type*> args,
-                   EvaluationStage earliest_eval_stage,
-                   const Source& source) override;
+    Result<Overload> Lookup(core::Function builtin_type,
+                            VectorRef<const type::Type*> args,
+                            EvaluationStage earliest_eval_stage,
+                            const Source& source) override;
 
-    UnaryOperator Lookup(core::UnaryOp op,
-                         const type::Type* arg,
-                         EvaluationStage earliest_eval_stage,
-                         const Source& source) override;
+    Result<Overload> Lookup(core::UnaryOp op,
+                            const type::Type* arg,
+                            EvaluationStage earliest_eval_stage,
+                            const Source& source) override;
 
-    BinaryOperator Lookup(core::BinaryOp op,
-                          const type::Type* lhs,
-                          const type::Type* rhs,
-                          EvaluationStage earliest_eval_stage,
-                          const Source& source,
-                          bool is_compound) override;
+    Result<Overload> Lookup(core::BinaryOp op,
+                            const type::Type* lhs,
+                            const type::Type* rhs,
+                            EvaluationStage earliest_eval_stage,
+                            const Source& source,
+                            bool is_compound) override;
 
-    CtorOrConv Lookup(CtorConv type,
-                      const type::Type* template_arg,
-                      VectorRef<const type::Type*> args,
-                      EvaluationStage earliest_eval_stage,
-                      const Source& source) override;
+    Result<Overload> Lookup(CtorConv type,
+                            const type::Type* template_arg,
+                            VectorRef<const type::Type*> args,
+                            EvaluationStage earliest_eval_stage,
+                            const Source& source) override;
 
   private:
     /// Candidate holds information about an overload evaluated for resolution.
@@ -165,7 +111,7 @@
         /// The template types and numbers
         TemplateState templates;
         /// The parameter types for the candidate overload
-        Vector<IntrinsicPrototype::Parameter, kNumFixedParams> parameters;
+        Vector<Table::Overload::Parameter, kNumFixedParams> parameters;
         /// The match-score of the candidate overload.
         /// A score of zero indicates an exact match.
         /// Non-zero scores are used for diagnostics when no overload matches.
@@ -194,15 +140,13 @@
     ///                  defined as `f32`.
     /// @param on_no_match an error callback when no intrinsic overloads matched the provided
     ///                    arguments.
-    /// @returns the matched intrinsic. If no intrinsic could be matched then IntrinsicPrototype
-    ///          will hold nullptrs for IntrinsicPrototype::overload and
-    ///          IntrinsicPrototype::return_type.
-    IntrinsicPrototype MatchIntrinsic(const IntrinsicInfo& intrinsic,
-                                      const char* intrinsic_name,
-                                      VectorRef<const type::Type*> args,
-                                      EvaluationStage earliest_eval_stage,
-                                      TemplateState templates,
-                                      const OnNoMatch& on_no_match) const;
+    /// @returns the matched intrinsic
+    Result<Table::Overload> MatchIntrinsic(const IntrinsicInfo& intrinsic,
+                                           const char* intrinsic_name,
+                                           VectorRef<const type::Type*> args,
+                                           EvaluationStage earliest_eval_stage,
+                                           TemplateState templates,
+                                           const OnNoMatch& on_no_match) const;
 
     /// Evaluates the single overload for the provided argument types.
     /// @param overload the overload being considered
@@ -256,12 +200,10 @@
                               TemplateState templates,
                               VectorRef<Candidate> candidates) const;
 
-    ProgramBuilder& builder;
     const TableData& data;
-    Hashmap<IntrinsicPrototype, sem::Builtin*, 64, IntrinsicPrototype::Hasher> builtins;
-    Hashmap<IntrinsicPrototype, sem::ValueConstructor*, 16, IntrinsicPrototype::Hasher>
-        constructors;
-    Hashmap<IntrinsicPrototype, sem::ValueConversion*, 16, IntrinsicPrototype::Hasher> converters;
+    type::Manager& types;
+    SymbolTable& symbols;
+    diag::List& diags;
 };
 
 /// @return a string representing a call to a builtin with the given argument
@@ -290,12 +232,13 @@
     return ss.str();
 }
 
-Impl::Impl(ProgramBuilder& b, const TableData& d) : builder(b), data(d) {}
+Impl::Impl(const TableData& td, type::Manager& tys, SymbolTable& syms, diag::List& d)
+    : data(td), types(tys), symbols(syms), diags(d) {}
 
-Impl::Builtin Impl::Lookup(core::Function builtin_type,
-                           VectorRef<const type::Type*> args,
-                           EvaluationStage earliest_eval_stage,
-                           const Source& source) {
+Result<Table::Overload> Impl::Lookup(core::Function builtin_type,
+                                     VectorRef<const type::Type*> args,
+                                     EvaluationStage earliest_eval_stage,
+                                     const Source& source) {
     const char* intrinsic_name = core::str(builtin_type);
 
     // Generates an error when no overloads match the provided arguments
@@ -308,66 +251,36 @@
                << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, intrinsic_name);
         }
-        builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
+        diags.add_error(diag::System::Intrinsics, ss.str(), source);
     };
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(data.builtins[static_cast<size_t>(builtin_type)], intrinsic_name,
-                                args, earliest_eval_stage, TemplateState{}, on_no_match);
-    if (!match.overload) {
-        return {};
-    }
-
-    // De-duplicate builtins that are identical.
-    auto* sem = builtins.GetOrCreate(match, [&] {
-        Vector<sem::Parameter*, kNumFixedParams> params;
-        params.Reserve(match.parameters.Length());
-        for (auto& p : match.parameters) {
-            params.Push(builder.create<sem::Parameter>(
-                nullptr, static_cast<uint32_t>(params.Length()), p.type,
-                core::AddressSpace::kUndefined, core::Access::kUndefined, p.usage));
-        }
-        sem::PipelineStageSet supported_stages;
-        auto& overload = *match.overload;
-        if (overload.flags.Contains(OverloadFlag::kSupportsVertexPipeline)) {
-            supported_stages.Add(ast::PipelineStage::kVertex);
-        }
-        if (overload.flags.Contains(OverloadFlag::kSupportsFragmentPipeline)) {
-            supported_stages.Add(ast::PipelineStage::kFragment);
-        }
-        if (overload.flags.Contains(OverloadFlag::kSupportsComputePipeline)) {
-            supported_stages.Add(ast::PipelineStage::kCompute);
-        }
-        auto eval_stage =
-            overload.const_eval_fn ? EvaluationStage::kConstant : EvaluationStage::kRuntime;
-        return builder.create<sem::Builtin>(builtin_type, match.return_type, std::move(params),
-                                            eval_stage, supported_stages,
-                                            overload.flags.Contains(OverloadFlag::kIsDeprecated),
-                                            overload.flags.Contains(OverloadFlag::kMustUse));
-    });
-    return Builtin{sem, match.overload->const_eval_fn};
+    return MatchIntrinsic(data.builtins[static_cast<size_t>(builtin_type)], intrinsic_name, args,
+                          earliest_eval_stage, TemplateState{}, on_no_match);
 }
 
-Table::UnaryOperator Impl::Lookup(core::UnaryOp op,
-                                  const type::Type* arg,
-                                  EvaluationStage earliest_eval_stage,
-                                  const Source& source) {
-    auto [intrinsic_info, intrinsic_name] = [&]() -> std::pair<const IntrinsicInfo*, const char*> {
-        switch (op) {
-            case core::UnaryOp::kComplement:
-                return {&data.unary_complement, "operator ~ "};
-            case core::UnaryOp::kNegation:
-                return {&data.unary_minus, "operator - "};
-            case core::UnaryOp::kNot:
-                return {&data.unary_not, "operator ! "};
-            default:
-                break;
-        }
-        TINT_UNREACHABLE() << "invalid unary op: " << op;
-        return {};
-    }();
-    if (!intrinsic_info) {
-        return {};
+Result<Table::Overload> Impl::Lookup(core::UnaryOp op,
+                                     const type::Type* arg,
+                                     EvaluationStage earliest_eval_stage,
+                                     const Source& source) {
+    const IntrinsicInfo* intrinsic_info = nullptr;
+    const char* intrinsic_name = nullptr;
+    switch (op) {
+        case core::UnaryOp::kComplement:
+            intrinsic_info = &data.unary_complement;
+            intrinsic_name = "operator ~ ";
+            break;
+        case core::UnaryOp::kNegation:
+            intrinsic_info = &data.unary_minus;
+            intrinsic_name = "operator - ";
+            break;
+        case core::UnaryOp::kNot:
+            intrinsic_info = &data.unary_not;
+            intrinsic_name = "operator ! ";
+            break;
+        default:
+            TINT_UNREACHABLE() << "invalid unary op: " << op;
+            return Failure;
     }
 
     Vector args{arg};
@@ -382,73 +295,95 @@
                << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, name);
         }
-        builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
+        diags.add_error(diag::System::Intrinsics, ss.str(), source);
     };
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
-                                TemplateState{}, on_no_match);
-    if (!match.overload) {
-        return {};
-    }
-
-    return UnaryOperator{
-        match.return_type,
-        match.parameters[0].type,
-        match.overload->const_eval_fn,
-    };
+    return MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
+                          TemplateState{}, on_no_match);
 }
 
-Table::BinaryOperator Impl::Lookup(core::BinaryOp op,
-                                   const type::Type* lhs,
-                                   const type::Type* rhs,
-                                   EvaluationStage earliest_eval_stage,
-                                   const Source& source,
-                                   bool is_compound) {
-    auto [intrinsic_info, intrinsic_name] = [&]() -> std::pair<const IntrinsicInfo*, const char*> {
-        switch (op) {
-            case core::BinaryOp::kAnd:
-                return {&data.binary_and, is_compound ? "operator &= " : "operator & "};
-            case core::BinaryOp::kOr:
-                return {&data.binary_or, is_compound ? "operator |= " : "operator | "};
-            case core::BinaryOp::kXor:
-                return {&data.binary_xor, is_compound ? "operator ^= " : "operator ^ "};
-            case core::BinaryOp::kLogicalAnd:
-                return {&data.binary_logical_and, "operator && "};
-            case core::BinaryOp::kLogicalOr:
-                return {&data.binary_logical_or, "operator || "};
-            case core::BinaryOp::kEqual:
-                return {&data.binary_equal, "operator == "};
-            case core::BinaryOp::kNotEqual:
-                return {&data.binary_not_equal, "operator != "};
-            case core::BinaryOp::kLessThan:
-                return {&data.binary_less_than, "operator < "};
-            case core::BinaryOp::kGreaterThan:
-                return {&data.binary_greater_than, "operator > "};
-            case core::BinaryOp::kLessThanEqual:
-                return {&data.binary_less_than_equal, "operator <= "};
-            case core::BinaryOp::kGreaterThanEqual:
-                return {&data.binary_greater_than_equal, "operator >= "};
-            case core::BinaryOp::kShiftLeft:
-                return {&data.binary_shift_left, is_compound ? "operator <<= " : "operator << "};
-            case core::BinaryOp::kShiftRight:
-                return {&data.binary_shift_right, is_compound ? "operator >>= " : "operator >> "};
-            case core::BinaryOp::kAdd:
-                return {&data.binary_plus, is_compound ? "operator += " : "operator + "};
-            case core::BinaryOp::kSubtract:
-                return {&data.binary_minus, is_compound ? "operator -= " : "operator - "};
-            case core::BinaryOp::kMultiply:
-                return {&data.binary_star, is_compound ? "operator *= " : "operator * "};
-            case core::BinaryOp::kDivide:
-                return {&data.binary_divide, is_compound ? "operator /= " : "operator / "};
-            case core::BinaryOp::kModulo:
-                return {&data.binary_modulo, is_compound ? "operator %= " : "operator % "};
-        }
-        TINT_UNREACHABLE() << "unhandled BinaryOp: " << op;
-        return {};
-    }();
-    if (!intrinsic_info) {
-        return {};
+Result<Table::Overload> Impl::Lookup(core::BinaryOp op,
+                                     const type::Type* lhs,
+                                     const type::Type* rhs,
+                                     EvaluationStage earliest_eval_stage,
+                                     const Source& source,
+                                     bool is_compound) {
+    const IntrinsicInfo* intrinsic_info = nullptr;
+    const char* intrinsic_name = nullptr;
+    switch (op) {
+        case core::BinaryOp::kAnd:
+            intrinsic_info = &data.binary_and;
+            intrinsic_name = is_compound ? "operator &= " : "operator & ";
+            break;
+        case core::BinaryOp::kOr:
+            intrinsic_info = &data.binary_or;
+            intrinsic_name = is_compound ? "operator |= " : "operator | ";
+            break;
+        case core::BinaryOp::kXor:
+            intrinsic_info = &data.binary_xor;
+            intrinsic_name = is_compound ? "operator ^= " : "operator ^ ";
+            break;
+        case core::BinaryOp::kLogicalAnd:
+            intrinsic_info = &data.binary_logical_and;
+            intrinsic_name = "operator && ";
+            break;
+        case core::BinaryOp::kLogicalOr:
+            intrinsic_info = &data.binary_logical_or;
+            intrinsic_name = "operator || ";
+            break;
+        case core::BinaryOp::kEqual:
+            intrinsic_info = &data.binary_equal;
+            intrinsic_name = "operator == ";
+            break;
+        case core::BinaryOp::kNotEqual:
+            intrinsic_info = &data.binary_not_equal;
+            intrinsic_name = "operator != ";
+            break;
+        case core::BinaryOp::kLessThan:
+            intrinsic_info = &data.binary_less_than;
+            intrinsic_name = "operator < ";
+            break;
+        case core::BinaryOp::kGreaterThan:
+            intrinsic_info = &data.binary_greater_than;
+            intrinsic_name = "operator > ";
+            break;
+        case core::BinaryOp::kLessThanEqual:
+            intrinsic_info = &data.binary_less_than_equal;
+            intrinsic_name = "operator <= ";
+            break;
+        case core::BinaryOp::kGreaterThanEqual:
+            intrinsic_info = &data.binary_greater_than_equal;
+            intrinsic_name = "operator >= ";
+            break;
+        case core::BinaryOp::kShiftLeft:
+            intrinsic_info = &data.binary_shift_left;
+            intrinsic_name = is_compound ? "operator <<= " : "operator << ";
+            break;
+        case core::BinaryOp::kShiftRight:
+            intrinsic_info = &data.binary_shift_right;
+            intrinsic_name = is_compound ? "operator >>= " : "operator >> ";
+            break;
+        case core::BinaryOp::kAdd:
+            intrinsic_info = &data.binary_plus;
+            intrinsic_name = is_compound ? "operator += " : "operator + ";
+            break;
+        case core::BinaryOp::kSubtract:
+            intrinsic_info = &data.binary_minus;
+            intrinsic_name = is_compound ? "operator -= " : "operator - ";
+            break;
+        case core::BinaryOp::kMultiply:
+            intrinsic_info = &data.binary_star;
+            intrinsic_name = is_compound ? "operator *= " : "operator * ";
+            break;
+        case core::BinaryOp::kDivide:
+            intrinsic_info = &data.binary_divide;
+            intrinsic_name = is_compound ? "operator /= " : "operator / ";
+            break;
+        case core::BinaryOp::kModulo:
+            intrinsic_info = &data.binary_modulo;
+            intrinsic_name = is_compound ? "operator %= " : "operator % ";
+            break;
     }
 
     Vector args{lhs, rhs};
@@ -463,29 +398,19 @@
                << (candidates.Length() > 1 ? "s:" : ":") << std::endl;
             PrintCandidates(ss, candidates, name);
         }
-        builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
+        diags.add_error(diag::System::Intrinsics, ss.str(), source);
     };
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
-                                TemplateState{}, on_no_match);
-    if (!match.overload) {
-        return {};
-    }
-
-    return BinaryOperator{
-        match.return_type,
-        match.parameters[0].type,
-        match.parameters[1].type,
-        match.overload->const_eval_fn,
-    };
+    return MatchIntrinsic(*intrinsic_info, intrinsic_name, args, earliest_eval_stage,
+                          TemplateState{}, on_no_match);
 }
 
-Table::CtorOrConv Impl::Lookup(CtorConv type,
-                               const type::Type* template_arg,
-                               VectorRef<const type::Type*> args,
-                               EvaluationStage earliest_eval_stage,
-                               const Source& source) {
+Result<Table::Overload> Impl::Lookup(CtorConv type,
+                                     const type::Type* template_arg,
+                                     VectorRef<const type::Type*> args,
+                                     EvaluationStage earliest_eval_stage,
+                                     const Source& source) {
     auto name = str(type);
 
     // Generates an error when no overloads match the provided arguments
@@ -513,7 +438,7 @@
                << std::endl;
             PrintCandidates(ss, conv, name);
         }
-        builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
+        diags.add_error(diag::System::Intrinsics, ss.str(), source);
     };
 
     // If a template type was provided, then close the 0'th type with this.
@@ -523,48 +448,16 @@
     }
 
     // Resolve the intrinsic overload
-    auto match = MatchIntrinsic(data.ctor_conv[static_cast<size_t>(type)], name, args,
-                                earliest_eval_stage, templates, on_no_match);
-    if (!match.overload) {
-        return {};
-    }
-
-    // Was this overload a constructor or conversion?
-    if (match.overload->flags.Contains(OverloadFlag::kIsConstructor)) {
-        Vector<sem::Parameter*, 8> params;
-        params.Reserve(match.parameters.Length());
-        for (auto& p : match.parameters) {
-            params.Push(builder.create<sem::Parameter>(
-                nullptr, static_cast<uint32_t>(params.Length()), p.type,
-                core::AddressSpace::kUndefined, core::Access::kUndefined, p.usage));
-        }
-        auto eval_stage =
-            match.overload->const_eval_fn ? EvaluationStage::kConstant : EvaluationStage::kRuntime;
-        auto* target = constructors.GetOrCreate(match, [&] {
-            return builder.create<sem::ValueConstructor>(match.return_type, std::move(params),
-                                                         eval_stage);
-        });
-        return CtorOrConv{target, match.overload->const_eval_fn};
-    }
-
-    // Conversion.
-    auto* target = converters.GetOrCreate(match, [&] {
-        auto param = builder.create<sem::Parameter>(
-            nullptr, 0u, match.parameters[0].type, core::AddressSpace::kUndefined,
-            core::Access::kUndefined, match.parameters[0].usage);
-        auto eval_stage =
-            match.overload->const_eval_fn ? EvaluationStage::kConstant : EvaluationStage::kRuntime;
-        return builder.create<sem::ValueConversion>(match.return_type, param, eval_stage);
-    });
-    return CtorOrConv{target, match.overload->const_eval_fn};
+    return MatchIntrinsic(data.ctor_conv[static_cast<size_t>(type)], name, args,
+                          earliest_eval_stage, templates, on_no_match);
 }
 
-IntrinsicPrototype Impl::MatchIntrinsic(const IntrinsicInfo& intrinsic,
-                                        const char* intrinsic_name,
-                                        VectorRef<const type::Type*> args,
-                                        EvaluationStage earliest_eval_stage,
-                                        TemplateState templates,
-                                        const OnNoMatch& on_no_match) const {
+Result<Table::Overload> Impl::MatchIntrinsic(const IntrinsicInfo& intrinsic,
+                                             const char* intrinsic_name,
+                                             VectorRef<const type::Type*> args,
+                                             EvaluationStage earliest_eval_stage,
+                                             TemplateState templates,
+                                             const OnNoMatch& on_no_match) const {
     size_t num_matched = 0;
     size_t match_idx = 0;
     Vector<Candidate, kNumFixedCandidates> candidates;
@@ -585,7 +478,7 @@
         // Sort the candidates with the most promising first
         SortCandidates(candidates);
         on_no_match(std::move(candidates));
-        return {};
+        return Failure;
     }
 
     Candidate match;
@@ -596,7 +489,7 @@
         match = ResolveCandidate(std::move(candidates), intrinsic_name, args, std::move(templates));
         if (!match.overload) {
             // Ambiguous overload. ResolveCandidate() will have already raised an error diagnostic.
-            return {};
+            return Failure;
         }
     }
 
@@ -608,13 +501,13 @@
             Match(match.templates, match.overload, indices, earliest_eval_stage).Type(&any);
         if (TINT_UNLIKELY(!return_type)) {
             TINT_ICE() << "MatchState.Match() returned null";
-            return {};
+            return Failure;
         }
     } else {
-        return_type = builder.create<type::Void>();
+        return_type = types.void_();
     }
 
-    return IntrinsicPrototype{match.overload, return_type, std::move(match.parameters)};
+    return Table::Overload{match.overload, return_type, std::move(match.parameters)};
 }
 
 Impl::Candidate Impl::ScoreOverload(const TableData::OverloadInfo* overload,
@@ -715,7 +608,7 @@
     }
 
     // Now that all the template types have been finalized, we can construct the parameters.
-    Vector<IntrinsicPrototype::Parameter, kNumFixedParams> parameters;
+    Vector<Table::Overload::Parameter, kNumFixedParams> parameters;
     if (score == 0) {
         parameters.Reserve(num_params);
         for (size_t p = 0; p < num_params; p++) {
@@ -799,8 +692,8 @@
                        const TableData::OverloadInfo* overload,
                        MatcherIndex const* matcher_indices,
                        EvaluationStage earliest_eval_stage) const {
-    return MatchState{builder.Types(), builder.Symbols(), templates,          data,
-                      overload,        matcher_indices,   earliest_eval_stage};
+    return MatchState{types,    symbols,         templates,          data,
+                      overload, matcher_indices, earliest_eval_stage};
 }
 
 void Impl::PrintOverload(StringStream& ss,
@@ -923,8 +816,11 @@
 
 }  // namespace
 
-std::unique_ptr<Table> Table::Create(ProgramBuilder& builder) {
-    return std::make_unique<Impl>(builder, CoreTableData());
+std::unique_ptr<Table> Table::Create(const TableData& data,
+                                     type::Manager& types,
+                                     SymbolTable& symbols,
+                                     diag::List& diags) {
+    return std::make_unique<Impl>(data, types, symbols, diags);
 }
 
 Table::~Table() = default;
diff --git a/src/tint/lang/core/intrinsic/table.h b/src/tint/lang/core/intrinsic/table.h
index f5a5d1e..cd5cbe8 100644
--- a/src/tint/lang/core/intrinsic/table.h
+++ b/src/tint/lang/core/intrinsic/table.h
@@ -19,65 +19,82 @@
 #include <string>
 
 #include "src/tint/lang/core/binary_op.h"
-#include "src/tint/lang/core/constant/eval.h"
+#include "src/tint/lang/core/function.h"
 #include "src/tint/lang/core/intrinsic/ctor_conv.h"
+#include "src/tint/lang/core/intrinsic/table_data.h"
+#include "src/tint/lang/core/parameter_usage.h"
 #include "src/tint/lang/core/unary_op.h"
-#include "src/tint/lang/wgsl/sem/builtin.h"
 #include "src/tint/utils/containers/vector.h"
 
 // Forward declarations
-namespace tint {
-class ProgramBuilder;
-}  // namespace tint
+namespace tint::diag {
+class List;
+}  // namespace tint::diag
+namespace tint::type {
+class Manager;
+}  // namespace tint::type
 
 namespace tint::core::intrinsic {
 
 /// Table is a lookup table of all the WGSL builtin functions and intrinsic operators
 class Table {
   public:
-    /// @param builder the program builder
+    /// @param data the intrinsic table data
+    /// @param types the type manager
+    /// @param symbols the symbol table
+    /// @param diags the diagnostic list to append errors to
     /// @return a pointer to a newly created Table
-    static std::unique_ptr<Table> Create(ProgramBuilder& builder);
+    static std::unique_ptr<Table> Create(const TableData& data,
+                                         type::Manager& types,
+                                         SymbolTable& symbols,
+                                         diag::List& diags);
 
     /// Destructor
     virtual ~Table();
 
-    /// Builtin describes a resolved builtin function
-    struct Builtin {
-        /// The semantic info for the builtin
-        const sem::Builtin* sem = nullptr;
-        /// The constant evaluation function
-        constant::Eval::Function const_eval_fn = nullptr;
-    };
+    /// Overload describes a fully matched builtin function overload
+    struct Overload {
+        /// Parameter describes a single parameter
+        struct Parameter {
+            /// Parameter type
+            const type::Type* const type;
+            /// Parameter usage
+            core::ParameterUsage const usage = core::ParameterUsage::kNone;
 
-    /// UnaryOperator describes a resolved unary operator
-    struct UnaryOperator {
-        /// The result type of the unary operator
-        const type::Type* result = nullptr;
-        /// The type of the parameter of the unary operator
-        const type::Type* parameter = nullptr;
-        /// The constant evaluation function
-        constant::Eval::Function const_eval_fn = nullptr;
-    };
+            /// Equality operator
+            /// @param other the parameter to compare against
+            /// @returns true if this parameter and @p other are the same
+            bool operator==(const Parameter& other) const {
+                return type == other.type && usage == other.usage;
+            }
 
-    /// BinaryOperator describes a resolved binary operator
-    struct BinaryOperator {
-        /// The result type of the binary operator
-        const type::Type* result = nullptr;
-        /// The type of LHS parameter of the binary operator
-        const type::Type* lhs = nullptr;
-        /// The type of RHS parameter of the binary operator
-        const type::Type* rhs = nullptr;
-        /// The constant evaluation function
-        constant::Eval::Function const_eval_fn = nullptr;
-    };
+            /// Inequality operator
+            /// @param other the parameter to compare against
+            /// @returns false if this parameter and @p other are the same
+            bool operator!=(const Parameter& other) const { return !(*this == other); }
+        };
 
-    /// CtorOrConv describes a resolved value constructor or conversion
-    struct CtorOrConv {
-        /// The result type of the value constructor or conversion
-        const sem::CallTarget* target = nullptr;
-        /// The constant evaluation function
-        constant::Eval::Function const_eval_fn = nullptr;
+        /// The overload information
+        const TableData::OverloadInfo* info = nullptr;
+
+        /// The resolved overload return type
+        type::Type const* return_type = nullptr;
+
+        /// The resolved overload parameters
+        Vector<Parameter, 8> parameters;
+
+        /// Equality operator
+        /// @param other the overload to compare against
+        /// @returns true if this overload and @p other are the same
+        bool operator==(const Overload& other) const {
+            return info == other.info && return_type == other.return_type &&
+                   parameters == other.parameters;
+        }
+
+        /// Inequality operator
+        /// @param other the overload to compare against
+        /// @returns false if this overload and @p other are the same
+        bool operator!=(const Overload& other) const { return !(*this == other); }
     };
 
     /// Lookup looks for the builtin overload with the given signature, raising an error diagnostic
@@ -91,11 +108,11 @@
     ///        abstract-numerics will have been materialized after shader creation time
     ///        (EvaluationStage::kConstant).
     /// @param source the source of the builtin call
-    /// @return the semantic builtin if found, otherwise nullptr
-    virtual Builtin Lookup(core::Function type,
-                           VectorRef<const type::Type*> args,
-                           EvaluationStage earliest_eval_stage,
-                           const Source& source) = 0;
+    /// @return the resolved builtin function overload
+    virtual Result<Overload> Lookup(core::Function type,
+                                    VectorRef<const type::Type*> args,
+                                    EvaluationStage earliest_eval_stage,
+                                    const Source& source) = 0;
 
     /// Lookup looks for the unary op overload with the given signature, raising an error
     /// diagnostic if the operator was not found.
@@ -108,12 +125,11 @@
     ///        will be considered, as all abstract-numerics will have been materialized
     ///        after shader creation time (EvaluationStage::kConstant).
     /// @param source the source of the operator call
-    /// @return the operator call target signature. If the operator was not found
-    ///         UnaryOperator::result will be nullptr.
-    virtual UnaryOperator Lookup(core::UnaryOp op,
-                                 const type::Type* arg,
-                                 EvaluationStage earliest_eval_stage,
-                                 const Source& source) = 0;
+    /// @return the resolved unary operator overload
+    virtual Result<Overload> Lookup(core::UnaryOp op,
+                                    const type::Type* arg,
+                                    EvaluationStage earliest_eval_stage,
+                                    const Source& source) = 0;
 
     /// Lookup looks for the binary op overload with the given signature, raising an error
     /// diagnostic if the operator was not found.
@@ -128,14 +144,13 @@
     ///        will be considered, as all abstract-numerics will have been materialized
     ///        after shader creation time (EvaluationStage::kConstant).
     /// @param is_compound true if the binary operator is being used as a compound assignment
-    /// @return the operator call target signature. If the operator was not found
-    ///         BinaryOperator::result will be nullptr.
-    virtual BinaryOperator Lookup(core::BinaryOp op,
-                                  const type::Type* lhs,
-                                  const type::Type* rhs,
-                                  EvaluationStage earliest_eval_stage,
-                                  const Source& source,
-                                  bool is_compound) = 0;
+    /// @return the resolved binary operator overload
+    virtual Result<Overload> Lookup(core::BinaryOp op,
+                                    const type::Type* lhs,
+                                    const type::Type* rhs,
+                                    EvaluationStage earliest_eval_stage,
+                                    const Source& source,
+                                    bool is_compound) = 0;
 
     /// Lookup looks for the value constructor or conversion overload for the given CtorConv.
     /// @param type the type being constructed or converted
@@ -148,14 +163,32 @@
     ///        will be considered, as all abstract-numerics will have been materialized
     ///        after shader creation time (EvaluationStage::kConstant).
     /// @param source the source of the call
-    /// @return a sem::ValueConstructor, sem::ValueConversion or nullptr if nothing matched
-    virtual CtorOrConv Lookup(CtorConv type,
-                              const type::Type* template_arg,
-                              VectorRef<const type::Type*> args,
-                              EvaluationStage earliest_eval_stage,
-                              const Source& source) = 0;
+    /// @return the resolved type constructor or conversion function overload
+    virtual Result<Overload> Lookup(CtorConv type,
+                                    const type::Type* template_arg,
+                                    VectorRef<const type::Type*> args,
+                                    EvaluationStage earliest_eval_stage,
+                                    const Source& source) = 0;
 };
 
 }  // namespace tint::core::intrinsic
 
+namespace tint {
+
+/// Hasher specialization for core::intrinsic::Table::Overload
+template <>
+struct Hasher<core::intrinsic::Table::Overload> {
+    /// @param i the core::intrinsic::Table::Overload to create a hash for
+    /// @return the hash value
+    inline std::size_t operator()(const core::intrinsic::Table::Overload& i) const {
+        size_t hash = Hash(i.parameters.Length());
+        for (auto& p : i.parameters) {
+            hash = HashCombine(hash, p.type, p.usage);
+        }
+        return Hash(hash, i.info, i.return_type);
+    }
+};
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_CORE_INTRINSIC_TABLE_H_
diff --git a/src/tint/lang/core/intrinsic/table_test.cc b/src/tint/lang/core/intrinsic/table_test.cc
index 3bf47da..05d084b 100644
--- a/src/tint/lang/core/intrinsic/table_test.cc
+++ b/src/tint/lang/core/intrinsic/table_test.cc
@@ -17,6 +17,8 @@
 #include <utility>
 
 #include "gmock/gmock.h"
+#include "src/tint/lang/core/intrinsic/table_data.h"
+#include "src/tint/lang/core/intrinsic_data.h"
 #include "src/tint/lang/core/type/atomic.h"
 #include "src/tint/lang/core/type/depth_multisampled_texture.h"
 #include "src/tint/lang/core/type/depth_texture.h"
@@ -27,7 +29,6 @@
 #include "src/tint/lang/core/type/sampled_texture.h"
 #include "src/tint/lang/core/type/storage_texture.h"
 #include "src/tint/lang/core/type/texture_dimension.h"
-#include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
 #include "src/tint/lang/wgsl/sem/value_constructor.h"
 #include "src/tint/lang/wgsl/sem/value_conversion.h"
@@ -49,26 +50,26 @@
 
 class IntrinsicTableTest : public testing::Test, public ProgramBuilder {
   public:
-    std::unique_ptr<Table> table = Table::Create(*this);
+    std::unique_ptr<Table> table =
+        Table::Create(core::kIntrinsicData, Types(), Symbols(), Diagnostics());
 };
 
 TEST_F(IntrinsicTableTest, MatchF32) {
     auto* f32 = create<type::F32>();
     auto result =
         table->Lookup(core::Function::kCos, Vector{f32}, EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kCos);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchF32) {
     auto* i32 = create<type::I32>();
     auto result =
         table->Lookup(core::Function::kCos, Vector{i32}, EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -78,19 +79,18 @@
     auto* vec2_f32 = create<type::Vector>(f32, 2u);
     auto result = table->Lookup(core::Function::kUnpack2X16Float, Vector{u32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kUnpack2X16Float);
-    EXPECT_EQ(result.sem->ReturnType(), vec2_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
+    EXPECT_EQ(result->return_type, vec2_f32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchU32) {
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kUnpack2X16Float, Vector{f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -101,17 +101,16 @@
     auto* tex = create<type::SampledTexture>(type::TextureDimension::k1d, f32);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kLevel);
+    EXPECT_EQ(result->return_type, vec4_f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MismatchI32) {
@@ -119,7 +118,7 @@
     auto* tex = create<type::SampledTexture>(type::TextureDimension::k1d, f32);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -127,31 +126,29 @@
     auto* i32 = create<type::I32>();
     auto result = table->Lookup(core::Function::kCountOneBits, Vector{i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kCountOneBits);
-    EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), i32);
+    EXPECT_EQ(result->return_type, i32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, i32);
 }
 
 TEST_F(IntrinsicTableTest, MatchIU32AsU32) {
     auto* u32 = create<type::U32>();
     auto result = table->Lookup(core::Function::kCountOneBits, Vector{u32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kCountOneBits);
-    EXPECT_EQ(result.sem->ReturnType(), u32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
+    EXPECT_EQ(result->return_type, u32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, u32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchIU32) {
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kCountOneBits, Vector{f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -159,49 +156,46 @@
     auto* i32 = create<type::I32>();
     auto result = table->Lookup(core::Function::kClamp, Vector{i32, i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kClamp);
-    EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
+    EXPECT_EQ(result->return_type, i32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, i32);
+    EXPECT_EQ(result->parameters[2].type, i32);
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsU32) {
     auto* u32 = create<type::U32>();
     auto result = table->Lookup(core::Function::kClamp, Vector{u32, u32, u32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kClamp);
-    EXPECT_EQ(result.sem->ReturnType(), u32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), u32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), u32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), u32);
+    EXPECT_EQ(result->return_type, u32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, u32);
+    EXPECT_EQ(result->parameters[1].type, u32);
+    EXPECT_EQ(result->parameters[2].type, u32);
 }
 
 TEST_F(IntrinsicTableTest, MatchFIU32AsF32) {
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kClamp, Vector{f32, f32, f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kClamp);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), f32);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, f32);
+    EXPECT_EQ(result->parameters[1].type, f32);
+    EXPECT_EQ(result->parameters[2].type, f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchFIU32) {
     auto* bool_ = create<type::Bool>();
     auto result = table->Lookup(core::Function::kClamp, Vector{bool_, bool_, bool_},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -210,21 +204,20 @@
     auto* bool_ = create<type::Bool>();
     auto result = table->Lookup(core::Function::kSelect, Vector{f32, f32, bool_},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kSelect);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), bool_);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, f32);
+    EXPECT_EQ(result->parameters[1].type, f32);
+    EXPECT_EQ(result->parameters[2].type, bool_);
 }
 
 TEST_F(IntrinsicTableTest, MismatchBool) {
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kSelect, Vector{f32, f32, f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -235,12 +228,11 @@
         create<type::Pointer>(core::AddressSpace::kWorkgroup, atomicI32, core::Access::kReadWrite);
     auto result = table->Lookup(core::Function::kAtomicLoad, Vector{ptr},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kAtomicLoad);
-    EXPECT_EQ(result.sem->ReturnType(), i32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), ptr);
+    EXPECT_EQ(result->return_type, i32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, ptr);
 }
 
 TEST_F(IntrinsicTableTest, MismatchPointer) {
@@ -248,7 +240,7 @@
     auto* atomicI32 = create<type::Atomic>(i32);
     auto result = table->Lookup(core::Function::kAtomicLoad, Vector{atomicI32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -259,12 +251,11 @@
         create<type::Pointer>(core::AddressSpace::kStorage, arr, core::Access::kReadWrite);
     auto result = table->Lookup(core::Function::kArrayLength, Vector{arr_ptr},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kArrayLength);
-    EXPECT_TRUE(result.sem->ReturnType()->Is<type::U32>());
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    auto* param_type = result.sem->Parameters()[0]->Type();
+    EXPECT_TRUE(result->return_type->Is<type::U32>());
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    auto* param_type = result->parameters[0].type;
     ASSERT_TRUE(param_type->Is<type::Pointer>());
     EXPECT_TRUE(param_type->As<type::Pointer>()->StoreType()->Is<type::Array>());
 }
@@ -273,7 +264,7 @@
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kArrayLength, Vector{f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -285,17 +276,16 @@
     auto* sampler = create<type::Sampler>(type::SamplerKind::kSampler);
     auto result = table->Lookup(core::Function::kTextureSample, Vector{tex, sampler, vec2_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureSample);
-    EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), sampler);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kSampler);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), vec2_f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kCoords);
+    EXPECT_EQ(result->return_type, vec4_f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, sampler);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kSampler);
+    EXPECT_EQ(result->parameters[2].type, vec2_f32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kCoords);
 }
 
 TEST_F(IntrinsicTableTest, MismatchSampler) {
@@ -304,7 +294,7 @@
     auto* tex = create<type::SampledTexture>(type::TextureDimension::k2d, f32);
     auto result = table->Lookup(core::Function::kTextureSample, Vector{tex, f32, vec2_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -316,17 +306,16 @@
     auto* tex = create<type::SampledTexture>(type::TextureDimension::k2d, f32);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kLevel);
+    EXPECT_EQ(result->return_type, vec4_f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MatchMultisampledTexture) {
@@ -337,17 +326,16 @@
     auto* tex = create<type::MultisampledTexture>(type::TextureDimension::k2d, f32);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kSampleIndex);
+    EXPECT_EQ(result->return_type, vec4_f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kSampleIndex);
 }
 
 TEST_F(IntrinsicTableTest, MatchDepthTexture) {
@@ -357,17 +345,16 @@
     auto* tex = create<type::DepthTexture>(type::TextureDimension::k2d);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kLevel);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kLevel);
 }
 
 TEST_F(IntrinsicTableTest, MatchDepthMultisampledTexture) {
@@ -377,17 +364,16 @@
     auto* tex = create<type::DepthMultisampledTexture>(type::TextureDimension::k2d);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32, i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), i32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kSampleIndex);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kSampleIndex);
 }
 
 TEST_F(IntrinsicTableTest, MatchExternalTexture) {
@@ -398,15 +384,14 @@
     auto* tex = create<type::ExternalTexture>();
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{tex, vec2_i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureLoad);
-    EXPECT_EQ(result.sem->ReturnType(), vec4_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 2u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
+    EXPECT_EQ(result->return_type, vec4_f32);
+    ASSERT_EQ(result->parameters.Length(), 2u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
 }
 
 TEST_F(IntrinsicTableTest, MatchWOStorageTexture) {
@@ -420,17 +405,16 @@
 
     auto result = table->Lookup(core::Function::kTextureStore, Vector{tex, vec2_i32, vec4_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kTextureStore);
-    EXPECT_TRUE(result.sem->ReturnType()->Is<type::Void>());
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), tex);
-    EXPECT_EQ(result.sem->Parameters()[0]->Usage(), ParameterUsage::kTexture);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_i32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Usage(), ParameterUsage::kCoords);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), vec4_f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Usage(), ParameterUsage::kValue);
+    EXPECT_TRUE(result->return_type->Is<type::Void>());
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, tex);
+    EXPECT_EQ(result->parameters[0].usage, ParameterUsage::kTexture);
+    EXPECT_EQ(result->parameters[1].type, vec2_i32);
+    EXPECT_EQ(result->parameters[1].usage, ParameterUsage::kCoords);
+    EXPECT_EQ(result->parameters[2].type, vec4_f32);
+    EXPECT_EQ(result->parameters[2].usage, ParameterUsage::kValue);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTexture) {
@@ -439,7 +423,7 @@
     auto* vec2_i32 = create<type::Vector>(i32, 2u);
     auto result = table->Lookup(core::Function::kTextureLoad, Vector{f32, vec2_i32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -451,25 +435,23 @@
             create<type::Reference>(core::AddressSpace::kFunction, f32, core::Access::kReadWrite),
         },
         EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kCos);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, f32);
 }
 
 TEST_F(IntrinsicTableTest, MatchTemplateType) {
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(core::Function::kClamp, Vector{f32, f32, f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kClamp);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), f32);
+    EXPECT_EQ(result->return_type, f32);
+    EXPECT_EQ(result->parameters[0].type, f32);
+    EXPECT_EQ(result->parameters[1].type, f32);
+    EXPECT_EQ(result->parameters[2].type, f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTemplateType) {
@@ -477,7 +459,7 @@
     auto* u32 = create<type::U32>();
     auto result = table->Lookup(core::Function::kClamp, Vector{f32, u32, f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -486,14 +468,13 @@
     auto* vec2_f32 = create<type::Vector>(f32, 2u);
     auto result = table->Lookup(core::Function::kClamp, Vector{vec2_f32, vec2_f32, vec2_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kClamp);
-    EXPECT_EQ(result.sem->ReturnType(), vec2_f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), vec2_f32);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), vec2_f32);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), vec2_f32);
+    EXPECT_EQ(result->return_type, vec2_f32);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, vec2_f32);
+    EXPECT_EQ(result->parameters[1].type, vec2_f32);
+    EXPECT_EQ(result->parameters[2].type, vec2_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchOpenSizeVector) {
@@ -502,7 +483,7 @@
     auto* vec2_f32 = create<type::Vector>(f32, 2u);
     auto result = table->Lookup(core::Function::kClamp, Vector{vec2_f32, u32, vec2_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -512,12 +493,11 @@
     auto* mat3_f32 = create<type::Matrix>(vec3_f32, 3u);
     auto result = table->Lookup(core::Function::kDeterminant, Vector{mat3_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Type(), core::Function::kDeterminant);
-    EXPECT_EQ(result.sem->ReturnType(), f32);
-    ASSERT_EQ(result.sem->Parameters().Length(), 1u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), mat3_f32);
+    EXPECT_EQ(result->return_type, f32);
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, mat3_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchOpenSizeMatrix) {
@@ -526,7 +506,7 @@
     auto* mat3x2_f32 = create<type::Matrix>(vec2_f32, 3u);
     auto result = table->Lookup(core::Function::kDeterminant, Vector{mat3x2_f32},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -535,15 +515,14 @@
     auto* bool_ = create<type::Bool>();
     auto result = table->Lookup(core::Function::kSelect, Vector{af, af, bool_},
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Stage(), EvaluationStage::kConstant);
-    EXPECT_EQ(result.sem->Type(), core::Function::kSelect);
-    EXPECT_EQ(result.sem->ReturnType(), af);
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_EQ(result.sem->Parameters()[0]->Type(), af);
-    EXPECT_EQ(result.sem->Parameters()[1]->Type(), af);
-    EXPECT_EQ(result.sem->Parameters()[2]->Type(), bool_);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
+    EXPECT_EQ(result->return_type, af);
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, af);
+    EXPECT_EQ(result->parameters[1].type, af);
+    EXPECT_EQ(result->parameters[2].type, bool_);
 }
 
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Builtin_RuntimeEval) {
@@ -552,15 +531,14 @@
                                              core::Access::kReadWrite);
     auto result = table->Lookup(core::Function::kSelect, Vector{af, af, bool_ref},
                                 EvaluationStage::kRuntime, Source{});
-    ASSERT_NE(result.sem, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.sem->Stage(), EvaluationStage::kConstant);
-    EXPECT_EQ(result.sem->Type(), core::Function::kSelect);
-    EXPECT_TRUE(result.sem->ReturnType()->Is<type::F32>());
-    ASSERT_EQ(result.sem->Parameters().Length(), 3u);
-    EXPECT_TRUE(result.sem->Parameters()[0]->Type()->Is<type::F32>());
-    EXPECT_TRUE(result.sem->Parameters()[1]->Type()->Is<type::F32>());
-    EXPECT_TRUE(result.sem->Parameters()[2]->Type()->Is<type::Bool>());
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
+    EXPECT_TRUE(result->return_type->Is<type::F32>());
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_TRUE(result->parameters[0].type->Is<type::F32>());
+    EXPECT_TRUE(result->parameters[1].type->Is<type::F32>());
+    EXPECT_TRUE(result->parameters[2].type->Is<type::Bool>());
 }
 
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Binary_ConstantEval) {
@@ -568,12 +546,12 @@
     auto* u32 = create<type::U32>();
     auto result = table->Lookup(core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kConstant,
                                 Source{}, false);
-    ASSERT_NE(result.result, nullptr) << Diagnostics().str();
-    ASSERT_NE(result.const_eval_fn, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
+    ASSERT_NE(result->info->const_eval_fn, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_EQ(result.result, ai);
-    EXPECT_EQ(result.lhs, ai);
-    EXPECT_EQ(result.rhs, u32);
+    EXPECT_EQ(result->return_type, ai);
+    EXPECT_EQ(result->parameters[0].type, ai);
+    EXPECT_EQ(result->parameters[1].type, u32);
 }
 
 TEST_F(IntrinsicTableTest, MatchDifferentArgsElementType_Binary_RuntimeEval) {
@@ -581,20 +559,21 @@
     auto* u32 = create<type::U32>();
     auto result = table->Lookup(core::BinaryOp::kShiftLeft, ai, u32, EvaluationStage::kRuntime,
                                 Source{}, false);
-    ASSERT_NE(result.result, nullptr) << Diagnostics().str();
-    ASSERT_NE(result.const_eval_fn, nullptr) << Diagnostics().str();
+    ASSERT_TRUE(result) << Diagnostics().str();
+    ASSERT_NE(result->info->const_eval_fn, nullptr) << Diagnostics().str();
     ASSERT_EQ(Diagnostics().str(), "");
-    EXPECT_TRUE(result.result->Is<type::I32>());
-    EXPECT_TRUE(result.lhs->Is<type::I32>());
-    EXPECT_TRUE(result.rhs->Is<type::U32>());
+    EXPECT_TRUE(result->return_type->Is<type::I32>());
+    EXPECT_TRUE(result->parameters[0].type->Is<type::I32>());
+    EXPECT_TRUE(result->parameters[1].type->Is<type::U32>());
 }
 
 TEST_F(IntrinsicTableTest, OverloadOrderByNumberOfParameters) {
     // None of the arguments match, so expect the overloads with 2 parameters to
     // come first
     auto* bool_ = create<type::Bool>();
-    table->Lookup(core::Function::kTextureDimensions, Vector{bool_, bool_},
-                  EvaluationStage::kConstant, Source{});
+    auto result = table->Lookup(core::Function::kTextureDimensions, Vector{bool_, bool_},
+                                EvaluationStage::kConstant, Source{});
+    EXPECT_FALSE(result);
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(bool, bool)
 
@@ -632,8 +611,9 @@
 TEST_F(IntrinsicTableTest, OverloadOrderByMatchingParameter) {
     auto* tex = create<type::DepthTexture>(type::TextureDimension::k2d);
     auto* bool_ = create<type::Bool>();
-    table->Lookup(core::Function::kTextureDimensions, Vector{tex, bool_},
-                  EvaluationStage::kConstant, Source{});
+    auto result = table->Lookup(core::Function::kTextureDimensions, Vector{tex, bool_},
+                                EvaluationStage::kConstant, Source{});
+    EXPECT_FALSE(result);
     ASSERT_EQ(Diagnostics().str(),
               R"(error: no matching call to textureDimensions(texture_depth_2d, bool)
 
@@ -668,35 +648,12 @@
 )");
 }
 
-TEST_F(IntrinsicTableTest, SameOverloadReturnsSameBuiltinPointer) {
-    auto* f32 = create<type::F32>();
-    auto* vec2_f32 = create<type::Vector>(create<type::F32>(), 2u);
-    auto* bool_ = create<type::Bool>();
-    auto a = table->Lookup(core::Function::kSelect, Vector{f32, f32, bool_},
-                           EvaluationStage::kConstant, Source{});
-    ASSERT_NE(a.sem, nullptr) << Diagnostics().str();
-
-    auto b = table->Lookup(core::Function::kSelect, Vector{f32, f32, bool_},
-                           EvaluationStage::kConstant, Source{});
-    ASSERT_NE(b.sem, nullptr) << Diagnostics().str();
-    ASSERT_EQ(Diagnostics().str(), "");
-
-    auto c = table->Lookup(core::Function::kSelect, Vector{vec2_f32, vec2_f32, bool_},
-                           EvaluationStage::kConstant, Source{});
-    ASSERT_NE(c.sem, nullptr) << Diagnostics().str();
-    ASSERT_EQ(Diagnostics().str(), "");
-
-    EXPECT_EQ(a.sem, b.sem);
-    EXPECT_NE(a.sem, c.sem);
-    EXPECT_NE(b.sem, c.sem);
-}
-
 TEST_F(IntrinsicTableTest, MatchUnaryOp) {
     auto* i32 = create<type::I32>();
     auto* vec3_i32 = create<type::Vector>(i32, 3u);
     auto result = table->Lookup(core::UnaryOp::kNegation, vec3_i32, EvaluationStage::kConstant,
                                 Source{{12, 34}});
-    EXPECT_EQ(result.result, vec3_i32);
+    EXPECT_EQ(result->return_type, vec3_i32);
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
@@ -704,7 +661,7 @@
     auto* bool_ = create<type::Bool>();
     auto result = table->Lookup(core::UnaryOp::kNegation, bool_, EvaluationStage::kConstant,
                                 Source{{12, 34}});
-    ASSERT_EQ(result.result, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator - (bool)
 
 2 candidate operators:
@@ -717,7 +674,7 @@
     auto* ai = create<type::AbstractInt>();
     auto result =
         table->Lookup(core::UnaryOp::kNegation, ai, EvaluationStage::kConstant, Source{{12, 34}});
-    EXPECT_EQ(result.result, ai);
+    EXPECT_EQ(result->return_type, ai);
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
@@ -725,8 +682,8 @@
     auto* ai = create<type::AbstractInt>();
     auto result =
         table->Lookup(core::UnaryOp::kNegation, ai, EvaluationStage::kRuntime, Source{{12, 34}});
-    EXPECT_NE(result.result, ai);
-    EXPECT_TRUE(result.result->Is<type::I32>());
+    EXPECT_NE(result->return_type, ai);
+    EXPECT_TRUE(result->return_type->Is<type::I32>());
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
@@ -736,9 +693,9 @@
     auto result = table->Lookup(core::BinaryOp::kMultiply, i32, vec3_i32,
                                 EvaluationStage::kConstant, Source{{12, 34}},
                                 /* is_compound */ false);
-    EXPECT_EQ(result.result, vec3_i32);
-    EXPECT_EQ(result.lhs, i32);
-    EXPECT_EQ(result.rhs, vec3_i32);
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, vec3_i32);
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
@@ -748,7 +705,7 @@
     auto result = table->Lookup(core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
                                 Source{{12, 34}},
                                 /* is_compound */ false);
-    ASSERT_EQ(result.result, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator * (f32, bool)
 
 9 candidate operators:
@@ -770,9 +727,9 @@
     auto result = table->Lookup(core::BinaryOp::kMultiply, i32, vec3_i32,
                                 EvaluationStage::kConstant, Source{{12, 34}},
                                 /* is_compound */ true);
-    EXPECT_EQ(result.result, vec3_i32);
-    EXPECT_EQ(result.lhs, i32);
-    EXPECT_EQ(result.rhs, vec3_i32);
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, vec3_i32);
     EXPECT_EQ(Diagnostics().str(), "");
 }
 
@@ -782,7 +739,7 @@
     auto result = table->Lookup(core::BinaryOp::kMultiply, f32, bool_, EvaluationStage::kConstant,
                                 Source{{12, 34}},
                                 /* is_compound */ true);
-    ASSERT_EQ(result.result, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator *= (f32, bool)
 
 9 candidate operators:
@@ -803,14 +760,14 @@
     auto* vec3_i32 = create<type::Vector>(i32, 3u);
     auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{i32, i32, i32},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->ReturnType(), vec3_i32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 3u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[1]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[2]->Type(), i32);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, i32);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeInitializerExplicit) {
@@ -818,14 +775,14 @@
     auto* vec3_i32 = create<type::Vector>(i32, 3u);
     auto result = table->Lookup(CtorConv::kVec3, i32, Vector{i32, i32, i32},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->ReturnType(), vec3_i32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 3u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[1]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[2]->Type(), i32);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, i32);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTypeInitializerImplicit) {
@@ -833,7 +790,7 @@
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{i32, f32, i32},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_EQ(result.target, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3(i32, f32, i32)
 
@@ -860,7 +817,7 @@
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(CtorConv::kVec3, i32, Vector{i32, f32, i32},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_EQ(result.target, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<i32>(i32, f32, i32)
 
@@ -887,12 +844,12 @@
     auto* vec3_ai = create<type::Vector>(ai, 3u);
     auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{vec3_ai},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->ReturnType(), vec3_ai);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), vec3_ai);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_EQ(result->return_type, vec3_ai);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, vec3_ai);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeInitializerImplicitMatFromVec) {
@@ -902,13 +859,13 @@
     auto* mat2x2_af = create<type::Matrix>(vec2_af, 2u);
     auto result = table->Lookup(CtorConv::kMat2x2, nullptr, Vector{vec2_ai, vec2_ai},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_TYPE(result.target->ReturnType(), mat2x2_af);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 2u);
-    EXPECT_TYPE(result.target->Parameters()[0]->Type(), vec2_af);
-    EXPECT_TYPE(result.target->Parameters()[1]->Type(), vec2_af);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_TYPE(result->return_type, mat2x2_af);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 2u);
+    EXPECT_TYPE(result->parameters[0].type, vec2_af);
+    EXPECT_TYPE(result->parameters[1].type, vec2_af);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeInitializer_ConstantEval) {
@@ -916,15 +873,15 @@
     auto* vec3_ai = create<type::Vector>(ai, 3u);
     auto result = table->Lookup(CtorConv::kVec3, nullptr, Vector{ai, ai, ai},
                                 EvaluationStage::kConstant, Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->Stage(), EvaluationStage::kConstant);
-    EXPECT_EQ(result.target->ReturnType(), vec3_ai);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 3u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), ai);
-    EXPECT_EQ(result.target->Parameters()[1]->Type(), ai);
-    EXPECT_EQ(result.target->Parameters()[2]->Type(), ai);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
+    EXPECT_EQ(result->return_type, vec3_ai);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, ai);
+    EXPECT_EQ(result->parameters[1].type, ai);
+    EXPECT_EQ(result->parameters[2].type, ai);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeInitializer_RuntimeEval) {
@@ -933,15 +890,15 @@
                                 EvaluationStage::kRuntime, Source{{12, 34}});
     auto* i32 = create<type::I32>();
     auto* vec3_i32 = create<type::Vector>(i32, 3u);
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->Stage(), EvaluationStage::kConstant);
-    EXPECT_EQ(result.target->ReturnType(), vec3_i32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConstructor>());
-    ASSERT_EQ(result.target->Parameters().Length(), 3u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[1]->Type(), i32);
-    EXPECT_EQ(result.target->Parameters()[2]->Type(), i32);
-    EXPECT_NE(result.const_eval_fn, nullptr);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_TRUE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 3u);
+    EXPECT_EQ(result->parameters[0].type, i32);
+    EXPECT_EQ(result->parameters[1].type, i32);
+    EXPECT_EQ(result->parameters[2].type, i32);
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeConversion) {
@@ -951,11 +908,11 @@
     auto* vec3_f32 = create<type::Vector>(f32, 3u);
     auto result = table->Lookup(CtorConv::kVec3, i32, Vector{vec3_f32}, EvaluationStage::kConstant,
                                 Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->ReturnType(), vec3_i32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConversion>());
-    ASSERT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), vec3_f32);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_EQ(result->return_type, vec3_i32);
+    EXPECT_FALSE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, vec3_f32);
 }
 
 TEST_F(IntrinsicTableTest, MismatchTypeConversion) {
@@ -964,7 +921,7 @@
     auto* f32 = create<type::F32>();
     auto result = table->Lookup(CtorConv::kVec3, f32, Vector{arr}, EvaluationStage::kConstant,
                                 Source{{12, 34}});
-    ASSERT_EQ(result.target, nullptr);
+    ASSERT_FALSE(result);
     EXPECT_EQ(Diagnostics().str(),
               R"(12:34 error: no matching constructor for vec3<f32>(array<u32>)
 
@@ -994,13 +951,13 @@
     auto* vec3_f32 = create<type::Vector>(f32, 3u);
     auto result = table->Lookup(CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kConstant,
                                 Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->Stage(), EvaluationStage::kConstant);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
     // NOTE: Conversions are explicit, so there's no way to have it return abstracts
-    EXPECT_EQ(result.target->ReturnType(), vec3_f32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConversion>());
-    ASSERT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), vec3_ai);
+    EXPECT_EQ(result->return_type, vec3_f32);
+    EXPECT_FALSE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, vec3_ai);
 }
 
 TEST_F(IntrinsicTableTest, MatchTypeConversion_RuntimeEval) {
@@ -1011,12 +968,12 @@
     auto* vec3_i32 = create<type::Vector>(create<type::I32>(), 3u);
     auto result = table->Lookup(CtorConv::kVec3, af, Vector{vec3_ai}, EvaluationStage::kRuntime,
                                 Source{{12, 34}});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->Stage(), EvaluationStage::kConstant);
-    EXPECT_EQ(result.target->ReturnType(), vec3_f32);
-    EXPECT_TRUE(result.target->Is<sem::ValueConversion>());
-    ASSERT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), vec3_i32);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_NE(result->info->const_eval_fn, nullptr);
+    EXPECT_EQ(result->return_type, vec3_f32);
+    EXPECT_FALSE(result->info->flags.Contains(TableData::OverloadFlag::kIsConstructor));
+    ASSERT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, vec3_i32);
 }
 
 TEST_F(IntrinsicTableTest, Err257Arguments) {  // crbug.com/1323605
@@ -1025,7 +982,7 @@
     arg_tys.Resize(257, f32);
     auto result = table->Lookup(core::Function::kAbs, std::move(arg_tys),
                                 EvaluationStage::kConstant, Source{});
-    ASSERT_EQ(result.sem, nullptr);
+    ASSERT_FALSE(result);
     ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
 }
 
@@ -1038,10 +995,10 @@
     auto* i32 = create<type::I32>();
     auto result =
         table->Lookup(CtorConv::kI32, nullptr, Vector{ai}, EvaluationStage::kConstant, Source{});
-    ASSERT_NE(result.target, nullptr);
-    EXPECT_EQ(result.target->ReturnType(), i32);
-    EXPECT_EQ(result.target->Parameters().Length(), 1u);
-    EXPECT_EQ(result.target->Parameters()[0]->Type(), ai);
+    ASSERT_TRUE(result) << Diagnostics().str();
+    EXPECT_EQ(result->return_type, i32);
+    EXPECT_EQ(result->parameters.Length(), 1u);
+    EXPECT_EQ(result->parameters[0].type, ai);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1073,7 +1030,8 @@
 };
 
 struct IntrinsicTableAbstractBinaryTest : public resolver::ResolverTestWithParam<Case> {
-    std::unique_ptr<Table> table = Table::Create(*this);
+    std::unique_ptr<Table> table =
+        Table::Create(core::kIntrinsicData, Types(), Symbols(), Diagnostics());
 };
 
 TEST_P(IntrinsicTableAbstractBinaryTest, MatchAdd) {
@@ -1083,18 +1041,20 @@
                                 Source{{12, 34}},
                                 /* is_compound */ false);
 
-    bool matched = result.result != nullptr;
+    bool matched = result;
     bool expected_match = GetParam().expected_match;
     EXPECT_EQ(matched, expected_match) << Diagnostics().str();
 
-    auto* expected_result = GetParam().expected_result(*this);
-    EXPECT_TYPE(result.result, expected_result);
+    if (matched) {
+        auto* expected_result = GetParam().expected_result(*this);
+        EXPECT_TYPE(result->return_type, expected_result);
 
-    auto* expected_param_lhs = GetParam().expected_param_lhs(*this);
-    EXPECT_TYPE(result.lhs, expected_param_lhs);
+        auto* expected_param_lhs = GetParam().expected_param_lhs(*this);
+        EXPECT_TYPE(result->parameters[0].type, expected_param_lhs);
 
-    auto* expected_param_rhs = GetParam().expected_param_rhs(*this);
-    EXPECT_TYPE(result.rhs, expected_param_rhs);
+        auto* expected_param_rhs = GetParam().expected_param_rhs(*this);
+        EXPECT_TYPE(result->parameters[1].type, expected_param_rhs);
+    }
 }
 
 INSTANTIATE_TEST_SUITE_P(AFloat_AInt,
@@ -1255,7 +1215,8 @@
 };
 
 struct IntrinsicTableAbstractTernaryTest : public resolver::ResolverTestWithParam<Case> {
-    std::unique_ptr<Table> table = Table::Create(*this);
+    std::unique_ptr<Table> table =
+        Table::Create(core::kIntrinsicData, Types(), Symbols(), Diagnostics());
 };
 
 TEST_P(IntrinsicTableAbstractTernaryTest, MatchClamp) {
@@ -1265,23 +1226,22 @@
     auto builtin = table->Lookup(core::Function::kClamp, Vector{arg_a, arg_b, arg_c},
                                  EvaluationStage::kConstant, Source{{12, 34}});
 
-    bool matched = builtin.sem != nullptr;
     bool expected_match = GetParam().expected_match;
-    EXPECT_EQ(matched, expected_match) << Diagnostics().str();
+    EXPECT_EQ(builtin == true, expected_match) << Diagnostics().str();
 
-    auto* result = builtin.sem ? builtin.sem->ReturnType() : nullptr;
+    auto* result = builtin ? builtin->return_type : nullptr;
     auto* expected_result = GetParam().expected_result(*this);
     EXPECT_TYPE(result, expected_result);
 
-    auto* param_a = builtin.sem ? builtin.sem->Parameters()[0]->Type() : nullptr;
+    auto* param_a = builtin ? builtin->parameters[0].type : nullptr;
     auto* expected_param_a = GetParam().expected_param_a(*this);
     EXPECT_TYPE(param_a, expected_param_a);
 
-    auto* param_b = builtin.sem ? builtin.sem->Parameters()[1]->Type() : nullptr;
+    auto* param_b = builtin ? builtin->parameters[1].type : nullptr;
     auto* expected_param_b = GetParam().expected_param_b(*this);
     EXPECT_TYPE(param_b, expected_param_b);
 
-    auto* param_c = builtin.sem ? builtin.sem->Parameters()[2]->Type() : nullptr;
+    auto* param_c = builtin ? builtin->parameters[2].type : nullptr;
     auto* expected_param_c = GetParam().expected_param_c(*this);
     EXPECT_TYPE(param_c, expected_param_c);
 }
diff --git a/src/tint/lang/core/intrinsic/core_table_data.cc b/src/tint/lang/core/intrinsic_data.cc
similarity index 98%
rename from src/tint/lang/core/intrinsic/core_table_data.cc
rename to src/tint/lang/core/intrinsic_data.cc
index c46680b..ea3daf9 100644
--- a/src/tint/lang/core/intrinsic/core_table_data.cc
+++ b/src/tint/lang/core/intrinsic_data.cc
@@ -15,7 +15,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 // File generated by tools/src/cmd/gen
 // using the template:
-//   src/tint/lang/core/intrinsic/core_table_data.cc.tmpl
+//   src/tint/lang/core/intrinsic_data.cc.tmpl
 //
 // Do not modify this file directly
 ////////////////////////////////////////////////////////////////////////////////
@@ -23,11 +23,11 @@
 #include <limits>
 #include <string>
 
-#include "src/tint/lang/core/intrinsic/core_table_data.h"
-#include "src/tint/lang/core/intrinsic/core_type_matchers.h"
+#include "src/tint/lang/core/intrinsic_data.h"
+#include "src/tint/lang/core/intrinsic_type_matchers.h"
 #include "src/tint/utils/text/string_stream.h"
 
-namespace tint::core::intrinsic {
+namespace tint::core {
 namespace {
 
 using IntrinsicInfo = tint::core::intrinsic::TableData::IntrinsicInfo;
@@ -14367,35 +14367,32 @@
 
 }  // anonymous namespace
 
-const TableData& CoreTableData() {
-    static const TableData data{
-        /* type_matchers */ kTypeMatchers,
-        /* number_matchers */ kNumberMatchers,
-        /* ctor_conv */ kConstructorsAndConverters,
-        /* builtins */ kBuiltins,
-        /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
-        /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
-        /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
-        /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
-        /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
-        /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
-        /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
-        /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
-        /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
-        /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
-        /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
-        /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
-        /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
-        /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
-        /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
-        /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
-        /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
-        /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
-        /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
-        /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
-        /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
-    };
-    return data;
-}
+const core::intrinsic::TableData kIntrinsicData{
+    /* type_matchers */ kTypeMatchers,
+    /* number_matchers */ kNumberMatchers,
+    /* ctor_conv */ kConstructorsAndConverters,
+    /* builtins */ kBuiltins,
+    /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
+    /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
+    /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
+    /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
+    /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
+    /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
+    /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
+    /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
+    /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
+    /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
+    /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
+    /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
+    /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
+    /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
+    /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
+    /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
+    /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
+    /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
+    /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
+    /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
+    /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
+};
 
-}  // namespace tint::core::intrinsic
+}  // namespace tint::core
diff --git a/src/tint/lang/core/intrinsic/core_table_data.cc.tmpl b/src/tint/lang/core/intrinsic_data.cc.tmpl
similarity index 77%
rename from src/tint/lang/core/intrinsic/core_table_data.cc.tmpl
rename to src/tint/lang/core/intrinsic_data.cc.tmpl
index e0b096b..25d601d 100644
--- a/src/tint/lang/core/intrinsic/core_table_data.cc.tmpl
+++ b/src/tint/lang/core/intrinsic_data.cc.tmpl
@@ -19,8 +19,8 @@
 #include <limits>
 #include <string>
 
-#include "src/tint/lang/core/intrinsic/core_table_data.h"
-#include "src/tint/lang/core/intrinsic/core_type_matchers.h"
+#include "src/tint/lang/core/intrinsic_data.h"
+#include "src/tint/lang/core/intrinsic_type_matchers.h"
 #include "src/tint/utils/text/string_stream.h"
 
-{{ Eval "Data" "Intrinsics" $I "Namespace" "tint::core::intrinsic" "Name" "CoreTableData" -}}
+{{ Eval "Data" "Intrinsics" $I "Namespace" "tint::core" "Name" "kIntrinsicData" -}}
diff --git a/src/tint/lang/core/intrinsic/core_table_data.h b/src/tint/lang/core/intrinsic_data.h
similarity index 69%
rename from src/tint/lang/core/intrinsic/core_table_data.h
rename to src/tint/lang/core/intrinsic_data.h
index a9a7bd1..7275126 100644
--- a/src/tint/lang/core/intrinsic/core_table_data.h
+++ b/src/tint/lang/core/intrinsic_data.h
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef SRC_TINT_LANG_CORE_INTRINSIC_CORE_TABLE_DATA_H_
-#define SRC_TINT_LANG_CORE_INTRINSIC_CORE_TABLE_DATA_H_
+#ifndef SRC_TINT_LANG_CORE_INTRINSIC_DATA_H_
+#define SRC_TINT_LANG_CORE_INTRINSIC_DATA_H_
 
 #include "src/tint/lang/core/intrinsic/table_data.h"
 
-namespace tint::core::intrinsic {
+namespace tint::core {
 
-const TableData& CoreTableData();
+extern const intrinsic::TableData kIntrinsicData;
 
-}  // namespace tint::core::intrinsic
+}  // namespace tint::core
 
-#endif  // SRC_TINT_LANG_CORE_INTRINSIC_CORE_TABLE_DATA_H_
+#endif  // SRC_TINT_LANG_CORE_INTRINSIC_DATA_H_
diff --git a/src/tint/lang/core/intrinsic_type_matchers.h b/src/tint/lang/core/intrinsic_type_matchers.h
new file mode 100644
index 0000000..3319b6c
--- /dev/null
+++ b/src/tint/lang/core/intrinsic_type_matchers.h
@@ -0,0 +1,594 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_LANG_CORE_INTRINSIC_TYPE_MATCHERS_H_
+#define SRC_TINT_LANG_CORE_INTRINSIC_TYPE_MATCHERS_H_
+
+#include "src/tint/lang/core/evaluation_stage.h"
+#include "src/tint/lang/core/intrinsic/table_data.h"
+#include "src/tint/lang/core/type/abstract_float.h"
+#include "src/tint/lang/core/type/abstract_int.h"
+#include "src/tint/lang/core/type/abstract_numeric.h"
+#include "src/tint/lang/core/type/array.h"
+#include "src/tint/lang/core/type/atomic.h"
+#include "src/tint/lang/core/type/bool.h"
+#include "src/tint/lang/core/type/builtin_structs.h"
+#include "src/tint/lang/core/type/depth_multisampled_texture.h"
+#include "src/tint/lang/core/type/depth_texture.h"
+#include "src/tint/lang/core/type/external_texture.h"
+#include "src/tint/lang/core/type/f16.h"
+#include "src/tint/lang/core/type/f32.h"
+#include "src/tint/lang/core/type/i32.h"
+#include "src/tint/lang/core/type/manager.h"
+#include "src/tint/lang/core/type/matrix.h"
+#include "src/tint/lang/core/type/multisampled_texture.h"
+#include "src/tint/lang/core/type/pointer.h"
+#include "src/tint/lang/core/type/sampled_texture.h"
+#include "src/tint/lang/core/type/storage_texture.h"
+#include "src/tint/lang/core/type/texture_dimension.h"
+#include "src/tint/lang/core/type/u32.h"
+#include "src/tint/lang/core/type/vector.h"
+
+namespace tint::core {
+
+inline bool match_bool(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::Bool>();
+}
+
+inline const type::AbstractFloat* build_fa(intrinsic::TableData::MatchState& state) {
+    return state.types.AFloat();
+}
+
+inline bool match_fa(intrinsic::TableData::MatchState& state, const type::Type* ty) {
+    return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
+           ty->IsAnyOf<intrinsic::TableData::Any, type::AbstractNumeric>();
+}
+
+inline const type::AbstractInt* build_ia(intrinsic::TableData::MatchState& state) {
+    return state.types.AInt();
+}
+
+inline bool match_ia(intrinsic::TableData::MatchState& state, const type::Type* ty) {
+    return (state.earliest_eval_stage <= EvaluationStage::kConstant) &&
+           ty->IsAnyOf<intrinsic::TableData::Any, type::AbstractInt>();
+}
+
+inline const type::Bool* build_bool(intrinsic::TableData::MatchState& state) {
+    return state.types.bool_();
+}
+
+inline const type::F16* build_f16(intrinsic::TableData::MatchState& state) {
+    return state.types.f16();
+}
+
+inline bool match_f16(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::F16, type::AbstractNumeric>();
+}
+
+inline const type::F32* build_f32(intrinsic::TableData::MatchState& state) {
+    return state.types.f32();
+}
+
+inline bool match_f32(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::F32, type::AbstractNumeric>();
+}
+
+inline const type::I32* build_i32(intrinsic::TableData::MatchState& state) {
+    return state.types.i32();
+}
+
+inline bool match_i32(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::I32, type::AbstractInt>();
+}
+
+inline const type::U32* build_u32(intrinsic::TableData::MatchState& state) {
+    return state.types.u32();
+}
+
+inline bool match_u32(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::U32, type::AbstractInt>();
+}
+
+inline bool match_vec(intrinsic::TableData::MatchState&,
+                      const type::Type* ty,
+                      intrinsic::TableData::Number& N,
+                      const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        N = intrinsic::TableData::Number::any;
+        T = ty;
+        return true;
+    }
+
+    if (auto* v = ty->As<type::Vector>()) {
+        N = v->Width();
+        T = v->type();
+        return true;
+    }
+    return false;
+}
+
+template <uint32_t N>
+inline bool match_vec(intrinsic::TableData::MatchState&,
+                      const type::Type* ty,
+                      const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+
+    if (auto* v = ty->As<type::Vector>()) {
+        if (v->Width() == N) {
+            T = v->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+inline const type::Vector* build_vec(intrinsic::TableData::MatchState& state,
+                                     intrinsic::TableData::Number N,
+                                     const type::Type* el) {
+    return state.types.vec(el, N.Value());
+}
+
+template <uint32_t N>
+inline const type::Vector* build_vec(intrinsic::TableData::MatchState& state,
+                                     const type::Type* el) {
+    return state.types.vec(el, N);
+}
+
+constexpr auto match_vec2 = match_vec<2>;
+constexpr auto match_vec3 = match_vec<3>;
+constexpr auto match_vec4 = match_vec<4>;
+
+constexpr auto build_vec2 = build_vec<2>;
+constexpr auto build_vec3 = build_vec<3>;
+constexpr auto build_vec4 = build_vec<4>;
+
+inline bool match_packedVec3(intrinsic::TableData::MatchState&,
+                             const type::Type* ty,
+                             const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+
+    if (auto* v = ty->As<type::Vector>()) {
+        if (v->Packed()) {
+            T = v->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+inline const type::Vector* build_packedVec3(intrinsic::TableData::MatchState& state,
+                                            const type::Type* el) {
+    return state.types.Get<type::Vector>(el, 3u, /* packed */ true);
+}
+
+inline bool match_mat(intrinsic::TableData::MatchState&,
+                      const type::Type* ty,
+                      intrinsic::TableData::Number& M,
+                      intrinsic::TableData::Number& N,
+                      const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        M = intrinsic::TableData::Number::any;
+        N = intrinsic::TableData::Number::any;
+        T = ty;
+        return true;
+    }
+    if (auto* m = ty->As<type::Matrix>()) {
+        M = m->columns();
+        N = m->ColumnType()->Width();
+        T = m->type();
+        return true;
+    }
+    return false;
+}
+
+template <uint32_t C, uint32_t R>
+inline bool match_mat(intrinsic::TableData::MatchState&,
+                      const type::Type* ty,
+                      const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+    if (auto* m = ty->As<type::Matrix>()) {
+        if (m->columns() == C && m->rows() == R) {
+            T = m->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+inline const type::Matrix* build_mat(intrinsic::TableData::MatchState& state,
+                                     intrinsic::TableData::Number C,
+                                     intrinsic::TableData::Number R,
+                                     const type::Type* T) {
+    auto* column_type = state.types.vec(T, R.Value());
+    return state.types.mat(column_type, C.Value());
+}
+
+template <uint32_t C, uint32_t R>
+inline const type::Matrix* build_mat(intrinsic::TableData::MatchState& state, const type::Type* T) {
+    auto* column_type = state.types.vec(T, R);
+    return state.types.mat(column_type, C);
+}
+
+constexpr auto build_mat2x2 = build_mat<2, 2>;
+constexpr auto build_mat2x3 = build_mat<2, 3>;
+constexpr auto build_mat2x4 = build_mat<2, 4>;
+constexpr auto build_mat3x2 = build_mat<3, 2>;
+constexpr auto build_mat3x3 = build_mat<3, 3>;
+constexpr auto build_mat3x4 = build_mat<3, 4>;
+constexpr auto build_mat4x2 = build_mat<4, 2>;
+constexpr auto build_mat4x3 = build_mat<4, 3>;
+constexpr auto build_mat4x4 = build_mat<4, 4>;
+
+constexpr auto match_mat2x2 = match_mat<2, 2>;
+constexpr auto match_mat2x3 = match_mat<2, 3>;
+constexpr auto match_mat2x4 = match_mat<2, 4>;
+constexpr auto match_mat3x2 = match_mat<3, 2>;
+constexpr auto match_mat3x3 = match_mat<3, 3>;
+constexpr auto match_mat3x4 = match_mat<3, 4>;
+constexpr auto match_mat4x2 = match_mat<4, 2>;
+constexpr auto match_mat4x3 = match_mat<4, 3>;
+constexpr auto match_mat4x4 = match_mat<4, 4>;
+
+inline bool match_array(intrinsic::TableData::MatchState&,
+                        const type::Type* ty,
+                        const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+
+    if (auto* a = ty->As<type::Array>()) {
+        if (a->Count()->Is<type::RuntimeArrayCount>()) {
+            T = a->ElemType();
+            return true;
+        }
+    }
+    return false;
+}
+
+inline const type::Array* build_array(intrinsic::TableData::MatchState& state,
+                                      const type::Type* el) {
+    return state.types.Get<type::Array>(el,
+                                        /* count */ state.types.Get<type::RuntimeArrayCount>(),
+                                        /* align */ 0u,
+                                        /* size */ 0u,
+                                        /* stride */ 0u,
+                                        /* stride_implicit */ 0u);
+}
+
+inline bool match_ptr(intrinsic::TableData::MatchState&,
+                      const type::Type* ty,
+                      intrinsic::TableData::Number& S,
+                      const type::Type*& T,
+                      intrinsic::TableData::Number& A) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        S = intrinsic::TableData::Number::any;
+        T = ty;
+        A = intrinsic::TableData::Number::any;
+        return true;
+    }
+
+    if (auto* p = ty->As<type::Pointer>()) {
+        S = intrinsic::TableData::Number(static_cast<uint32_t>(p->AddressSpace()));
+        T = p->StoreType();
+        A = intrinsic::TableData::Number(static_cast<uint32_t>(p->Access()));
+        return true;
+    }
+    return false;
+}
+
+inline const type::Pointer* build_ptr(intrinsic::TableData::MatchState& state,
+                                      intrinsic::TableData::Number S,
+                                      const type::Type* T,
+                                      intrinsic::TableData::Number& A) {
+    return state.types.ptr(static_cast<core::AddressSpace>(S.Value()), T,
+                           static_cast<core::Access>(A.Value()));
+}
+
+inline bool match_atomic(intrinsic::TableData::MatchState&,
+                         const type::Type* ty,
+                         const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+
+    if (auto* a = ty->As<type::Atomic>()) {
+        T = a->Type();
+        return true;
+    }
+    return false;
+}
+
+inline const type::Atomic* build_atomic(intrinsic::TableData::MatchState& state,
+                                        const type::Type* T) {
+    return state.types.atomic(T);
+}
+
+inline bool match_sampler(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        return true;
+    }
+    return ty->Is([](const type::Sampler* s) { return s->kind() == type::SamplerKind::kSampler; });
+}
+
+inline const type::Sampler* build_sampler(intrinsic::TableData::MatchState& state) {
+    return state.types.sampler();
+}
+
+inline bool match_sampler_comparison(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        return true;
+    }
+    return ty->Is(
+        [](const type::Sampler* s) { return s->kind() == type::SamplerKind::kComparisonSampler; });
+}
+
+inline const type::Sampler* build_sampler_comparison(intrinsic::TableData::MatchState& state) {
+    return state.types.comparison_sampler();
+}
+
+inline bool match_texture(intrinsic::TableData::MatchState&,
+                          const type::Type* ty,
+                          type::TextureDimension dim,
+                          const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+    if (auto* v = ty->As<type::SampledTexture>()) {
+        if (v->dim() == dim) {
+            T = v->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+#define JOIN(a, b) a##b
+
+#define DECLARE_SAMPLED_TEXTURE(suffix, dim)                                               \
+    inline bool JOIN(match_texture_, suffix)(intrinsic::TableData::MatchState & state,     \
+                                             const type::Type* ty, const type::Type*& T) { \
+        return match_texture(state, ty, dim, T);                                           \
+    }                                                                                      \
+    inline const type::SampledTexture* JOIN(build_texture_, suffix)(                       \
+        intrinsic::TableData::MatchState & state, const type::Type* T) {                   \
+        return state.types.Get<type::SampledTexture>(dim, T);                              \
+    }
+
+DECLARE_SAMPLED_TEXTURE(1d, type::TextureDimension::k1d)
+DECLARE_SAMPLED_TEXTURE(2d, type::TextureDimension::k2d)
+DECLARE_SAMPLED_TEXTURE(2d_array, type::TextureDimension::k2dArray)
+DECLARE_SAMPLED_TEXTURE(3d, type::TextureDimension::k3d)
+DECLARE_SAMPLED_TEXTURE(cube, type::TextureDimension::kCube)
+DECLARE_SAMPLED_TEXTURE(cube_array, type::TextureDimension::kCubeArray)
+#undef DECLARE_SAMPLED_TEXTURE
+
+inline bool match_texture_multisampled(intrinsic::TableData::MatchState&,
+                                       const type::Type* ty,
+                                       type::TextureDimension dim,
+                                       const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+    if (auto* v = ty->As<type::MultisampledTexture>()) {
+        if (v->dim() == dim) {
+            T = v->type();
+            return true;
+        }
+    }
+    return false;
+}
+
+#define DECLARE_MULTISAMPLED_TEXTURE(suffix, dim)                                               \
+    inline bool JOIN(match_texture_multisampled_, suffix)(                                      \
+        intrinsic::TableData::MatchState & state, const type::Type* ty, const type::Type*& T) { \
+        return match_texture_multisampled(state, ty, dim, T);                                   \
+    }                                                                                           \
+    inline const type::MultisampledTexture* JOIN(build_texture_multisampled_, suffix)(          \
+        intrinsic::TableData::MatchState & state, const type::Type* T) {                        \
+        return state.types.Get<type::MultisampledTexture>(dim, T);                              \
+    }
+
+DECLARE_MULTISAMPLED_TEXTURE(2d, type::TextureDimension::k2d)
+#undef DECLARE_MULTISAMPLED_TEXTURE
+
+inline bool match_texture_depth(intrinsic::TableData::MatchState&,
+                                const type::Type* ty,
+                                type::TextureDimension dim) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        return true;
+    }
+    return ty->Is([&](const type::DepthTexture* t) { return t->dim() == dim; });
+}
+
+#define DECLARE_DEPTH_TEXTURE(suffix, dim)                                                    \
+    inline bool JOIN(match_texture_depth_, suffix)(intrinsic::TableData::MatchState & state,  \
+                                                   const type::Type* ty) {                    \
+        return match_texture_depth(state, ty, dim);                                           \
+    }                                                                                         \
+    inline const type::DepthTexture* JOIN(build_texture_depth_,                               \
+                                          suffix)(intrinsic::TableData::MatchState & state) { \
+        return state.types.Get<type::DepthTexture>(dim);                                      \
+    }
+
+DECLARE_DEPTH_TEXTURE(2d, type::TextureDimension::k2d)
+DECLARE_DEPTH_TEXTURE(2d_array, type::TextureDimension::k2dArray)
+DECLARE_DEPTH_TEXTURE(cube, type::TextureDimension::kCube)
+DECLARE_DEPTH_TEXTURE(cube_array, type::TextureDimension::kCubeArray)
+#undef DECLARE_DEPTH_TEXTURE
+
+inline bool match_texture_depth_multisampled_2d(intrinsic::TableData::MatchState&,
+                                                const type::Type* ty) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        return true;
+    }
+    return ty->Is([&](const type::DepthMultisampledTexture* t) {
+        return t->dim() == type::TextureDimension::k2d;
+    });
+}
+
+inline type::DepthMultisampledTexture* build_texture_depth_multisampled_2d(
+    intrinsic::TableData::MatchState& state) {
+    return state.types.Get<type::DepthMultisampledTexture>(type::TextureDimension::k2d);
+}
+
+inline bool match_texture_storage(intrinsic::TableData::MatchState&,
+                                  const type::Type* ty,
+                                  type::TextureDimension dim,
+                                  intrinsic::TableData::Number& F,
+                                  intrinsic::TableData::Number& A) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        F = intrinsic::TableData::Number::any;
+        A = intrinsic::TableData::Number::any;
+        return true;
+    }
+    if (auto* v = ty->As<type::StorageTexture>()) {
+        if (v->dim() == dim) {
+            F = intrinsic::TableData::Number(static_cast<uint32_t>(v->texel_format()));
+            A = intrinsic::TableData::Number(static_cast<uint32_t>(v->access()));
+            return true;
+        }
+    }
+    return false;
+}
+
+#define DECLARE_STORAGE_TEXTURE(suffix, dim)                                      \
+    inline bool JOIN(match_texture_storage_, suffix)(                             \
+        intrinsic::TableData::MatchState & state, const type::Type* ty,           \
+        intrinsic::TableData::Number& F, intrinsic::TableData::Number& A) {       \
+        return match_texture_storage(state, ty, dim, F, A);                       \
+    }                                                                             \
+    inline const type::StorageTexture* JOIN(build_texture_storage_, suffix)(      \
+        intrinsic::TableData::MatchState & state, intrinsic::TableData::Number F, \
+        intrinsic::TableData::Number A) {                                         \
+        auto format = static_cast<TexelFormat>(F.Value());                        \
+        auto access = static_cast<Access>(A.Value());                             \
+        auto* T = type::StorageTexture::SubtypeFor(format, state.types);          \
+        return state.types.Get<type::StorageTexture>(dim, format, access, T);     \
+    }
+
+DECLARE_STORAGE_TEXTURE(1d, type::TextureDimension::k1d)
+DECLARE_STORAGE_TEXTURE(2d, type::TextureDimension::k2d)
+DECLARE_STORAGE_TEXTURE(2d_array, type::TextureDimension::k2dArray)
+DECLARE_STORAGE_TEXTURE(3d, type::TextureDimension::k3d)
+#undef DECLARE_STORAGE_TEXTURE
+
+inline bool match_texture_external(intrinsic::TableData::MatchState&, const type::Type* ty) {
+    return ty->IsAnyOf<intrinsic::TableData::Any, type::ExternalTexture>();
+}
+
+inline const type::ExternalTexture* build_texture_external(
+    intrinsic::TableData::MatchState& state) {
+    return state.types.Get<type::ExternalTexture>();
+}
+
+// Builtin types starting with a _ prefix cannot be declared in WGSL, so they
+// can only be used as return types. Because of this, they must only match Any,
+// which is used as the return type matcher.
+inline bool match_modf_result(intrinsic::TableData::MatchState&,
+                              const type::Type* ty,
+                              const type::Type*& T) {
+    if (!ty->Is<intrinsic::TableData::Any>()) {
+        return false;
+    }
+    T = ty;
+    return true;
+}
+inline bool match_modf_result_vec(intrinsic::TableData::MatchState&,
+                                  const type::Type* ty,
+                                  intrinsic::TableData::Number& N,
+                                  const type::Type*& T) {
+    if (!ty->Is<intrinsic::TableData::Any>()) {
+        return false;
+    }
+    N = intrinsic::TableData::Number::any;
+    T = ty;
+    return true;
+}
+inline bool match_frexp_result(intrinsic::TableData::MatchState&,
+                               const type::Type* ty,
+                               const type::Type*& T) {
+    if (!ty->Is<intrinsic::TableData::Any>()) {
+        return false;
+    }
+    T = ty;
+    return true;
+}
+inline bool match_frexp_result_vec(intrinsic::TableData::MatchState&,
+                                   const type::Type* ty,
+                                   intrinsic::TableData::Number& N,
+                                   const type::Type*& T) {
+    if (!ty->Is<intrinsic::TableData::Any>()) {
+        return false;
+    }
+    N = intrinsic::TableData::Number::any;
+    T = ty;
+    return true;
+}
+
+inline bool match_atomic_compare_exchange_result(intrinsic::TableData::MatchState&,
+                                                 const type::Type* ty,
+                                                 const type::Type*& T) {
+    if (ty->Is<intrinsic::TableData::Any>()) {
+        T = ty;
+        return true;
+    }
+    return false;
+}
+
+inline const type::Struct* build_modf_result(intrinsic::TableData::MatchState& state,
+                                             const type::Type* el) {
+    return type::CreateModfResult(state.types, state.symbols, el);
+}
+
+inline const type::Struct* build_modf_result_vec(intrinsic::TableData::MatchState& state,
+                                                 intrinsic::TableData::Number& n,
+                                                 const type::Type* el) {
+    auto* vec = state.types.vec(el, n.Value());
+    return type::CreateModfResult(state.types, state.symbols, vec);
+}
+
+inline const type::Struct* build_frexp_result(intrinsic::TableData::MatchState& state,
+                                              const type::Type* el) {
+    return type::CreateFrexpResult(state.types, state.symbols, el);
+}
+
+inline const type::Struct* build_frexp_result_vec(intrinsic::TableData::MatchState& state,
+                                                  intrinsic::TableData::Number& n,
+                                                  const type::Type* el) {
+    auto* vec = state.types.vec(el, n.Value());
+    return type::CreateFrexpResult(state.types, state.symbols, vec);
+}
+
+inline const type::Struct* build_atomic_compare_exchange_result(
+    intrinsic::TableData::MatchState& state,
+    const type::Type* ty) {
+    return type::CreateAtomicCompareExchangeResult(state.types, state.symbols, ty);
+}
+
+}  // namespace tint::core
+
+#endif  // SRC_TINT_LANG_CORE_INTRINSIC_TYPE_MATCHERS_H_
diff --git a/src/tint/lang/wgsl/resolver/builtin_test.cc b/src/tint/lang/wgsl/resolver/builtin_test.cc
index d9c0a96..52ee20f 100644
--- a/src/tint/lang/wgsl/resolver/builtin_test.cc
+++ b/src/tint/lang/wgsl/resolver/builtin_test.cc
@@ -73,6 +73,26 @@
         R"(12:34 error: const initializer requires a const-expression, but expression is a runtime-expression)");
 }
 
+TEST_F(ResolverBuiltinTest, SameOverloadReturnsSameCallTarget) {
+    // let i = 42i;
+    // let a = select(1_i, 2_i, true);
+    // let b = select(3_i, 4_i, false);
+    // let c = select(5_u, 6_u, true);
+    auto* select_a = Call(core::Function::kSelect, 1_i, 2_i, true);
+    auto* select_b = Call(core::Function::kSelect, 3_i, 4_i, false);
+    auto* select_c = Call(core::Function::kSelect, 5_u, 6_u, true);
+    WrapInFunction(Decl(Let("i", Expr(42_i))),  //
+                   Decl(Let("a", select_a)),    //
+                   Decl(Let("b", select_b)),    //
+                   Decl(Let("c", select_c)));
+
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+
+    EXPECT_EQ(Sem().Get<sem::Call>(select_a)->Target(), Sem().Get<sem::Call>(select_b)->Target());
+    EXPECT_NE(Sem().Get<sem::Call>(select_a)->Target(), Sem().Get<sem::Call>(select_c)->Target());
+    EXPECT_NE(Sem().Get<sem::Call>(select_b)->Target(), Sem().Get<sem::Call>(select_c)->Target());
+}
+
 // Tests for Logical builtins
 namespace logical_builtin_tests {
 
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 2badb72..ab58d14 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -22,6 +22,7 @@
 
 #include "src/tint/lang/core/builtin.h"
 #include "src/tint/lang/core/constant/scalar.h"
+#include "src/tint/lang/core/intrinsic_data.h"
 #include "src/tint/lang/core/type/abstract_float.h"
 #include "src/tint/lang/core/type/abstract_int.h"
 #include "src/tint/lang/core/type/array.h"
@@ -101,6 +102,7 @@
 namespace {
 
 using CtorConvIntrinsic = core::intrinsic::CtorConv;
+using OverloadFlag = core::intrinsic::TableData::OverloadFlag;
 
 constexpr int64_t kMaxArrayElementCount = 65536;
 constexpr uint32_t kMaxStatementDepth = 127;
@@ -112,7 +114,10 @@
     : builder_(builder),
       diagnostics_(builder->Diagnostics()),
       const_eval_(builder->constants, diagnostics_),
-      intrinsic_table_(core::intrinsic::Table::Create(*builder)),
+      intrinsic_table_(core::intrinsic::Table::Create(core::kIntrinsicData,
+                                                      builder->Types(),
+                                                      builder->Symbols(),
+                                                      builder->Diagnostics())),
       sem_(builder),
       validator_(builder,
                  sem_,
@@ -2061,32 +2066,62 @@
     // sem::ValueConversion call for a CtorConvIntrinsic with an optional template argument type.
     auto ctor_or_conv = [&](CtorConvIntrinsic ty, const type::Type* template_arg) -> sem::Call* {
         auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type(); });
-        auto entry = intrinsic_table_->Lookup(ty, template_arg, arg_tys, args_stage, expr->source);
-        if (!entry.target) {
+        auto match = intrinsic_table_->Lookup(ty, template_arg, arg_tys, args_stage, expr->source);
+        if (!match) {
             return nullptr;
         }
-        if (!MaybeMaterializeAndLoadArguments(args, entry.target)) {
+
+        auto overload_stage = match->info->const_eval_fn ? core::EvaluationStage::kConstant
+                                                         : core::EvaluationStage::kRuntime;
+
+        sem::CallTarget* target_sem = nullptr;
+
+        // Is this overload a constructor or conversion?
+        if (match->info->flags.Contains(OverloadFlag::kIsConstructor)) {
+            // Type constructor
+            auto params = Transform(match->parameters, [&](auto& p, size_t i) {
+                return builder_->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type,
+                                                        core::AddressSpace::kUndefined,
+                                                        core::Access::kUndefined, p.usage);
+            });
+            target_sem = constructors_.GetOrCreate(match.Get(), [&] {
+                return builder_->create<sem::ValueConstructor>(match->return_type,
+                                                               std::move(params), overload_stage);
+            });
+        } else {
+            // Type conversion
+            target_sem = converters_.GetOrCreate(match.Get(), [&] {
+                auto param = builder_->create<sem::Parameter>(
+                    nullptr, 0u, match->parameters[0].type, core::AddressSpace::kUndefined,
+                    core::Access::kUndefined, match->parameters[0].usage);
+                return builder_->create<sem::ValueConversion>(match->return_type, param,
+                                                              overload_stage);
+            });
+        }
+
+        if (!MaybeMaterializeAndLoadArguments(args, target_sem)) {
             return nullptr;
         }
 
         const core::constant::Value* value = nullptr;
-        auto stage = core::EarliestStage(entry.target->Stage(), args_stage);
+        auto stage = core::EarliestStage(overload_stage, args_stage);
         if (stage == core::EvaluationStage::kConstant && skip_const_eval_.Contains(expr)) {
             stage = core::EvaluationStage::kNotEvaluated;
         }
         if (stage == core::EvaluationStage::kConstant) {
-            auto const_args = ConvertArguments(args, entry.target);
+            auto const_args = ConvertArguments(args, target_sem);
             if (!const_args) {
                 return nullptr;
             }
-            if (auto r = (const_eval_.*entry.const_eval_fn)(entry.target->ReturnType(),
-                                                            const_args.Get(), expr->source)) {
+            auto const_eval_fn = match->info->const_eval_fn;
+            if (auto r = (const_eval_.*const_eval_fn)(target_sem->ReturnType(), const_args.Get(),
+                                                      expr->source)) {
                 value = r.Get();
             } else {
                 return nullptr;
             }
         }
-        return builder_->create<sem::Call>(expr, entry.target, stage, std::move(args),
+        return builder_->create<sem::Call>(expr, target_sem, stage, std::move(args),
                                            current_statement_, value, has_side_effects);
     };
 
@@ -2345,53 +2380,75 @@
 
 template <size_t N>
 sem::Call* Resolver::BuiltinCall(const ast::CallExpression* expr,
-                                 core::Function builtin_type,
+                                 core::Function fn,
                                  Vector<const sem::ValueExpression*, N>& args) {
     auto arg_stage = core::EvaluationStage::kConstant;
     for (auto* arg : args) {
         arg_stage = core::EarliestStage(arg_stage, arg->Stage());
     }
 
-    core::intrinsic::Table::Builtin builtin;
-    {
-        auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type(); });
-        builtin = intrinsic_table_->Lookup(builtin_type, arg_tys, arg_stage, expr->source);
-        if (!builtin.sem) {
-            return nullptr;
-        }
+    auto arg_tys = tint::Transform(args, [](auto* arg) { return arg->Type(); });
+    auto overload = intrinsic_table_->Lookup(fn, arg_tys, arg_stage, expr->source);
+    if (!overload) {
+        return nullptr;
     }
 
-    if (builtin_type == core::Function::kTintMaterialize) {
+    // De-duplicate builtins that are identical.
+    auto* target = builtins_.GetOrCreate(std::make_pair(overload.Get(), fn), [&] {
+        auto params = Transform(overload->parameters, [&](auto& p, size_t i) {
+            return builder_->create<sem::Parameter>(nullptr, static_cast<uint32_t>(i), p.type,
+                                                    core::AddressSpace::kUndefined,
+                                                    core::Access::kUndefined, p.usage);
+        });
+        sem::PipelineStageSet supported_stages;
+        auto flags = overload->info->flags;
+        if (flags.Contains(OverloadFlag::kSupportsVertexPipeline)) {
+            supported_stages.Add(ast::PipelineStage::kVertex);
+        }
+        if (flags.Contains(OverloadFlag::kSupportsFragmentPipeline)) {
+            supported_stages.Add(ast::PipelineStage::kFragment);
+        }
+        if (flags.Contains(OverloadFlag::kSupportsComputePipeline)) {
+            supported_stages.Add(ast::PipelineStage::kCompute);
+        }
+        auto eval_stage = overload->info->const_eval_fn ? core::EvaluationStage::kConstant
+                                                        : core::EvaluationStage::kRuntime;
+        return builder_->create<sem::Builtin>(
+            fn, overload->return_type, std::move(params), eval_stage, supported_stages,
+            flags.Contains(OverloadFlag::kIsDeprecated), flags.Contains(OverloadFlag::kMustUse));
+    });
+
+    if (fn == core::Function::kTintMaterialize) {
         args[0] = Materialize(args[0]);
         if (!args[0]) {
             return nullptr;
         }
     } else {
         // Materialize arguments if the parameter type is not abstract
-        if (!MaybeMaterializeAndLoadArguments(args, builtin.sem)) {
+        if (!MaybeMaterializeAndLoadArguments(args, target)) {
             return nullptr;
         }
     }
 
-    if (builtin.sem->IsDeprecated()) {
+    if (target->IsDeprecated()) {
         AddWarning("use of deprecated builtin", expr->source);
     }
 
     // If the builtin is @const, and all arguments have constant values, evaluate the builtin
     // now.
     const core::constant::Value* value = nullptr;
-    auto stage = core::EarliestStage(arg_stage, builtin.sem->Stage());
+    auto stage = core::EarliestStage(arg_stage, target->Stage());
     if (stage == core::EvaluationStage::kConstant && skip_const_eval_.Contains(expr)) {
         stage = core::EvaluationStage::kNotEvaluated;
     }
     if (stage == core::EvaluationStage::kConstant) {
-        auto const_args = ConvertArguments(args, builtin.sem);
+        auto const_args = ConvertArguments(args, target);
         if (!const_args) {
             return nullptr;
         }
-
-        if (auto r = (const_eval_.*builtin.const_eval_fn)(builtin.sem->ReturnType(),
-                                                          const_args.Get(), expr->source)) {
+        auto const_eval_fn = overload->info->const_eval_fn;
+        if (auto r = (const_eval_.*const_eval_fn)(target->ReturnType(), const_args.Get(),
+                                                  expr->source)) {
             value = r.Get();
         } else {
             return nullptr;
@@ -2399,13 +2456,13 @@
     }
 
     bool has_side_effects =
-        builtin.sem->HasSideEffects() ||
+        target->HasSideEffects() ||
         std::any_of(args.begin(), args.end(), [](auto* e) { return e->HasSideEffects(); });
-    auto* call = builder_->create<sem::Call>(expr, builtin.sem, stage, std::move(args),
+    auto* call = builder_->create<sem::Call>(expr, target, stage, std::move(args),
                                              current_statement_, value, has_side_effects);
 
     if (current_function_) {
-        current_function_->AddDirectlyCalledBuiltin(builtin.sem);
+        current_function_->AddDirectlyCalledBuiltin(target);
         current_function_->AddDirectCall(call);
     }
 
@@ -2413,14 +2470,14 @@
         return nullptr;
     }
 
-    if (IsTextureBuiltin(builtin_type)) {
+    if (IsTextureBuiltin(fn)) {
         if (!validator_.TextureBuiltinFunction(call)) {
             return nullptr;
         }
-        CollectTextureSamplerPairs(builtin.sem, call->Arguments());
+        CollectTextureSamplerPairs(target, call->Arguments());
     }
 
-    if (builtin_type == core::Function::kWorkgroupUniformLoad) {
+    if (fn == core::Function::kWorkgroupUniformLoad) {
         if (!validator_.WorkgroupUniformLoad(call)) {
             return nullptr;
         }
@@ -3458,22 +3515,26 @@
         return nullptr;
     }
 
-    auto* lhs_ty = lhs->Type();
-    auto* rhs_ty = rhs->Type();
-
     auto stage = core::EarliestStage(lhs->Stage(), rhs->Stage());
-    auto op = intrinsic_table_->Lookup(expr->op, lhs_ty, rhs_ty, stage, expr->source, false);
-    if (!op.result) {
+    auto overload =
+        intrinsic_table_->Lookup(expr->op, lhs->Type(), rhs->Type(), stage, expr->source, false);
+    if (!overload) {
         return nullptr;
     }
-    if (ShouldMaterializeArgument(op.lhs)) {
-        lhs = Materialize(lhs, op.lhs);
+
+    auto* res_ty = overload->return_type;
+
+    // Parameter types
+    auto* lhs_ty = overload->parameters[0].type;
+    auto* rhs_ty = overload->parameters[1].type;
+    if (ShouldMaterializeArgument(lhs_ty)) {
+        lhs = Materialize(lhs, lhs_ty);
         if (!lhs) {
             return nullptr;
         }
     }
-    if (ShouldMaterializeArgument(op.rhs)) {
-        rhs = Materialize(rhs, op.rhs);
+    if (ShouldMaterializeArgument(rhs_ty)) {
+        rhs = Materialize(rhs, rhs_ty);
         if (!rhs) {
             return nullptr;
         }
@@ -3491,18 +3552,19 @@
         stage = core::EvaluationStage::kConstant;
     } else if (stage == core::EvaluationStage::kConstant) {
         // Both LHS and RHS have expressions that are constant evaluation stage.
-        if (op.const_eval_fn) {  // Do we have a @const operator?
+        auto const_eval_fn = overload->info->const_eval_fn;
+        if (const_eval_fn) {  // Do we have a @const operator?
             // Yes. Perform any required abstract argument values implicit conversions to the
             // overload parameter types, and const-eval.
             Vector const_args{lhs->ConstantValue(), rhs->ConstantValue()};
             // Implicit conversion (e.g. AInt -> AFloat)
-            if (!Convert(const_args[0], op.lhs, lhs->Declaration()->source)) {
+            if (!Convert(const_args[0], lhs_ty, lhs->Declaration()->source)) {
                 return nullptr;
             }
-            if (!Convert(const_args[1], op.rhs, rhs->Declaration()->source)) {
+            if (!Convert(const_args[1], rhs_ty, rhs->Declaration()->source)) {
                 return nullptr;
             }
-            if (auto r = (const_eval_.*op.const_eval_fn)(op.result, const_args, expr->source)) {
+            if (auto r = (const_eval_.*const_eval_fn)(res_ty, const_args, expr->source)) {
                 value = r.Get();
             } else {
                 return nullptr;
@@ -3515,7 +3577,7 @@
     }
 
     bool has_side_effects = lhs->HasSideEffects() || rhs->HasSideEffects();
-    auto* sem = builder_->create<sem::ValueExpression>(expr, op.result, stage, current_statement_,
+    auto* sem = builder_->create<sem::ValueExpression>(expr, res_ty, stage, current_statement_,
                                                        value, has_side_effects);
     sem->Behaviors() = lhs->Behaviors() + rhs->Behaviors();
 
@@ -3575,13 +3637,14 @@
 
         default: {
             stage = expr->Stage();
-            auto op = intrinsic_table_->Lookup(unary->op, expr_ty, stage, unary->source);
-            if (!op.result) {
+            auto overload = intrinsic_table_->Lookup(unary->op, expr_ty, stage, unary->source);
+            if (!overload) {
                 return nullptr;
             }
-            ty = op.result;
-            if (ShouldMaterializeArgument(op.parameter)) {
-                expr = Materialize(expr, op.parameter);
+            ty = overload->return_type;
+            auto* param_ty = overload->parameters[0].type;
+            if (ShouldMaterializeArgument(param_ty)) {
+                expr = Materialize(expr, param_ty);
                 if (!expr) {
                     return nullptr;
                 }
@@ -3595,9 +3658,10 @@
 
             stage = expr->Stage();
             if (stage == core::EvaluationStage::kConstant) {
-                if (op.const_eval_fn) {
-                    if (auto r = (const_eval_.*op.const_eval_fn)(ty, Vector{expr->ConstantValue()},
-                                                                 expr->Declaration()->source)) {
+                auto const_eval_fn = overload->info->const_eval_fn;
+                if (const_eval_fn) {
+                    if (auto r = (const_eval_.*const_eval_fn)(ty, Vector{expr->ConstantValue()},
+                                                              expr->Declaration()->source)) {
                         value = r.Get();
                     } else {
                         return nullptr;
@@ -4599,22 +4663,22 @@
 
         sem->Behaviors() = rhs->Behaviors() + lhs->Behaviors();
 
-        auto* lhs_ty = lhs->Type()->UnwrapRef();
-        auto* rhs_ty = rhs->Type()->UnwrapRef();
         auto stage = core::EarliestStage(lhs->Stage(), rhs->Stage());
 
-        auto op = intrinsic_table_->Lookup(stmt->op, lhs_ty, rhs_ty, stage, stmt->source, true);
-        if (!op.result) {
+        auto overload =
+            intrinsic_table_->Lookup(stmt->op, lhs->Type()->UnwrapRef(), rhs->Type()->UnwrapRef(),
+                                     stage, stmt->source, true);
+        if (!overload) {
             return false;
         }
 
         // Load or materialize the RHS if necessary.
-        rhs = Load(Materialize(rhs, op.rhs));
+        rhs = Load(Materialize(rhs, overload->parameters[1].type));
         if (!rhs) {
             return false;
         }
 
-        return validator_.Assignment(stmt, op.result);
+        return validator_.Assignment(stmt, overload->return_type);
     });
 }
 
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 4a56d53..fce71e5 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -67,6 +67,8 @@
 class Statement;
 class StructMember;
 class SwitchStatement;
+class ValueConstructor;
+class ValueConversion;
 class WhileStatement;
 }  // namespace tint::sem
 namespace tint::type {
@@ -627,6 +629,10 @@
     Hashset<const ast::Expression*, 8> skip_const_eval_;
     IdentifierResolveHint identifier_resolve_hint_;
     Hashmap<const type::Type*, size_t, 8> nest_depth_;
+    Hashmap<std::pair<core::intrinsic::Table::Overload, core::Function>, sem::Builtin*, 64>
+        builtins_;
+    Hashmap<core::intrinsic::Table::Overload, sem::ValueConstructor*, 16> constructors_;
+    Hashmap<core::intrinsic::Table::Overload, sem::ValueConversion*, 16> converters_;
 };
 
 }  // namespace tint::resolver
diff --git a/src/tint/utils/containers/slice.h b/src/tint/utils/containers/slice.h
index 259ae6a..10378d9 100644
--- a/src/tint/utils/containers/slice.h
+++ b/src/tint/utils/containers/slice.h
@@ -114,10 +114,10 @@
     size_t cap = 0;
 
     /// Constructor
-    Slice() = default;
+    constexpr Slice() = default;
 
     /// Constructor
-    Slice(EmptyType) {}  // NOLINT
+    constexpr Slice(EmptyType) {}  // NOLINT
 
     /// Copy constructor with covariance / const conversion
     /// @param other the vector to copy
@@ -132,12 +132,12 @@
     /// @param d pointer to the first element in the slice
     /// @param l total number of elements in the slice
     /// @param c total capacity of the backing store for the slice
-    Slice(T* d, size_t l, size_t c) : data(d), len(l), cap(c) {}
+    constexpr Slice(T* d, size_t l, size_t c) : data(d), len(l), cap(c) {}
 
     /// Constructor
     /// @param elements c-array of elements
     template <size_t N>
-    Slice(T (&elements)[N])  // NOLINT
+    constexpr Slice(T (&elements)[N])  // NOLINT
         : data(elements), len(N), cap(N) {}
 
     /// Reinterprets this slice as `const Slice<TO>&`
diff --git a/src/tint/utils/diagnostic/diagnostic.h b/src/tint/utils/diagnostic/diagnostic.h
index 0eb7727..0fb3f36 100644
--- a/src/tint/utils/diagnostic/diagnostic.h
+++ b/src/tint/utils/diagnostic/diagnostic.h
@@ -33,14 +33,14 @@
     return static_cast<int>(a) >= static_cast<int>(b);
 }
 
-/// System is an enumerator of Tint systems that can be the originator of a
-/// diagnostic message.
+/// System is an enumerator of Tint systems that can be the originator of a diagnostic message.
 enum class System {
     AST,
     Builtin,
     Clone,
     Constant,
     Inspector,
+    Intrinsics,
     IR,
     Program,
     ProgramBuilder,
diff --git a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
index 467a871..e01c8fb 100644
--- a/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
+++ b/src/tint/utils/templates/intrinsic_table_data.tmpl.inc
@@ -211,36 +211,33 @@
 
 }  // anonymous namespace
 
-const TableData& {{$.Name}}() {
-  static const TableData data{
-    /* type_matchers */ kTypeMatchers,
-    /* number_matchers */ kNumberMatchers,
-    /* ctor_conv */ kConstructorsAndConverters,
-    /* builtins */ kBuiltins,
-    /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
-    /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
-    /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
-    /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
-    /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
-    /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
-    /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
-    /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
-    /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
-    /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
-    /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
-    /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
-    /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
-    /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
-    /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
-    /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
-    /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
-    /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
-    /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
-    /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
-    /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
-  };
-  return data;
-}
+const core::intrinsic::TableData {{$.Name}}{
+  /* type_matchers */ kTypeMatchers,
+  /* number_matchers */ kNumberMatchers,
+  /* ctor_conv */ kConstructorsAndConverters,
+  /* builtins */ kBuiltins,
+  /* binary_plus */ kBinaryOperators[kBinaryOperatorPlus],
+  /* binary_minus */ kBinaryOperators[kBinaryOperatorMinus],
+  /* binary_star */ kBinaryOperators[kBinaryOperatorStar],
+  /* binary_divide */ kBinaryOperators[kBinaryOperatorDivide],
+  /* binary_modulo */ kBinaryOperators[kBinaryOperatorModulo],
+  /* binary_xor */ kBinaryOperators[kBinaryOperatorXor],
+  /* binary_and */ kBinaryOperators[kBinaryOperatorAnd],
+  /* binary_or */ kBinaryOperators[kBinaryOperatorOr],
+  /* binary_logical_and */ kBinaryOperators[kBinaryOperatorLogicalAnd],
+  /* binary_logical_or */ kBinaryOperators[kBinaryOperatorLogicalOr],
+  /* binary_equal */ kBinaryOperators[kBinaryOperatorEqual],
+  /* binary_not_equal */ kBinaryOperators[kBinaryOperatorNotEqual],
+  /* binary_less_than */ kBinaryOperators[kBinaryOperatorLessThan],
+  /* binary_greater_than */ kBinaryOperators[kBinaryOperatorGreaterThan],
+  /* binary_less_than_equal */ kBinaryOperators[kBinaryOperatorLessThanEqual],
+  /* binary_greater_than_equal */ kBinaryOperators[kBinaryOperatorGreaterThanEqual],
+  /* binary_shift_left */ kBinaryOperators[kBinaryOperatorShiftLeft],
+  /* binary_shift_right */ kBinaryOperators[kBinaryOperatorShiftRight],
+  /* unary_not */ kUnaryOperators[kUnaryOperatorNot],
+  /* unary_complement */ kUnaryOperators[kUnaryOperatorComplement],
+  /* unary_minus */ kUnaryOperators[kUnaryOperatorMinus],
+};
 
 }  // namespace {{$.Namespace}}
 
diff --git a/tools/src/cmd/git-stats/main.go b/tools/src/cmd/git-stats/main.go
index 38374de..183f2f1 100644
--- a/tools/src/cmd/git-stats/main.go
+++ b/tools/src/cmd/git-stats/main.go
@@ -62,7 +62,8 @@
 		"package-lock.json",
 		"src/tint/builtin_table.inl",
 		"src/tint/lang/core/intrinsic/table.inl",
-		"src/tint/lang/core/intrinsic/core_table_data.cc.tmpl",
+		"src/tint/lang/core/*.cc",
+		"src/tint/lang/core/*.h",
 		"test/tint/",
 		"third_party/gn/webgpu-cts/test_list.txt",
 		"third_party/khronos/",