Merge remote-tracking branch 'tint/main' into HEAD

Integrates Tint repo into Dawn

KIs:
- Building docs for Tint is turned off, because it fails due to lack
  of annotations in Dawn source files.
- Dawn CQ needs to be updated to run Tint specific tests
- Significant post-merge cleanup needed

R=bclayton,cwallez
BUG=dawn:1339

Change-Id: I6c9714a0030934edd6c51f3cac4684dcd59d1ea3
diff --git a/.clang-format b/.clang-format
index 4d9e9c84..ff58eea 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,3 +1,4 @@
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
 BasedOnStyle: Chromium
 Standard: Cpp11
 
diff --git a/.gitignore b/.gitignore
index a028df0..42d8414 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,12 +9,15 @@
 /testing
 /third_party/abseil-cpp/
 /third_party/angle
+/third_party/benchmark
+/third_party/binutils
 /third_party/catapult
 /third_party/clang-format
+/third_party/cpplint
 /third_party/glfw
 /third_party/googletest
 /third_party/gpuweb
-/third_party/webgpu-cts
+/third_party/gpuweb-cts
 /third_party/jinja2
 /third_party/jsoncpp
 /third_party/llvm-build
@@ -22,10 +25,11 @@
 /third_party/node
 /third_party/node-addon-api
 /third_party/node-api-headers
+/third_party/protobuf
 /third_party/swiftshader
-/third_party/tint
 /third_party/vulkan-deps
 /third_party/vulkan_memory_allocator
+/third_party/webgpu-cts
 /third_party/zlib
 /tools/clang
 /tools/cmake
@@ -33,7 +37,7 @@
 /tools/memory
 /out
 
-# Modified from https://www.gitignore.io/api/vim,macos,linux,emacs,windows,sublimetext,visualstudio,visualstudiocode
+# Modified from https://www.gitignore.io/api/vim,macos,linux,emacs,windows,sublimetext,visualstudio,visualstudiocode,intellij
 
 ### Emacs ###
 *~
@@ -101,8 +105,19 @@
 Desktop.ini
 $RECYCLE.BIN/
 
+### Intellij ###
+.idea
+
 ### Dawn node tools binaries
 src/dawn/node/tools/bin/
 
 ### Cached node transpiled tools
 /.node_transpile_work_dir
+
+# Misc inherited from Tint
+/test.wgsl
+coverage.summary
+default.profraw
+lcov.info
+/cmake-build-*/
+/testing
diff --git a/AUTHORS b/AUTHORS
index 32a6c3c..bded374 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,6 +1,7 @@
-# This is the list of Dawn authors for copyright purposes.
+# This is the list of Dawn & Tint authors for copyright purposes.
 #
 # This does not necessarily list everyone who has contributed code, since in
 # some cases, their employer may be the copyright holder.  To see the full list
 # of contributors, see the revision history in source control.
-Google Inc.
+Google LLC
+Vasyl Teliman
diff --git a/AUTHORS.dawn b/AUTHORS.dawn
new file mode 100644
index 0000000..32a6c3c
--- /dev/null
+++ b/AUTHORS.dawn
@@ -0,0 +1,6 @@
+# This is the list of Dawn authors for copyright purposes.
+#
+# This does not necessarily list everyone who has contributed code, since in
+# some cases, their employer may be the copyright holder.  To see the full list
+# of contributors, see the revision history in source control.
+Google Inc.
diff --git a/AUTHORS.tint b/AUTHORS.tint
new file mode 100644
index 0000000..a66d09e
--- /dev/null
+++ b/AUTHORS.tint
@@ -0,0 +1,8 @@
+# This is the list of the Tint authors for copyright purposes.
+#
+# This does not necessarily list everyone who has contributed code, since in
+# some cases, their employer may be the copyright holder.  To see the full list
+# of contributors, see the revision history in source control.
+
+Google LLC
+Vasyl Teliman
diff --git a/BUILD.gn b/BUILD.gn
index 99df240..c33776d 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -21,9 +21,15 @@
     "src/dawn/native:webgpu_dawn",
     "src/dawn/tests",
     "src/fuzzers/dawn:dawn_fuzzers",
+    "src/tint/fuzzers",
+    "src/tint:libtint",
+    "test/tint:tint_unittests",
   ]
   if (dawn_standalone) {
-    deps += [ "samples/dawn:samples" ]
+    deps += [
+      "samples/dawn:samples",
+      "src/tint/cmd:tint",
+    ]
   }
 }
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bdcfdf1..f125476 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-# Copyright 2020 The Dawn Authors
+# Copyright 2022 The Dawn & Tint Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.10.2)
 
 # When upgrading to CMake 3.11 we can remove DAWN_DUMMY_FILE because source-less add_library
 # becomes available.
@@ -26,13 +26,18 @@
     DESCRIPTION "Dawn, a WebGPU implementation"
     LANGUAGES C CXX
 )
+enable_testing()
 
 set_property(GLOBAL PROPERTY USE_FOLDERS ON)
 
-if(NOT CMAKE_BUILD_TYPE)
-    message(WARNING "CMAKE_BUILD_TYPE not set, forcing it to Debug")
-    set(CMAKE_BUILD_TYPE "Debug" CACHE STRING
-        "Build type (Debug, Release, RelWithDebInfo, MinSizeRel)" FORCE)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_DEBUG_POSTFIX "")
+
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "")
+  message(STATUS "No build type selected, default to Debug")
+  set(CMAKE_BUILD_TYPE "Debug")
 endif()
 
 set(DAWN_BUILD_GEN_DIR "${Dawn_BINARY_DIR}/gen")
@@ -99,7 +104,7 @@
 endif()
 
 # GLFW is not supported in UWP
-if((WIN32 AND NOT WINDOWS_STORE) OR UNIX AND NOT ANDROID)
+if ((WIN32 AND NOT WINDOWS_STORE) OR UNIX AND NOT ANDROID)
     set(DAWN_SUPPORTS_GLFW_FOR_WINDOWING ON)
 endif()
 
@@ -132,7 +137,7 @@
 set_if_not_defined(DAWN_JINJA2_DIR "${DAWN_THIRD_PARTY_DIR}/jinja2" "Directory in which to find Jinja2")
 set_if_not_defined(DAWN_SPIRV_HEADERS_DIR "${DAWN_THIRD_PARTY_DIR}/vulkan-deps/spirv-headers/src" "Directory in which to find SPIRV-Headers")
 set_if_not_defined(DAWN_SPIRV_TOOLS_DIR "${DAWN_THIRD_PARTY_DIR}/vulkan-deps/spirv-tools/src" "Directory in which to find SPIRV-Tools")
-set_if_not_defined(DAWN_TINT_DIR "${DAWN_THIRD_PARTY_DIR}/tint" "Directory in which to find Tint")
+set_if_not_defined(DAWN_TINT_DIR "${Dawn_SOURCE_DIR}" "Directory in which to find Tint")
 set_if_not_defined(DAWN_VULKAN_HEADERS_DIR "${DAWN_THIRD_PARTY_DIR}/vulkan-deps/vulkan-headers/src" "Directory in which to find Vulkan-Headers")
 
 # Dependencies for DAWN_BUILD_NODE_BINDINGS
@@ -204,10 +209,354 @@
 set(CMAKE_CXX_STANDARD "17")
 
 ################################################################################
+# Tint
+################################################################################
+
+# TINT_IS_SUBPROJECT is 1 if added via add_subdirectory() from another project.
+get_directory_property(TINT_IS_SUBPROJECT PARENT_DIRECTORY)
+if(TINT_IS_SUBPROJECT)
+  set(TINT_IS_SUBPROJECT 1)
+
+  # If tint is used as a subproject, default to disabling the building of
+  # documentation and tests. These are unlikely to be desirable, but can be
+  # enabled.
+  set(TINT_BUILD_DOCS_DEFAULT OFF)
+  set(TINT_BUILD_TESTS_DEFAULT OFF)
+else()
+  set(TINT_BUILD_DOCS_DEFAULT ON)
+  set(TINT_BUILD_TESTS_DEFAULT ON)
+endif()
+
+# Forcing building docs off right now, since currently this will try to build docs for both Tint & Dawn, and Dawn isn't annotated yet.
+set(TINT_BUILD_DOCS_DEFAULT OFF)
+
+option_if_not_defined(TINT_BUILD_SAMPLES "Build samples" ON)
+option_if_not_defined(TINT_BUILD_DOCS "Build documentation" ${TINT_BUILD_DOCS_DEFAULT})
+option_if_not_defined(TINT_DOCS_WARN_AS_ERROR "When building documentation, treat warnings as errors" OFF)
+option_if_not_defined(TINT_BUILD_SPV_READER "Build the SPIR-V input reader" ON)
+option_if_not_defined(TINT_BUILD_WGSL_READER "Build the WGSL input reader" ON)
+option_if_not_defined(TINT_BUILD_GLSL_WRITER "Build the GLSL output writer" ON)
+option_if_not_defined(TINT_BUILD_HLSL_WRITER "Build the HLSL output writer" ON)
+option_if_not_defined(TINT_BUILD_MSL_WRITER "Build the MSL output writer" ON)
+option_if_not_defined(TINT_BUILD_SPV_WRITER "Build the SPIR-V output writer" ON)
+option_if_not_defined(TINT_BUILD_WGSL_WRITER "Build the WGSL output writer" ON)
+option_if_not_defined(TINT_BUILD_FUZZERS "Build fuzzers" OFF)
+option_if_not_defined(TINT_BUILD_SPIRV_TOOLS_FUZZER "Build SPIRV-Tools fuzzer" OFF)
+option_if_not_defined(TINT_BUILD_AST_FUZZER "Build AST fuzzer" OFF)
+option_if_not_defined(TINT_BUILD_REGEX_FUZZER "Build regex fuzzer" OFF)
+option_if_not_defined(TINT_BUILD_BENCHMARKS "Build benchmarks" OFF)
+option_if_not_defined(TINT_BUILD_TESTS "Build tests" ${TINT_BUILD_TESTS_DEFAULT})
+option_if_not_defined(TINT_BUILD_AS_OTHER_OS "Override OS detection to force building of *_other.cc files" OFF)
+option_if_not_defined(TINT_BUILD_REMOTE_COMPILE "Build the remote-compile tool for validating shaders on a remote machine" OFF)
+
+set(TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS "" CACHE STRING "Used by OSS-Fuzz to control, via link options, which fuzzing engine should be used")
+
+option_if_not_defined(TINT_ENABLE_MSAN "Enable memory sanitizer" OFF)
+option_if_not_defined(TINT_ENABLE_ASAN "Enable address sanitizer" OFF)
+option_if_not_defined(TINT_ENABLE_UBSAN "Enable undefined behaviour sanitizer" OFF)
+
+option_if_not_defined(TINT_ENABLE_BREAK_IN_DEBUGGER "Enable tint::debugger::Break()" OFF)
+
+option_if_not_defined(TINT_EMIT_COVERAGE "Emit code coverage information" OFF)
+
+option_if_not_defined(TINT_CHECK_CHROMIUM_STYLE "Check for [chromium-style] issues during build" OFF)
+
+option_if_not_defined(TINT_SYMBOL_STORE_DEBUG_NAME "Enable storing of name in tint::ast::Symbol to help debugging the AST" OFF)
+
+message(STATUS "Tint build samples: ${TINT_BUILD_SAMPLES}")
+message(STATUS "Tint build docs: ${TINT_BUILD_DOCS}")
+message(STATUS "Tint build docs with warn as error: ${TINT_DOCS_WARN_AS_ERROR}")
+message(STATUS "Tint build SPIR-V reader: ${TINT_BUILD_SPV_READER}")
+message(STATUS "Tint build WGSL reader: ${TINT_BUILD_WGSL_READER}")
+message(STATUS "Tint build GLSL writer: ${TINT_BUILD_GLSL_WRITER}")
+message(STATUS "Tint build HLSL writer: ${TINT_BUILD_HLSL_WRITER}")
+message(STATUS "Tint build MSL writer: ${TINT_BUILD_MSL_WRITER}")
+message(STATUS "Tint build SPIR-V writer: ${TINT_BUILD_SPV_WRITER}")
+message(STATUS "Tint build WGSL writer: ${TINT_BUILD_WGSL_WRITER}")
+message(STATUS "Tint build fuzzers: ${TINT_BUILD_FUZZERS}")
+message(STATUS "Tint build SPIRV-Tools fuzzer: ${TINT_BUILD_SPIRV_TOOLS_FUZZER}")
+message(STATUS "Tint build AST fuzzer: ${TINT_BUILD_AST_FUZZER}")
+message(STATUS "Tint build regex fuzzer: ${TINT_BUILD_REGEX_FUZZER}")
+message(STATUS "Tint build benchmarks: ${TINT_BUILD_BENCHMARKS}")
+message(STATUS "Tint build tests: ${TINT_BUILD_TESTS}")
+message(STATUS "Tint build with ASAN: ${TINT_ENABLE_ASAN}")
+message(STATUS "Tint build with MSAN: ${TINT_ENABLE_MSAN}")
+message(STATUS "Tint build with UBSAN: ${TINT_ENABLE_UBSAN}")
+message(STATUS "Tint build checking [chromium-style]: ${TINT_CHECK_CHROMIUM_STYLE}")
+message(STATUS "Tint build remote-compile tool: ${TINT_BUILD_REMOTE_COMPILE}")
+
+if (NOT ${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS} STREQUAL "")
+  message(STATUS "Using provided LIB_FUZZING_ENGINE options: ${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS}")
+endif()
+
+message(STATUS "Using python3")
+find_package(PythonInterp 3 REQUIRED)
+
+if (${TINT_BUILD_SPIRV_TOOLS_FUZZER})
+  message(STATUS "TINT_BUILD_SPIRV_TOOLS_FUZZER is ON - setting
+      TINT_BUILD_FUZZERS
+      TINT_BUILD_SPV_READER
+      TINT_BUILD_SPV_WRITER
+      TINT_BUILD_WGSL_READER
+      TINT_BUILD_WGSL_WRITER
+      TINT_BUILD_GLSL_WRITER
+      TINT_BUILD_HLSL_WRITER
+      TINT_BUILD_MSL_WRITER to ON")
+  set(TINT_BUILD_FUZZERS ON CACHE BOOL "Build tint fuzzers" FORCE)
+  set(TINT_BUILD_SPV_READER ON CACHE BOOL "Build SPIR-V reader" FORCE)
+  set(TINT_BUILD_SPV_WRITER ON CACHE BOOL "Build SPIR-V writer" FORCE)
+  set(TINT_BUILD_WGSL_READER ON CACHE BOOL "Build WGSL reader" FORCE)
+  set(TINT_BUILD_WGSL_WRITER ON CACHE BOOL "Build WGSL writer" FORCE)
+  set(TINT_BUILD_GLSL_WRITER ON CACHE BOOL "Build HLSL writer" FORCE)
+  set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "Build HLSL writer" FORCE)
+  set(TINT_BUILD_MSL_WRITER ON CACHE BOOL "Build MSL writer" FORCE)
+endif()
+
+if (${TINT_BUILD_AST_FUZZER})
+  message(STATUS "TINT_BUILD_AST_FUZZER is ON - setting
+      TINT_BUILD_FUZZERS
+      TINT_BUILD_WGSL_READER
+      TINT_BUILD_WGSL_WRITER
+      TINT_BUILD_SPV_WRITER
+      TINT_BUILD_MSL_WRITER
+      TINT_BUILD_GLSL_WRITER
+      TINT_BUILD_HLSL_WRITER to ON")
+  set(TINT_BUILD_FUZZERS ON CACHE BOOL "Build tint fuzzers" FORCE)
+  set(TINT_BUILD_WGSL_READER ON CACHE BOOL "Build WGSL reader" FORCE)
+  set(TINT_BUILD_WGSL_WRITER ON CACHE BOOL "Build WGSL writer" FORCE)
+  set(TINT_BUILD_SPV_WRITER ON CACHE BOOL "Build SPIR-V writer" FORCE)
+  set(TINT_BUILD_MSL_WRITER ON CACHE BOOL "Build MSL writer" FORCE)
+  set(TINT_BUILD_GLSL_WRITER ON CACHE BOOL "Build GLSL writer" FORCE)
+  set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "Build HLSL writer" FORCE)
+endif()
+
+if (${TINT_BUILD_REGEX_FUZZER})
+  message(STATUS "TINT_BUILD_REGEX_FUZZER is ON - setting
+      TINT_BUILD_FUZZERS
+      TINT_BUILD_WGSL_READER
+      TINT_BUILD_WGSL_WRITER
+      TINT_BUILD_SPV_WRITER
+      TINT_BUILD_MSL_WRITER
+      TINT_BUILD_GLSL_WRITER
+      TINT_BUILD_HLSL_WRITER to ON")
+      set(TINT_BUILD_FUZZERS ON CACHE BOOL "Build tint fuzzers" FORCE)
+      set(TINT_BUILD_WGSL_READER ON CACHE BOOL "Build WGSL reader" FORCE)
+      set(TINT_BUILD_WGSL_WRITER ON CACHE BOOL "Build WGSL writer" FORCE)
+      set(TINT_BUILD_SPV_WRITER ON CACHE BOOL "Build SPIR-V writer" FORCE)
+      set(TINT_BUILD_MSL_WRITER ON CACHE BOOL "Build MSL writer" FORCE)
+      set(TINT_BUILD_GLSL_WRITER ON CACHE BOOL "Build GLSL writer" FORCE)
+      set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "Build HLSL writer" FORCE)
+endif()
+
+set(TINT_ROOT_SOURCE_DIR ${PROJECT_SOURCE_DIR})
+
+# CMake < 3.15 sets /W3 in CMAKE_CXX_FLAGS. Remove it if it's there.
+# See https://gitlab.kitware.com/cmake/cmake/-/issues/18317
+if (MSVC)
+  if (CMAKE_CXX_FLAGS MATCHES "/W3")
+    string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+  endif()
+endif()
+
+if (${TINT_CHECK_CHROMIUM_STYLE})
+   set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Xclang -add-plugin -Xclang find-bad-constructs")
+endif()
+
+if (${TINT_BUILD_SPV_READER})
+  include_directories("${DAWN_THIRD_PARTY_DIR}/vulkan-deps/spirv-tools/include")
+endif()
+
+if((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") AND (CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC"))
+  set(COMPILER_IS_CLANG_CL TRUE)
+endif()
+
+if((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR
+    (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") OR
+    ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") AND
+     (NOT COMPILER_IS_CLANG_CL)))
+  set(COMPILER_IS_LIKE_GNU TRUE)
+endif()
+
+# Enable msbuild multiprocessor builds
+if (MSVC AND NOT COMPILER_IS_CLANG_CL)
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
+endif()
+
+set(TINT_OS_CC_SUFFIX "other")
+if (NOT TINT_BUILD_AS_OTHER_OS)
+  if(UNIX OR APPLE)
+    set(TINT_OS_CC_SUFFIX "posix")
+    set(TINT_OS_CC_SUFFIX "posix")
+  elseif(WIN32)
+    set(TINT_OS_CC_SUFFIX "windows")
+    set(TINT_OS_CC_SUFFIX "windows")
+  endif()
+endif()
+
+if(${TINT_BUILD_DOCS})
+  find_package(Doxygen)
+  if(DOXYGEN_FOUND)
+    set(DOXYGEN_WARN_AS_ERROR NO)
+    if(TINT_DOCS_WARN_AS_ERROR)
+      set(DOXYGEN_WARN_AS_ERROR YES)
+    endif()
+
+    set(DOXYGEN_WARN_FORMAT "$file:$line: $text")
+    if (MSVC)
+      set(DOXYGEN_WARN_FORMAT "$file($line): $text")
+    endif()
+
+    add_custom_target(tint-docs ALL
+        COMMAND ${CMAKE_COMMAND}
+          -E env
+          "DOXYGEN_OUTPUT_DIRECTORY=${CMAKE_BINARY_DIR}/docs"
+          "DOXYGEN_WARN_AS_ERROR=${DOXYGEN_WARN_AS_ERROR}"
+          "DOXYGEN_WARN_FORMAT=${DOXYGEN_WARN_FORMAT}"
+          ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        COMMENT "Generating API documentation"
+        VERBATIM)
+  else()
+    message("Doxygen not found. Skipping documentation")
+  endif(DOXYGEN_FOUND)
+endif()
+
+function(tint_core_compile_options TARGET)
+  target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}")
+  target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}/include")
+
+  if (${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
+    target_include_directories(${TARGET} PUBLIC
+        "${DAWN_THIRD_PARTY_DIR}/spirv-headers/include")
+  endif()
+
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_MSL_WRITER=$<BOOL:${TINT_BUILD_MSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_WRITER=$<BOOL:${TINT_BUILD_SPV_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
+
+  if (COMPILER_IS_LIKE_GNU)
+    target_compile_options(${TARGET} PRIVATE
+      -std=c++17
+      -fno-exceptions
+      -fno-rtti
+    )
+
+    if (${TINT_ENABLE_MSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=memory)
+      target_link_options(${TARGET} PRIVATE -fsanitize=memory)
+    elseif (${TINT_ENABLE_ASAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=address)
+      target_link_options(${TARGET} PRIVATE -fsanitize=address)
+    elseif (${TINT_ENABLE_UBSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=undefined)
+      target_link_options(${TARGET} PRIVATE -fsanitize=undefined)
+    endif()
+  endif(COMPILER_IS_LIKE_GNU)
+
+  if (TINT_EMIT_COVERAGE)
+    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
+        target_compile_options(${TARGET} PRIVATE "--coverage")
+        target_link_options(${TARGET} PRIVATE "gcov")
+    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+        target_compile_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
+        target_link_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
+    else()
+        message(FATAL_ERROR "Coverage generation not supported for the ${CMAKE_CXX_COMPILER_ID} toolchain")
+    endif()
+  endif(TINT_EMIT_COVERAGE)
+endfunction()
+
+function(tint_default_compile_options TARGET)
+  tint_core_compile_options(${TARGET})
+
+  set(COMMON_GNU_OPTIONS
+    -Wall
+    -Werror
+    -Wextra
+    -Wno-documentation-unknown-command
+    -Wno-padded
+    -Wno-switch-enum
+    -Wno-unknown-pragmas
+  )
+
+  set(COMMON_CLANG_OPTIONS
+    -Wno-c++98-compat
+    -Wno-c++98-compat-pedantic
+    -Wno-format-pedantic
+    -Wno-return-std-move-in-c++11
+    -Wno-unknown-warning-option
+    -Wno-undefined-var-template
+    -Wno-used-but-marked-unused
+    -Weverything
+  )
+
+  if (COMPILER_IS_LIKE_GNU)
+    target_compile_options(${TARGET} PRIVATE
+      -pedantic-errors
+      ${COMMON_GNU_OPTIONS}
+    )
+
+    if (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") OR
+        ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang"))
+      target_compile_options(${TARGET} PRIVATE
+        ${COMMON_CLANG_OPTIONS}
+      )
+    endif()
+  endif(COMPILER_IS_LIKE_GNU)
+
+  if (MSVC)
+    # Specify /EHs for exception handling.
+    target_compile_options(${TARGET} PRIVATE
+      /bigobj
+      /EHsc
+      /W4
+      /WX
+      /wd4068
+      /wd4127
+      /wd4244
+      /wd4267
+      /wd4324
+      /wd4458
+      /wd4514
+      /wd4571
+      /wd4625
+      /wd4626
+      /wd4710
+      /wd4774
+      /wd4820
+      /wd5026
+      /wd5027
+    )
+
+    # When building with clang-cl on Windows, try to match our clang build
+    # options as much as possible.
+    if (COMPILER_IS_CLANG_CL)
+      target_compile_options(${TARGET} PRIVATE
+        ${COMMON_GNU_OPTIONS}
+        ${COMMON_CLANG_OPTIONS}
+        # Disable warnings that are usually disabled in downstream deps for
+        # gcc/clang, but aren't for clang-cl.
+        -Wno-global-constructors
+        -Wno-zero-as-null-pointer-constant
+        -Wno-shorten-64-to-32
+        -Wno-shadow-field-in-constructor
+        -Wno-reserved-id-macro
+        -Wno-language-extension-token
+      )
+    endif()
+  endif()
+endfunction()
+
+################################################################################
 # Run on all subdirectories
 ################################################################################
 
 add_subdirectory(third_party)
+add_subdirectory(src/tint)
 add_subdirectory(generator)
 add_subdirectory(src/dawn)
 
@@ -220,3 +569,42 @@
     #add_subdirectory(src/utils)
     add_subdirectory(samples/dawn)
 endif()
+
+if (TINT_BUILD_SAMPLES)
+  add_subdirectory(src/tint/cmd)
+endif()
+
+if (TINT_BUILD_FUZZERS)
+  add_subdirectory(src/tint/fuzzers)
+endif()
+
+add_custom_target(tint-lint
+  COMMAND ./tools/lint
+  WORKING_DIRECTORY ${TINT_ROOT_SOURCE_DIR}
+  COMMENT "Running linter"
+  VERBATIM)
+
+add_custom_target(tint-format
+  COMMAND ./tools/format
+  WORKING_DIRECTORY ${TINT_ROOT_SOURCE_DIR}
+  COMMENT "Running formatter"
+  VERBATIM)
+
+
+if (TINT_EMIT_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+  # Generates a lcov.info file at the project root.
+  # This can be used by tools such as VSCode's Coverage Gutters extension to
+  # visualize code coverage in the editor.
+  get_filename_component(CLANG_BIN_DIR ${CMAKE_C_COMPILER} DIRECTORY)
+  set(PATH_WITH_CLANG "${CLANG_BIN_DIR}:$ENV{PATH}")
+  add_custom_target(tint-generate-coverage
+    COMMAND ${CMAKE_COMMAND} -E env PATH=${PATH_WITH_CLANG} ./tools/tint-generate-coverage $<TARGET_FILE:tint_unittests>
+    DEPENDS tint_unittests
+    WORKING_DIRECTORY ${TINT_ROOT_SOURCE_DIR}
+    COMMENT "Generating tint coverage data"
+    VERBATIM)
+endif()
+
+if (TINT_BUILD_REMOTE_COMPILE)
+  add_subdirectory(tools/src/cmd/remote-compile)
+endif()
diff --git a/CMakeSettings.json b/CMakeSettings.json
new file mode 100644
index 0000000..ee3ee56
--- /dev/null
+++ b/CMakeSettings.json
@@ -0,0 +1,100 @@
+{

+  "configurations": [

+    {

+      "name": "x64-Debug",

+      "generator": "Ninja",

+      "configurationType": "Debug",

+      "inheritEnvironments": [ "msvc_x64_x64" ],

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "variables": []

+    },

+    {

+      "name": "x64-Release",

+      "generator": "Ninja",

+      "configurationType": "RelWithDebInfo",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "msvc_x64_x64" ],

+      "variables": []

+    },

+    {

+      "name": "x86-Debug",

+      "generator": "Ninja",

+      "configurationType": "Debug",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "msvc_x86" ],

+      "variables": []

+    },

+    {

+      "name": "x86-Release",

+      "generator": "Ninja",

+      "configurationType": "RelWithDebInfo",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "msvc_x86" ],

+      "variables": []

+    },

+    {

+      "name": "x64-Clang-Debug",

+      "generator": "Ninja",

+      "configurationType": "Debug",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "clang_cl_x64_x64" ],

+      "variables": []

+    },

+    {

+      "name": "x64-Clang-Release",

+      "generator": "Ninja",

+      "configurationType": "RelWithDebInfo",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "clang_cl_x64_x64" ],

+      "variables": []

+    },

+    {

+      "name": "x86-Clang-Debug",

+      "generator": "Ninja",

+      "configurationType": "Debug",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "clang_cl_x86" ],

+      "variables": []

+    },

+    {

+      "name": "x86-Clang-Release",

+      "generator": "Ninja",

+      "configurationType": "RelWithDebInfo",

+      "buildRoot": "${projectDir}\\out\\build\\${name}",

+      "installRoot": "${projectDir}\\out\\install\\${name}",

+      "cmakeCommandArgs": "",

+      "buildCommandArgs": "",

+      "ctestCommandArgs": "",

+      "inheritEnvironments": [ "clang_cl_x86" ],

+      "variables": []

+    }

+  ]

+}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..12921d9
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,93 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+*   Using welcoming and inclusive language
+*   Being respectful of differing viewpoints and experiences
+*   Gracefully accepting constructive criticism
+*   Focusing on what is best for the community
+*   Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+*   The use of sexualized language or imagery and unwelcome sexual attention or
+    advances
+*   Trolling, insulting/derogatory comments, and personal or political attacks
+*   Public or private harassment
+*   Publishing others' private information, such as a physical or electronic
+    address, without explicit permission
+*   Other conduct which could reasonably be considered inappropriate in a
+    professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to David Neto <dneto@google.com>, the
+Project Steward(s) for Tint. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..329011e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,45 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use [Dawn's Gerrit](https://dawn-review.googlesource.com/) for this purpose.
+
+Submissions should follow the [Tint style guide](docs/tint/style_guide.md).
+
+## Pushing to Gerrit
+
+Each change requires a `Change-Id` field in the commit message, which is generated by the [Gerrit commit-msg hook](](https://gerrit-review.googlesource.com/Documentation/cmd-hook-commit-msg.html)). \
+In a bash terminal, with the current path set to your tint source tree, this can be obtained by running the following:
+
+```bash
+f=`git rev-parse --git-dir`/hooks/commit-msg ; mkdir -p $(dirname $f) ; curl -Lo $f https://gerrit-review.googlesource.com/tools/hooks/commit-msg ; chmod +x $f
+```
+
+If you've already locally committed a change without the `Change-Id`, running `git commit --amend` will add the missing `Change-Id`.
+
+To create a Gerrit change for review, type:
+
+```bash
+git push origin HEAD:refs/for/main
+```
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
diff --git a/CPPLINT.cfg b/CPPLINT.cfg
new file mode 100644
index 0000000..36d9cb2
--- /dev/null
+++ b/CPPLINT.cfg
@@ -0,0 +1 @@
+set noparent
diff --git a/DEPS b/DEPS
index 79df40c..0c13f63 100644
--- a/DEPS
+++ b/DEPS
@@ -42,7 +42,6 @@
     'url': '{chromium_git}/external/github.com/llvm/llvm-project/clang/tools/clang-format.git@99803d74e35962f63a775f29477882afd4d57d94',
     'condition': 'dawn_standalone',
   },
-
   'buildtools/linux64': {
     'packages': [{
       'package': 'gn/gn/linux-amd64',
@@ -116,11 +115,6 @@
     'condition': 'dawn_standalone',
   },
 
-  # WGSL support
-  'third_party/tint': {
-    'url': '{dawn_git}/tint@a730eb738e9f00fb52e9ac38cebe978373602a1e',
-  },
-
   # GLFW for tests and samples
   'third_party/glfw': {
     'url': '{chromium_git}/external/github.com/glfw/glfw@94773111300fee0453844a4c9407af7e880b4df8',
@@ -176,6 +170,10 @@
     'url': '{github_git}/gpuweb/gpuweb.git@881403b5fda2d9ac9ffc5daa24e34738205bf155',
     'condition': 'dawn_node',
   },
+  'third_party/gpuweb-cts': {
+    'url': '{chromium_git}/external/github.com/gpuweb/cts@b0291fd966b55a5efc496772555b94842bde1085',
+    'condition': 'dawn_standalone',
+  },
 
   'tools/golang': {
     'condition': 'dawn_node',
@@ -194,6 +192,16 @@
     }],
     'dep_type': 'cipd',
   },
+
+  # Misc dependencies inherited from Tint
+  'third_party/benchmark': {
+    'url': '{chromium_git}/external/github.com/google/benchmark.git@e991355c02b93fe17713efe04cbc2e278e00fdbd',
+    'condition': 'dawn_standalone',
+  },
+  'third_party/protobuf': {
+    'url': '{chromium_git}/external/github.com/protocolbuffers/protobuf.git@fde7cf7358ec7cd69e8db9be4f1fa6a5c431386a',
+    'condition': 'dawn_standalone',
+  },
 }
 
 hooks = [
@@ -253,33 +261,22 @@
     'condition': 'dawn_standalone and host_os == "win"',
     'action': [ 'download_from_google_storage',
                 '--no_resume',
+                '--platform=win32',
                 '--no_auth',
                 '--bucket', 'chromium-clang-format',
                 '-s', 'buildtools/win/clang-format.exe.sha1',
     ],
   },
   {
-    'name': 'clang_format_mac_x64',
+    'name': 'clang_format_mac',
     'pattern': '.',
-    'condition': 'dawn_standalone and host_os == "mac" and host_cpu == "x64"',
+    'condition': 'dawn_standalone and host_os == "mac"',
     'action': [ 'download_from_google_storage',
                 '--no_resume',
+                '--platform=darwin',
                 '--no_auth',
                 '--bucket', 'chromium-clang-format',
-                '-s', 'buildtools/mac/clang-format.x64.sha1',
-                '-o', 'buildtools/mac/clang-format',
-    ],
-  },
-  {
-    'name': 'clang_format_mac_arm64',
-    'pattern': '.',
-    'condition': 'dawn_standalone and host_os == "mac" and host_cpu == "arm64"',
-    'action': [ 'download_from_google_storage',
-                '--no_resume',
-                '--no_auth',
-                '--bucket', 'chromium-clang-format',
-                '-s', 'buildtools/mac/clang-format.arm64.sha1',
-                '-o', 'buildtools/mac/clang-format',
+                '-s', 'buildtools/mac/clang-format.sha1',
     ],
   },
   {
@@ -288,11 +285,59 @@
     'condition': 'dawn_standalone and host_os == "linux"',
     'action': [ 'download_from_google_storage',
                 '--no_resume',
+                '--platform=linux*',
                 '--no_auth',
                 '--bucket', 'chromium-clang-format',
                 '-s', 'buildtools/linux64/clang-format.sha1',
     ],
   },
+  # Pull the compilers and system libraries for hermetic builds
+  {
+    'name': 'sysroot_x86',
+    'pattern': '.',
+    'condition': 'checkout_linux and ((checkout_x86 or checkout_x64))',
+    'action': ['python3', 'build/linux/sysroot_scripts/install-sysroot.py',
+               '--arch=x86'],
+  },
+  {
+    'name': 'sysroot_x64',
+    'pattern': '.',
+    'condition': 'checkout_linux and (checkout_x64)',
+    'action': ['python3', 'build/linux/sysroot_scripts/install-sysroot.py',
+               '--arch=x64'],
+  },
+  {
+    # Update the Mac toolchain if necessary.
+    'name': 'mac_toolchain',
+    'pattern': '.',
+    'condition': 'checkout_mac',
+    'action': ['python3', 'build/mac_toolchain.py'],
+  },
+  {
+    # Update the Windows toolchain if necessary. Must run before 'clang' below.
+    'name': 'win_toolchain',
+    'pattern': '.',
+    'condition': 'checkout_win',
+    'action': ['python3', 'build/vs_toolchain.py', 'update', '--force'],
+  },
+  {
+    # Note: On Win, this should run after win_toolchain, as it may use it.
+    'name': 'clang',
+    'pattern': '.',
+    'action': ['python3', 'tools/clang/scripts/update.py'],
+  },
+  {
+    # Pull rc binaries using checked-in hashes.
+    'name': 'rc_win',
+    'pattern': '.',
+    'condition': 'checkout_win and (host_os == "win")',
+    'action': [ 'download_from_google_storage',
+                '--no_resume',
+                '--no_auth',
+                '--bucket', 'chromium-browser-clang/rc',
+                '-s', 'build/toolchain/win/rc/win/rc.exe.sha1',
+    ],
+  },
   # Update build/util/LASTCHANGE.
   {
     'name': 'lastchange',
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..08eac60
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,2474 @@
+# Doxyfile 1.8.14
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all text
+# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv
+# built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "Tint"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = Tint
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = $(DOXYGEN_OUTPUT_DIRECTORY)
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 2
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+
+ALIASES                =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, Javascript,
+# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran:
+# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran:
+# Fortran. In the later case the parser tries to guess whether the code is fixed
+# or free formatted code, this is the default for Fortran type files), VHDL. For
+# instance to make doxygen treat .inc files as Fortran files (default is PHP),
+# and .f files as C (default is Fortran), use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 0.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 0
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = yes
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = yes
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# (class|struct|union) declarations. If set to NO, these declarations will be
+# included in the documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = YES
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = YES
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = YES
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = YES
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = YES
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = YES
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = $(DOXYGEN_WARN_AS_ERROR)
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT             = $(DOXYGEN_WARN_FORMAT)
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = CODE_OF_CONDUCT.md \
+                         src/tint/fuzzers/tint_spirv_tools_fuzzer \
+                         src \
+                         tools/src \
+                         src/tint/fuzzers/tint_spirv_tools_fuzzer \
+                         src/tint/fuzzers/tint_ast_fuzzer
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
+# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f \
+                         *.for \
+                         *.tcl \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       = *_test.cc
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE = ./README.md
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# function all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the config file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via Javascript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have Javascript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+#HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: https://developer.apple.com/tools/xcode/), introduced with
+# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: http://doc.qt.io/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: http://doc.qt.io/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: http://doc.qt.io/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# http://doc.qt.io/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side Javascript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using Javascript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when enabling USE_PDFLATEX this option is only used for generating
+# bitmaps for formulas in the HTML output, but not in the Makefile that is
+# written to the output directory.
+# The default file is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's config
+# file, i.e. a series of assignments. You only have to provide replacements,
+# missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's config file. A template extensions file can be generated
+# using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             = DOXYGEN
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of 'which perl').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+#PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see:
+# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+#MSCGEN_PATH            =
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/OWNERS b/OWNERS
index 1856f30..a7cef5e 100644
--- a/OWNERS
+++ b/OWNERS
@@ -9,3 +9,12 @@
 per-file dawn.json=kainino@chromium.org
 per-file DEPS=*
 per-file README.md=file://docs/dawn/OWNERS
+
+# Tint specific OWNERS
+amaiorano@google.com
+bclayton@chromium.org
+bclayton@google.com
+cwallez@chromium.org
+dneto@google.com
+jrprice@google.com
+rharrison@chromium.org
diff --git a/OWNERS.dawn b/OWNERS.dawn
new file mode 100644
index 0000000..1856f30
--- /dev/null
+++ b/OWNERS.dawn
@@ -0,0 +1,11 @@
+cwallez@chromium.org
+enga@chromium.org
+jiawei.shao@intel.com
+
+# Backup reviewers if needed.
+bclayton@google.com
+kainino@chromium.org
+
+per-file dawn.json=kainino@chromium.org
+per-file DEPS=*
+per-file README.md=file://docs/dawn/OWNERS
diff --git a/OWNERS.tint b/OWNERS.tint
new file mode 100644
index 0000000..18239af
--- /dev/null
+++ b/OWNERS.tint
@@ -0,0 +1,7 @@
+amaiorano@google.com
+bclayton@chromium.org
+bclayton@google.com
+cwallez@chromium.org
+dneto@google.com
+jrprice@google.com
+rharrison@chromium.org
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 899e0e2..968a27c 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -1,4 +1,4 @@
-# Copyright 2018 The Dawn Authors
+# Copyright 2022 The Dawn & Tint Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,12 +12,97 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import os
-import platform
-import subprocess
+import re
 
 USE_PYTHON3 = True
 
+NONINCLUSIVE_REGEXES = [
+    r"(?i)black[-_]?list",
+    r"(?i)white[-_]?list",
+    r"(?i)gr[ea]y[-_]?list",
+    r"(?i)(first class citizen)",
+    r"(?i)black[-_]?hat",
+    r"(?i)white[-_]?hat",
+    r"(?i)gr[ea]y[-_]?hat",
+    r"(?i)master",
+    r"(?i)slave",
+    r"(?i)\bhim\b",
+    r"(?i)\bhis\b",
+    r"(?i)\bshe\b",
+    r"(?i)\bher\b",
+    r"(?i)\bguys\b",
+    r"(?i)\bhers\b",
+    r"(?i)\bman\b",
+    r"(?i)\bwoman\b",
+    r"(?i)\she\s",
+    r"(?i)\she$",
+    r"(?i)^he\s",
+    r"(?i)^he$",
+    r"(?i)\she['|\u2019]d\s",
+    r"(?i)\she['|\u2019]d$",
+    r"(?i)^he['|\u2019]d\s",
+    r"(?i)^he['|\u2019]d$",
+    r"(?i)\she['|\u2019]s\s",
+    r"(?i)\she['|\u2019]s$",
+    r"(?i)^he['|\u2019]s\s",
+    r"(?i)^he['|\u2019]s$",
+    r"(?i)\she['|\u2019]ll\s",
+    r"(?i)\she['|\u2019]ll$",
+    r"(?i)^he['|\u2019]ll\s",
+    r"(?i)^he['|\u2019]ll$",
+    r"(?i)grandfather",
+    r"(?i)\bmitm\b",
+    r"(?i)\bcrazy\b",
+    r"(?i)\binsane\b",
+    r"(?i)\bblind\sto\b",
+    r"(?i)\bflying\sblind\b",
+    r"(?i)\bblind\seye\b",
+    r"(?i)\bcripple\b",
+    r"(?i)\bcrippled\b",
+    r"(?i)\bdumb\b",
+    r"(?i)\bdummy\b",
+    r"(?i)\bparanoid\b",
+    r"(?i)\bsane\b",
+    r"(?i)\bsanity\b",
+    r"(?i)red[-_]?line",
+]
+
+NONINCLUSIVE_REGEX_LIST = []
+for reg in NONINCLUSIVE_REGEXES:
+    NONINCLUSIVE_REGEX_LIST.append(re.compile(reg))
+
+
+def _CheckNonInclusiveLanguage(input_api, output_api, source_file_filter=None):
+    """Checks the files for non-inclusive language."""
+
+    matches = []
+    for f in input_api.AffectedFiles(include_deletes=False,
+                                     file_filter=source_file_filter):
+        for line_num, line in f.ChangedContents():
+            for reg in NONINCLUSIVE_REGEX_LIST:
+                match = reg.search(line)
+                if match:
+                    matches.append(
+                        "{} ({}): found non-inclusive language: {}".format(
+                            f.LocalPath(), line_num, match.group(0)))
+
+    if len(matches):
+        return [
+            output_api.PresubmitPromptWarning('Non-inclusive language found:',
+                                              items=matches)
+        ]
+
+    return []
+
+
+def _NonInclusiveFileFilter(file):
+    filter_list = [
+        "PRESUBMIT.py",  # Non-inclusive language check data
+        "docs/tint/spirv-input-output-variables.md",  # External URL
+        "test/tint/samples/compute_boids.wgsl ",  # External URL
+    ]
+    return file in filter_list
+
 
 def _DoCommonChecks(input_api, output_api):
     results = []
@@ -27,6 +112,30 @@
         input_api.canned_checks.CheckPatchFormatted(input_api,
                                                     output_api,
                                                     check_python=True))
+    results.extend(
+        input_api.canned_checks.CheckChangeHasDescription(
+            input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckGNFormatted(input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol(
+            input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckChangeHasNoTabs(input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckChangeTodoHasOwner(input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
+            input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckDoNotSubmit(input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckChangeLintsClean(input_api,
+                                                      output_api,
+                                                      lint_filters=""))
+    results.extend(
+        _CheckNonInclusiveLanguage(input_api, output_api,
+                                   _NonInclusiveFileFilter))
     return results
 
 
diff --git a/PRESUBMIT.py.dawn b/PRESUBMIT.py.dawn
new file mode 100644
index 0000000..899e0e2
--- /dev/null
+++ b/PRESUBMIT.py.dawn
@@ -0,0 +1,38 @@
+# Copyright 2018 The Dawn 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.
+
+import os
+import platform
+import subprocess
+
+USE_PYTHON3 = True
+
+
+def _DoCommonChecks(input_api, output_api):
+    results = []
+    results.extend(
+        input_api.canned_checks.CheckChangedLUCIConfigs(input_api, output_api))
+    results.extend(
+        input_api.canned_checks.CheckPatchFormatted(input_api,
+                                                    output_api,
+                                                    check_python=True))
+    return results
+
+
+def CheckChangeOnUpload(input_api, output_api):
+    return _DoCommonChecks(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+    return _DoCommonChecks(input_api, output_api)
diff --git a/PRESUBMIT.py.tint b/PRESUBMIT.py.tint
new file mode 100755
index 0000000..97623c1
--- /dev/null
+++ b/PRESUBMIT.py.tint
@@ -0,0 +1,167 @@
+# Copyright 2020 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.
+"""Presubmit script for Tint.
+See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
+for more details about the presubmit API built into depot_tools.
+"""
+
+import re
+
+USE_PYTHON3 = True
+
+
+def _LicenseHeader(input_api):
+    """Returns the license header regexp."""
+    # Accept any year number from 2019 to the current year
+    current_year = int(input_api.time.strftime('%Y'))
+    allowed_years = (str(s) for s in reversed(xrange(2019, current_year + 1)))
+    years_re = '(' + '|'.join(allowed_years) + ')'
+    license_header = (
+        r'.*? Copyright( \(c\))? %(year)s The Tint [Aa]uthors\n '
+        r'.*?\n'
+        r'.*? Licensed under the Apache License, Version 2.0 (the "License");\n'
+        r'.*? you may not use this file except in compliance with the License.\n'
+        r'.*? You may obtain a copy of the License at\n'
+        r'.*?\n'
+        r'.*?     http://www.apache.org/licenses/LICENSE-2.0\n'
+        r'.*?\n'
+        r'.*? Unless required by applicable law or agreed to in writing, software\n'
+        r'.*? distributed under the License is distributed on an "AS IS" BASIS,\n'
+        r'.*? WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n'
+        r'.*? See the License for the specific language governing permissions and\n'
+        r'.*? limitations under the License.\n') % {
+            'year': years_re,
+        }
+    return license_header
+
+
+REGEXES = [
+    r"(?i)black[-_]?list",
+    r"(?i)white[-_]?list",
+    r"(?i)gr[ea]y[-_]?list",
+    r"(?i)(first class citizen)",
+    r"(?i)black[-_]?hat",
+    r"(?i)white[-_]?hat",
+    r"(?i)gr[ea]y[-_]?hat",
+    r"(?i)master",
+    r"(?i)slave",
+    r"(?i)\bhim\b",
+    r"(?i)\bhis\b",
+    r"(?i)\bshe\b",
+    r"(?i)\bher\b",
+    r"(?i)\bguys\b",
+    r"(?i)\bhers\b",
+    r"(?i)\bman\b",
+    r"(?i)\bwoman\b",
+    r"(?i)\she\s",
+    r"(?i)\she$",
+    r"(?i)^he\s",
+    r"(?i)^he$",
+    r"(?i)\she['|\u2019]d\s",
+    r"(?i)\she['|\u2019]d$",
+    r"(?i)^he['|\u2019]d\s",
+    r"(?i)^he['|\u2019]d$",
+    r"(?i)\she['|\u2019]s\s",
+    r"(?i)\she['|\u2019]s$",
+    r"(?i)^he['|\u2019]s\s",
+    r"(?i)^he['|\u2019]s$",
+    r"(?i)\she['|\u2019]ll\s",
+    r"(?i)\she['|\u2019]ll$",
+    r"(?i)^he['|\u2019]ll\s",
+    r"(?i)^he['|\u2019]ll$",
+    r"(?i)grandfather",
+    r"(?i)\bmitm\b",
+    r"(?i)\bcrazy\b",
+    r"(?i)\binsane\b",
+    r"(?i)\bblind\sto\b",
+    r"(?i)\bflying\sblind\b",
+    r"(?i)\bblind\seye\b",
+    r"(?i)\bcripple\b",
+    r"(?i)\bcrippled\b",
+    r"(?i)\bdumb\b",
+    r"(?i)\bdummy\b",
+    r"(?i)\bparanoid\b",
+    r"(?i)\bsane\b",
+    r"(?i)\bsanity\b",
+    r"(?i)red[-_]?line",
+]
+
+REGEX_LIST = []
+for reg in REGEXES:
+    REGEX_LIST.append(re.compile(reg))
+
+def CheckNonInclusiveLanguage(input_api, output_api, source_file_filter=None):
+    """Checks the files for non-inclusive language."""
+
+    matches = []
+    for f in input_api.AffectedFiles(include_deletes=False,
+                                     file_filter=source_file_filter):
+        for line_num, line in f.ChangedContents():
+            for reg in REGEX_LIST:
+                match = reg.search(line)
+                if match:
+                    matches.append(
+                        "{} ({}): found non-inclusive language: {}".format(
+                            f.LocalPath(), line_num, match.group(0)))
+
+    if len(matches):
+        return [
+            output_api.PresubmitPromptWarning('Non-inclusive language found:',
+                                              items=matches)
+        ]
+
+    return []
+
+
+def CheckChange(input_api, output_api):
+    results = []
+
+    results += input_api.canned_checks.CheckChangeHasDescription(
+        input_api, output_api)
+    results += input_api.canned_checks.CheckPatchFormatted(input_api,
+                                                           output_api,
+                                                           check_python=True)
+    results += input_api.canned_checks.CheckGNFormatted(input_api, output_api)
+    results += input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol(
+        input_api, output_api)
+    results += input_api.canned_checks.CheckChangeHasNoTabs(
+        input_api, output_api)
+    results += input_api.canned_checks.CheckChangeTodoHasOwner(
+        input_api, output_api)
+    results += input_api.canned_checks.CheckChangeHasNoStrayWhitespace(
+        input_api, output_api)
+    results += input_api.canned_checks.CheckDoNotSubmit(input_api, output_api)
+    results += input_api.canned_checks.CheckChangeLintsClean(input_api,
+                                                             output_api,
+                                                             lint_filters="")
+
+    def NonInclusiveFileFilter(file):
+        filter_list = [
+            "docs/tint/spirv-input-output-variables.md",  # External URL
+            "test/tint/samples/compute_boids.wgsl ",  # External URL
+        ]
+        return file in filter_list
+
+    results += CheckNonInclusiveLanguage(input_api, output_api,
+                                         NonInclusiveFileFilter)
+
+    return results
+
+
+def CheckChangeOnUpload(input_api, output_api):
+    return CheckChange(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+    return CheckChange(input_api, output_api)
diff --git a/README.md b/README.md
index 1871388..a430410 100644
--- a/README.md
+++ b/README.md
@@ -50,3 +50,110 @@
 ## Disclaimer
 
 This is not an officially supported Google product.
+
+# Tint
+
+Tint is a compiler for the WebGPU Shader Language (WGSL).
+
+This is not an officially supported Google product.
+
+## Requirements
+ * Git
+ * CMake (3.10.2 or later)
+ * Ninja (or other build tool)
+ * Python, for fetching dependencies
+ * [depot_tools] in your path
+
+## Build options
+ * `TINT_BUILD_SPV_READER` : enable the SPIR-V input reader (off by default)
+ * `TINT_BUILD_WGSL_READER` : enable the WGSL input reader (on by default)
+ * `TINT_BUILD_SPV_WRITER` : enable the SPIR-V output writer (on by default)
+ * `TINT_BUILD_WGSL_WRITER` : enable the WGSL output writer (on by default)
+ * `TINT_BUILD_FUZZERS` : enable building fuzzzers (off by default)
+
+## Building
+Tint uses Chromium dependency management so you need to install [depot_tools]
+and add it to your PATH.
+
+[depot_tools]: http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up
+
+### Getting source & dependencies
+
+```sh
+# Clone the repo as "tint"
+git clone https://dawn.googlesource.com/tint tint
+cd tint
+
+# Bootstrap the gclient configuration
+cp standalone.gclient .gclient
+
+# Fetch external dependencies and toolchains with gclient
+gclient sync
+```
+
+### Compiling using CMake + Ninja
+```sh
+mkdir -p out/Debug
+cd out/Debug
+cmake -GNinja ../..
+ninja # or autoninja
+```
+
+### Compiling using CMake + make
+```sh
+mkdir -p out/Debug
+cd out/Debug
+cmake ../..
+make # -j N for N-way parallel build
+```
+
+### Compiling using gn + ninja
+```sh
+mkdir -p out/Debug
+gn gen out/Debug
+autoninja -C out/Debug
+```
+
+### Fuzzers on MacOS
+If you are attempting fuzz, using `TINT_BUILD_FUZZERS=ON`, the version of llvm
+in the XCode SDK does not have the needed libfuzzer functionality included.
+
+The build error that you will see from using the XCode SDK will look something
+like this:
+```
+ld: file not found:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.0/lib/darwin/libclang_rt.fuzzer_osx.a
+```
+
+The solution to this problem is to use a full version llvm, like what you would
+get via homebrew, `brew install llvm`, and use something like `CC=<path to full
+clang> cmake ..` to setup a build using that toolchain.
+
+### Checking [chromium-style] issues in CMake builds
+The gn based work flow uses the Chromium toolchain for building in anticipation
+of integration of Tint into Chromium based projects. This toolchain has
+additional plugins for checking for style issues, which are marked with
+[chromium-style] in log messages. This means that this toolchain is more strict
+then the default clang toolchain.
+
+In the future we will have a CQ that will build this work flow and flag issues
+automatically. Until that is in place, to avoid causing breakages you can run
+the [chromium-style] checks using the CMake based work flows. This requires
+setting `CC` to the version of clang checked out by `gclient sync` and setting
+the `TINT_CHECK_CHROMIUM_STYLE` to `ON`.
+
+```sh
+mkdir -p out/style
+cd out/style
+cmake ../..
+CC=../../third_party/llvm-build/Release+Asserts/bin/clang cmake -DTINT_CHECK_CHROMIUM_STYLE=ON ../../ # add -GNinja for ninja builds
+```
+
+## Issues
+Please file any issues or feature requests at
+https://bugs.chromium.org/p/tint/issues/entry
+
+## Contributing
+Please see the CONTRIBUTING and CODE_OF_CONDUCT files on how to contribute to
+Tint.
+
+Tint has a process for supporting [experimental extensions](docs/tint/experimental_extensions.md).
diff --git a/README.md.dawn b/README.md.dawn
new file mode 100644
index 0000000..1871388
--- /dev/null
+++ b/README.md.dawn
@@ -0,0 +1,52 @@
+![Dawn's logo: a sun rising behind a stylized mountain inspired by the WebGPU logo. The text "Dawn" is written below it.](docs/imgs/dawn_logo.png "Dawn's logo")
+
+# Dawn, a WebGPU implementation
+
+Dawn is an open-source and cross-platform implementation of the work-in-progress [WebGPU](https://webgpu.dev) standard.
+More precisely it implements [`webgpu.h`](https://github.com/webgpu-native/webgpu-headers/blob/master/webgpu.h) that is a one-to-one mapping with the WebGPU IDL.
+Dawn is meant to be integrated as part of a larger system and is the underlying implementation of WebGPU in Chromium.
+
+Dawn provides several WebGPU building blocks:
+ - **WebGPU C/C++ headers** that applications and other building blocks use.
+   - The `webgpu.h` version that Dawn implements.
+   - A C++ wrapper for the `webgpu.h`.
+ - **A "native" implementation of WebGPU** using platforms' GPU APIs:
+   - **D3D12** on Windows 10
+   - **Metal** on macOS and iOS
+   - **Vulkan** on Windows, Linux, ChromeOS, Android and Fuchsia
+   - OpenGL as best effort where available
+ - **A client-server implementation of WebGPU** for applications that are in a sandbox without access to native drivers
+
+Helpful links:
+
+ - [Dawn's bug tracker](https://bugs.chromium.org/p/dawn/issues/entry) if you find issues with Dawn.
+ - [Dawn's mailing list](https://groups.google.com/forum/#!members/dawn-graphics) for other discussions related to Dawn.
+ - [Dawn's source code](https://dawn.googlesource.com/dawn)
+ - [Dawn's Matrix chatroom](https://matrix.to/#/#webgpu-dawn:matrix.org) for live discussion around contributing or using Dawn.
+ - [WebGPU's Matrix chatroom](https://matrix.to/#/#WebGPU:matrix.org)
+
+## Documentation table of content
+
+Developer documentation:
+
+ - [Dawn overview](docs/dawn/overview.md)
+ - [Building Dawn](docs/dawn/building.md)
+ - [Contributing to Dawn](docs/dawn/contributing.md)
+ - [Testing Dawn](docs/dawn/testing.md)
+ - [Debugging Dawn](docs/dawn/debugging.md)
+ - [Dawn's infrastructure](docs/dawn/infra.md)
+ - [Dawn errors](docs/dawn/errors.md)
+
+User documentation: (TODO, figure out what overlaps with the webgpu.h docs)
+
+## Status
+
+(TODO)
+
+## License
+
+Apache 2.0 Public License, please see [LICENSE](/LICENSE).
+
+## Disclaimer
+
+This is not an officially supported Google product.
diff --git a/README.md.tint b/README.md.tint
new file mode 100644
index 0000000..fbe6cfb
--- /dev/null
+++ b/README.md.tint
@@ -0,0 +1,106 @@
+# Tint
+
+Tint is a compiler for the WebGPU Shader Language (WGSL).
+
+This is not an officially supported Google product.
+
+## Requirements
+ * Git
+ * CMake (3.10.2 or later)
+ * Ninja (or other build tool)
+ * Python, for fetching dependencies
+ * [depot_tools] in your path
+
+## Build options
+ * `TINT_BUILD_SPV_READER` : enable the SPIR-V input reader (off by default)
+ * `TINT_BUILD_WGSL_READER` : enable the WGSL input reader (on by default)
+ * `TINT_BUILD_SPV_WRITER` : enable the SPIR-V output writer (on by default)
+ * `TINT_BUILD_WGSL_WRITER` : enable the WGSL output writer (on by default)
+ * `TINT_BUILD_FUZZERS` : enable building fuzzzers (off by default)
+
+## Building
+Tint uses Chromium dependency management so you need to install [depot_tools]
+and add it to your PATH.
+
+[depot_tools]: http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up
+
+### Getting source & dependencies
+
+```sh
+# Clone the repo as "tint"
+git clone https://dawn.googlesource.com/tint tint
+cd tint
+
+# Bootstrap the gclient configuration
+cp standalone.gclient .gclient
+
+# Fetch external dependencies and toolchains with gclient
+gclient sync
+```
+
+### Compiling using CMake + Ninja
+```sh
+mkdir -p out/Debug
+cd out/Debug
+cmake -GNinja ../..
+ninja # or autoninja
+```
+
+### Compiling using CMake + make
+```sh
+mkdir -p out/Debug
+cd out/Debug
+cmake ../..
+make # -j N for N-way parallel build
+```
+
+### Compiling using gn + ninja
+```sh
+mkdir -p out/Debug
+gn gen out/Debug
+autoninja -C out/Debug
+```
+
+### Fuzzers on MacOS
+If you are attempting fuzz, using `TINT_BUILD_FUZZERS=ON`, the version of llvm
+in the XCode SDK does not have the needed libfuzzer functionality included.
+
+The build error that you will see from using the XCode SDK will look something
+like this:
+```
+ld: file not found:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/11.0.0/lib/darwin/libclang_rt.fuzzer_osx.a
+```
+
+The solution to this problem is to use a full version llvm, like what you would
+get via homebrew, `brew install llvm`, and use something like `CC=<path to full
+clang> cmake ..` to setup a build using that toolchain.
+
+### Checking [chromium-style] issues in CMake builds
+The gn based work flow uses the Chromium toolchain for building in anticipation
+of integration of Tint into Chromium based projects. This toolchain has
+additional plugins for checking for style issues, which are marked with
+[chromium-style] in log messages. This means that this toolchain is more strict
+then the default clang toolchain.
+
+In the future we will have a CQ that will build this work flow and flag issues
+automatically. Until that is in place, to avoid causing breakages you can run
+the [chromium-style] checks using the CMake based work flows. This requires
+setting `CC` to the version of clang checked out by `gclient sync` and setting
+the `TINT_CHECK_CHROMIUM_STYLE` to `ON`.
+
+```sh
+mkdir -p out/style
+cd out/style
+cmake ../..
+CC=../../third_party/llvm-build/Release+Asserts/bin/clang cmake -DTINT_CHECK_CHROMIUM_STYLE=ON ../../ # add -GNinja for ninja builds
+```
+
+## Issues
+Please file any issues or feature requests at
+https://bugs.chromium.org/p/tint/issues/entry
+
+## Contributing
+Please see the CONTRIBUTING and CODE_OF_CONDUCT files on how to contribute to
+Tint.
+
+Tint has a process for supporting [experimental extensions](docs/tint/experimental_extensions.md).
diff --git a/build_overrides/build.gni b/build_overrides/build.gni
index e883854..8717867 100644
--- a/build_overrides/build.gni
+++ b/build_overrides/build.gni
@@ -1,4 +1,4 @@
-# Copyright 2018 The Dawn Authors
+# Copyright 2022 The Dawn Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/build_overrides/dawn.gni b/build_overrides/dawn.gni
index bbded06..87e1ded 100644
--- a/build_overrides/dawn.gni
+++ b/build_overrides/dawn.gni
@@ -34,7 +34,6 @@
 dawn_googletest_dir = "//third_party/googletest"
 dawn_spirv_tools_dir = "//third_party/vulkan-deps/spirv-tools/src"
 dawn_swiftshader_dir = "//third_party/swiftshader"
-dawn_tint_dir = "//third_party/tint"
 dawn_vulkan_loader_dir = "//third_party/vulkan-deps/vulkan-loader/src"
 dawn_vulkan_validation_layers_dir =
     "//third_party/vulkan-deps/vulkan-validation-layers/src"
diff --git a/build_overrides/tint.gni b/build_overrides/tint.gni
index c4d4d12..8349998 100644
--- a/build_overrides/tint.gni
+++ b/build_overrides/tint.gni
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-tint_root_dir = "//third_party/tint"
 tint_spirv_tools_dir = "//third_party/vulkan-deps/spirv-tools/src"
 tint_spirv_headers_dir = "//third_party/vulkan-deps/spirv-headers/src"
 
diff --git a/docs/tint/arch.md b/docs/tint/arch.md
new file mode 100644
index 0000000..204ff64
--- /dev/null
+++ b/docs/tint/arch.md
@@ -0,0 +1,195 @@
+# Tint Architecture
+
+```
+                   ┏━━━━━━━━┓                   ┏━━━━━━┓
+                   ┃ SPIR━V ┃                   ┃ WGSL ┃
+                   ┗━━━━┃━━━┛                   ┗━━━┃━━┛
+                        ▼                           ▼
+              ┏━━━━━━━━━┃━━━━━━━━━━━━━━━━━━━━━━━━━━━┃━━━━━━━━┓
+              ┃         ┃          Reader           ┃        ┃
+              ┃         ┃                           ┃        ┃
+              ┃ ┏━━━━━━━┻━━━━━━┓             ┏━━━━━━┻━━━━━━┓ ┃
+              ┃ ┃ SPIRV-Reader ┃             ┃ WGSL-Reader ┃ ┃
+              ┃ ┗━━━━━━━━━━━━━━┛             ┗━━━━━━━━━━━━━┛ ┃
+              ┗━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┛
+                                      ▼
+                    ┏━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━┓
+                    ┃           ProgramBuilder          ┃
+                    ┃             (mutable)             ┃
+      ┏━━━━━━━━━━━━►┫   ┏━━━━━┓ ┏━━━━━━━┓ ┏━━━━━━━━━┓   ┃
+      ┃             ┃   ┃ AST ┃ ┃ Types ┃ ┃ Symbols ┃   ┃
+      ┃             ┃   ┗━━━━━┛ ┗━━━━━━━┛ ┗━━━━━━━━━┛   ┃
+      ┃             ┗━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┛
+      ┃                               ▼
+      ┃             ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┃┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+      ▲             ┆ Build           ▼                ┆
+  ┏━━━┻━━━┓         ┆        ┏━━━━━━━━┻━━━━━━━━┓       ┆
+  ┃ Clone ┃         ┆        ┃    Resolver     ┃       ┆
+  ┗━━━┳━━━┛         ┆        ┗━━━━━━━━━━━━━━━━━┛       ┆
+      ▲             └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┃┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+      ┃                               ▼
+      ┃       ┏━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┓
+      ┃       ┃                    Program                   ┃
+      ┃       ┃                  (immutable)                 ┃
+      ┣━━━━━━◄┫  ┏━━━━━┓ ┏━━━━━━━┓ ┏━━━━━━━━━━┓ ┏━━━━━━━━━┓  ┃
+      ┃       ┃  ┃ AST ┃ ┃ Types ┃ ┃ Semantic ┃ ┃ Symbols ┃  ┃
+      ┃       ┃  ┗━━━━━┛ ┗━━━━━━━┛ ┗━━━━━━━━━━┛ ┗━━━━━━━━━┛  ┃
+      ┃       ┗━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┛
+      ▲                               ▼
+┏━━━━━┻━━━━━┓                         ┃             ┏━━━━━━━━━━━┓
+┃ Transform ┃◄━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━►┃ Inspector ┃
+┗━━━━━━━━━━━┛                         ┃             ┗━━━━━━━━━━━┛
+                                      ▼
+┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+┃                                  Writers                                    ┃
+┃                                                                             ┃
+┃ ┏━━━━━━━━━━━━━━┓┏━━━━━━━━━━━━━┓┏━━━━━━━━━━━━━┓┏━━━━━━━━━━━━━┓┏━━━━━━━━━━━━┓ ┃
+┃ ┃ SPIRV-Writer ┃┃ WGSL-Writer ┃┃ HLSL-Writer ┃┃ GLSL-Writer ┃┃ MSL-Writer ┃ ┃
+┃ ┗━━━━━━━┳━━━━━━┛┗━━━━━━┳━━━━━━┛┗━━━━━━┳━━━━━━┛┗━━━━━━┳━━━━━━┛┗━━━━━━┳━━━━━┛ ┃
+┗━━━━━━━━━┃━━━━━━━━━━━━━━┃━━━━━━━━━━━━━━┃━━━━━━━━━━━━━━┃━━━━━━━━━━━━━━┃━━━━━━━┛
+          ▼              ▼              ▼              ▼              ▼
+     ┏━━━━┻━━━┓      ┏━━━┻━━┓       ┏━━━┻━━┓       ┏━━━┻━━┓        ┏━━┻━━┓
+     ┃ SPIR-V ┃      ┃ WGSL ┃       ┃ HLSL ┃       ┃ GLSL ┃        ┃ MSL ┃
+     ┗━━━━━━━━┛      ┗━━━━━━┛       ┗━━━━━━┛       ┗━━━━━━┛        ┗━━━━━┛
+```
+
+## Reader
+
+Readers are responsible for parsing a shader program and populating a
+`ProgramBuilder` with the parsed AST, type and symbol information.
+
+The WGSL reader is a recursive descent parser. It closely follows the WGSL
+grammar in the naming of the parse methods.
+
+## ProgramBuilder
+
+A `ProgramBuilder` is the primary interface to construct an immutable `Program`.
+There are a number of methods exposed which make creating of the `Program`
+simpler. A `ProgramBuilder` can only be used once, and must be discarded after
+the `Program` is constructed.
+
+A `Program` is built from the `ProgramBuilder` by `std::move()`ing the
+`ProgramBuilder` to a new `Program` object. When built, resolution is performed
+so the produced `Program` will contain all the needed semantic information.
+
+At any time before building the `Program`, `ProgramBuilder::IsValid()` may be
+called to ensure the AST is **structurally** correct. This checks that things
+like `if` statements have a condition and body attached.
+
+If further changes to the `Program` are needed (say via a `Transform`) then a
+new `ProgramBuilder` can be produced by cloning the `Program` into a new
+`ProgramBuilder`.
+
+Unlike `Program`s, `ProgramBuilder`s are not part of the public Tint API.
+
+## AST
+
+The Abstract Syntax Tree is a directed acyclic graph of `ast::Node`s which
+encode the syntactic structure of the WGSL program.
+
+The root of the AST is the `ast::Module` class which holds each of the declared
+functions, variables and user defined types (type aliases and structures).
+
+Each `ast::Node` represents a **single** part of the program's source, and so
+`ast::Node`s are not shared.
+
+The AST does not perform any verification of its content. For example, the
+`ast::StrideAttribute` node has numeric stride parameter, which is a count of
+the number of bytes from the start of one array element to the start of the
+next. The AST node itself does not constrain the set of stride values that you
+can set, aside from storing it as an unsigned integer.
+
+## Types
+
+Types are constructed during the Reader and resolution phases, and are
+held by the `Program` or `ProgramBuilder`. AST and semantic nodes can both
+reference types.
+
+Each `type::Type` node **uniquely** represents a particular spelling of a WGSL
+type within the program, so you can compare `type::Type*` pointers to check for
+equivalence of type expressions.
+For example, there is only one `type::Type` node for the `i32` type, no matter
+how many times it is mentioned in the source program.
+However, if `MyI32` is a type alias for `i32`, then they will have two different
+type nodes.
+
+## Semantic information
+
+Semantic information is held by `sem::Node`s which describe the program at
+a higher / more abstract level than the AST. This includes information such as
+the resolved type of each expression, the resolved overload of a builtin
+function call, and the module scoped variables used by each function.
+
+Semantic information is generated by the `Resolver` when the `Program`
+is built from a `ProgramBuilder`.
+
+The `sem::Info` class holds a map of `ast::Node`s to `sem::Node`s.
+This map is **many-to-one** - i.e. while a AST node might have a single
+corresponding semantic node, the reverse may not be true. For example:
+many `ast::IdentifierExpression` nodes may map to a single `sem::Variable`,
+and so the `sem::Variable` does not have a single corresponding
+`ast::Node`.
+
+Unlike `ast::Node`s, semantic nodes may not necessarily form a directed acyclic
+graph, and the semantic graph may contain diamonds.
+
+## Symbols
+
+Symbols represent a unique string identifier in the source program. These string
+identifiers are transformed into symbols within the `Reader`s.
+
+During the Writer phase, symbols may be emitted as strings using a `Namer`.
+A `Namer` may output the symbol in any form that preserves the uniqueness of
+that symbol.
+
+## Resolver
+
+The `Resolver` will automatically run when a `Program` is built.
+A `Resolver` creates the `Program`s semantic information by analyzing the
+`Program`s AST and type information.
+
+The `Resolver` will validate to make sure the generated `Program` is
+semantically valid.
+
+## Program
+
+A `Program` holds an immutable version of the information from the
+`ProgramBuilder` along with semantic information generated by the
+`Resolver`.
+
+Like `ProgramBuilder`, `Program::IsValid()` may be called to ensure the AST is
+structurally correct and semantically valid, and that the `Resolver` did not
+report any errors.
+
+Unlike the `ProgramBuilder`, a `Program` is fully immutable, and is part of the
+public Tint API. The immutable nature of `Program`s make these entirely safe
+to share between multiple threads without the use of synchronization primitives.
+
+## Inspector
+
+The inspectors job is to go through the `Program` and pull out various pieces of
+information. The information may be used to pass information into the downstream
+compilers (things like specialization constants) or may be used to pass into
+transforms to update the AST before generating the resulting code.
+
+The input `Program` to the inspector must be valid (pass validation).
+
+## Transforms
+
+There maybe various transforms we want to run over the `Program`.
+This is for things like Vertex Pulling or Robust Buffer Access.
+
+A transform operates by cloning the input `Program` into a new `ProgramBuilder`,
+applying the required changes, and then finally building and returning a new
+output `Program`. As the resolver is always run when a `Program` is built,
+Transforms will always emit a `Program` with semantic information.
+
+The input `Program` to a transform must be valid (pass validation).
+If the input `Program` of a transform is valid then the transform must guarantee
+that the output program is also valid.
+
+## Writers
+
+A writer is responsible for writing the `Program` in the target shader language.
+
+The input `Program` to a writer must be valid (pass validation).
diff --git a/docs/tint/compound_statements.md b/docs/tint/compound_statements.md
new file mode 100644
index 0000000..a113cce
--- /dev/null
+++ b/docs/tint/compound_statements.md
@@ -0,0 +1,119 @@
+# Compound Statements
+
+Compound statements are statements that can hold other statements.
+
+This document maps the WGSL compound statements to their semantic tree representations.
+
+## if statement
+
+WGSL:
+```
+if (condition_a) {
+    statement_a;
+} else if (condition_b) {
+    statement_b;
+} else {
+    statement_c;
+}
+```
+
+Semantic tree:
+```
+sem::IfStatement {
+    condition_a
+    sem::BlockStatement {
+        statement_a
+    }
+    sem::ElseStatement {
+        condition_b
+        sem::BlockStatement {
+            statement_b
+        }
+    }
+    sem::ElseStatement {
+        sem::BlockStatement {
+            statement_c
+        }
+    }
+}
+```
+
+## for loop
+
+WGSL:
+```
+for (initializer; condition; continuing) {
+    statement;
+}
+```
+
+Semantic tree:
+```
+sem::ForLoopStatement {
+    sem::Statement  initializer
+    sem::Expression condition
+    sem::Statement  continuing
+
+    sem::LoopBlockStatement {
+        sem::Statement statement
+    }
+}
+```
+
+## loop
+
+WGSL:
+```
+loop (condition) {
+    statement_a;
+    continuing {
+        statement_b;
+    }
+}
+```
+
+Semantic tree:
+```
+sem::LoopStatement {
+    sem::Expression condition
+
+    sem::LoopBlockStatement {
+        sem::Statement statement_a
+        sem::LoopContinuingBlockStatement {
+            sem::Statement statement_b
+        }
+    }
+}
+```
+
+
+## switch statement
+
+WGSL:
+```
+switch (condition) {
+    case literal_a, literal_b: {
+        statement_a;
+    }
+    default {
+        statement_b;
+    }
+}
+```
+
+Semantic tree:
+```
+sem::SwitchStatement {
+    sem::Expression condition
+    sem::CaseStatement {
+        sem::BlockStatement {
+            sem::Statement statement_a
+        }
+    }
+    sem::CaseStatement {
+        sem::BlockStatement {
+            sem::Statement statement_b
+        }
+    }
+}
+```
diff --git a/docs/tint/coverage-info.md b/docs/tint/coverage-info.md
new file mode 100644
index 0000000..fdb79df
--- /dev/null
+++ b/docs/tint/coverage-info.md
@@ -0,0 +1,24 @@
+# Generating and viewing Tint code-coverage
+
+Requirements:
+
+* Host running Linux or macOS
+* Clang toolchain on the `PATH` environment variable
+
+## Building Tint with coverage generation enabled
+
+Follow the steps [to build Tint with CMake](../README.md), but include the additional `-DTINT_EMIT_COVERAGE=1` CMake flag.
+
+## Generate coverage information
+
+Use the `<tint>/tools/tint-generate-coverage` script to run the tint executable or unit tests and generate the coverage information.
+
+The script takes the executable to invoke as the first command line argument, followed by additional arguments to pass to the executable.
+
+For example, to see the code coverage for all unit tests, run:
+`<tint>/tools/tint-generate-coverage <build>/tint_unittests --gtest_brief`
+
+The script will emit two files at the root of the tint directory:
+
+* `coverage.summary` - A text file giving a coverage summary for all Tint source files.
+* `lcov.info` - A binary coverage file that can be consumed with the [VSCode Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension.
diff --git a/docs/tint/diagnostics_guide.md b/docs/tint/diagnostics_guide.md
new file mode 100644
index 0000000..fc8fa22
--- /dev/null
+++ b/docs/tint/diagnostics_guide.md
@@ -0,0 +1,126 @@
+# Tint diagnostic style guide
+
+This guide provides a set of best practices when writing code that emits
+diagnostic messages in Tint. These diagnostics are messages presented to the
+user in case of error or warning.
+
+The goal of this document is to have our diagnostic messages be clear and
+understandable to our users, so that problems are easy to fix, and to try and
+keep a consistent style.
+
+## Message style
+
+* Start diagnostic messages with a lower-case letter
+* Try to keep the message to a single sentence, if possible
+* Do not end the message with punctuation (full stop, exclamation mark, etc)
+
+**Don't:**
+
+```
+shader.wgsl:7:1 error: Cannot take the address of expression.
+```
+
+**Do:**
+
+```
+shader.wgsl:7:1 error: cannot take the address of expression
+```
+
+**Justification:**
+
+Succinct messages are more important than grammatical correctness. \
+This style matches the style found in most other compilers.
+
+## Prefer to use a `Source` location instead of quoting the code in the message
+
+**Don't:**
+
+```
+shader.wgsl:5:7 error: cannot multiply 'expr_a * expr_b' with types i32 and f32
+
+var res : f32 = expr_a * expr_b
+                ^^^^^^^^^^^^^^^
+```
+
+**Do:**
+
+```
+shader.wgsl:5:7 error: cannot multiply types i32 and f32
+
+var res : f32 = expr_a * expr_b
+                ^^^^^^^^^^^^^^^
+```
+
+**Justification:**
+
+The highlighted line provides even more contextual information than the quoted
+source, and duplicating this information doesn't provide any more help to the
+developer. \
+Quoting single word identifiers or keywords from the source is not discouraged.
+
+## Use `note` diagnostics for providing additional links to relevant code
+
+**Don't:**
+
+```
+shader.wgsl:5:11 error: type cannot be used in storage class 'storage' as it is non-host-shareable
+
+    cond : bool;
+           ^^^^
+```
+
+**Do:**
+
+```
+shader.wgsl:5:11 error: type cannot be used in storage class 'storage' as it is non-host-shareable
+
+    cond : bool;
+           ^^^^
+
+shader.wgsl:8:4 note: while instantiating variable 'StorageBuffer'
+
+var<storage> sb : StorageBuffer;
+             ^^
+```
+
+**Justification:**
+
+To properly understand some diagnostics requires looking at more than a single
+line. \
+Multi-source links can greatly reduce the time it takes to properly
+understand a diagnostic message. \
+This is especially important for diagnostics raised from complex whole-program
+analysis, but can also greatly aid simple diagnostics like symbol collision errors.
+
+## Use simple terminology
+
+**Don't:**
+
+```
+shader.wgsl:7:1 error: the originating variable of the left-hand side of an assignment expression must not be declared with read access control.
+```
+
+**Do:**
+
+```
+shader.wgsl:7:1 error: cannot assign to variable with read access control
+
+x.y = 1;
+^^^^^^^
+
+shader.wgsl:2:8 note: read access control declared here
+
+var<storage, read> x : i32;
+             ^^^^
+```
+
+**Justification:**
+
+Diagnostics will be read by Web developers who may not be native English
+speakers and are unlikely to be familiar with WGSL specification terminology.
+Too much technical jargon can be intimidating and confusing. \
+Diagnostics should give enough information to explain what's wrong, and most
+importantly, give enough information so that a fix actionable.
+
+**Caution:** Be careful to not over simplify. Use the specification terminology
+if there's potential ambiguity by not including it.
diff --git a/docs/tint/end-to-end-tests.md b/docs/tint/end-to-end-tests.md
new file mode 100644
index 0000000..8a34f76
--- /dev/null
+++ b/docs/tint/end-to-end-tests.md
@@ -0,0 +1,35 @@
+# Tint end-to-end tests
+
+This repo contains a large number of end-to-end tests at `<tint>/test`.
+
+## Test files
+
+Test input files have either the `.wgsl`, `.spv` or `.spvasm` file extension.
+
+Each test input file is tested against each of the Tint backends. There are `<number-of-input-files>` &times; `<number-of-tint-backends>` tests that are performed on an unfiltered end-to-end test run.
+
+Each backend test can have an **expectation file**. This expectation file sits next to the input file, with a `<input-file>.expected.<format>` extension. For example the test `test/foo.wgsl` would have the HLSL expectation file `test/foo.wgsl.expected.hlsl`.
+
+An expectation file contains the expected output of Tint, when passed the input file for the given backend.
+
+If the first line of the expectation file starts `SKIP`, then the test will be skipped instead of failing the end-to-end test run. It is good practice to include after the `SKIP` a reason for why the test is being skipped, along with any additional details, such as compiler error messages.
+
+## Running
+
+To run the end-to-end tests use the `<tint>/test/test-all.sh` script, passing the path to the tint executable as the first command line argument.
+
+You can pass `--help` to see the full list of command line flags.\
+The most commonly used flags are:
+
+| flag                 | description |
+|----------------------|-------------|
+|`--filter`            | Filters the testing to subset of the tests. The filter argument is a glob pattern that can include `*` for any substring of a file or directory, and `**` for any number of directories.<br>Example: `--filter 'expressions/**/i32.wgsl'` will test all the `i32.wgsl` expression tests.
+|`--format`            | Filters the tests to the particular backend.<br>Example: `--format hlsl` will just test the HLSL backend.
+|`--generate-expected` | Generate expectation files for the tests that previously had no expectation file, or were marked as `SKIP` but now pass.
+|`--generate-skip`     | Generate `SKIP` expectation files for tests that are not currently passing.
+
+## Authoring guidelines
+
+Each test should be as small as possible, and focused on the particular feature being tested.
+
+Use sub-directories whenever possible to group similar tests, and try to keep the pattern of directories as consistent as possible between different tests. This helps filter tests using the `--filter` glob patterns.
diff --git a/docs/tint/experimental_extensions.md b/docs/tint/experimental_extensions.md
new file mode 100644
index 0000000..8fccd13
--- /dev/null
+++ b/docs/tint/experimental_extensions.md
@@ -0,0 +1,44 @@
+# Experimental extensions
+
+Sometimes a language feature proposed for WGSL requires experiementation
+to prove its worth.  Tint needs to support these, in general to enable
+that experimentation.
+
+The steps for doing so are:
+
+1. Choose a name for the feature, to be used in an `enable` directive.
+   An experimental extension should use prefix of `google_experimental_`
+   Example:
+
+      enable google_experimental_f16;
+
+2. Write down what the feature is supposed to mean.
+   This informs the Tint implementation, and tells shader authors what
+   has changed.
+   Ideally, this will take the form of one of the following:
+
+   - A PR against the WGSL spec.
+
+   - A description of what the contents of that PR would be, committed
+     as a document in this Tint repository.
+
+3. File a tracking bug for adding the feature.
+   Note: Should the Tint repo have a label for experimental features?
+
+4. File a tracking bug for removing the feature or converting it to
+   non-experimental.
+
+5. Write a plan for removal of the experiment.
+   - Ideally, this plan is committed to this repository, especially the
+     description of public activities and commitments. However, we recognize
+     that some internal goals or metrics may be sensitive, and can be hidden.
+   - The plan is about process, not technical details.  It should include:
+       - Who is the point of contact for this feature? The point of contact
+         is responsible when the feature causes an issue or gets in the way.
+       - What is your target date for declaring the experiment a success or
+         failure. In Chrome an experiment must be shipped or removed, in
+         finite time.
+       - What experience are you hoping to gain?  Do you have target metrics?
+       - What approvals, if any, do you need from W3C? What is your plan to
+         present your case to W3C?
+       - The bug tracking removal of the experiment.
diff --git a/docs/tint/origin-trial-changes.md b/docs/tint/origin-trial-changes.md
new file mode 100644
index 0000000..3f3c61d
--- /dev/null
+++ b/docs/tint/origin-trial-changes.md
@@ -0,0 +1,133 @@
+# Tint changes during Origin Trial
+
+## Changes for M102
+
+### New Features
+
+* Parentheses are no longer required around expressions for if and switch statements [tint:1424](crbug.com/tint/1424)
+* Compound assignment statements are now supported. [tint:1325](https://crbug.com/tint/1325)
+* The colon in case statements is now optional. [tint:1485](crbug.com/tint/1485)
+
+### Breaking changes
+
+* Struct members are now separated by commas. [tint:1475](crbug.com/tint/1475)
+* The `@block` attribute has been removed. [tint:1324](crbug.com/tint/1324)
+* The `@stride` attribute has been removed. [tint:1381](crbug.com/tint/1381)
+* Attributes using `[[attribute]]` syntax are no longer supported. [tint:1382](crbug.com/tint/1382)
+* The `elseif` keyword is no longer supported. [tint:1289](crbug.com/tint/1289)
+
+### Deprecated Features
+
+* The `smoothStep()` builtin has been renamed to `smoothstep()`. [tint:1483](crbug.com/tint/1483)
+
+## Changes for M101
+
+### New Features
+
+* Tint now supports unicode identifiers. [tint:1437](crbug.com/tint/1437)
+
+### Breaking changes
+
+* The `isNan()`, `isInf()`, `isFinite()`, and `isNormal()` builtins have been removed. [tint:1312](https://crbug.com/tint/1312)
+
+## Changes for M100
+
+### Breaking changes
+
+* The `@interpolate(flat)` attribute must now be specified on integral user-defined IO. [tint:1224](crbug.com/tint/1224)
+* The `ignore()` intrinsic has been removed. Use phoney-assignment instead: `ignore(expr);` -> `_ = expr;`.
+* `break` statements in `continuing` blocks are now correctly validated.
+
+### New Features
+
+* Module-scope declarations can now be declared in any order. [tint:1266](crbug.com/tint/1266)
+* The `override` keyword and `@id()` attribute for pipeline-overridable constants are now supported, replacing the `@override` attribute. [tint:1403](crbug.com/tint/1403)
+
+## Changes for M99
+
+### Breaking changes
+
+Obviously infinite loops (no condition, no break) are now a validation error.
+
+### Deprecated Features
+
+The following features have been deprecated and will be removed in M102:
+
+* The `[[block]]` attribute has been deprecated. [tint:1324](https://crbug.com/tint/1324)
+* Attributes now use the `@decoration` syntax instead of the `[[decoration]]` syntax. [tint:1382](https://crbug.com/tint/1382)
+* `elseif` has been replaced with `else if`. [tint:1289](https://crbug.com/tint/1289)
+* The `[[stride]]` attribute has been deprecated. [tint:1381](https://crbug.com/tint/1381)
+
+### New Features
+
+* Vector and matrix element type can now be inferred from constructor argument types. [tint:1334](https://crbug.com/tint/1334)
+* Added builtins `degrees()` and `radians()` for converting between degrees and radians. [tint:1329](https://crbug.com/tint/1329)
+* `let` arrays and matrices can now be dynamically indexed. [tint:1352](https://crbug.com/tint/1352)
+* Storage and Uniform buffer types no longer have to be structures. [tint:1372](crbug.com/tint/1372)
+* A struct declaration does not have to be followed by a semicolon. [tint:1380](crbug.com/tint/1380)
+
+### Fixes
+
+* Fixed an issue where for-loops that contain array or structure constructors in the loop initializer statements, condition expressions or continuing statements could fail to compile. [tint:1364](https://crbug.com/tint/1364)
+
+## Changes for M98
+
+### Breaking Changes
+
+* Taking the address of a vector component is no longer allowed.
+* Module-scope declarations can no longer alias a builtin name. [tint:1318](https://crbug.com/tint/1318)
+* It is now an error to call a function either directly or transitively, from a loop continuing block, that uses `discard`. [tint:1302](https://crbug.com/tint/1302)
+
+### Deprecated Features
+
+* The `isNan()`, `isInf()`, `isFinite()` and `isNormal()` builtins has been deprecated and will be removed in M101. [tint:1312](https://crbug.com/tint/1312)
+
+### New Features
+
+* New texture gather builtins: `textureGather()` and `textureGatherCompare()`. [tint:1330](https://crbug.com/tint/1330)
+* Shadowing is now fully supported. [tint:819](https://crbug.com/tint/819)
+* The `dot()` builtin now supports integer vector types.
+* Identifiers can now start with a single leading underscore.  [tint:1292](https://crbug.com/tint/1292)
+* Control flow analysis has been improved, and functions no longer need to `return` if the statement is unreachable. [tint:1302](https://crbug.com/tint/1302)
+* Unreachable statements now produce a warning instead of an error, to allow WGSL code to be updated to the new analysis behavior. These warnings may become errors in the future [gpuweb#2378](https://github.com/gpuweb/gpuweb/issues/2378)
+
+### Fixes
+
+* Fixed an issue where using a module-scoped `let` in a `workgroup_size` may result in a compilation error. [tint:1320](https://crbug.com/tint/1320)
+
+## Changes for M97
+
+### Breaking Changes
+
+* Deprecated `modf()` and `frexp()` builtin overloads that take a pointer second parameter have been removed.
+* Deprecated texture builtin functions that accepted a `read` access controlled storage texture have been removed.
+* Storage textures must now only use the `write` access control.
+
+### Deprecated Features
+
+* The `ignore()` builtin has been replaced with phony-assignment. [gpuweb#2127](https://github.com/gpuweb/gpuweb/pull/2127)
+
+### New Features
+
+* `any()` and `all()` now support a `bool` parameter. These simply return the passed argument. [tint:1253](https://crbug.com/tint/1253)
+* Call statements may now include functions that return a value (`ignore()` is no longer needed).
+* The `interpolate(flat)` attribute can now be specified on integral user-defined IO. It will eventually become an error to define integral user-defined IO without this attribute.
+* Matrix construction from scalar element values is now supported.
+
+### Fixes
+
+* Swizzling of `vec3` types in `storage` and `uniform` buffers has been fixed for Metal 1.x. [tint:1249](https://crbug.com/tint/1249)
+* Calling a function that returns an unused value no longer produces an FXC compilation error. [tint:1259](https://crbug.com/tint/1259)
+* `abs()` fixed for unsigned integers on SPIR-V backend
+
+## Changes for M95
+
+### New Features
+
+* The size of an array can now be defined using a non-overridable module-scope constant
+* The `num_workgroups` builtin is now supported.
+
+### Fixes
+
+* Hex floats: now correctly errors when the magnitude is non-zero, and the exponent would cause overflow. [tint:1150](https://crbug.com/tint/1150), [tint:1166](https://crbug.com/tint/1166)
+* Identifiers beginning with an underscore are now correctly rejected.  [tint:1179](https://crbug.com/tint/1179)
diff --git a/docs/tint/spirv-input-output-variables.md b/docs/tint/spirv-input-output-variables.md
new file mode 100644
index 0000000..0f149e0
--- /dev/null
+++ b/docs/tint/spirv-input-output-variables.md
@@ -0,0 +1,267 @@
+# SPIR-V translation of shader input and output variables
+
+WGSL [MR 1315](https://github.com/gpuweb/gpuweb/issues/1315) changed WGSL so
+that pipeline inputs and outputs are handled similar to HLSL:
+
+- Shader pipeline inputs are the WGSL entry point function arguments.
+- Shader pipeline outputs are the WGSL entry point return value.
+
+Note: In both cases, a struct may be used to pack multiple values together.
+In that case, I/O specific attributes appear on struct members at the struct declaration.
+
+Resource variables, e.g. buffers, samplers, and textures, are still declared
+as variables at module scope.
+
+## Vulkan SPIR-V today
+
+SPIR-V for Vulkan models inputs and outputs as module-scope variables in
+the Input and Output storage classes, respectively.
+
+The `OpEntryPoint` instruction has a list of module-scope variables that must
+be a superset of all the input and output variables that are statically
+accessed in the shader call tree.
+From SPIR-V 1.4 onward, all interface variables that might be statically accessed
+must appear on that list.
+So that includes all resource variables that might be statically accessed
+by the shader call tree.
+
+## Translation scheme for SPIR-V to WGSL
+
+A translation scheme from SPIR-V to WGSL is as follows:
+
+Each SPIR-V entry point maps to a set of Private variables proxying the
+inputs and outputs, and two functions:
+
+- An inner function with no arguments or return values, and whose body
+  is the same as the original SPIR-V entry point.
+- Original input variables are mapped to pseudo-in Private variables
+  with the same store types, but no other attributes or properties copied.
+  In Vulkan, Input variables don't have initalizers.
+- Original output variables are mapped to pseudo-out Private variables
+  with the same store types and optional initializer, but no other attributes
+  or properties are copied.
+- A wrapper entry point function whose arguments correspond in type, location
+  and builtin attributes the original input variables, and whose return type is
+  a structure containing members correspond in type, location, and builtin
+  attributes to the original output variables.
+  The body of the wrapper function the following phases:
+  - Copy formal parameter values into pseudo-in variables.
+    - Insert a bitcast if the WGSL builtin variable has different signedness
+      from the SPIR-V declared type.
+  - Execute the inner function.
+  - Copy pseudo-out variables into the return structure.
+    - Insert a bitcast if the WGSL builtin variable has different signedness
+      from the SPIR-V declared type.
+  - Return the return structure.
+
+- Replace uses of the the original input/output variables to the pseudo-in and
+  pseudo-out variables, respectively.
+- Remap pointer-to-Input with pointer-to-Private
+- Remap pointer-to-Output with pointer-to-Private
+
+We are not concerned with the cost of extra copying input/output values.
+First, the pipeline inputs/outputs tend to be small.
+Second, we expect the backend compiler in the driver will be able to see
+through the copying and optimize the result.
+
+### Example
+
+
+```glsl
+    #version 450
+
+    layout(location = 0) out vec4 frag_colour;
+    layout(location = 0) in vec4 the_colour;
+
+    void bar() {
+      frag_colour = the_colour;
+    }
+
+    void main() {
+        bar();
+    }
+```
+
+Current translation, through SPIR-V, SPIR-V reader, WGSL writer:
+
+```groovy
+    @location(0) var<out> frag_colour : vec4<f32>;
+    @location(0) var<in> the_colour : vec4<f32>;
+
+    fn bar_() -> void {
+      const x_14 : vec4<f32> = the_colour;
+      frag_colour = x_14;
+      return;
+    }
+
+    @stage(fragment)
+    fn main() -> void {
+      bar_();
+      return;
+    }
+```
+
+Proposed translation, through SPIR-V, SPIR-V reader, WGSL writer:
+
+```groovy
+    // 'in' variables are now 'private'.
+    var<private> frag_colour : vec4<f32>;
+    var<private> the_colour : vec4<f32>;
+
+    fn bar_() -> void {
+      // Accesses to the module-scope variables do not change.
+      // This is a big simplifying advantage.
+      const x_14 : vec4<f32> = the_colour;
+      frag_colour = x_14;
+      return;
+    }
+
+    fn main_inner() -> void {
+      bar_();
+      return;
+    }
+
+    // Declare a structure type to collect the return values.
+    struct main_result_type {
+      @location(0) frag_color : vec4<f32>;
+    };
+
+    @stage(fragment)
+    fn main(
+
+      // 'in' variables are entry point parameters
+      @location(0) the_color_arg : vec4<f32>
+
+    ) -> main_result_type {
+
+      // Save 'in' arguments to 'private' variables.
+      the_color = the_color_arg;
+
+      // Initialize 'out' variables.
+      // Use the zero value, since no initializer was specified.
+      frag_color = vec4<f32>();
+
+      // Invoke the original entry point.
+      main_inner();
+
+      // Collect outputs into a structure and return it.
+      var result : main_outer_result_type;
+      result.frag_color = frag_color;
+      return result;
+    }
+```
+
+Alternately, we could emit the body of the original entry point at
+the point of invocation.
+However that is more complex because the original entry point function
+may return from multiple locations, and we would like to have only
+a single exit path to construct and return the result value.
+
+### Handling fragment discard
+
+In SPIR-V `OpKill` causes immediate termination of the shader.
+Is the shader obligated to write its outputs when `OpKill` is executed?
+
+The Vulkan fragment operations are as follows:
+(see [6. Fragment operations](https://www.khronos.org/registry/vulkan/specs/1.2/html/vkspec.html#fragops)).
+
+* Scissor test
+* Sample mask test
+* Fragment shading
+* Multisample coverage
+* Depth bounds test
+* Stencil test
+* Depth test
+* Sample counting
+* Coverage reduction
+
+After that, the fragment results are used to update output attachments, including
+colour, depth, and stencil attachments.
+
+Vulkan says:
+
+> If a fragment operation results in all bits of the coverage mask being 0,
+> the fragment is discarded, and no further operations are performed.
+> Fragments can also be programmatically discarded in a fragment shader by executing one of
+>
+>     OpKill.
+
+I interpret this to mean that the outputs of a discarded fragment are ignored.
+
+Therefore, `OpKill` does not require us to modify the basic scheme from the previous
+section.
+
+The `OpDemoteToHelperInvocationEXT`
+instruction is an alternative way to throw away a fragment, but which
+does not immediately terminate execution of the invocation.
+It is introduced in the [`SPV_EXT_demote_to_helper_invocation](http://htmlpreview.github.io/?https://github.com/KhronosGroup/SPIRV-Registry/blob/master/extensions/EXT/SPV_EXT_demote_to_helper_invocation.html)
+extension.  WGSL does not have this feature, but we expect it will be introduced by a
+future WGSL extension.  The same analysis applies to demote-to-helper.  When introduced,
+it will not affect translation of pipeline outputs.
+
+### Handling depth-replacing mode
+
+A Vulkan fragment shader must write to the fragment depth builtin if and only if it
+has a `DepthReplacing` execution mode. Otherwise behaviour is undefined.
+
+We will ignore the case where the SPIR-V shader writes to the `FragDepth` builtin
+and then discards the fragment.
+This is justified because "no further operations" are performed by the pipeline
+after the fragment is discarded, and that includes writing to depth output attachments.
+
+Assuming the shader is valid, no special translation is required.
+
+### Handling output sample mask
+
+By the same reasoning as for depth-replacing, it is ok to incidentally not write
+to the sample-mask builtin variable when the fragment is discarded.
+
+### Handling clip distance and cull distance
+
+Most builtin variables are scalars or vectors.
+However, the `ClipDistance` and `CullDistance` builtin variables are arrays of 32-bit float values.
+Each entry defines a clip half-plane (respectively cull half-plane)
+A Vulkan implementation must support array sizes of up to 8 elements.
+
+How prevalent are shaders that use these features?
+These variables are supported when Vulkan features `shaderClipDistance` and `shaderCullDistance`
+are supported.
+According to gpuinfo.org as of this writing, those
+Vulkan features appear to be nearly universally supported on Windows devices (>99%),
+but by only 70% on Android.
+It appears that Qualcomm devices support them, but Mali devices do not (e.g. Mali-G77).
+
+The proposed translation scheme forces a copy of each array from private
+variables into the return value of a vertex shader, or into a private
+variable of a fragment shader.
+In addition to the register pressure, there may be a performance degradation
+due to the bulk copying of data.
+
+We think this is an acceptable tradeoff for the gain in usability and
+consistency with other pipeline inputs and outputs.
+
+## Translation scheme for WGSL AST to SPIR-V
+
+To translate from the WGSL AST to SPIR-V, do the following:
+
+- Each entry point formal parameter is mapped to a SPIR-V `Input` variable.
+  - Struct and array inputs may have to be broken down into individual variables.
+- The return of the entry point is broken down into fields, with one
+  `Output` variable per field.
+- In the above, builtins must be separated from user attributes.
+  - Builtin attributes are moved to the corresponding variable.
+  - Location and interpolation attributes are moved to the corresponding
+    variables.
+- This translation relies on the fact that pipeline inputs and pipeline
+  outputs are IO-shareable types. IO-shareable types are always storable,
+  and can be the store type of input/output variables.
+- Input function parameters will be automatically initialized by the system
+  as part of setting up the pipeline inputs to the entry point.
+- Replace each return statement in the entry point with a code sequence
+  which writes the return value components to the synthesized output variables,
+  and then executes an `OpReturn` (without value).
+
+This translation is sufficient even for fragment shaders with discard.
+In that case, outputs will be ignored because downstream pipeline
+operations will not be performed.
+This is the same rationale as for translation from SPIR-V to WGSL AST.
diff --git a/docs/tint/spirv-ptr-ref.md b/docs/tint/spirv-ptr-ref.md
new file mode 100644
index 0000000..615e12c
--- /dev/null
+++ b/docs/tint/spirv-ptr-ref.md
@@ -0,0 +1,115 @@
+# SPIR-V translation of WGSL pointers and references
+
+WGSL was updated to have two kinds of memory views: pointers and references.
+See https://github.com/gpuweb/gpuweb/pull/1569
+
+In summary:
+
+* Reference types are never explicitly mentioned in WGSL source.
+* A use of a variable is a value of reference type corresponding
+  to the reference memory view of the storage allocated for the
+  variable.
+* Let-declared constants can be of pointer type, but not reference
+  type.
+* Function parameter can be of pointer type, but not reference type.
+* A variable's store type is never a pointer type, and never a
+  reference type.
+* The "Load Rule" allows a reference to decay to the underlying
+  store type, by issuing a load of the value in the underlying memory.
+* For an assignment:
+  * The right-hand side evaluates to a non-reference type (atomic-free
+    plain type).
+  * The left-hand side evaluates to a reference type, whose store
+    type is the same as the result of evaluating the right hand side.
+* The address-of (unary `&`) operator converts a reference to a
+  pointer.
+* The dereference (unary `*`) operator converts a pointer to a
+  reference.
+
+TODO: Passing textures and samplers to helper functions might be
+done by "handler value", or by pointer-to-handle.
+
+## Writing SPIR-V from WGSL
+
+The distinction in WGSL between reference and pointer disappears
+at the SPIR-V level.  Both types map into pointer types in SPIR-V.
+
+To translate a valid WGSL program to SPIR-V:
+
+* The dereference operator (unary `*`) is the identity operation.
+* The address-of operator (unary `&`) is the identity operation.
+* Assignment maps to OpStore.
+* The Load Rule translates to OpLoad.
+
+## Reading SPIR-V to create WGSL
+
+The main changes to the SPIR-V reader are:
+
+* When translating a SPIR-V pointer expression, track whether the
+  corresponding WGSL expression is of corresponding WGSL pointer
+  type or correspoinding WGSL type.
+* Insert dereference (unary-`*`) or address-of (unary-`&`) operators
+  as needed to generate valid WGSL expressions.
+
+The choices can be made deterministic, as described below.
+
+The SPIR-V reader only supports baseline functionality in Vulkan.
+Therefore we assume no VariablePointers or VariablePointersStorageBuffer
+capabilities.  All pointers are
+[SPIR-V logical pointers](https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#LogicalPointerType).
+The [SPIR-V Universal Validation Rules](https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#_universal_validation_rules)
+specify where logical pointers can appear as results of instructions
+or operands of instructions.
+
+Each SPIR-V pointer result expression is a logical pointer, and
+therefore is one of:
+
+* OpVariable: map to the reference type.
+* OpFunctionParameter: map to the pointer type.
+* OpCopyObject:
+   * When these only have one use, then these often fold away.
+     Otherwise, they map to a a let-declared constant.
+   * Map to the pointer type.
+* OpAccessChain, OpInBoundsAccessChain:
+   * This could map to either pointer or reference, and adjustments
+     in other areas could make it work.  However, we recommend mapping
+     this to the reference type.
+* OpImageTexelPointer is not supported in WGSL.
+   It is used to get a pointer into a storage texture, for use with
+   atomic instructions.  But image atomics is not supported in
+   WebGPU/WGSL.
+
+Each SPIR-V pointer operand is also a logical pointer, and is an
+operand to one of:
+* OpLoad Pointer operand:
+   * Map to reference, inserting a dereference operator if needed.
+* OpStore Pointer operand:
+   * Map to reference, inserting a dereference operator if needed.
+* OpStore Pointer operand:
+* OpAccessChain, OpInBoundsAccessChain Base operand:
+   * WGSL array-access and subfield access only works on references.
+      * [Gpuweb issue 1530](https://github.com/gpuweb/gpuweb/issues/1530)
+        is filed to allow those operations to work on pointers.
+   * Map to reference, inserting a dereference operator if needed.
+* OpFunctionCall function argument pointer operands
+   * Function operands can't be references.
+   * Map to pointer, inserting an address-of operator if needed.
+* OpAtomic instruction Pointer operand
+   * These map to WGSL atomic builtins.
+   * Map to pointer, inserting an address-of operator if needed.
+   * Note: As of this writing, the atomic instructions are not supported
+     by the SPIR-V reader.
+* OpCopyObject source operand
+   * This could have been mapped either way, but it's easiest to
+     map to pointer, to match the choice for OpCopyObject result type.
+   * Map to pointer, inserting an address-of operator if needed.
+* OpCopyMemory, source and destination operands
+   * This acts as an assignment.
+   * Map both source and destination to reference, inserting dereference
+     operators if needed.
+   * Note: As of this writing, OpCopyMemory is not supported by the
+     SPIR-V reader.
+* Extended instruction set instructions Modf and Frexp
+   * These map to builtins.
+   * Map the pointer operand to pointer, inserting an address-of
+     operator if needed.
diff --git a/docs/tint/style_guide.md b/docs/tint/style_guide.md
new file mode 100644
index 0000000..52b08c5
--- /dev/null
+++ b/docs/tint/style_guide.md
@@ -0,0 +1,47 @@
+# Tint style guide
+
+* Generally, follow the [Chromium style guide for C++](https://chromium.googlesource.com/chromium/src/+/HEAD/styleguide/c++/c++.md)
+  which itself is built on the [Google C++ style guide](https://google.github.io/styleguide/cppguide.html).
+
+* Overall try to use the same style and convention as code around your change.
+
+* Code must be formatted. Use `clang-format` with the provided [.clang-format](../.clang-format)
+  file.  The `tools/format` script runs the formatter.
+
+* Code should not have linting errors.
+    The `tools/lint` script runs the linter. So does `git cl upload`.
+
+* Do not use C++ exceptions
+
+* Do not use C++ RTTI.
+   Instead, use `tint::Castable::As<T>()` from
+   [src/castable.h](../src/castable.h)
+
+* Generally, avoid `assert`.  Instead, issue a [diagnostic](../src/diagnostic.h)
+  and fail gracefully, possibly by returning an error sentinel value.
+  Code that should not be reachable should call `TINT_UNREACHABLE` macro
+  and other internal error conditions should call the `TINT_ICE` macro.
+  See [src/debug.h](../src/debug.h)
+
+* Use `type` as part of a name only when the name refers to a type
+  in WGSL or another shader language processed by Tint.  If the concept you are
+  trying to name is about distinguishing between alternatives, use `kind` instead.
+
+## Compiler support
+
+Tint requires C++17.
+
+Tint uses the Chromium build system and will stay synchronized with that system.
+Compiler configurations beyond that baseline is on a best-effort basis.
+We strive to support recent GCC and MSVC compilers.
+
+## Test code
+
+We might relax the above rules rules for test code, since test code
+shouldn't ship to users.
+
+However, test code should still be readable and maintainable.
+
+For test code, the tradeoff between readability and maintainability
+and other factors is weighted even more strongly toward readability
+and maintainability.
diff --git a/docs/tint/translations.md b/docs/tint/translations.md
new file mode 100644
index 0000000..5119434
--- /dev/null
+++ b/docs/tint/translations.md
@@ -0,0 +1,187 @@
+# Translations
+
+This document attempts to document how WGSL translates into the various backends
+for the cases where the translation is not a direct mapping.
+
+# Access Control
+
+## HLSL
+ * ReadOnly -> `ByteAddressBuffer`
+ * ReadWrite -> `RWByteAddressBuffer`
+
+## MSL
+ * ReadOnly -> `const`
+
+## SPIR-V
+There are two ways this can be achieved in SPIR-V. Either the variable can be
+decorated with `NonWritable` or each member of the struct can be decorated with
+`NonWritable`. We chose to go the struct member route.
+ * The read-only becomes part of the type in this case. Otherwise, you are
+   treating the readonly type information as part of the variable which is
+   confusing.
+ * Treating the readonly as part of the variable means we should be
+   deduplicating the types behind the access control, which causes confusing
+   with the type_names and various tracking systems within Tint.
+
+
+# Builtin Decorations
+| Name | SPIR-V | MSL | HLSL |
+|------|--------|-----|------|
+| position | SpvBuiltInPosition |position | SV_Position |
+| vertex_index | SpvBuiltInVertexIndex |vertex_id | SV_VertexID |
+| instance_index | SpvBuiltInInstanceIndex | instance_id| SV_InstanceID |
+| front_facing | SpvBuiltInFrontFacing | front_facing | SV_IsFrontFacing |
+| frag_coord | SpvBuiltInFragCoord | position | SV_Position |
+| frag_depth | SpvBuiltInFragDepth | depth(any) | SV_Depth |
+| local_invocation_id | SpvBuiltInLocalInvocationId | thread_position_in_threadgroup | SV_GroupThreadID |
+| local_invocation_index | SpvBuiltInLocalInvocationIndex | thread_index_in_threadgroup | SV_GroupIndex |
+| global_invocation_id | SpvBuiltInGlobalInvocationId | thread_position_in_grid | SV_DispatchThreadID |
+
+
+# Builtins Methods
+| Name | SPIR-V | MSL | HLSL |
+| ------|--------|-----|------ |
+| abs | GLSLstd450FAbs or GLSLstd450SAbs| fabs or abs | abs |
+| acos | GLSLstd450Acos | acos | acos |
+| all | SpvOpAll | all | all |
+| any | SpvOpAny | any | any |
+| arrayLength | SpvOpArrayLength | | |
+| asin | GLSLstd450Asin | asin | asin |
+| atan | GLSLstd450Atan | atan | atan |
+| atan2 | GLSLstd450Atan2| atan2 | atan2 |
+| ceil | GLSLstd450Ceil| ceil | ceil |
+| clamp | GLSLstd450NClamp or GLSLstd450UClamp or GLSLstd450SClamp| clamp | clamp |
+| cos | GLSLstd450Cos | cos | cos |
+| cosh | GLSLstd450Cosh | cosh | cosh |
+| countOneBits | SpvOpBitCount | popcount | countbits |
+| cross | GLSLstd450Cross | cross | cross |
+| determinant | GLSLstd450Determinant | determinant | determinant |
+| distance | GLSLstd450Distance | distance | distance |
+| dot | SpOpDot | dot | dot |
+| dpdx | SpvOpDPdx | dpdx | ddx |
+| dpdxCoarse | SpvOpDPdxCoarse | dpdx | ddx_coarse |
+| dpdxFine | SpvOpDPdxFine | dpdx | ddx_fine |
+| dpdy | SpvOpDPdy | dpdy | ddy |
+| dpdyCoarse | SpvOpDPdyCoarse | dpdy | ddy_coarse |
+| dpdyFine | SpvOpDPdyFine | dpdy | ddy_fine |
+| exp | GLSLstd450Exp | exp |  exp |
+| exp2 | GLSLstd450Exp2 | exp2 | exp2 |
+| faceForward | GLSLstd450FaceForward | faceforward | faceforward |
+| floor | GLSLstd450Floor | floor | floor |
+| fma | GLSLstd450Fma | fma | fma |
+| fract | GLSLstd450Fract | fract | frac |
+| frexp | GLSLstd450Frexp | | |
+| fwidth | SpvOpFwidth | fwidth | fwidth |
+| fwidthCoarse | SpvOpFwidthCoarse | fwidth | fwidth |
+| fwidthFine | SpvOpFwidthFine | fwidth | fwidth |
+| inverseSqrt | GLSLstd450InverseSqrt | rsqrt | rsqrt |
+| ldexp | GLSLstd450Ldexp | | |
+| length | GLSLstd450Length | length | length |
+| log | GLSLstd450Log | log | log |
+| log2 | GLSLstd450Log2 | log2 | log2 |
+| max | GLSLstd450NMax or GLSLstd450SMax or GLSLstd450UMax | fmax or max | max |
+| min | GLSLstd450NMin or GLSLstd450SMin or GLSLstd450UMin | fmin or min | min |
+| mix | GLSLstd450FMix | mix | mix |
+| modf | GLSLstd450Modf | | |
+| normalize | GLSLstd450Normalize | normalize | normalize |
+| pow | GLSLstd450Pow | pow | pow |
+| reflect | GLSLstd450Reflect | reflect | reflect |
+| reverseBits | SpvOpBitReverse | reverse_bits | reversebits |
+| round | GLSLstd450Round | round | round |
+| select | SpvOpSelect | select | |
+| sign | GLSLstd450FSign | sign | sign |
+| sin | GLSLstd450Sin | sin | sin |
+| sinh | GLSLstd450Sinh | sinh | sinh |
+| smoothStep | GLSLstd450SmoothStep | smoothstep | smoothstep |
+| sqrt | GLSLstd450Sqrt | sqrt | sqrt |
+| step | GLSLstd450Step | step | step |
+| tan | GLSLstd450Tan | tan | tan |
+| tanh | GLSLstd450Tanh | tanh | tanh |
+| trunc | GLSLstd450Trunc | trunc | trunc |
+
+# Types
+## Sampler Types
+| WGSL | SPIR-V | MSL | HLSL |
+|------|--------|-----|------|
+| sampler | OpTypeSampler | sampler | SamplerState |
+| sampler_comparison | OpTypeSampler | sampler | SamplerComparisonState |
+
+## Texture Types
+| WGSL | SPIR-V | MSL | HLSL |
+|------|--------|-----|------|
+| texture_1d&lt;type&gt; | OpTypeImage 1D Sampled=1 | texture1d&lt;type, access::sample&gt; | Texture1D |
+| texture_2d&lt;type&gt; | OpTypeImage 2D Sampled=1 | texture2d&lt;type, access::sample&gt; | Texture2D |
+| texture_2d_array&lt;type&gt; | OpTypeImage 2D Arrayed=1 Sampled=1 | texture2d_array&lt;type, access::sample&gt; | Texture2DArray |
+| texture_3d&lt;type&gt; | OpTypeImage 3D Sampled=1 | texture3d&lt;type, access::sample&gt; | Texture3D |
+| texture_cube&lt;type&gt; | OpTypeImage Cube Sampled=1 | texturecube&lt;type, access::sample&gt; | TextureCube |
+| texture_cube_array&lt;type&gt; | OpTypeImage Cube Arrayed=1 Sampled=1 | texturecube_array&lt;type, access::sample&gt; | TextureCubeArray |
+| | | |
+| texture_multisampled_2d&lt;type&gt; | OpTypeImage 2D MS=1 Sampled=1 | texture2d_ms&lt;type, access::sample&gt; | Texture2D |
+| | | |
+| texture_depth_2d | OpTypeImage 2D Depth=1 Sampled=1 | depth2d&lt;float, access::sample&gt;| Texture2D |
+| texture_depth_2d_array | OpTypeImage 2D Depth=1 Arrayed=1 Sampled=1 | depth2d_array&lt;float, access::sample&gt; | Texture2DArray |
+| texture_depth_cube | OpTypeImage Cube Depth=1 Sampled=1 | depthcube&lt;float, access::sample&gt; | TextureCube |
+| texture_depth_cube_array | OpTypeImage Cube Depth=1 Arrayed=1 Sampled=1 | depthcube_array&lt;float, access::sample&gt; | TextureCubeArray |
+| texture_depth_multisampled_2d | OpTypeImage 2D Depth=1 MS=1 Sampled=1 | depth2d&lt;float, access::sample&gt;| Texture2DMSArray |
+| | | |
+| texture_storage_1d&lt;image_storage_type&gt; | OpTypeImage 1D Sampled=2| texture1d&lt;type, access::read&gt; | RWTexture1D |
+| texture_storage_2d&lt;image_storage_type&gt; | OpTypeImage 2D Sampled=2 | texture2d&lt;type, access::read&gt; | RWTexture2D |
+| texture_storage_2d_array&lt;image_storage_type&gt; | OpTypeImage 2D Arrayed=1 Sampled=2 | texture2d_array&lt;type, access::read&gt; | RWTexture2DArray |
+| texture_storage_3d&lt;image_storage_type&gt; | OpTypeImage 3D Sampled=2 | texture3d&lt;type, access::read&gt; | RWTexture3D |
+| | | |
+| texture_storage_1d&lt;image_storage_type&gt; | OpTypeImage 1D Sampled=2 | texture1d&lt;type, access::write&gt; | RWTexture1D |
+| texture_storage_2d&lt;image_storage_type&gt; | OpTypeImage 2D Sampled=1 | texture2d&lt;type, access::write&gt; | RWTexture2D |
+| texture_storage_2d_array&lt;image_storage_type&gt; | OpTypeImage 2D Arrayed=1 Sampled=2 | texture2d_array&lt;type, access::write&gt; | RWTexture2DArray |
+| texture_storage_3d&lt;image_storage_type&gt; | OpTypeImage 3D Sampled=2 | texture3d&lt;type, access::write&gt; | RWTexture3D|
+
+# Short-circuting
+## HLSL
+TODO(dsinclair): Nested if's
+
+## SPIR-V
+TODO(dsinclair): Nested if's
+
+# Storage classes
+TODO(dsinclair): do ...
+
+# Storage buffers
+## HLSL
+TODO(dsinclair): Rewriting of accessors to loads
+
+# Loop blocks
+## HLSL
+TODO(dsinclair): Rewrite with bools
+
+## MSL
+TODO(dsinclair): Rewrite with bools
+
+# Input / Output storage class
+## HLSL
+TODO(dsinclair): Structs and params
+
+## MSL
+TODO(dsinclair): Structs and params
+
+# Discard
+## HLSL
+ * `discard`
+
+## MSL
+ * `discard_fragment()`
+
+
+# Specialization constants
+## HLSL
+```
+#ifndef WGSL_SPEC_CONSTANT_<id>
+-- if default provided
+#define WGSL_SPEC_CONSTANT_<id> default value
+-- else
+#error spec constant required for constant id
+--
+#endif
+static const <type> <name> = WGSL_SPEC_CONSTANT_<id>
+```
+
+## MSL
+`@function_constant(<id>)`
diff --git a/include/tint/tint.h b/include/tint/tint.h
new file mode 100644
index 0000000..1a04196
--- /dev/null
+++ b/include/tint/tint.h
@@ -0,0 +1,68 @@
+// Copyright 2020 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 INCLUDE_TINT_TINT_H_
+#define INCLUDE_TINT_TINT_H_
+
+// TODO(tint:88): When implementing support for an install target, all of these
+//                headers will need to be moved to include/tint/.
+
+#include "src/tint/ast/pipeline_stage.h"
+#include "src/tint/demangler.h"
+#include "src/tint/diagnostic/printer.h"
+#include "src/tint/inspector/inspector.h"
+#include "src/tint/reader/reader.h"
+#include "src/tint/sem/type_manager.h"
+#include "src/tint/transform/binding_remapper.h"
+#include "src/tint/transform/first_index_offset.h"
+#include "src/tint/transform/fold_trivial_single_use_lets.h"
+#include "src/tint/transform/manager.h"
+#include "src/tint/transform/multiplanar_external_texture.h"
+#include "src/tint/transform/renamer.h"
+#include "src/tint/transform/robustness.h"
+#include "src/tint/transform/single_entry_point.h"
+#include "src/tint/transform/vertex_pulling.h"
+#include "src/tint/writer/writer.h"
+
+#if TINT_BUILD_SPV_READER
+#include "src/tint/reader/spirv/parser.h"
+#endif  // TINT_BUILD_SPV_READER
+
+#if TINT_BUILD_WGSL_READER
+#include "src/tint/reader/wgsl/parser.h"
+#endif  // TINT_BUILD_WGSL_READER
+
+#if TINT_BUILD_SPV_WRITER
+#include "spirv-tools/libspirv.hpp"
+#include "src/tint/writer/spirv/generator.h"
+#endif  // TINT_BUILD_SPV_WRITER
+
+#if TINT_BUILD_WGSL_WRITER
+#include "src/tint/writer/wgsl/generator.h"
+#endif  // TINT_BUILD_WGSL_WRITER
+
+#if TINT_BUILD_MSL_WRITER
+#include "src/tint/writer/msl/generator.h"
+#endif  // TINT_BUILD_MSL_WRITER
+
+#if TINT_BUILD_HLSL_WRITER
+#include "src/tint/writer/hlsl/generator.h"
+#endif  // TINT_BUILD_HLSL_WRITER
+
+#if TINT_BUILD_GLSL_WRITER
+#include "src/tint/transform/glsl.h"
+#include "src/tint/writer/glsl/generator.h"
+#endif  // TINT_BUILD_GLSL_WRITER
+
+#endif  // INCLUDE_TINT_TINT_H_
diff --git a/kokoro/linux/build.sh b/kokoro/linux/build.sh
new file mode 100755
index 0000000..430f6c5
--- /dev/null
+++ b/kokoro/linux/build.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+ROOT_DIR="$( cd "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd )"
+
+# Inside the docker VM, we clone the project to a new directory.
+# We do this so that the docker script can be tested in a local development
+# checkout, without having the build litter the local checkout with artifacts.
+# This directory is mapped to the host temporary directory.
+# Kokoro uses a '/tmpfs' root, where as most linux enviroments just have '/tmp'
+if [ -d "/tmpfs" ]; then
+    TMP_DIR=/tmpfs
+else
+    TMP_DIR=/tmp
+fi
+
+
+# --privileged is required for some sanitizer builds, as they seem to require PTRACE privileges
+docker run --rm -i \
+  --privileged \
+  --volume "${ROOT_DIR}:${ROOT_DIR}" \
+  --volume "${TMP_DIR}/kokoro/tint:/tint" \
+  --volume "${KOKORO_ARTIFACTS_DIR}:/mnt/artifacts" \
+  --workdir "${ROOT_DIR}" \
+  --env SRC_DIR="/tint/src" \
+  --env BUILD_DIR="/tint/build" \
+  --env BUILD_TYPE=$BUILD_TYPE \
+  --env BUILD_SYSTEM=$BUILD_SYSTEM \
+  --env BUILD_SANITIZER=$BUILD_SANITIZER \
+  --env BUILD_TOOLCHAIN=$BUILD_TOOLCHAIN \
+  --entrypoint "${SCRIPT_DIR}/docker.sh" \
+  "gcr.io/shaderc-build/radial-build:latest"
diff --git a/kokoro/linux/cmake-clang-debug-asan/build.sh b/kokoro/linux/cmake-clang-debug-asan/build.sh
new file mode 100755
index 0000000..5548770
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug-asan/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Debug
+export BUILD_SANITIZER=asan
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-debug-asan/presubmit.cfg b/kokoro/linux/cmake-clang-debug-asan/presubmit.cfg
new file mode 100644
index 0000000..bf23a27
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug-asan/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-debug-asan/build.sh"
diff --git a/kokoro/linux/cmake-clang-debug-ubsan/build.sh b/kokoro/linux/cmake-clang-debug-ubsan/build.sh
new file mode 100755
index 0000000..55f4db1
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug-ubsan/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Debug
+export BUILD_SANITIZER=ubsan
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-debug-ubsan/presubmit.cfg b/kokoro/linux/cmake-clang-debug-ubsan/presubmit.cfg
new file mode 100644
index 0000000..5fa246e
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug-ubsan/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-debug-ubsan/build.sh"
diff --git a/kokoro/linux/cmake-clang-debug/build.sh b/kokoro/linux/cmake-clang-debug/build.sh
new file mode 100755
index 0000000..fa4fb4c
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug/build.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Debug
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-debug/presubmit.cfg b/kokoro/linux/cmake-clang-debug/presubmit.cfg
new file mode 100644
index 0000000..0cc8654
--- /dev/null
+++ b/kokoro/linux/cmake-clang-debug/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-debug/build.sh"
diff --git a/kokoro/linux/cmake-clang-release-asan/build.sh b/kokoro/linux/cmake-clang-release-asan/build.sh
new file mode 100755
index 0000000..ed598ae
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release-asan/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Release
+export BUILD_SANITIZER=asan
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-release-asan/presubmit.cfg b/kokoro/linux/cmake-clang-release-asan/presubmit.cfg
new file mode 100644
index 0000000..460c927
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release-asan/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-release-asan/build.sh"
diff --git a/kokoro/linux/cmake-clang-release-ubsan/build.sh b/kokoro/linux/cmake-clang-release-ubsan/build.sh
new file mode 100755
index 0000000..c476289
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release-ubsan/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Release
+export BUILD_SANITIZER=ubsan
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-release-ubsan/presubmit.cfg b/kokoro/linux/cmake-clang-release-ubsan/presubmit.cfg
new file mode 100644
index 0000000..f968295
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release-ubsan/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-release-ubsan/build.sh"
diff --git a/kokoro/linux/cmake-clang-release/build.sh b/kokoro/linux/cmake-clang-release/build.sh
new file mode 100755
index 0000000..394f5bb
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release/build.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=clang
+export BUILD_TYPE=Release
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-clang-release/presubmit.cfg b/kokoro/linux/cmake-clang-release/presubmit.cfg
new file mode 100644
index 0000000..ccc9651
--- /dev/null
+++ b/kokoro/linux/cmake-clang-release/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-clang-release/build.sh"
diff --git a/kokoro/linux/cmake-gcc-debug/build.sh b/kokoro/linux/cmake-gcc-debug/build.sh
new file mode 100755
index 0000000..2bd68bd
--- /dev/null
+++ b/kokoro/linux/cmake-gcc-debug/build.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=gcc
+export BUILD_TYPE=Debug
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-gcc-debug/presubmit.cfg b/kokoro/linux/cmake-gcc-debug/presubmit.cfg
new file mode 100644
index 0000000..eb1d4d8
--- /dev/null
+++ b/kokoro/linux/cmake-gcc-debug/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-gcc-debug/build.sh"
diff --git a/kokoro/linux/cmake-gcc-release/build.sh b/kokoro/linux/cmake-gcc-release/build.sh
new file mode 100755
index 0000000..83af742
--- /dev/null
+++ b/kokoro/linux/cmake-gcc-release/build.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e # Fail on any error.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+
+export BUILD_SYSTEM=cmake
+export BUILD_TOOLCHAIN=gcc
+export BUILD_TYPE=Release
+
+${SCRIPT_DIR}/../build.sh
diff --git a/kokoro/linux/cmake-gcc-release/presubmit.cfg b/kokoro/linux/cmake-gcc-release/presubmit.cfg
new file mode 100644
index 0000000..95a6cfc
--- /dev/null
+++ b/kokoro/linux/cmake-gcc-release/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/linux/cmake-gcc-release/build.sh"
diff --git a/kokoro/linux/docker.sh b/kokoro/linux/docker.sh
new file mode 100755
index 0000000..d0ec4c5
--- /dev/null
+++ b/kokoro/linux/docker.sh
@@ -0,0 +1,183 @@
+#!/bin/bash
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://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.
+
+# This is the bash script invoked inside a docker container.
+# The script expects that the CWD points to a clean checkout of Tint.
+# As `gclient sync` will litter the tint checkout with fetched tools and
+# projects, this script will first clone the pristine tint checkout to
+# ${SRC_DIR}. This allows developers to locally run this script without having
+# to worry about their local tint copy being touched.
+#
+# This script expects the following environment variables to be set on entry:
+#
+# SRC_DIR         - Path to where the local Tint copy will be made. See above.
+# BUILD_DIR       - Path to where Tint will be built.
+# BUILD_TYPE      - Either: 'Debug' or 'Release'
+# BUILD_SYSTEM    - Must be 'cmake'
+# BUILD_SANITIZER - Either: '', 'asan', or 'ubstan'
+# BUILD_TOOLCHAIN - Either: 'clang' or 'gcc'
+
+set -e # Fail on any error.
+
+function show_cmds { set -x; }
+function hide_cmds { { set +x; } 2>/dev/null; }
+function task_begin {
+    TASK_NAME="$@"
+    SECONDS=0
+}
+function print_last_task_duration {
+    if [ ! -z "${TASK_NAME}" ]; then
+        echo "${TASK_NAME} completed in $(($SECONDS / 3600))h$((($SECONDS / 60) % 60))m$(($SECONDS % 60))s"
+    fi
+}
+function status {
+    echo ""
+    echo ""
+    print_last_task_duration
+    echo ""
+    echo "*****************************************************************"
+    echo "* $@"
+    echo "*****************************************************************"
+    echo ""
+    task_begin $@
+}
+function with_retry {
+  local MAX_ATTEMPTS=5
+  local RETRY_DELAY_SECS=5
+  local ATTEMPT=1
+  while true; do
+    "$@" && break
+    if [[ $ATTEMPT -ge $MAX_ATTEMPTS ]]; then
+        echo "The command has failed after $ATTEMPT attempts."
+        exit $?
+    fi
+    ((ATTEMPT++))
+    echo "'$@' failed. Attempt ($ATTEMPT/$MAX_ATTEMPTS). Retrying..."
+    sleep $RETRY_DELAY_SECS;
+  done
+}
+
+CLONE_SRC_DIR="$(pwd)"
+
+. /bin/using.sh # Declare the bash `using` function for configuring toolchains.
+
+using depot_tools
+using go-1.14.4      # Speeds up ./tools/lint
+using doxygen-1.8.18
+
+status "Creating source directory '${SRC_DIR}' and build directory '${BUILD_DIR}'"
+mkdir -p ${SRC_DIR}
+mkdir -p ${BUILD_DIR}
+
+status "Cloning to source directory '${SRC_DIR}'"
+cd ${SRC_DIR}
+git clone ${CLONE_SRC_DIR} .
+
+status "Fetching dependencies"
+cp standalone.gclient .gclient
+with_retry gclient sync
+
+status "Linting"
+./tools/lint
+
+status "Configuring build system"
+if [ "$BUILD_SYSTEM" == "cmake" ]; then
+    using cmake-3.17.2
+
+    COMMON_CMAKE_FLAGS=""
+    COMMON_CMAKE_FLAGS+=" -DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
+    COMMON_CMAKE_FLAGS+=" -DTINT_DOCS_WARN_AS_ERROR=1"
+    COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_BENCHMARKS=1"
+
+    if [ "$BUILD_TOOLCHAIN" == "clang" ]; then
+        using clang-10.0.0
+        COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_FUZZERS=1"
+        COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_SPIRV_TOOLS_FUZZER=1"
+        COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_AST_FUZZER=1"
+        COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_REGEX_FUZZER=1"
+    elif [ "$BUILD_TOOLCHAIN" == "gcc" ]; then
+        using gcc-9
+    fi
+
+    if [ "$BUILD_SANITIZER" == "asan" ]; then
+        COMMON_CMAKE_FLAGS+=" -DTINT_ENABLE_ASAN=1"
+    elif [ "$BUILD_SANITIZER" == "ubsan" ]; then
+        COMMON_CMAKE_FLAGS+=" -DTINT_ENABLE_UBSAN=1"
+        export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
+    fi
+
+    cd ${BUILD_DIR}
+
+    status "Running Doxygen"
+    echo "NOTE: This will fail on first warning. Run with -DTINT_DOCS_WARN_AS_ERROR=OFF to see all warnings".
+    echo ""
+    show_cmds
+        # NOTE: If we upgrade Doxygen to a more recent version, we can set DOXYGEN_WARN_AS_ERROR to
+        # "FAIL_ON_WARNINGS" instead of "YES" in our CMakeLists.txt so see all warnings, and then
+        # fail. See https://www.doxygen.nl/manual/config.html#cfg_warn_as_error
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS}
+        cmake --build . --target tint-docs
+    hide_cmds
+
+    status "Building tint in '${BUILD_DIR}'"
+    show_cmds
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS}
+        cmake --build . -- --jobs=$(nproc)
+    hide_cmds
+
+    status "Running tint_unittests"
+    show_cmds
+        ./tint_unittests
+    hide_cmds
+
+    if [ -f ./tint_ast_fuzzer_unittests ]; then
+        status "Running tint_ast_fuzzer_unittests"
+        show_cmds
+            ./tint_ast_fuzzer_unittests
+        hide_cmds
+    fi
+
+    if [ -f ./tint_regex_fuzzer_unittests ]; then
+        status "Running tint_regex_fuzzer_unittests"
+        show_cmds
+            ./tint_regex_fuzzer_unittests
+        hide_cmds
+    fi
+
+    status "Testing test/tint/test-all.sh"
+    show_cmds
+        ${SRC_DIR}/test/tint/test-all.sh "${BUILD_DIR}/tint" --verbose
+    hide_cmds
+
+    status "Checking _other.cc files also build"
+    show_cmds
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_AS_OTHER_OS=ON
+        cmake --build . -- --jobs=$(nproc)
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_AS_OTHER_OS=OFF
+    hide_cmds
+
+    status "Checking disabling all readers and writers also builds"
+    show_cmds
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=OFF -DTINT_BUILD_SPV_WRITER=OFF -DTINT_BUILD_WGSL_READER=OFF -DTINT_BUILD_WGSL_WRITER=OFF -DTINT_BUILD_MSL_WRITER=OFF -DTINT_BUILD_HLSL_WRITER=OFF -DTINT_BUILD_BENCHMARKS=OFF
+        cmake --build . -- --jobs=$(nproc)
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=ON -DTINT_BUILD_SPV_WRITER=ON -DTINT_BUILD_WGSL_READER=ON -DTINT_BUILD_WGSL_WRITER=ON -DTINT_BUILD_MSL_WRITER=ON -DTINT_BUILD_HLSL_WRITER=ON -DTINT_BUILD_BENCHMARKS=ON
+    hide_cmds
+else
+    status "Unsupported build system: $BUILD_SYSTEM"
+    exit 1
+fi
+
+status "Done"
diff --git a/kokoro/windows/build.bat b/kokoro/windows/build.bat
new file mode 100644
index 0000000..c6df367
--- /dev/null
+++ b/kokoro/windows/build.bat
@@ -0,0 +1,167 @@
+@rem Copyright 2021 The Tint Authors.

+@rem

+@rem Licensed under the Apache License, Version 2.0 (the "License");

+@rem you may not use this file except in compliance with the License.

+@rem You may obtain a copy of the License at

+@rem

+@rem     http://www.apache.org/licenses/LICENSE-2.0

+@rem

+@rem Unless required by applicable law or agreed to in writing, software

+@rem distributed under the License is distributed on an "AS IS" BASIS,

+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+@rem See the License for the specific language governing permissions and

+@rem limitations under the License.

+

+@echo off

+SETLOCAL ENABLEDELAYEDEXPANSION

+

+goto :main

+

+:task_begin

+set TASK_NAME=%~1

+echo %TASK_NAME% starting at %Time%

+exit /b 0

+

+:print_last_task_duration

+if not "%TASK_NAME%" == "" (

+    echo %TASK_NAME% completed at %Time%

+)

+exit /b 0

+

+:status

+echo.

+echo.

+call :print_last_task_duration

+echo.

+echo *****************************************************************

+echo %~1

+echo *****************************************************************

+echo.

+call :task_begin "%~1"

+exit /b 0

+

+:main

+

+set ORIGINAL_SRC_DIR= %~dp0\..\..

+set TEMP_DIR=%TEMP%\tint-temp

+set SRC_DIR="%TEMP_DIR%\tint-src"

+set BUILD_DIR="%TEMP_DIR%\tint-build"

+

+cd /d %ORIGINAL_SRC_DIR%

+if not exist ".git\" (

+    echo "ORIGINAL_SRC_DIR should point to project root: %ORIGINAL_SRC_DIR%"

+    goto :error

+)

+

+if exist %TEMP_DIR% (

+    call :status "Deleting %TEMP_DIR%"

+    del /q/f/s %TEMP_DIR% > NUL || goto :error

+    rmdir /q/s %TEMP_DIR% > NUL || goto :error

+)

+mkdir %TEMP_DIR% || goto :error

+

+call :status "Fetching and installing DXC"

+@echo on

+set DXC_RELEASE="https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.6.2112/dxc_2021_12_08.zip"

+curl -k -L %DXC_RELEASE% --output "%TEMP_DIR%\dxc_release.zip" || goto :error

+powershell.exe -Command "Expand-Archive -LiteralPath '%TEMP_DIR%\dxc_release.zip' -DestinationPath '%TEMP_DIR%\dxc'" || goto :error

+set DXC_PATH=%TEMP_DIR%\dxc\bin\x64

+

+rem Patch with artifact build that contains fixes not present in the release build

+set DXC_ARTIFACT="https://ci.appveyor.com/api/projects/dnovillo/directxshadercompiler/artifacts/build%%2FRelease%%2Fdxc-artifacts.zip?branch=master&pr=false&job=image%%3A%%20Visual%%20Studio%%202019"

+curl -k -L %DXC_ARTIFACT% --output "%TEMP_DIR%\dxc_artifact.zip" || goto :error

+powershell.exe -Command "Expand-Archive -Force -LiteralPath '%TEMP_DIR%\dxc_artifact.zip' -DestinationPath '%TEMP_DIR%\dxc_artifact'" || goto :error

+move /Y %TEMP_DIR%\dxc_artifact\bin\* %DXC_PATH%

+@echo off

+

+call :status "Fetching and installing Windows SDK for d3dcompiler DLL"

+@echo on

+set WINSDK_DLL_INSTALLER=https://go.microsoft.com/fwlink/?linkid=2164145

+set WINSDK_VERSION=10.0.20348.0

+curl -k -L %WINSDK_DLL_INSTALLER% --output "%TEMP_DIR%\winsdksetup.exe" || goto :error

+start "download" /wait "%TEMP_DIR%\winsdksetup.exe" /quiet /norestart /ceip off /features OptionId.DesktopCPPx64 /layout "%TEMP_DIR%\winsdkinstall" || goto :error

+start "install" /wait "%TEMP_DIR%\winsdkinstall\Installers\Windows SDK for Windows Store Apps Tools-x86_en-us.msi" || goto :error

+set D3DCOMPILER_PATH=C:\Program Files (x86)\Windows Kits\10\bin\%WINSDK_VERSION%\x64

+@echo off

+

+call :status "Installing depot_tools"

+@echo on

+pushd %TEMP_DIR%

+rem For Windows, we must download and extract a bundle.

+rem See https://chromium.googlesource.com/chromium/src/+/HEAD/docs/windows_build_instructions.md#install

+powershell -Command "(New-Object Net.WebClient).DownloadFile('https://storage.googleapis.com/chrome-infra/depot_tools.zip', 'depot_tools.zip')" || goto :error

+powershell -Command "Expand-Archive -Force 'depot_tools.zip' 'depot_tools'" || goto :error

+rem Run gclient once to install deps

+set PATH=%TEMP_DIR%\depot_tools;%PATH%

+set DEPOT_TOOLS_UPDATE=1

+set DEPOT_TOOLS_WIN_TOOLCHAIN=0

+call gclient || goto :error

+@echo off

+popd

+

+call :status "Cloning to clean source directory"

+@echo on

+mkdir %SRC_DIR% || goto :error

+cd /d %SRC_DIR% || goto :error

+call git clone %ORIGINAL_SRC_DIR% . || goto :error

+@echo off

+

+call :status "Fetching dependencies"

+@echo on

+copy standalone.gclient .gclient || goto :error

+call gclient sync || goto :error

+@echo off

+

+call :status "Configuring build system"

+@echo on

+mkdir %BUILD_DIR%

+cd /d %BUILD_DIR%

+set COMMON_CMAKE_FLAGS=-DTINT_BUILD_DOCS=O -DTINT_BUILD_BENCHMARKS=1 -DCMAKE_BUILD_TYPE=%BUILD_TYPE%

+@echo off

+

+call :status "Building tint"

+@echo on

+rem Disable msbuild "Intermediate or Output directory cannot reside in Temporary directory"

+set IgnoreWarnIntDirInTempDetected=true

+rem Add Python3 to path as this Kokoro image only has Python2 in it

+set PATH=C:\Python37;%PATH%

+rem To use ninja with CMake requires VC env vars

+call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"

+@echo on

+rem Note that we need to specify the C and C++ compiler only because Cygwin is in PATH and CMake finds GCC and picks that over MSVC

+cmake %SRC_DIR% -G "Ninja" -DCMAKE_C_COMPILER="cl.exe" -DCMAKE_CXX_COMPILER="cl.exe" %COMMON_CMAKE_FLAGS% || goto :error

+cmake --build . || goto :error

+call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" /clean_env

+@echo off

+

+call :status "Running tint_unittests"

+@echo on

+tint_unittests.exe || goto :error

+@echo off

+

+call :status "Testing test/tint/test-all.sh"

+@echo on

+cd /d %SRC_DIR% || goto :error

+rem Run tests with DXC and Metal validation

+set OLD_PATH=%PATH%

+set PATH=C:\Program Files\Metal Developer Tools\macos\bin;%PATH%

+where metal.exe

+set PATH=%DXC_PATH%;%OLD_PATH%

+where dxc.exe dxil.dll

+call git bash -- ./test/tint/test-all.sh ../tint-build/tint.exe --verbose || goto :error

+@echo on

+set PATH=%OLD_PATH%

+rem Run again to test with FXC validation

+set PATH=%D3DCOMPILER_PATH%;%OLD_PATH%

+where d3dcompiler_47.dll

+call git bash -- ./test/tint/test-all.sh ../tint-build/tint.exe --verbose --format hlsl --fxc || goto :error

+@echo on

+set PATH=%OLD_PATH%

+@echo off

+

+call :status "Done"

+exit /b 0

+

+:error

+echo BUILD FAILED! errorlevel: %errorlevel%

+exit /b %errorlevel%

diff --git a/kokoro/windows/cmake-msvc2019-debug/build.bat b/kokoro/windows/cmake-msvc2019-debug/build.bat
new file mode 100644
index 0000000..25db347
--- /dev/null
+++ b/kokoro/windows/cmake-msvc2019-debug/build.bat
@@ -0,0 +1,18 @@
+@rem Copyright 2021 The Tint Authors.

+@rem

+@rem Licensed under the Apache License, Version 2.0 (the "License");

+@rem you may not use this file except in compliance with the License.

+@rem You may obtain a copy of the License at

+@rem

+@rem     http://www.apache.org/licenses/LICENSE-2.0

+@rem

+@rem Unless required by applicable law or agreed to in writing, software

+@rem distributed under the License is distributed on an "AS IS" BASIS,

+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+@rem See the License for the specific language governing permissions and

+@rem limitations under the License.

+

+@echo on

+set BUILD_TYPE=Debug

+call %~dp0\..\build.bat

+exit /b %errorlevel%

diff --git a/kokoro/windows/cmake-msvc2019-debug/presubmit.cfg b/kokoro/windows/cmake-msvc2019-debug/presubmit.cfg
new file mode 100644
index 0000000..c977aa8
--- /dev/null
+++ b/kokoro/windows/cmake-msvc2019-debug/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/windows/cmake-msvc2019-debug/build.bat"
diff --git a/kokoro/windows/cmake-msvc2019-release/build.bat b/kokoro/windows/cmake-msvc2019-release/build.bat
new file mode 100644
index 0000000..a2f6072
--- /dev/null
+++ b/kokoro/windows/cmake-msvc2019-release/build.bat
@@ -0,0 +1,18 @@
+@rem Copyright 2021 The Tint Authors.

+@rem

+@rem Licensed under the Apache License, Version 2.0 (the "License");

+@rem you may not use this file except in compliance with the License.

+@rem You may obtain a copy of the License at

+@rem

+@rem     http://www.apache.org/licenses/LICENSE-2.0

+@rem

+@rem Unless required by applicable law or agreed to in writing, software

+@rem distributed under the License is distributed on an "AS IS" BASIS,

+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

+@rem See the License for the specific language governing permissions and

+@rem limitations under the License.

+

+@echo on

+set BUILD_TYPE=Release

+call %~dp0\..\build.bat

+exit /b %errorlevel%

diff --git a/kokoro/windows/cmake-msvc2019-release/presubmit.cfg b/kokoro/windows/cmake-msvc2019-release/presubmit.cfg
new file mode 100644
index 0000000..00acdd6
--- /dev/null
+++ b/kokoro/windows/cmake-msvc2019-release/presubmit.cfg
@@ -0,0 +1,3 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+build_file: "tint/kokoro/windows/cmake-msvc2019-release/build.bat"
diff --git a/scripts/dawn_overrides_with_defaults.gni b/scripts/dawn_overrides_with_defaults.gni
index c667693..46f44ef 100644
--- a/scripts/dawn_overrides_with_defaults.gni
+++ b/scripts/dawn_overrides_with_defaults.gni
@@ -75,11 +75,6 @@
   dawn_vulkan_validation_layers_dir = ""
 }
 
-if (!defined(dawn_tint_dir)) {
-  # Default to Tint being Dawn's DEPS
-  dawn_tint_dir = "${dawn_root}/third_party/tint"
-}
-
 if (!defined(dawn_abseil_dir)) {
   dawn_abseil_dir = "//third_party/abseil-cpp"
 }
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index ce7d97f..5d97a8e 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -163,7 +163,7 @@
     "${dawn_root}/src/dawn/common",
     "${dawn_spirv_tools_dir}:spvtools_opt",
     "${dawn_spirv_tools_dir}:spvtools_val",
-    "${dawn_tint_dir}/src/tint:libtint",
+    "${dawn_root}/src/tint:libtint",
   ]
   defines = []
   libs = []
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
new file mode 100644
index 0000000..58ec6a4
--- /dev/null
+++ b/src/tint/BUILD.gn
@@ -0,0 +1,799 @@
+# Copyright 2021 The Tint Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+import("../../tint_overrides_with_defaults.gni")
+
+###############################################################################
+# Common - Configs, etc. shared across targets
+###############################################################################
+
+config("tint_common_config") {
+  include_dirs = [
+    "${target_gen_dir}",
+    "${tint_root_dir}/",
+    "${tint_spirv_headers_dir}/include",
+    "${tint_spirv_tools_dir}/",
+    "${tint_spirv_tools_dir}/include",
+  ]
+}
+
+config("tint_public_config") {
+  defines = []
+  if (tint_build_spv_reader) {
+    defines += [ "TINT_BUILD_SPV_READER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_SPV_READER=0" ]
+  }
+
+  if (tint_build_spv_writer) {
+    defines += [ "TINT_BUILD_SPV_WRITER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_SPV_WRITER=0" ]
+  }
+
+  if (tint_build_wgsl_reader) {
+    defines += [ "TINT_BUILD_WGSL_READER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_WGSL_READER=0" ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    defines += [ "TINT_BUILD_WGSL_WRITER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_WGSL_WRITER=0" ]
+  }
+
+  if (tint_build_msl_writer) {
+    defines += [ "TINT_BUILD_MSL_WRITER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_MSL_WRITER=0" ]
+  }
+
+  if (tint_build_hlsl_writer) {
+    defines += [ "TINT_BUILD_HLSL_WRITER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_HLSL_WRITER=0" ]
+  }
+
+  if (tint_build_glsl_writer) {
+    defines += [ "TINT_BUILD_GLSL_WRITER=1" ]
+  } else {
+    defines += [ "TINT_BUILD_GLSL_WRITER=0" ]
+  }
+
+  include_dirs = [
+    "${tint_root_dir}/",
+    "${tint_root_dir}/include/",
+    "${tint_spirv_headers_dir}/include",
+  ]
+}
+
+config("tint_config") {
+  include_dirs = []
+  if (tint_build_spv_reader || tint_build_spv_writer) {
+    include_dirs += [ "${tint_spirv_tools_dir}/include/" ]
+  }
+}
+
+###############################################################################
+# Helper library for IO operations
+# Only to be used by tests and sample executable
+###############################################################################
+source_set("tint_utils_io") {
+  sources = [
+    "utils/io/command.h",
+    "utils/io/tmpfile.h",
+  ]
+
+  if (is_linux || is_mac) {
+    sources += [ "utils/io/command_posix.cc" ]
+    sources += [ "utils/io/tmpfile_posix.cc" ]
+  } else if (is_win) {
+    sources += [ "utils/io/command_windows.cc" ]
+    sources += [ "utils/io/tmpfile_windows.cc" ]
+  } else {
+    sources += [ "utils/io/command_other.cc" ]
+    sources += [ "utils/io/tmpfile_other.cc" ]
+  }
+
+  public_deps = [ ":libtint_core_all_src" ]
+}
+
+###############################################################################
+# Helper library for validating generated shaders
+# As this depends on tint_utils_io, this is only to be used by tests and sample
+# executable
+###############################################################################
+source_set("tint_val") {
+  sources = [
+    "val/hlsl.cc",
+    "val/msl.cc",
+    "val/val.h",
+  ]
+  public_deps = [ ":tint_utils_io" ]
+}
+
+###############################################################################
+# Library - Tint core and optional modules of libtint
+###############################################################################
+# libtint source sets are divided into a non-optional core in :libtint_core_src
+# and optional :libtint_*_src subsets, because ninja does not like having
+# multiple source files with the same name, like function.cc, in the same
+# source set
+# target.
+#
+# Targets that want to use tint as a library should depend on ":libtint" and
+# use the build flags to control what is included, instead of trying to specify
+# the subsets that they want.
+
+template("libtint_source_set") {
+  source_set(target_name) {
+    forward_variables_from(invoker, "*", [ "configs" ])
+
+    if (!defined(invoker.deps)) {
+      deps = []
+    }
+    deps += [
+      "${tint_spirv_headers_dir}:spv_headers",
+      "${tint_spirv_tools_dir}:spvtools_core_enums_unified1",
+      "${tint_spirv_tools_dir}:spvtools_core_tables_unified1",
+      "${tint_spirv_tools_dir}:spvtools_headers",
+      "${tint_spirv_tools_dir}:spvtools_language_header_cldebuginfo100",
+      "${tint_spirv_tools_dir}:spvtools_language_header_debuginfo",
+      "${tint_spirv_tools_dir}:spvtools_language_header_vkdebuginfo100",
+    ]
+
+    if (defined(invoker.configs)) {
+      configs += invoker.configs
+    }
+    configs += [ ":tint_common_config" ]
+    if (build_with_chromium) {
+      configs -= [ "//build/config/compiler:chromium_code" ]
+      configs += [ "//build/config/compiler:no_chromium_code" ]
+    }
+
+    if (!defined(invoker.public_configs)) {
+      public_configs = []
+    }
+    public_configs += [ ":tint_public_config" ]
+  }
+}
+
+libtint_source_set("libtint_core_all_src") {
+  sources = [
+    "ast/access.cc",
+    "ast/access.h",
+    "ast/alias.cc",
+    "ast/alias.h",
+    "ast/array.cc",
+    "ast/array.h",
+    "ast/assignment_statement.cc",
+    "ast/assignment_statement.h",
+    "ast/ast_type.cc",  # TODO(bclayton) - rename to type.cc
+    "ast/atomic.cc",
+    "ast/atomic.h",
+    "ast/attribute.cc",
+    "ast/attribute.h",
+    "ast/binary_expression.cc",
+    "ast/binary_expression.h",
+    "ast/binding_attribute.cc",
+    "ast/binding_attribute.h",
+    "ast/bitcast_expression.cc",
+    "ast/bitcast_expression.h",
+    "ast/block_statement.cc",
+    "ast/block_statement.h",
+    "ast/bool.cc",
+    "ast/bool.h",
+    "ast/bool_literal_expression.cc",
+    "ast/bool_literal_expression.h",
+    "ast/break_statement.cc",
+    "ast/break_statement.h",
+    "ast/builtin.cc",
+    "ast/builtin.h",
+    "ast/builtin_attribute.cc",
+    "ast/builtin_attribute.h",
+    "ast/call_expression.cc",
+    "ast/call_expression.h",
+    "ast/call_statement.cc",
+    "ast/call_statement.h",
+    "ast/case_statement.cc",
+    "ast/case_statement.h",
+    "ast/compound_assignment_statement.cc",
+    "ast/compound_assignment_statement.h",
+    "ast/continue_statement.cc",
+    "ast/continue_statement.h",
+    "ast/depth_multisampled_texture.cc",
+    "ast/depth_multisampled_texture.h",
+    "ast/depth_texture.cc",
+    "ast/depth_texture.h",
+    "ast/disable_validation_attribute.cc",
+    "ast/disable_validation_attribute.h",
+    "ast/discard_statement.cc",
+    "ast/discard_statement.h",
+    "ast/else_statement.cc",
+    "ast/else_statement.h",
+    "ast/expression.cc",
+    "ast/expression.h",
+    "ast/external_texture.cc",
+    "ast/external_texture.h",
+    "ast/f32.cc",
+    "ast/f32.h",
+    "ast/fallthrough_statement.cc",
+    "ast/fallthrough_statement.h",
+    "ast/float_literal_expression.cc",
+    "ast/float_literal_expression.h",
+    "ast/for_loop_statement.cc",
+    "ast/for_loop_statement.h",
+    "ast/function.cc",
+    "ast/function.h",
+    "ast/group_attribute.cc",
+    "ast/group_attribute.h",
+    "ast/i32.cc",
+    "ast/i32.h",
+    "ast/id_attribute.cc",
+    "ast/id_attribute.h",
+    "ast/identifier_expression.cc",
+    "ast/identifier_expression.h",
+    "ast/if_statement.cc",
+    "ast/if_statement.h",
+    "ast/index_accessor_expression.cc",
+    "ast/index_accessor_expression.h",
+    "ast/int_literal_expression.cc",
+    "ast/int_literal_expression.h",
+    "ast/internal_attribute.cc",
+    "ast/internal_attribute.h",
+    "ast/interpolate_attribute.cc",
+    "ast/interpolate_attribute.h",
+    "ast/invariant_attribute.cc",
+    "ast/invariant_attribute.h",
+    "ast/literal_expression.cc",
+    "ast/literal_expression.h",
+    "ast/location_attribute.cc",
+    "ast/location_attribute.h",
+    "ast/loop_statement.cc",
+    "ast/loop_statement.h",
+    "ast/matrix.cc",
+    "ast/matrix.h",
+    "ast/member_accessor_expression.cc",
+    "ast/member_accessor_expression.h",
+    "ast/module.cc",
+    "ast/module.h",
+    "ast/multisampled_texture.cc",
+    "ast/multisampled_texture.h",
+    "ast/node.cc",
+    "ast/node.h",
+    "ast/phony_expression.cc",
+    "ast/phony_expression.h",
+    "ast/pipeline_stage.cc",
+    "ast/pipeline_stage.h",
+    "ast/pointer.cc",
+    "ast/pointer.h",
+    "ast/return_statement.cc",
+    "ast/return_statement.h",
+    "ast/sampled_texture.cc",
+    "ast/sampled_texture.h",
+    "ast/sampler.cc",
+    "ast/sampler.h",
+    "ast/sint_literal_expression.cc",
+    "ast/sint_literal_expression.h",
+    "ast/stage_attribute.cc",
+    "ast/stage_attribute.h",
+    "ast/statement.cc",
+    "ast/statement.h",
+    "ast/storage_class.cc",
+    "ast/storage_class.h",
+    "ast/storage_texture.cc",
+    "ast/storage_texture.h",
+    "ast/stride_attribute.cc",
+    "ast/stride_attribute.h",
+    "ast/struct.cc",
+    "ast/struct.h",
+    "ast/struct_member.cc",
+    "ast/struct_member.h",
+    "ast/struct_member_align_attribute.cc",
+    "ast/struct_member_align_attribute.h",
+    "ast/struct_member_offset_attribute.cc",
+    "ast/struct_member_offset_attribute.h",
+    "ast/struct_member_size_attribute.cc",
+    "ast/struct_member_size_attribute.h",
+    "ast/switch_statement.cc",
+    "ast/switch_statement.h",
+    "ast/texture.cc",
+    "ast/texture.h",
+    "ast/traverse_expressions.h",
+    "ast/type.h",
+    "ast/type_decl.cc",
+    "ast/type_decl.h",
+    "ast/type_name.cc",
+    "ast/type_name.h",
+    "ast/u32.cc",
+    "ast/u32.h",
+    "ast/uint_literal_expression.cc",
+    "ast/uint_literal_expression.h",
+    "ast/unary_op.cc",
+    "ast/unary_op.h",
+    "ast/unary_op_expression.cc",
+    "ast/unary_op_expression.h",
+    "ast/variable.cc",
+    "ast/variable.h",
+    "ast/variable_decl_statement.cc",
+    "ast/variable_decl_statement.h",
+    "ast/vector.cc",
+    "ast/vector.h",
+    "ast/void.cc",
+    "ast/void.h",
+    "ast/workgroup_attribute.cc",
+    "ast/workgroup_attribute.h",
+    "builtin_table.cc",
+    "builtin_table.h",
+    "builtin_table.inl",
+    "castable.cc",
+    "castable.h",
+    "clone_context.cc",
+    "clone_context.h",
+    "debug.cc",
+    "debug.h",
+    "demangler.cc",
+    "demangler.h",
+    "diagnostic/diagnostic.cc",
+    "diagnostic/diagnostic.h",
+    "diagnostic/formatter.cc",
+    "diagnostic/formatter.h",
+    "diagnostic/printer.cc",
+    "diagnostic/printer.h",
+    "inspector/entry_point.cc",
+    "inspector/entry_point.h",
+    "inspector/inspector.cc",
+    "inspector/inspector.h",
+    "inspector/resource_binding.cc",
+    "inspector/resource_binding.h",
+    "inspector/scalar.cc",
+    "inspector/scalar.h",
+    "program.cc",
+    "program.h",
+    "program_builder.cc",
+    "program_builder.h",
+    "program_id.cc",
+    "program_id.h",
+    "reader/reader.cc",
+    "reader/reader.h",
+    "resolver/dependency_graph.cc",
+    "resolver/dependency_graph.h",
+    "resolver/resolver.cc",
+    "resolver/resolver.h",
+    "resolver/resolver_constants.cc",
+    "resolver/resolver_validation.cc",
+    "scope_stack.h",
+    "sem/array.h",
+    "sem/atomic_type.h",
+    "sem/behavior.h",
+    "sem/binding_point.h",
+    "sem/bool_type.h",
+    "sem/builtin.h",
+    "sem/builtin_type.h",
+    "sem/call.h",
+    "sem/call_target.h",
+    "sem/constant.h",
+    "sem/depth_multisampled_texture_type.h",
+    "sem/depth_texture_type.h",
+    "sem/expression.h",
+    "sem/external_texture_type.h",
+    "sem/f32_type.h",
+    "sem/for_loop_statement.h",
+    "sem/i32_type.h",
+    "sem/if_statement.h",
+    "sem/info.h",
+    "sem/loop_statement.h",
+    "sem/matrix_type.h",
+    "sem/module.h",
+    "sem/multisampled_texture_type.h",
+    "sem/node.h",
+    "sem/parameter_usage.h",
+    "sem/pipeline_stage_set.h",
+    "sem/pointer_type.h",
+    "sem/reference_type.h",
+    "sem/sampled_texture_type.h",
+    "sem/sampler_texture_pair.h",
+    "sem/sampler_type.h",
+    "sem/storage_texture_type.h",
+    "sem/switch_statement.h",
+    "sem/texture_type.h",
+    "sem/type.h",
+    "sem/type_constructor.h",
+    "sem/type_conversion.h",
+    "sem/type_manager.h",
+    "sem/type_mappings.h",
+    "sem/u32_type.h",
+    "sem/vector_type.h",
+    "sem/void_type.h",
+    "source.cc",
+    "source.h",
+    "symbol.cc",
+    "symbol.h",
+    "symbol_table.cc",
+    "symbol_table.h",
+    "text/unicode.cc",
+    "text/unicode.h",
+    "traits.h",
+    "transform/add_empty_entry_point.cc",
+    "transform/add_empty_entry_point.h",
+    "transform/add_spirv_block_attribute.cc",
+    "transform/add_spirv_block_attribute.h",
+    "transform/array_length_from_uniform.cc",
+    "transform/array_length_from_uniform.h",
+    "transform/binding_remapper.cc",
+    "transform/binding_remapper.h",
+    "transform/builtin_polyfill.cc",
+    "transform/builtin_polyfill.h",
+    "transform/calculate_array_length.cc",
+    "transform/calculate_array_length.h",
+    "transform/canonicalize_entry_point_io.cc",
+    "transform/canonicalize_entry_point_io.h",
+    "transform/combine_samplers.cc",
+    "transform/combine_samplers.h",
+    "transform/decompose_memory_access.cc",
+    "transform/decompose_memory_access.h",
+    "transform/decompose_strided_array.cc",
+    "transform/decompose_strided_array.h",
+    "transform/decompose_strided_matrix.cc",
+    "transform/decompose_strided_matrix.h",
+    "transform/first_index_offset.cc",
+    "transform/first_index_offset.h",
+    "transform/fold_constants.cc",
+    "transform/fold_constants.h",
+    "transform/fold_trivial_single_use_lets.cc",
+    "transform/fold_trivial_single_use_lets.h",
+    "transform/for_loop_to_loop.cc",
+    "transform/for_loop_to_loop.h",
+    "transform/expand_compound_assignment.cc",
+    "transform/expand_compound_assignment.h",
+    "transform/localize_struct_array_assignment.cc",
+    "transform/localize_struct_array_assignment.h",
+    "transform/loop_to_for_loop.cc",
+    "transform/loop_to_for_loop.h",
+    "transform/manager.cc",
+    "transform/manager.h",
+    "transform/module_scope_var_to_entry_point_param.cc",
+    "transform/module_scope_var_to_entry_point_param.h",
+    "transform/multiplanar_external_texture.cc",
+    "transform/multiplanar_external_texture.h",
+    "transform/num_workgroups_from_uniform.cc",
+    "transform/num_workgroups_from_uniform.h",
+    "transform/promote_initializers_to_const_var.cc",
+    "transform/promote_initializers_to_const_var.h",
+    "transform/promote_side_effects_to_decl.cc",
+    "transform/promote_side_effects_to_decl.h",
+    "transform/remove_continue_in_switch.cc",
+    "transform/remove_continue_in_switch.h",
+    "transform/remove_phonies.cc",
+    "transform/remove_phonies.h",
+    "transform/remove_unreachable_statements.cc",
+    "transform/remove_unreachable_statements.h",
+    "transform/renamer.cc",
+    "transform/renamer.h",
+    "transform/robustness.cc",
+    "transform/robustness.h",
+    "transform/simplify_pointers.cc",
+    "transform/simplify_pointers.h",
+    "transform/single_entry_point.cc",
+    "transform/single_entry_point.h",
+    "transform/transform.cc",
+    "transform/transform.h",
+    "transform/unshadow.cc",
+    "transform/unshadow.h",
+    "transform/unwind_discard_functions.cc",
+    "transform/unwind_discard_functions.h",
+    "transform/utils/get_insertion_point.cc",
+    "transform/utils/get_insertion_point.h",
+    "transform/utils/hoist_to_decl_before.cc",
+    "transform/utils/hoist_to_decl_before.h",
+    "transform/var_for_dynamic_index.cc",
+    "transform/var_for_dynamic_index.h",
+    "transform/vectorize_scalar_matrix_constructors.cc",
+    "transform/vectorize_scalar_matrix_constructors.h",
+    "transform/vertex_pulling.cc",
+    "transform/vertex_pulling.h",
+    "transform/wrap_arrays_in_structs.cc",
+    "transform/wrap_arrays_in_structs.h",
+    "transform/zero_init_workgroup_memory.cc",
+    "transform/zero_init_workgroup_memory.h",
+    "utils/block_allocator.h",
+    "utils/crc32.h",
+    "utils/debugger.cc",
+    "utils/debugger.h",
+    "utils/enum_set.h",
+    "utils/hash.h",
+    "utils/map.h",
+    "utils/math.h",
+    "utils/scoped_assignment.h",
+    "utils/string.h",
+    "utils/unique_allocator.h",
+    "utils/unique_vector.h",
+    "writer/append_vector.cc",
+    "writer/append_vector.h",
+    "writer/array_length_from_uniform_options.cc",
+    "writer/array_length_from_uniform_options.h",
+    "writer/float_to_string.cc",
+    "writer/float_to_string.h",
+    "writer/generate_external_texture_bindings.cc",
+    "writer/generate_external_texture_bindings.h",
+    "writer/text.cc",
+    "writer/text.h",
+    "writer/text_generator.cc",
+    "writer/text_generator.h",
+    "writer/writer.cc",
+    "writer/writer.h",
+  ]
+
+  if (is_linux) {
+    sources += [ "diagnostic/printer_linux.cc" ]
+  } else if (is_win) {
+    sources += [ "diagnostic/printer_windows.cc" ]
+  } else {
+    sources += [ "diagnostic/printer_other.cc" ]
+  }
+}
+
+libtint_source_set("libtint_sem_src") {
+  sources = [
+    "sem/array.cc",
+    "sem/array.h",
+    "sem/atomic_type.cc",
+    "sem/atomic_type.h",
+    "sem/behavior.cc",
+    "sem/behavior.h",
+    "sem/binding_point.h",
+    "sem/block_statement.cc",
+    "sem/bool_type.cc",
+    "sem/bool_type.h",
+    "sem/builtin.cc",
+    "sem/builtin.h",
+    "sem/builtin_type.cc",
+    "sem/builtin_type.h",
+    "sem/call.cc",
+    "sem/call.h",
+    "sem/call_target.cc",
+    "sem/call_target.h",
+    "sem/constant.cc",
+    "sem/constant.h",
+    "sem/depth_multisampled_texture_type.cc",
+    "sem/depth_multisampled_texture_type.h",
+    "sem/depth_texture_type.cc",
+    "sem/depth_texture_type.h",
+    "sem/expression.cc",
+    "sem/expression.h",
+    "sem/external_texture_type.cc",
+    "sem/external_texture_type.h",
+    "sem/f32_type.cc",
+    "sem/f32_type.h",
+    "sem/for_loop_statement.cc",
+    "sem/for_loop_statement.h",
+    "sem/function.cc",
+    "sem/i32_type.cc",
+    "sem/i32_type.h",
+    "sem/if_statement.cc",
+    "sem/if_statement.h",
+    "sem/info.cc",
+    "sem/info.h",
+    "sem/loop_statement.cc",
+    "sem/loop_statement.h",
+    "sem/matrix_type.cc",
+    "sem/matrix_type.h",
+    "sem/member_accessor_expression.cc",
+    "sem/module.cc",
+    "sem/module.h",
+    "sem/multisampled_texture_type.cc",
+    "sem/multisampled_texture_type.h",
+    "sem/node.cc",
+    "sem/node.h",
+    "sem/parameter_usage.cc",
+    "sem/parameter_usage.h",
+    "sem/pipeline_stage_set.h",
+    "sem/pointer_type.cc",
+    "sem/pointer_type.h",
+    "sem/reference_type.cc",
+    "sem/reference_type.h",
+    "sem/sampled_texture_type.cc",
+    "sem/sampled_texture_type.h",
+    "sem/sampler_type.cc",
+    "sem/sampler_type.h",
+    "sem/statement.cc",
+    "sem/storage_texture_type.cc",
+    "sem/storage_texture_type.h",
+    "sem/struct.cc",
+    "sem/switch_statement.cc",
+    "sem/switch_statement.h",
+    "sem/texture_type.cc",
+    "sem/texture_type.h",
+    "sem/type.cc",
+    "sem/type.h",
+    "sem/type_constructor.cc",
+    "sem/type_constructor.h",
+    "sem/type_conversion.cc",
+    "sem/type_conversion.h",
+    "sem/type_manager.cc",
+    "sem/type_manager.h",
+    "sem/type_mappings.h",
+    "sem/u32_type.cc",
+    "sem/u32_type.h",
+    "sem/variable.cc",
+    "sem/vector_type.cc",
+    "sem/vector_type.h",
+    "sem/void_type.cc",
+    "sem/void_type.h",
+  ]
+
+  public_deps = [ ":libtint_core_all_src" ]
+}
+
+libtint_source_set("libtint_core_src") {
+  public_deps = [
+    ":libtint_core_all_src",
+    ":libtint_sem_src",
+  ]
+}
+
+libtint_source_set("libtint_spv_reader_src") {
+  sources = [
+    "reader/spirv/construct.cc",
+    "reader/spirv/construct.h",
+    "reader/spirv/entry_point_info.cc",
+    "reader/spirv/entry_point_info.h",
+    "reader/spirv/enum_converter.cc",
+    "reader/spirv/enum_converter.h",
+    "reader/spirv/fail_stream.h",
+    "reader/spirv/function.cc",
+    "reader/spirv/function.h",
+    "reader/spirv/namer.cc",
+    "reader/spirv/namer.h",
+    "reader/spirv/parser.cc",
+    "reader/spirv/parser.h",
+    "reader/spirv/parser_impl.cc",
+    "reader/spirv/parser_impl.h",
+    "reader/spirv/parser_type.cc",
+    "reader/spirv/parser_type.h",
+    "reader/spirv/usage.cc",
+    "reader/spirv/usage.h",
+  ]
+
+  public_deps = [
+    ":libtint_core_src",
+    "${tint_spirv_tools_dir}/:spvtools_opt",
+  ]
+
+  public_configs = [ "${tint_spirv_tools_dir}/:spvtools_internal_config" ]
+}
+
+libtint_source_set("libtint_spv_writer_src") {
+  sources = [
+    "writer/spirv/binary_writer.cc",
+    "writer/spirv/binary_writer.h",
+    "writer/spirv/builder.cc",
+    "writer/spirv/builder.h",
+    "writer/spirv/function.cc",
+    "writer/spirv/function.h",
+    "writer/spirv/generator.cc",
+    "writer/spirv/generator.h",
+    "writer/spirv/instruction.cc",
+    "writer/spirv/instruction.h",
+    "writer/spirv/operand.cc",
+    "writer/spirv/operand.h",
+    "writer/spirv/scalar_constant.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+libtint_source_set("libtint_wgsl_reader_src") {
+  sources = [
+    "reader/wgsl/lexer.cc",
+    "reader/wgsl/lexer.h",
+    "reader/wgsl/parser.cc",
+    "reader/wgsl/parser.h",
+    "reader/wgsl/parser_impl.cc",
+    "reader/wgsl/parser_impl.h",
+    "reader/wgsl/parser_impl_detail.h",
+    "reader/wgsl/token.cc",
+    "reader/wgsl/token.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+libtint_source_set("libtint_wgsl_writer_src") {
+  sources = [
+    "writer/wgsl/generator.cc",
+    "writer/wgsl/generator.h",
+    "writer/wgsl/generator_impl.cc",
+    "writer/wgsl/generator_impl.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+libtint_source_set("libtint_msl_writer_src") {
+  sources = [
+    "writer/msl/generator.cc",
+    "writer/msl/generator.h",
+    "writer/msl/generator_impl.cc",
+    "writer/msl/generator_impl.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+libtint_source_set("libtint_hlsl_writer_src") {
+  sources = [
+    "writer/hlsl/generator.cc",
+    "writer/hlsl/generator.h",
+    "writer/hlsl/generator_impl.cc",
+    "writer/hlsl/generator_impl.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+libtint_source_set("libtint_glsl_writer_src") {
+  sources = [
+    "transform/glsl.cc",
+    "transform/glsl.h",
+    "writer/glsl/generator.cc",
+    "writer/glsl/generator.h",
+    "writer/glsl/generator_impl.cc",
+    "writer/glsl/generator_impl.h",
+  ]
+
+  public_deps = [ ":libtint_core_src" ]
+}
+
+source_set("libtint") {
+  public_deps = [ ":libtint_core_src" ]
+
+  if (tint_build_spv_reader) {
+    public_deps += [ ":libtint_spv_reader_src" ]
+  }
+
+  if (tint_build_spv_writer) {
+    public_deps += [ ":libtint_spv_writer_src" ]
+  }
+
+  if (tint_build_wgsl_reader) {
+    public_deps += [ ":libtint_wgsl_reader_src" ]
+  }
+
+  if (tint_build_wgsl_writer) {
+    public_deps += [ ":libtint_wgsl_writer_src" ]
+  }
+
+  if (tint_build_msl_writer) {
+    public_deps += [ ":libtint_msl_writer_src" ]
+  }
+
+  if (tint_build_hlsl_writer) {
+    public_deps += [ ":libtint_hlsl_writer_src" ]
+  }
+
+  if (tint_build_glsl_writer) {
+    public_deps += [ ":libtint_glsl_writer_src" ]
+  }
+
+  configs += [ ":tint_common_config" ]
+  public_configs = [ ":tint_public_config" ]
+
+  if (build_with_chromium) {
+    configs -= [ "//build/config/compiler:chromium_code" ]
+    configs += [ "//build/config/compiler:no_chromium_code" ]
+  }
+}
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
new file mode 100644
index 0000000..138dfde
--- /dev/null
+++ b/src/tint/CMakeLists.txt
@@ -0,0 +1,1239 @@
+# Copyright 2020 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.
+
+function(tint_spvtools_compile_options TARGET)
+  # We'll use the optimizer for its nice SPIR-V in-memory representation
+  target_link_libraries(${TARGET} SPIRV-Tools-opt SPIRV-Tools)
+
+  # We'll be cheating: using internal interfaces to the SPIRV-Tools
+  # optimizer.
+  target_include_directories(${TARGET} PRIVATE
+    ${spirv-tools_SOURCE_DIR}
+    ${spirv-tools_BINARY_DIR}
+  )
+
+  if (${CMAKE_CXX_COMPILER_ID} MATCHES Clang)
+    # The SPIRV-Tools code is conditioned against C++ and an older version of Clang.
+    # Suppress warnings triggered in our current compilation environment.
+    # TODO(dneto): Fix the issues upstream.
+    target_compile_options(${TARGET} PRIVATE
+      -Wno-newline-eof
+      -Wno-sign-conversion
+      -Wno-old-style-cast
+      -Wno-weak-vtables
+    )
+  endif()
+endfunction()
+
+## Tint diagnostic utilities. Used by libtint and tint_utils_io.
+add_library(tint_diagnostic_utils
+  debug.cc
+  debug.h
+  source.h
+  source.cc
+  diagnostic/diagnostic.cc
+  diagnostic/diagnostic.h
+  diagnostic/formatter.cc
+  diagnostic/formatter.h
+  diagnostic/printer.cc
+  diagnostic/printer.h
+  utils/debugger.cc
+  utils/debugger.h
+)
+tint_default_compile_options(tint_diagnostic_utils)
+
+if (TINT_ENABLE_BREAK_IN_DEBUGGER)
+  set_source_files_properties(utils/debugger.cc
+    PROPERTIES COMPILE_DEFINITIONS "TINT_ENABLE_BREAK_IN_DEBUGGER=1" )
+endif()
+
+set(TINT_LIB_SRCS
+  ../../include/tint/tint.h
+  ast/access.cc
+  ast/access.h
+  ast/attribute.cc
+  ast/attribute.h
+  ast/alias.cc
+  ast/alias.h
+  ast/index_accessor_expression.cc
+  ast/index_accessor_expression.h
+  ast/array.cc
+  ast/array.h
+  ast/assignment_statement.cc
+  ast/assignment_statement.h
+  ast/atomic.cc
+  ast/atomic.h
+  ast/binary_expression.cc
+  ast/binary_expression.h
+  ast/binding_attribute.cc
+  ast/binding_attribute.h
+  ast/bitcast_expression.cc
+  ast/bitcast_expression.h
+  ast/block_statement.cc
+  ast/block_statement.h
+  ast/bool_literal_expression.cc
+  ast/bool_literal_expression.h
+  ast/bool.cc
+  ast/bool.h
+  ast/break_statement.cc
+  ast/break_statement.h
+  ast/builtin_attribute.cc
+  ast/builtin_attribute.h
+  ast/builtin.cc
+  ast/builtin.h
+  ast/call_expression.cc
+  ast/call_expression.h
+  ast/call_statement.cc
+  ast/call_statement.h
+  ast/case_statement.cc
+  ast/case_statement.h
+  ast/compound_assignment_statement.cc
+  ast/compound_assignment_statement.h
+  ast/continue_statement.cc
+  ast/continue_statement.h
+  ast/depth_multisampled_texture.cc
+  ast/depth_multisampled_texture.h
+  ast/disable_validation_attribute.cc
+  ast/disable_validation_attribute.h
+  ast/depth_texture.cc
+  ast/depth_texture.h
+  ast/discard_statement.cc
+  ast/discard_statement.h
+  ast/else_statement.cc
+  ast/else_statement.h
+  ast/expression.cc
+  ast/expression.h
+  ast/external_texture.cc
+  ast/external_texture.h
+  ast/f32.cc
+  ast/f32.h
+  ast/fallthrough_statement.cc
+  ast/fallthrough_statement.h
+  ast/float_literal_expression.cc
+  ast/float_literal_expression.h
+  ast/for_loop_statement.cc
+  ast/for_loop_statement.h
+  ast/function.cc
+  ast/function.h
+  ast/group_attribute.cc
+  ast/group_attribute.h
+  ast/i32.cc
+  ast/i32.h
+  ast/id_attribute.cc
+  ast/id_attribute.h
+  ast/identifier_expression.cc
+  ast/identifier_expression.h
+  ast/if_statement.cc
+  ast/if_statement.h
+  ast/int_literal_expression.cc
+  ast/int_literal_expression.h
+  ast/internal_attribute.cc
+  ast/internal_attribute.h
+  ast/interpolate_attribute.cc
+  ast/interpolate_attribute.h
+  ast/invariant_attribute.cc
+  ast/invariant_attribute.h
+  ast/literal_expression.cc
+  ast/literal_expression.h
+  ast/location_attribute.cc
+  ast/location_attribute.h
+  ast/loop_statement.cc
+  ast/loop_statement.h
+  ast/matrix.cc
+  ast/matrix.h
+  ast/member_accessor_expression.cc
+  ast/member_accessor_expression.h
+  ast/module.cc
+  ast/module.h
+  ast/multisampled_texture.cc
+  ast/multisampled_texture.h
+  ast/node.cc
+  ast/node.h
+  ast/phony_expression.cc
+  ast/phony_expression.h
+  ast/pipeline_stage.cc
+  ast/pipeline_stage.h
+  ast/pointer.cc
+  ast/pointer.h
+  ast/return_statement.cc
+  ast/return_statement.h
+  ast/sampled_texture.cc
+  ast/sampled_texture.h
+  ast/sampler.cc
+  ast/sampler.h
+  ast/sint_literal_expression.cc
+  ast/sint_literal_expression.h
+  ast/stage_attribute.cc
+  ast/stage_attribute.h
+  ast/statement.cc
+  ast/statement.h
+  ast/storage_class.cc
+  ast/storage_class.h
+  ast/storage_texture.cc
+  ast/storage_texture.h
+  ast/stride_attribute.cc
+  ast/stride_attribute.h
+  ast/struct_member_align_attribute.cc
+  ast/struct_member_align_attribute.h
+  ast/struct_member_offset_attribute.cc
+  ast/struct_member_offset_attribute.h
+  ast/struct_member_size_attribute.cc
+  ast/struct_member_size_attribute.h
+  ast/struct_member.cc
+  ast/struct_member.h
+  ast/struct.cc
+  ast/struct.h
+  ast/switch_statement.cc
+  ast/switch_statement.h
+  ast/texture.cc
+  ast/texture.h
+  ast/traverse_expressions.h
+  ast/type_name.cc
+  ast/type_name.h
+  ast/ast_type.cc  # TODO(bclayton) - rename to type.cc
+  ast/type.h
+  ast/type_decl.cc
+  ast/type_decl.h
+  ast/type_name.cc
+  ast/type_name.h
+  ast/u32.cc
+  ast/u32.h
+  ast/uint_literal_expression.cc
+  ast/uint_literal_expression.h
+  ast/unary_op_expression.cc
+  ast/unary_op_expression.h
+  ast/unary_op.cc
+  ast/unary_op.h
+  ast/variable_decl_statement.cc
+  ast/variable_decl_statement.h
+  ast/variable.cc
+  ast/variable.h
+  ast/vector.cc
+  ast/vector.h
+  ast/void.cc
+  ast/void.h
+  ast/workgroup_attribute.cc
+  ast/workgroup_attribute.h
+  builtin_table.cc
+  builtin_table.h
+  builtin_table.inl
+  castable.cc
+  castable.h
+  clone_context.cc
+  clone_context.h
+  demangler.cc
+  demangler.h
+  inspector/entry_point.cc
+  inspector/entry_point.h
+  inspector/inspector.cc
+  inspector/inspector.h
+  inspector/resource_binding.cc
+  inspector/resource_binding.h
+  inspector/scalar.cc
+  inspector/scalar.h
+  program_builder.cc
+  program_builder.h
+  program_id.cc
+  program_id.h
+  program.cc
+  program.h
+  reader/reader.cc
+  reader/reader.h
+  resolver/dependency_graph.cc
+  resolver/dependency_graph.h
+  resolver/resolver.cc
+  resolver/resolver_constants.cc
+  resolver/resolver_validation.cc
+  resolver/resolver.h
+  scope_stack.h
+  sem/array.cc
+  sem/array.h
+  sem/atomic_type.cc
+  sem/atomic_type.h
+  sem/behavior.cc
+  sem/behavior.h
+  sem/binding_point.h
+  sem/block_statement.cc
+  sem/block_statement.h
+  sem/builtin_type.cc
+  sem/builtin_type.h
+  sem/builtin.cc
+  sem/builtin.h
+  sem/call_target.cc
+  sem/call_target.h
+  sem/call.cc
+  sem/call.h
+  sem/constant.cc
+  sem/constant.h
+  sem/depth_multisampled_texture_type.cc
+  sem/depth_multisampled_texture_type.h
+  sem/expression.cc
+  sem/expression.h
+  sem/function.cc
+  sem/info.cc
+  sem/info.h
+  sem/member_accessor_expression.cc
+  sem/parameter_usage.cc
+  sem/parameter_usage.h
+  sem/pipeline_stage_set.h
+  sem/node.cc
+  sem/node.h
+  sem/module.cc
+  sem/module.h
+  sem/sampler_texture_pair.h
+  sem/statement.cc
+  sem/struct.cc
+  sem/type_mappings.h
+  sem/variable.cc
+  symbol_table.cc
+  symbol_table.h
+  symbol.cc
+  symbol.h
+  text/unicode.cc
+  text/unicode.h
+  traits.h
+  transform/add_empty_entry_point.cc
+  transform/add_empty_entry_point.h
+  transform/add_spirv_block_attribute.cc
+  transform/add_spirv_block_attribute.h
+  transform/array_length_from_uniform.cc
+  transform/array_length_from_uniform.h
+  transform/binding_remapper.cc
+  transform/binding_remapper.h
+  transform/builtin_polyfill.cc
+  transform/builtin_polyfill.h
+  transform/calculate_array_length.cc
+  transform/calculate_array_length.h
+  transform/combine_samplers.cc
+  transform/combine_samplers.h
+  transform/canonicalize_entry_point_io.cc
+  transform/canonicalize_entry_point_io.h
+  transform/decompose_memory_access.cc
+  transform/decompose_memory_access.h
+  transform/decompose_strided_array.cc
+  transform/decompose_strided_array.h
+  transform/decompose_strided_matrix.cc
+  transform/decompose_strided_matrix.h
+  transform/first_index_offset.cc
+  transform/first_index_offset.h
+  transform/fold_constants.cc
+  transform/fold_constants.h
+  transform/fold_trivial_single_use_lets.cc
+  transform/fold_trivial_single_use_lets.h
+  transform/localize_struct_array_assignment.cc
+  transform/localize_struct_array_assignment.h
+  transform/for_loop_to_loop.cc
+  transform/for_loop_to_loop.h
+  transform/expand_compound_assignment.cc
+  transform/expand_compound_assignment.h
+  transform/glsl.cc
+  transform/glsl.h
+  transform/loop_to_for_loop.cc
+  transform/loop_to_for_loop.h
+  transform/manager.cc
+  transform/manager.h
+  transform/module_scope_var_to_entry_point_param.cc
+  transform/module_scope_var_to_entry_point_param.h
+  transform/multiplanar_external_texture.cc
+  transform/multiplanar_external_texture.h
+  transform/num_workgroups_from_uniform.cc
+  transform/num_workgroups_from_uniform.h
+  transform/promote_initializers_to_const_var.cc
+  transform/promote_initializers_to_const_var.h
+  transform/promote_side_effects_to_decl.cc
+  transform/promote_side_effects_to_decl.h
+  transform/remove_phonies.cc
+  transform/remove_phonies.h
+  transform/remove_continue_in_switch.cc
+  transform/remove_continue_in_switch.h
+  transform/remove_unreachable_statements.cc
+  transform/remove_unreachable_statements.h
+  transform/renamer.cc
+  transform/renamer.h
+  transform/robustness.cc
+  transform/robustness.h
+  transform/simplify_pointers.cc
+  transform/simplify_pointers.h
+  transform/single_entry_point.cc
+  transform/single_entry_point.h
+  transform/transform.cc
+  transform/transform.h
+  transform/unshadow.cc
+  transform/unshadow.h
+  transform/unwind_discard_functions.cc
+  transform/unwind_discard_functions.h
+  transform/vectorize_scalar_matrix_constructors.cc
+  transform/vectorize_scalar_matrix_constructors.h
+  transform/var_for_dynamic_index.cc
+  transform/var_for_dynamic_index.h
+  transform/vertex_pulling.cc
+  transform/vertex_pulling.h
+  transform/wrap_arrays_in_structs.cc
+  transform/wrap_arrays_in_structs.h
+  transform/zero_init_workgroup_memory.cc
+  transform/zero_init_workgroup_memory.h
+  transform/utils/get_insertion_point.cc
+  transform/utils/get_insertion_point.h
+  transform/utils/hoist_to_decl_before.cc
+  transform/utils/hoist_to_decl_before.h
+  sem/bool_type.cc
+  sem/bool_type.h
+  sem/depth_texture_type.cc
+  sem/depth_texture_type.h
+  sem/external_texture_type.cc
+  sem/external_texture_type.h
+  sem/f32_type.cc
+  sem/f32_type.h
+  sem/for_loop_statement.cc
+  sem/for_loop_statement.h
+  sem/i32_type.cc
+  sem/i32_type.h
+  sem/if_statement.cc
+  sem/if_statement.h
+  sem/loop_statement.cc
+  sem/loop_statement.h
+  sem/matrix_type.cc
+  sem/matrix_type.h
+  sem/multisampled_texture_type.cc
+  sem/multisampled_texture_type.h
+  sem/pointer_type.cc
+  sem/pointer_type.h
+  sem/reference_type.cc
+  sem/reference_type.h
+  sem/sampled_texture_type.cc
+  sem/sampled_texture_type.h
+  sem/sampler_type.cc
+  sem/sampler_type.h
+  sem/storage_texture_type.cc
+  sem/storage_texture_type.h
+  sem/switch_statement.cc
+  sem/switch_statement.h
+  sem/texture_type.cc
+  sem/texture_type.h
+  sem/type_constructor.cc
+  sem/type_constructor.h
+  sem/type_conversion.cc
+  sem/type_conversion.h
+  sem/type.cc
+  sem/type.h
+  sem/type_manager.cc
+  sem/type_manager.h
+  sem/u32_type.cc
+  sem/u32_type.h
+  sem/vector_type.cc
+  sem/vector_type.h
+  sem/void_type.cc
+  sem/void_type.h
+  utils/block_allocator.h
+  utils/crc32.h
+  utils/enum_set.h
+  utils/hash.h
+  utils/map.h
+  utils/math.h
+  utils/scoped_assignment.h
+  utils/string.h
+  utils/unique_allocator.h
+  utils/unique_vector.h
+  writer/append_vector.cc
+  writer/append_vector.h
+  writer/array_length_from_uniform_options.cc
+  writer/array_length_from_uniform_options.h
+  writer/float_to_string.cc
+  writer/float_to_string.h
+  writer/generate_external_texture_bindings.cc
+  writer/generate_external_texture_bindings.h
+  writer/text_generator.cc
+  writer/text_generator.h
+  writer/text.cc
+  writer/text.h
+  writer/writer.cc
+  writer/writer.h
+)
+
+if(UNIX)
+  list(APPEND TINT_LIB_SRCS diagnostic/printer_linux.cc)
+elseif(WIN32)
+  list(APPEND TINT_LIB_SRCS diagnostic/printer_windows.cc)
+else()
+  list(APPEND TINT_LIB_SRCS diagnostic/printer_other.cc)
+endif()
+
+if(${TINT_BUILD_SPV_READER})
+  list(APPEND TINT_LIB_SRCS
+    reader/spirv/construct.h
+    reader/spirv/construct.cc
+    reader/spirv/entry_point_info.h
+    reader/spirv/entry_point_info.cc
+    reader/spirv/enum_converter.h
+    reader/spirv/enum_converter.cc
+    reader/spirv/fail_stream.h
+    reader/spirv/function.cc
+    reader/spirv/function.h
+    reader/spirv/namer.cc
+    reader/spirv/namer.h
+    reader/spirv/parser_type.cc
+    reader/spirv/parser_type.h
+    reader/spirv/parser.cc
+    reader/spirv/parser.h
+    reader/spirv/parser_impl.cc
+    reader/spirv/parser_impl.h
+    reader/spirv/usage.cc
+    reader/spirv/usage.h
+  )
+endif()
+
+if(${TINT_BUILD_WGSL_READER})
+  list(APPEND TINT_LIB_SRCS
+    reader/wgsl/lexer.cc
+    reader/wgsl/lexer.h
+    reader/wgsl/parser.cc
+    reader/wgsl/parser.h
+    reader/wgsl/parser_impl.cc
+    reader/wgsl/parser_impl.h
+    reader/wgsl/parser_impl_detail.h
+    reader/wgsl/token.cc
+    reader/wgsl/token.h
+  )
+endif()
+
+if(${TINT_BUILD_SPV_WRITER})
+  list(APPEND TINT_LIB_SRCS
+    writer/spirv/binary_writer.cc
+    writer/spirv/binary_writer.h
+    writer/spirv/builder.cc
+    writer/spirv/builder.h
+    writer/spirv/function.cc
+    writer/spirv/function.h
+    writer/spirv/generator.cc
+    writer/spirv/generator.h
+    writer/spirv/instruction.cc
+    writer/spirv/instruction.h
+    writer/spirv/operand.cc
+    writer/spirv/operand.h
+    writer/spirv/scalar_constant.h
+  )
+endif()
+
+if(${TINT_BUILD_WGSL_WRITER})
+  list(APPEND TINT_LIB_SRCS
+    writer/wgsl/generator.cc
+    writer/wgsl/generator.h
+    writer/wgsl/generator_impl.cc
+    writer/wgsl/generator_impl.h
+  )
+endif()
+
+if(${TINT_BUILD_MSL_WRITER})
+  list(APPEND TINT_LIB_SRCS
+    writer/msl/generator.cc
+    writer/msl/generator.h
+    writer/msl/generator_impl.cc
+    writer/msl/generator_impl.h
+  )
+endif()
+
+if(${TINT_BUILD_GLSL_WRITER})
+  list(APPEND TINT_LIB_SRCS
+    writer/glsl/generator.cc
+    writer/glsl/generator.h
+    writer/glsl/generator_impl.cc
+    writer/glsl/generator_impl.h
+    writer/glsl/version.h
+  )
+endif()
+
+if(${TINT_BUILD_HLSL_WRITER})
+  list(APPEND TINT_LIB_SRCS
+    writer/hlsl/generator.cc
+    writer/hlsl/generator.h
+    writer/hlsl/generator_impl.cc
+    writer/hlsl/generator_impl.h
+  )
+endif()
+
+if(MSVC)
+  list(APPEND TINT_LIB_SRCS
+    tint.natvis
+  )
+endif()
+
+## Tint IO utilities. Used by tint_val.
+add_library(tint_utils_io
+  utils/io/command_${TINT_OS_CC_SUFFIX}.cc
+  utils/io/command.h
+  utils/io/tmpfile_${TINT_OS_CC_SUFFIX}.cc
+  utils/io/tmpfile.h
+)
+tint_default_compile_options(tint_utils_io)
+target_link_libraries(tint_utils_io tint_diagnostic_utils)
+
+## Tint validation utilities. Used by tests and the tint executable.
+add_library(tint_val
+  val/hlsl.cc
+  val/msl.cc
+  val/val.h
+)
+
+# If we're building on mac / ios and we have CoreGraphics, then we can use the
+# metal API to validate our shaders. This is roughly 4x faster than invoking
+# the metal shader compiler executable.
+if(APPLE)
+  find_library(LIB_CORE_GRAPHICS CoreGraphics)
+  if(LIB_CORE_GRAPHICS)
+    target_sources(tint_val PRIVATE "val/msl_metal.mm")
+    target_compile_definitions(tint_val PUBLIC "-DTINT_ENABLE_MSL_VALIDATION_USING_METAL_API=1")
+    target_compile_options(tint_val PRIVATE "-fmodules" "-fcxx-modules")
+    target_link_options(tint_val PUBLIC "-framework" "CoreGraphics")
+  endif()
+endif()
+
+tint_default_compile_options(tint_val)
+target_link_libraries(tint_val tint_utils_io)
+
+## Tint library
+add_library(libtint ${TINT_LIB_SRCS})
+tint_default_compile_options(libtint)
+target_link_libraries(libtint tint_diagnostic_utils)
+if (${COMPILER_IS_LIKE_GNU})
+  target_compile_options(libtint PRIVATE -fvisibility=hidden)
+endif()
+if (${TINT_SYMBOL_STORE_DEBUG_NAME})
+    target_compile_definitions(libtint PUBLIC "TINT_SYMBOL_STORE_DEBUG_NAME=1")
+endif()
+set_target_properties(libtint PROPERTIES OUTPUT_NAME "tint")
+
+if (${TINT_BUILD_FUZZERS})
+  # Tint library with fuzzer instrumentation
+  add_library(libtint-fuzz ${TINT_LIB_SRCS})
+  tint_default_compile_options(libtint-fuzz)
+  target_link_libraries(libtint-fuzz tint_diagnostic_utils)
+  if (${COMPILER_IS_LIKE_GNU})
+    target_compile_options(libtint-fuzz PRIVATE -fvisibility=hidden)
+  endif()
+
+  if (NOT ${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS} STREQUAL "")
+    # This is set when the fuzzers are being built by OSS-Fuzz. In this case the
+    # variable provides the necessary linker flags, and OSS-Fuzz will take care
+    # of passing suitable compiler flags.
+    target_link_options(libtint-fuzz PUBLIC ${TINT_LIB_FUZZING_ENGINE_LINK_OPTIONS})
+  else()
+    # When the fuzzers are being built outside of OSS-Fuzz, specific libFuzzer
+    # arguments to enable fuzzing are used.
+    target_compile_options(libtint-fuzz PUBLIC -fsanitize=fuzzer -fsanitize-coverage=trace-cmp)
+    target_link_options(libtint-fuzz PUBLIC -fsanitize=fuzzer -fsanitize-coverage=trace-cmp)
+  endif()
+endif()
+
+if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
+  tint_spvtools_compile_options(libtint)
+  if (${TINT_BUILD_FUZZERS})
+    tint_spvtools_compile_options(libtint-fuzz)
+  endif()
+endif()
+
+################################################################################
+# Tests
+################################################################################
+if(TINT_BUILD_TESTS)
+  set(TINT_TEST_SRCS
+    ast/alias_test.cc
+    ast/array_test.cc
+    ast/assignment_statement_test.cc
+    ast/atomic_test.cc
+    ast/binary_expression_test.cc
+    ast/binding_attribute_test.cc
+    ast/bitcast_expression_test.cc
+    ast/block_statement_test.cc
+    ast/bool_literal_expression_test.cc
+    ast/bool_test.cc
+    ast/break_statement_test.cc
+    ast/builtin_attribute_test.cc
+    ast/builtin_texture_helper_test.cc
+    ast/builtin_texture_helper_test.h
+    ast/call_expression_test.cc
+    ast/call_statement_test.cc
+    ast/case_statement_test.cc
+    ast/compound_assignment_statement_test.cc
+    ast/continue_statement_test.cc
+    ast/depth_multisampled_texture_test.cc
+    ast/depth_texture_test.cc
+    ast/discard_statement_test.cc
+    ast/else_statement_test.cc
+    ast/external_texture_test.cc
+    ast/f32_test.cc
+    ast/fallthrough_statement_test.cc
+    ast/float_literal_expression_test.cc
+    ast/for_loop_statement_test.cc
+    ast/function_test.cc
+    ast/group_attribute_test.cc
+    ast/i32_test.cc
+    ast/id_attribute_test.cc
+    ast/identifier_expression_test.cc
+    ast/if_statement_test.cc
+    ast/index_accessor_expression_test.cc
+    ast/int_literal_expression_test.cc
+    ast/interpolate_attribute_test.cc
+    ast/invariant_attribute_test.cc
+    ast/location_attribute_test.cc
+    ast/loop_statement_test.cc
+    ast/matrix_test.cc
+    ast/member_accessor_expression_test.cc
+    ast/module_clone_test.cc
+    ast/module_test.cc
+    ast/multisampled_texture_test.cc
+    ast/phony_expression_test.cc
+    ast/pointer_test.cc
+    ast/return_statement_test.cc
+    ast/sampled_texture_test.cc
+    ast/sampler_test.cc
+    ast/sint_literal_expression_test.cc
+    ast/stage_attribute_test.cc
+    ast/storage_texture_test.cc
+    ast/stride_attribute_test.cc
+    ast/struct_member_align_attribute_test.cc
+    ast/struct_member_offset_attribute_test.cc
+    ast/struct_member_size_attribute_test.cc
+    ast/struct_member_test.cc
+    ast/struct_test.cc
+    ast/switch_statement_test.cc
+    ast/test_helper.h
+    ast/texture_test.cc
+    ast/traverse_expressions_test.cc
+    ast/u32_test.cc
+    ast/uint_literal_expression_test.cc
+    ast/unary_op_expression_test.cc
+    ast/variable_decl_statement_test.cc
+    ast/variable_test.cc
+    ast/vector_test.cc
+    ast/workgroup_attribute_test.cc
+    builtin_table_test.cc
+    castable_test.cc
+    clone_context_test.cc
+    debug_test.cc
+    demangler_test.cc
+    diagnostic/diagnostic_test.cc
+    diagnostic/formatter_test.cc
+    diagnostic/printer_test.cc
+    program_test.cc
+    resolver/array_accessor_test.cc
+    resolver/assignment_validation_test.cc
+    resolver/atomics_test.cc
+    resolver/atomics_validation_test.cc
+    resolver/bitcast_validation_test.cc
+    resolver/builtins_validation_test.cc
+    resolver/builtin_test.cc
+    resolver/builtin_validation_test.cc
+    resolver/call_test.cc
+    resolver/call_validation_test.cc
+    resolver/compound_assignment_validation_test.cc
+    resolver/compound_statement_test.cc
+    resolver/control_block_validation_test.cc
+    resolver/attribute_validation_test.cc
+    resolver/dependency_graph_test.cc
+    resolver/entry_point_validation_test.cc
+    resolver/function_validation_test.cc
+    resolver/host_shareable_validation_test.cc
+    resolver/inferred_type_test.cc
+    resolver/is_host_shareable_test.cc
+    resolver/is_storeable_test.cc
+    resolver/pipeline_overridable_constant_test.cc
+    resolver/ptr_ref_test.cc
+    resolver/ptr_ref_validation_test.cc
+    resolver/resolver_behavior_test.cc
+    resolver/resolver_constants_test.cc
+    resolver/resolver_test_helper.cc
+    resolver/resolver_test_helper.h
+    resolver/resolver_test.cc
+    resolver/side_effects_test.cc
+    resolver/storage_class_layout_validation_test.cc
+    resolver/storage_class_validation_test.cc
+    resolver/struct_layout_test.cc
+    resolver/struct_pipeline_stage_use_test.cc
+    resolver/struct_storage_class_use_test.cc
+    resolver/type_constructor_validation_test.cc
+    resolver/type_validation_test.cc
+    resolver/validation_test.cc
+    resolver/var_let_test.cc
+    resolver/var_let_validation_test.cc
+    scope_stack_test.cc
+    sem/atomic_type_test.cc
+    sem/bool_type_test.cc
+    sem/builtin_test.cc
+    sem/depth_multisampled_texture_type_test.cc
+    sem/depth_texture_type_test.cc
+    sem/external_texture_type_test.cc
+    sem/f32_type_test.cc
+    sem/i32_type_test.cc
+    sem/matrix_type_test.cc
+    sem/multisampled_texture_type_test.cc
+    sem/pointer_type_test.cc
+    sem/reference_type_test.cc
+    sem/sampled_texture_type_test.cc
+    sem/sampler_type_test.cc
+    sem/sem_array_test.cc
+    sem/sem_struct_test.cc
+    sem/storage_texture_type_test.cc
+    sem/texture_type_test.cc
+    sem/type_manager_test.cc
+    sem/u32_type_test.cc
+    sem/vector_type_test.cc
+    source_test.cc
+    symbol_table_test.cc
+    symbol_test.cc
+    test_main.cc
+    text/unicode_test.cc
+    traits_test.cc
+    transform/transform_test.cc
+    utils/block_allocator_test.cc
+    utils/crc32_test.cc
+    utils/defer_test.cc
+    utils/enum_set_test.cc
+    utils/hash_test.cc
+    utils/io/command_test.cc
+    utils/io/tmpfile_test.cc
+    utils/map_test.cc
+    utils/math_test.cc
+    utils/reverse_test.cc
+    utils/scoped_assignment_test.cc
+    utils/string_test.cc
+    utils/transform_test.cc
+    utils/unique_allocator_test.cc
+    utils/unique_vector_test.cc
+    writer/append_vector_test.cc
+    writer/float_to_string_test.cc
+    writer/generate_external_texture_bindings_test.cc
+    writer/text_generator_test.cc
+  )
+
+  # Inspector tests depend on WGSL reader
+  if(${TINT_BUILD_WGSL_READER})
+    list(APPEND TINT_TEST_SRCS
+      inspector/inspector_test.cc
+      inspector/test_inspector_builder.cc
+      inspector/test_inspector_builder.h
+      inspector/test_inspector_runner.cc
+      inspector/test_inspector_runner.h
+    )
+  endif()
+
+  if(${TINT_BUILD_SPV_READER} AND ${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      reader/spirv/enum_converter_test.cc
+      reader/spirv/fail_stream_test.cc
+      reader/spirv/function_arithmetic_test.cc
+      reader/spirv/function_bit_test.cc
+      reader/spirv/function_cfg_test.cc
+      reader/spirv/function_call_test.cc
+      reader/spirv/function_composite_test.cc
+      reader/spirv/function_conversion_test.cc
+      reader/spirv/function_decl_test.cc
+      reader/spirv/function_glsl_std_450_test.cc
+      reader/spirv/function_logical_test.cc
+      reader/spirv/function_memory_test.cc
+      reader/spirv/function_misc_test.cc
+      reader/spirv/function_var_test.cc
+      reader/spirv/namer_test.cc
+      reader/spirv/parser_impl_barrier_test.cc
+      reader/spirv/parser_impl_convert_member_decoration_test.cc
+      reader/spirv/parser_impl_convert_type_test.cc
+      reader/spirv/parser_impl_function_decl_test.cc
+      reader/spirv/parser_impl_get_decorations_test.cc
+      reader/spirv/parser_impl_handle_test.cc
+      reader/spirv/parser_impl_import_test.cc
+      reader/spirv/parser_impl_module_var_test.cc
+      reader/spirv/parser_impl_named_types_test.cc
+      reader/spirv/parser_impl_test_helper.cc
+      reader/spirv/parser_impl_test_helper.h
+      reader/spirv/parser_impl_test.cc
+      reader/spirv/parser_impl_user_name_test.cc
+      reader/spirv/parser_type_test.cc
+      reader/spirv/parser_test.cc
+      reader/spirv/spirv_tools_helpers_test.cc
+      reader/spirv/spirv_tools_helpers_test.h
+      reader/spirv/usage_test.cc
+    )
+  endif()
+
+  if(${TINT_BUILD_WGSL_READER})
+    list(APPEND TINT_TEST_SRCS
+      reader/wgsl/lexer_test.cc
+      reader/wgsl/parser_test.cc
+      reader/wgsl/parser_impl_additive_expression_test.cc
+      reader/wgsl/parser_impl_and_expression_test.cc
+      reader/wgsl/parser_impl_argument_expression_list_test.cc
+      reader/wgsl/parser_impl_assignment_stmt_test.cc
+      reader/wgsl/parser_impl_body_stmt_test.cc
+      reader/wgsl/parser_impl_break_stmt_test.cc
+      reader/wgsl/parser_impl_bug_cases_test.cc
+      reader/wgsl/parser_impl_call_stmt_test.cc
+      reader/wgsl/parser_impl_case_body_test.cc
+      reader/wgsl/parser_impl_const_expr_test.cc
+      reader/wgsl/parser_impl_const_literal_test.cc
+      reader/wgsl/parser_impl_continue_stmt_test.cc
+      reader/wgsl/parser_impl_continuing_stmt_test.cc
+      reader/wgsl/parser_impl_depth_texture_type_test.cc
+      reader/wgsl/parser_impl_external_texture_type_test.cc
+      reader/wgsl/parser_impl_elseif_stmt_test.cc
+      reader/wgsl/parser_impl_equality_expression_test.cc
+      reader/wgsl/parser_impl_error_msg_test.cc
+      reader/wgsl/parser_impl_error_resync_test.cc
+      reader/wgsl/parser_impl_exclusive_or_expression_test.cc
+      reader/wgsl/parser_impl_for_stmt_test.cc
+      reader/wgsl/parser_impl_function_decl_test.cc
+      reader/wgsl/parser_impl_function_attribute_list_test.cc
+      reader/wgsl/parser_impl_function_attribute_test.cc
+      reader/wgsl/parser_impl_function_header_test.cc
+      reader/wgsl/parser_impl_global_constant_decl_test.cc
+      reader/wgsl/parser_impl_global_decl_test.cc
+      reader/wgsl/parser_impl_global_variable_decl_test.cc
+      reader/wgsl/parser_impl_if_stmt_test.cc
+      reader/wgsl/parser_impl_inclusive_or_expression_test.cc
+      reader/wgsl/parser_impl_logical_and_expression_test.cc
+      reader/wgsl/parser_impl_logical_or_expression_test.cc
+      reader/wgsl/parser_impl_loop_stmt_test.cc
+      reader/wgsl/parser_impl_multiplicative_expression_test.cc
+      reader/wgsl/parser_impl_param_list_test.cc
+      reader/wgsl/parser_impl_paren_rhs_stmt_test.cc
+      reader/wgsl/parser_impl_pipeline_stage_test.cc
+      reader/wgsl/parser_impl_primary_expression_test.cc
+      reader/wgsl/parser_impl_relational_expression_test.cc
+      reader/wgsl/parser_impl_reserved_keyword_test.cc
+      reader/wgsl/parser_impl_sampled_texture_type_test.cc
+      reader/wgsl/parser_impl_sampler_type_test.cc
+      reader/wgsl/parser_impl_shift_expression_test.cc
+      reader/wgsl/parser_impl_singular_expression_test.cc
+      reader/wgsl/parser_impl_statement_test.cc
+      reader/wgsl/parser_impl_statements_test.cc
+      reader/wgsl/parser_impl_storage_class_test.cc
+      reader/wgsl/parser_impl_storage_texture_type_test.cc
+      reader/wgsl/parser_impl_struct_body_decl_test.cc
+      reader/wgsl/parser_impl_struct_decl_test.cc
+      reader/wgsl/parser_impl_struct_attribute_decl_test.cc
+      reader/wgsl/parser_impl_struct_member_attribute_decl_test.cc
+      reader/wgsl/parser_impl_struct_member_attribute_test.cc
+      reader/wgsl/parser_impl_struct_member_test.cc
+      reader/wgsl/parser_impl_switch_body_test.cc
+      reader/wgsl/parser_impl_switch_stmt_test.cc
+      reader/wgsl/parser_impl_test.cc
+      reader/wgsl/parser_impl_test_helper.cc
+      reader/wgsl/parser_impl_test_helper.h
+      reader/wgsl/parser_impl_texel_format_test.cc
+      reader/wgsl/parser_impl_texture_sampler_types_test.cc
+      reader/wgsl/parser_impl_type_alias_test.cc
+      reader/wgsl/parser_impl_type_decl_test.cc
+      reader/wgsl/parser_impl_unary_expression_test.cc
+      reader/wgsl/parser_impl_variable_decl_test.cc
+      reader/wgsl/parser_impl_variable_attribute_list_test.cc
+      reader/wgsl/parser_impl_variable_attribute_test.cc
+      reader/wgsl/parser_impl_variable_ident_decl_test.cc
+      reader/wgsl/parser_impl_variable_stmt_test.cc
+      reader/wgsl/parser_impl_variable_qualifier_test.cc
+      reader/wgsl/token_test.cc
+    )
+  endif()
+
+  if(${TINT_BUILD_SPV_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      writer/spirv/binary_writer_test.cc
+      writer/spirv/builder_accessor_expression_test.cc
+      writer/spirv/builder_assign_test.cc
+      writer/spirv/builder_binary_expression_test.cc
+      writer/spirv/builder_bitcast_expression_test.cc
+      writer/spirv/builder_block_test.cc
+      writer/spirv/builder_builtin_test.cc
+      writer/spirv/builder_builtin_texture_test.cc
+      writer/spirv/builder_call_test.cc
+      writer/spirv/builder_constructor_expression_test.cc
+      writer/spirv/builder_discard_test.cc
+      writer/spirv/builder_entry_point_test.cc
+      writer/spirv/builder_format_conversion_test.cc
+      writer/spirv/builder_function_attribute_test.cc
+      writer/spirv/builder_function_test.cc
+      writer/spirv/builder_function_variable_test.cc
+      writer/spirv/builder_global_variable_test.cc
+      writer/spirv/builder_ident_expression_test.cc
+      writer/spirv/builder_if_test.cc
+      writer/spirv/builder_literal_test.cc
+      writer/spirv/builder_loop_test.cc
+      writer/spirv/builder_return_test.cc
+      writer/spirv/builder_switch_test.cc
+      writer/spirv/builder_test.cc
+      writer/spirv/builder_type_test.cc
+      writer/spirv/builder_unary_op_expression_test.cc
+      writer/spirv/instruction_test.cc
+      writer/spirv/operand_test.cc
+      writer/spirv/scalar_constant_test.cc
+      writer/spirv/spv_dump.cc
+      writer/spirv/spv_dump.h
+      writer/spirv/test_helper.h
+    )
+  endif()
+
+  if(${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      writer/wgsl/generator_impl_test.cc
+      writer/wgsl/generator_impl_alias_type_test.cc
+      writer/wgsl/generator_impl_array_accessor_test.cc
+      writer/wgsl/generator_impl_assign_test.cc
+      writer/wgsl/generator_impl_binary_test.cc
+      writer/wgsl/generator_impl_bitcast_test.cc
+      writer/wgsl/generator_impl_block_test.cc
+      writer/wgsl/generator_impl_break_test.cc
+      writer/wgsl/generator_impl_call_test.cc
+      writer/wgsl/generator_impl_case_test.cc
+      writer/wgsl/generator_impl_cast_test.cc
+      writer/wgsl/generator_impl_constructor_test.cc
+      writer/wgsl/generator_impl_continue_test.cc
+      writer/wgsl/generator_impl_discard_test.cc
+      writer/wgsl/generator_impl_fallthrough_test.cc
+      writer/wgsl/generator_impl_function_test.cc
+      writer/wgsl/generator_impl_global_decl_test.cc
+      writer/wgsl/generator_impl_identifier_test.cc
+      writer/wgsl/generator_impl_if_test.cc
+      writer/wgsl/generator_impl_loop_test.cc
+      writer/wgsl/generator_impl_literal_test.cc
+      writer/wgsl/generator_impl_member_accessor_test.cc
+      writer/wgsl/generator_impl_return_test.cc
+      writer/wgsl/generator_impl_switch_test.cc
+      writer/wgsl/generator_impl_type_test.cc
+      writer/wgsl/generator_impl_unary_op_test.cc
+      writer/wgsl/generator_impl_variable_decl_statement_test.cc
+      writer/wgsl/generator_impl_variable_test.cc
+      writer/wgsl/test_helper.h
+    )
+  endif()
+
+  if(${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      transform/add_empty_entry_point_test.cc
+      transform/add_spirv_block_attribute_test.cc
+      transform/array_length_from_uniform_test.cc
+      transform/binding_remapper_test.cc
+      transform/builtin_polyfill_test.cc
+      transform/calculate_array_length_test.cc
+      transform/canonicalize_entry_point_io_test.cc
+      transform/combine_samplers_test.cc
+      transform/decompose_memory_access_test.cc
+      transform/decompose_strided_array_test.cc
+      transform/decompose_strided_matrix_test.cc
+      transform/first_index_offset_test.cc
+      transform/fold_constants_test.cc
+      transform/fold_trivial_single_use_lets_test.cc
+      transform/for_loop_to_loop_test.cc
+      transform/expand_compound_assignment.cc
+      transform/localize_struct_array_assignment_test.cc
+      transform/loop_to_for_loop_test.cc
+      transform/module_scope_var_to_entry_point_param_test.cc
+      transform/multiplanar_external_texture_test.cc
+      transform/num_workgroups_from_uniform_test.cc
+      transform/promote_initializers_to_const_var_test.cc
+      transform/promote_side_effects_to_decl_test.cc
+      transform/remove_continue_in_switch_test.cc
+      transform/remove_phonies_test.cc
+      transform/remove_unreachable_statements_test.cc
+      transform/renamer_test.cc
+      transform/robustness_test.cc
+      transform/simplify_pointers_test.cc
+      transform/single_entry_point_test.cc
+      transform/test_helper.h
+      transform/unshadow_test.cc
+      transform/unwind_discard_functions_test.cc
+      transform/var_for_dynamic_index_test.cc
+      transform/vectorize_scalar_matrix_constructors_test.cc
+      transform/vertex_pulling_test.cc
+      transform/wrap_arrays_in_structs_test.cc
+      transform/zero_init_workgroup_memory_test.cc
+      transform/utils/get_insertion_point_test.cc
+      transform/utils/hoist_to_decl_before_test.cc
+    )
+  endif()
+
+  if(${TINT_BUILD_MSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      writer/msl/generator_impl_array_accessor_test.cc
+      writer/msl/generator_impl_assign_test.cc
+      writer/msl/generator_impl_binary_test.cc
+      writer/msl/generator_impl_bitcast_test.cc
+      writer/msl/generator_impl_block_test.cc
+      writer/msl/generator_impl_break_test.cc
+      writer/msl/generator_impl_builtin_test.cc
+      writer/msl/generator_impl_builtin_texture_test.cc
+      writer/msl/generator_impl_call_test.cc
+      writer/msl/generator_impl_case_test.cc
+      writer/msl/generator_impl_cast_test.cc
+      writer/msl/generator_impl_constructor_test.cc
+      writer/msl/generator_impl_continue_test.cc
+      writer/msl/generator_impl_discard_test.cc
+      writer/msl/generator_impl_function_test.cc
+      writer/msl/generator_impl_identifier_test.cc
+      writer/msl/generator_impl_if_test.cc
+      writer/msl/generator_impl_import_test.cc
+      writer/msl/generator_impl_loop_test.cc
+      writer/msl/generator_impl_member_accessor_test.cc
+      writer/msl/generator_impl_module_constant_test.cc
+      writer/msl/generator_impl_return_test.cc
+      writer/msl/generator_impl_sanitizer_test.cc
+      writer/msl/generator_impl_switch_test.cc
+      writer/msl/generator_impl_test.cc
+      writer/msl/generator_impl_type_test.cc
+      writer/msl/generator_impl_unary_op_test.cc
+      writer/msl/generator_impl_variable_decl_statement_test.cc
+      writer/msl/test_helper.h
+    )
+  endif()
+
+  if (${TINT_BUILD_GLSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      writer/glsl/generator_impl_array_accessor_test.cc
+      writer/glsl/generator_impl_assign_test.cc
+      writer/glsl/generator_impl_binary_test.cc
+      writer/glsl/generator_impl_bitcast_test.cc
+      writer/glsl/generator_impl_block_test.cc
+      writer/glsl/generator_impl_break_test.cc
+      writer/glsl/generator_impl_builtin_test.cc
+      writer/glsl/generator_impl_builtin_texture_test.cc
+      writer/glsl/generator_impl_call_test.cc
+      writer/glsl/generator_impl_case_test.cc
+      writer/glsl/generator_impl_cast_test.cc
+      writer/glsl/generator_impl_constructor_test.cc
+      writer/glsl/generator_impl_continue_test.cc
+      writer/glsl/generator_impl_discard_test.cc
+      writer/glsl/generator_impl_function_test.cc
+      writer/glsl/generator_impl_identifier_test.cc
+      writer/glsl/generator_impl_if_test.cc
+      writer/glsl/generator_impl_import_test.cc
+      writer/glsl/generator_impl_loop_test.cc
+      writer/glsl/generator_impl_member_accessor_test.cc
+      writer/glsl/generator_impl_module_constant_test.cc
+      writer/glsl/generator_impl_return_test.cc
+      writer/glsl/generator_impl_sanitizer_test.cc
+      writer/glsl/generator_impl_storage_buffer_test.cc
+      writer/glsl/generator_impl_switch_test.cc
+      writer/glsl/generator_impl_test.cc
+      writer/glsl/generator_impl_type_test.cc
+      writer/glsl/generator_impl_unary_op_test.cc
+      writer/glsl/generator_impl_uniform_buffer_test.cc
+      writer/glsl/generator_impl_variable_decl_statement_test.cc
+      writer/glsl/generator_impl_workgroup_var_test.cc
+      writer/glsl/test_helper.h
+    )
+  endif()
+
+  if (${TINT_BUILD_HLSL_WRITER})
+    list(APPEND TINT_TEST_SRCS
+      writer/hlsl/generator_impl_array_accessor_test.cc
+      writer/hlsl/generator_impl_assign_test.cc
+      writer/hlsl/generator_impl_binary_test.cc
+      writer/hlsl/generator_impl_bitcast_test.cc
+      writer/hlsl/generator_impl_block_test.cc
+      writer/hlsl/generator_impl_break_test.cc
+      writer/hlsl/generator_impl_builtin_test.cc
+      writer/hlsl/generator_impl_builtin_texture_test.cc
+      writer/hlsl/generator_impl_call_test.cc
+      writer/hlsl/generator_impl_case_test.cc
+      writer/hlsl/generator_impl_cast_test.cc
+      writer/hlsl/generator_impl_constructor_test.cc
+      writer/hlsl/generator_impl_continue_test.cc
+      writer/hlsl/generator_impl_discard_test.cc
+      writer/hlsl/generator_impl_function_test.cc
+      writer/hlsl/generator_impl_identifier_test.cc
+      writer/hlsl/generator_impl_if_test.cc
+      writer/hlsl/generator_impl_import_test.cc
+      writer/hlsl/generator_impl_loop_test.cc
+      writer/hlsl/generator_impl_member_accessor_test.cc
+      writer/hlsl/generator_impl_module_constant_test.cc
+      writer/hlsl/generator_impl_return_test.cc
+      writer/hlsl/generator_impl_sanitizer_test.cc
+      writer/hlsl/generator_impl_switch_test.cc
+      writer/hlsl/generator_impl_test.cc
+      writer/hlsl/generator_impl_type_test.cc
+      writer/hlsl/generator_impl_unary_op_test.cc
+      writer/hlsl/generator_impl_variable_decl_statement_test.cc
+      writer/hlsl/generator_impl_workgroup_var_test.cc
+      writer/hlsl/test_helper.h
+    )
+  endif()
+
+  if (${TINT_BUILD_FUZZERS})
+    list(APPEND TINT_TEST_SRCS
+      fuzzers/mersenne_twister_engine.cc
+      fuzzers/mersenne_twister_engine.h
+      fuzzers/random_generator.cc
+      fuzzers/random_generator.h
+      fuzzers/random_generator_engine.cc
+      fuzzers/random_generator_engine.h
+      fuzzers/random_generator_test.cc
+    )
+  endif()
+
+  add_executable(tint_unittests ${TINT_TEST_SRCS})
+  set_target_properties(${target} PROPERTIES FOLDER "Tests")
+
+  if(NOT MSVC)
+    target_compile_options(tint_unittests PRIVATE
+      -Wno-global-constructors
+      -Wno-weak-vtables
+    )
+  endif()
+
+  ## Test executable
+  target_include_directories(
+      tint_unittests PRIVATE ${gmock_SOURCE_DIR}/include)
+  target_link_libraries(tint_unittests libtint gmock tint_utils_io)
+  tint_default_compile_options(tint_unittests)
+
+  if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
+    tint_spvtools_compile_options(tint_unittests)
+  endif()
+
+  add_test(NAME tint_unittests COMMAND tint_unittests)
+endif(TINT_BUILD_TESTS)
+
+################################################################################
+# Benchmarks
+################################################################################
+if(TINT_BUILD_BENCHMARKS)
+  if(NOT TINT_BUILD_WGSL_READER)
+    message(FATAL_ERROR "TINT_BUILD_BENCHMARKS requires TINT_BUILD_WGSL_READER")
+  endif()
+
+  set(TINT_BENCHMARK_SRC
+    "castable_bench.cc"
+    "bench/benchmark.cc"
+    "reader/wgsl/parser_bench.cc"
+  )
+
+  if (${TINT_BUILD_GLSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/glsl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_HLSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/hlsl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_MSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/msl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_SPV_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/spirv/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/wgsl/generator_bench.cc)
+  endif()
+
+  add_executable(tint-benchmark ${TINT_BENCHMARK_SRC})
+  set_target_properties(${target} PROPERTIES FOLDER "Benchmarks")
+
+  tint_core_compile_options(tint-benchmark)
+
+  target_link_libraries(tint-benchmark PRIVATE benchmark::benchmark libtint)
+endif(TINT_BUILD_BENCHMARKS)
diff --git a/src/tint/ast/access.cc b/src/tint/ast/access.cc
new file mode 100644
index 0000000..cb5f864
--- /dev/null
+++ b/src/tint/ast/access.cc
@@ -0,0 +1,43 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/access.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, Access access) {
+  switch (access) {
+    case ast::Access::kUndefined: {
+      out << "undefined";
+      break;
+    }
+    case ast::Access::kRead: {
+      out << "read";
+      break;
+    }
+    case ast::Access::kReadWrite: {
+      out << "read_write";
+      break;
+    }
+    case ast::Access::kWrite: {
+      out << "write";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/access.h b/src/tint/ast/access.h
new file mode 100644
index 0000000..67ad714
--- /dev/null
+++ b/src/tint/ast/access.h
@@ -0,0 +1,46 @@
+// Copyright 2020 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_AST_ACCESS_H_
+#define SRC_TINT_AST_ACCESS_H_
+
+#include <ostream>
+#include <string>
+
+namespace tint {
+namespace ast {
+
+/// The access control settings
+enum Access {
+  /// Not declared in the source
+  kUndefined = 0,
+  /// Read only
+  kRead,
+  /// Write only
+  kWrite,
+  /// Read write
+  kReadWrite,
+  // Last valid access mode
+  kLastValid = kReadWrite,
+};
+
+/// @param out the std::ostream to write to
+/// @param access the Access
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, Access access);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ACCESS_H_
diff --git a/src/tint/ast/alias.cc b/src/tint/ast/alias.cc
new file mode 100644
index 0000000..d852667
--- /dev/null
+++ b/src/tint/ast/alias.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/alias.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Alias);
+
+namespace tint {
+namespace ast {
+
+Alias::Alias(ProgramID pid,
+             const Source& src,
+             const Symbol& n,
+             const Type* subtype)
+    : Base(pid, src, n), type(subtype) {
+  TINT_ASSERT(AST, type);
+}
+
+Alias::Alias(Alias&&) = default;
+
+Alias::~Alias() = default;
+
+const Alias* Alias::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto sym = ctx->Clone(name);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<Alias>(src, sym, ty);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/alias.h b/src/tint/ast/alias.h
new file mode 100644
index 0000000..c21b3e2
--- /dev/null
+++ b/src/tint/ast/alias.h
@@ -0,0 +1,54 @@
+// Copyright 2020 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_AST_ALIAS_H_
+#define SRC_TINT_AST_ALIAS_H_
+
+#include <string>
+
+#include "src/tint/ast/type_decl.h"
+
+namespace tint {
+namespace ast {
+
+/// A type alias type. Holds a name and pointer to another type.
+class Alias final : public Castable<Alias, TypeDecl> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param name the symbol for the alias
+  /// @param subtype the alias'd type
+  Alias(ProgramID pid,
+        const Source& src,
+        const Symbol& name,
+        const Type* subtype);
+  /// Move constructor
+  Alias(Alias&&);
+  /// Destructor
+  ~Alias() override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Alias* Clone(CloneContext* ctx) const override;
+
+  /// the alias type
+  const Type* const type;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ALIAS_H_
diff --git a/src/tint/ast/alias_test.cc b/src/tint/ast/alias_test.cc
new file mode 100644
index 0000000..db82082
--- /dev/null
+++ b/src/tint/ast/alias_test.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/texture.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstAliasTest = TestHelper;
+
+TEST_F(AstAliasTest, Create) {
+  auto* u32 = create<U32>();
+  auto* a = Alias("a_type", u32);
+  EXPECT_EQ(a->name, Symbol(1, ID()));
+  EXPECT_EQ(a->type, u32);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/array.cc b/src/tint/ast/array.cc
new file mode 100644
index 0000000..99b8d56
--- /dev/null
+++ b/src/tint/ast/array.cc
@@ -0,0 +1,78 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/array.h"
+
+#include <cmath>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Array);
+
+namespace tint {
+namespace ast {
+
+namespace {
+// Returns the string representation of an array size expression.
+std::string SizeExprToString(const Expression* size,
+                             const SymbolTable& symbols) {
+  if (auto* ident = size->As<IdentifierExpression>()) {
+    return symbols.NameFor(ident->symbol);
+  }
+  if (auto* literal = size->As<IntLiteralExpression>()) {
+    return std::to_string(literal->ValueAsU32());
+  }
+  // This will never be exposed to the user as the Resolver will reject this
+  // expression for array size.
+  return "<invalid>";
+}
+}  // namespace
+
+Array::Array(ProgramID pid,
+             const Source& src,
+             const Type* subtype,
+             const Expression* cnt,
+             AttributeList attrs)
+    : Base(pid, src), type(subtype), count(cnt), attributes(attrs) {}
+
+Array::Array(Array&&) = default;
+
+Array::~Array() = default;
+
+std::string Array::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  for (auto* attr : attributes) {
+    if (auto* stride = attr->As<ast::StrideAttribute>()) {
+      out << "@stride(" << stride->stride << ") ";
+    }
+  }
+  out << "array<" << type->FriendlyName(symbols);
+  if (!IsRuntimeArray()) {
+    out << ", " << SizeExprToString(count, symbols);
+  }
+  out << ">";
+  return out.str();
+}
+
+const Array* Array::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  auto* cnt = ctx->Clone(count);
+  auto attrs = ctx->Clone(attributes);
+  return ctx->dst->create<Array>(src, ty, cnt, attrs);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/array.h b/src/tint/ast/array.h
new file mode 100644
index 0000000..413e4d3
--- /dev/null
+++ b/src/tint/ast/array.h
@@ -0,0 +1,75 @@
+// Copyright 2020 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_AST_ARRAY_H_
+#define SRC_TINT_AST_ARRAY_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declarations.
+class Expression;
+
+/// An array type. If size is zero then it is a runtime array.
+class Array final : public Castable<Array, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param subtype the type of the array elements
+  /// @param count the number of elements in the array. nullptr represents a
+  /// runtime-sized array.
+  /// @param attributes the array attributes
+  Array(ProgramID pid,
+        const Source& src,
+        const Type* subtype,
+        const Expression* count,
+        AttributeList attributes);
+  /// Move constructor
+  Array(Array&&);
+  ~Array() override;
+
+  /// @returns true if this is a runtime array.
+  /// i.e. the size is determined at runtime
+  bool IsRuntimeArray() const { return count == nullptr; }
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Array* Clone(CloneContext* ctx) const override;
+
+  /// the array element type
+  const Type* const type;
+
+  /// the array size in elements, or nullptr for a runtime array
+  const Expression* const count;
+
+  /// the array attributes
+  const AttributeList attributes;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ARRAY_H_
diff --git a/src/tint/ast/array_test.cc b/src/tint/ast/array_test.cc
new file mode 100644
index 0000000..ff97734
--- /dev/null
+++ b/src/tint/ast/array_test.cc
@@ -0,0 +1,71 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/array.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstArrayTest = TestHelper;
+
+TEST_F(AstArrayTest, CreateSizedArray) {
+  auto* u32 = create<U32>();
+  auto* count = Expr(3);
+  auto* arr = create<Array>(u32, count, AttributeList{});
+  EXPECT_EQ(arr->type, u32);
+  EXPECT_EQ(arr->count, count);
+  EXPECT_TRUE(arr->Is<Array>());
+  EXPECT_FALSE(arr->IsRuntimeArray());
+}
+
+TEST_F(AstArrayTest, CreateRuntimeArray) {
+  auto* u32 = create<U32>();
+  auto* arr = create<Array>(u32, nullptr, AttributeList{});
+  EXPECT_EQ(arr->type, u32);
+  EXPECT_EQ(arr->count, nullptr);
+  EXPECT_TRUE(arr->Is<Array>());
+  EXPECT_TRUE(arr->IsRuntimeArray());
+}
+
+TEST_F(AstArrayTest, FriendlyName_RuntimeSized) {
+  auto* i32 = create<I32>();
+  auto* arr = create<Array>(i32, nullptr, AttributeList{});
+  EXPECT_EQ(arr->FriendlyName(Symbols()), "array<i32>");
+}
+
+TEST_F(AstArrayTest, FriendlyName_LiteralSized) {
+  auto* i32 = create<I32>();
+  auto* arr = create<Array>(i32, Expr(5), AttributeList{});
+  EXPECT_EQ(arr->FriendlyName(Symbols()), "array<i32, 5>");
+}
+
+TEST_F(AstArrayTest, FriendlyName_ConstantSized) {
+  auto* i32 = create<I32>();
+  auto* arr = create<Array>(i32, Expr("size"), AttributeList{});
+  EXPECT_EQ(arr->FriendlyName(Symbols()), "array<i32, size>");
+}
+
+TEST_F(AstArrayTest, FriendlyName_WithStride) {
+  auto* i32 = create<I32>();
+  auto* arr =
+      create<Array>(i32, Expr(5), AttributeList{create<StrideAttribute>(32)});
+  EXPECT_EQ(arr->FriendlyName(Symbols()), "@stride(32) array<i32, 5>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/assignment_statement.cc b/src/tint/ast/assignment_statement.cc
new file mode 100644
index 0000000..a2340b7
--- /dev/null
+++ b/src/tint/ast/assignment_statement.cc
@@ -0,0 +1,48 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/assignment_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::AssignmentStatement);
+
+namespace tint {
+namespace ast {
+
+AssignmentStatement::AssignmentStatement(ProgramID pid,
+                                         const Source& src,
+                                         const Expression* l,
+                                         const Expression* r)
+    : Base(pid, src), lhs(l), rhs(r) {
+  TINT_ASSERT(AST, lhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
+  TINT_ASSERT(AST, rhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, rhs, program_id);
+}
+
+AssignmentStatement::AssignmentStatement(AssignmentStatement&&) = default;
+
+AssignmentStatement::~AssignmentStatement() = default;
+
+const AssignmentStatement* AssignmentStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* l = ctx->Clone(lhs);
+  auto* r = ctx->Clone(rhs);
+  return ctx->dst->create<AssignmentStatement>(src, l, r);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/assignment_statement.h b/src/tint/ast/assignment_statement.h
new file mode 100644
index 0000000..55e07b8
--- /dev/null
+++ b/src/tint/ast/assignment_statement.h
@@ -0,0 +1,57 @@
+// Copyright 2020 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_AST_ASSIGNMENT_STATEMENT_H_
+#define SRC_TINT_AST_ASSIGNMENT_STATEMENT_H_
+
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An assignment statement
+class AssignmentStatement final
+    : public Castable<AssignmentStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the assignment statement source
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  AssignmentStatement(ProgramID program_id,
+                      const Source& source,
+                      const Expression* lhs,
+                      const Expression* rhs);
+  /// Move constructor
+  AssignmentStatement(AssignmentStatement&&);
+  ~AssignmentStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const AssignmentStatement* Clone(CloneContext* ctx) const override;
+
+  /// left side expression
+  const Expression* const lhs;
+
+  /// right side expression
+  const Expression* const rhs;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ASSIGNMENT_STATEMENT_H_
diff --git a/src/tint/ast/assignment_statement_test.cc b/src/tint/ast/assignment_statement_test.cc
new file mode 100644
index 0000000..4a41a7c
--- /dev/null
+++ b/src/tint/ast/assignment_statement_test.cc
@@ -0,0 +1,94 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/assignment_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AssignmentStatementTest = TestHelper;
+
+TEST_F(AssignmentStatementTest, Creation) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* stmt = create<AssignmentStatement>(lhs, rhs);
+  EXPECT_EQ(stmt->lhs, lhs);
+  EXPECT_EQ(stmt->rhs, rhs);
+}
+
+TEST_F(AssignmentStatementTest, CreationWithSource) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* stmt =
+      create<AssignmentStatement>(Source{Source::Location{20, 2}}, lhs, rhs);
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(AssignmentStatementTest, IsAssign) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* stmt = create<AssignmentStatement>(lhs, rhs);
+  EXPECT_TRUE(stmt->Is<AssignmentStatement>());
+}
+
+TEST_F(AssignmentStatementTest, Assert_Null_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<AssignmentStatement>(nullptr, b.Expr(1));
+      },
+      "internal compiler error");
+}
+
+TEST_F(AssignmentStatementTest, Assert_Null_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<AssignmentStatement>(b.Expr(1), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(AssignmentStatementTest, Assert_DifferentProgramID_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<AssignmentStatement>(b2.Expr("lhs"), b1.Expr("rhs"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(AssignmentStatementTest, Assert_DifferentProgramID_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<AssignmentStatement>(b1.Expr("lhs"), b2.Expr("rhs"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/ast_type.cc b/src/tint/ast/ast_type.cc
new file mode 100644
index 0000000..cb01679
--- /dev/null
+++ b/src/tint/ast/ast_type.cc
@@ -0,0 +1,41 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/type.h"
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/texture.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+#include "src/tint/symbol_table.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Type);
+
+namespace tint {
+namespace ast {
+
+Type::Type(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Type::Type(Type&&) = default;
+
+Type::~Type() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/atomic.cc b/src/tint/ast/atomic.cc
new file mode 100644
index 0000000..addefee
--- /dev/null
+++ b/src/tint/ast/atomic.cc
@@ -0,0 +1,45 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/atomic.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Atomic);
+
+namespace tint {
+namespace ast {
+
+Atomic::Atomic(ProgramID pid, const Source& src, const Type* const subtype)
+    : Base(pid, src), type(subtype) {}
+
+std::string Atomic::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "atomic<" << type->FriendlyName(symbols) << ">";
+  return out.str();
+}
+
+Atomic::Atomic(Atomic&&) = default;
+
+Atomic::~Atomic() = default;
+
+const Atomic* Atomic::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<Atomic>(src, ty);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/atomic.h b/src/tint/ast/atomic.h
new file mode 100644
index 0000000..d5e1b9b
--- /dev/null
+++ b/src/tint/ast/atomic.h
@@ -0,0 +1,54 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_ATOMIC_H_
+#define SRC_TINT_AST_ATOMIC_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// An atomic type.
+class Atomic final : public Castable<Atomic, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param subtype the pointee type
+  Atomic(ProgramID pid, const Source& src, const Type* const subtype);
+  /// Move constructor
+  Atomic(Atomic&&);
+  ~Atomic() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Atomic* Clone(CloneContext* ctx) const override;
+
+  /// the pointee type
+  const Type* const type;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ATOMIC_H_
diff --git a/src/tint/ast/atomic_test.cc b/src/tint/ast/atomic_test.cc
new file mode 100644
index 0000000..636654b
--- /dev/null
+++ b/src/tint/ast/atomic_test.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/atomic.h"
+
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstAtomicTest = TestHelper;
+
+TEST_F(AstAtomicTest, Creation) {
+  auto* i32 = create<I32>();
+  auto* p = create<Atomic>(i32);
+  EXPECT_EQ(p->type, i32);
+}
+
+TEST_F(AstAtomicTest, FriendlyName) {
+  auto* i32 = create<I32>();
+  auto* p = create<Atomic>(i32);
+  EXPECT_EQ(p->FriendlyName(Symbols()), "atomic<i32>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/attribute.cc b/src/tint/ast/attribute.cc
new file mode 100644
index 0000000..87ad3f1
--- /dev/null
+++ b/src/tint/ast/attribute.cc
@@ -0,0 +1,25 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/attribute.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Attribute);
+
+namespace tint {
+namespace ast {
+
+Attribute::~Attribute() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/attribute.h b/src/tint/ast/attribute.h
new file mode 100644
index 0000000..d336727
--- /dev/null
+++ b/src/tint/ast/attribute.h
@@ -0,0 +1,71 @@
+// Copyright 2020 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_AST_ATTRIBUTE_H_
+#define SRC_TINT_AST_ATTRIBUTE_H_
+
+#include <string>
+#include <vector>
+
+#include "src/tint/ast/node.h"
+
+namespace tint {
+namespace ast {
+
+/// The base class for all attributes
+class Attribute : public Castable<Attribute, Node> {
+ public:
+  ~Attribute() override;
+
+  /// @returns the WGSL name for the attribute
+  virtual std::string Name() const = 0;
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Attribute(ProgramID pid, const Source& src) : Base(pid, src) {}
+};
+
+/// A list of attributes
+using AttributeList = std::vector<const Attribute*>;
+
+/// @param attributes the list of attributes to search
+/// @returns true if `attributes` includes a attribute of type `T`
+template <typename T>
+bool HasAttribute(const AttributeList& attributes) {
+  for (auto* attr : attributes) {
+    if (attr->Is<T>()) {
+      return true;
+    }
+  }
+  return false;
+}
+
+/// @param attributes the list of attributes to search
+/// @returns a pointer to `T` from `attributes` if found, otherwise nullptr.
+template <typename T>
+const T* GetAttribute(const AttributeList& attributes) {
+  for (auto* attr : attributes) {
+    if (attr->Is<T>()) {
+      return attr->As<T>();
+    }
+  }
+  return nullptr;
+}
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ATTRIBUTE_H_
diff --git a/src/tint/ast/binary_expression.cc b/src/tint/ast/binary_expression.cc
new file mode 100644
index 0000000..6b2be73
--- /dev/null
+++ b/src/tint/ast/binary_expression.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/binary_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BinaryExpression);
+
+namespace tint {
+namespace ast {
+
+BinaryExpression::BinaryExpression(ProgramID pid,
+                                   const Source& src,
+                                   BinaryOp o,
+                                   const Expression* l,
+                                   const Expression* r)
+    : Base(pid, src), op(o), lhs(l), rhs(r) {
+  TINT_ASSERT(AST, lhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
+  TINT_ASSERT(AST, rhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, rhs, program_id);
+  TINT_ASSERT(AST, op != BinaryOp::kNone);
+}
+
+BinaryExpression::BinaryExpression(BinaryExpression&&) = default;
+
+BinaryExpression::~BinaryExpression() = default;
+
+const BinaryExpression* BinaryExpression::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* l = ctx->Clone(lhs);
+  auto* r = ctx->Clone(rhs);
+  return ctx->dst->create<BinaryExpression>(src, op, l, r);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/binary_expression.h b/src/tint/ast/binary_expression.h
new file mode 100644
index 0000000..bcacdd4
--- /dev/null
+++ b/src/tint/ast/binary_expression.h
@@ -0,0 +1,264 @@
+// Copyright 2020 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_AST_BINARY_EXPRESSION_H_
+#define SRC_TINT_AST_BINARY_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// The operator type
+enum class BinaryOp {
+  kNone = 0,
+  kAnd,  // &
+  kOr,   // |
+  kXor,
+  kLogicalAnd,  // &&
+  kLogicalOr,   // ||
+  kEqual,
+  kNotEqual,
+  kLessThan,
+  kGreaterThan,
+  kLessThanEqual,
+  kGreaterThanEqual,
+  kShiftLeft,
+  kShiftRight,
+  kAdd,
+  kSubtract,
+  kMultiply,
+  kDivide,
+  kModulo,
+};
+
+/// An binary expression
+class BinaryExpression final : public Castable<BinaryExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the binary expression source
+  /// @param op the operation type
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  BinaryExpression(ProgramID program_id,
+                   const Source& source,
+                   BinaryOp op,
+                   const Expression* lhs,
+                   const Expression* rhs);
+  /// Move constructor
+  BinaryExpression(BinaryExpression&&);
+  ~BinaryExpression() override;
+
+  /// @returns true if the op is and
+  bool IsAnd() const { return op == BinaryOp::kAnd; }
+  /// @returns true if the op is or
+  bool IsOr() const { return op == BinaryOp::kOr; }
+  /// @returns true if the op is xor
+  bool IsXor() const { return op == BinaryOp::kXor; }
+  /// @returns true if the op is logical and
+  bool IsLogicalAnd() const { return op == BinaryOp::kLogicalAnd; }
+  /// @returns true if the op is logical or
+  bool IsLogicalOr() const { return op == BinaryOp::kLogicalOr; }
+  /// @returns true if the op is equal
+  bool IsEqual() const { return op == BinaryOp::kEqual; }
+  /// @returns true if the op is not equal
+  bool IsNotEqual() const { return op == BinaryOp::kNotEqual; }
+  /// @returns true if the op is less than
+  bool IsLessThan() const { return op == BinaryOp::kLessThan; }
+  /// @returns true if the op is greater than
+  bool IsGreaterThan() const { return op == BinaryOp::kGreaterThan; }
+  /// @returns true if the op is less than equal
+  bool IsLessThanEqual() const { return op == BinaryOp::kLessThanEqual; }
+  /// @returns true if the op is greater than equal
+  bool IsGreaterThanEqual() const { return op == BinaryOp::kGreaterThanEqual; }
+  /// @returns true if the op is shift left
+  bool IsShiftLeft() const { return op == BinaryOp::kShiftLeft; }
+  /// @returns true if the op is shift right
+  bool IsShiftRight() const { return op == BinaryOp::kShiftRight; }
+  /// @returns true if the op is add
+  bool IsAdd() const { return op == BinaryOp::kAdd; }
+  /// @returns true if the op is subtract
+  bool IsSubtract() const { return op == BinaryOp::kSubtract; }
+  /// @returns true if the op is multiply
+  bool IsMultiply() const { return op == BinaryOp::kMultiply; }
+  /// @returns true if the op is divide
+  bool IsDivide() const { return op == BinaryOp::kDivide; }
+  /// @returns true if the op is modulo
+  bool IsModulo() const { return op == BinaryOp::kModulo; }
+  /// @returns true if the op is an arithmetic operation
+  bool IsArithmetic() const;
+  /// @returns true if the op is a comparison operation
+  bool IsComparison() const;
+  /// @returns true if the op is a bitwise operation
+  bool IsBitwise() const;
+  /// @returns true if the op is a bit shift operation
+  bool IsBitshift() const;
+  /// @returns true if the op is a logical expression
+  bool IsLogical() const;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BinaryExpression* Clone(CloneContext* ctx) const override;
+
+  /// the binary op type
+  const BinaryOp op;
+  /// the left side expression
+  const Expression* const lhs;
+  /// the right side expression
+  const Expression* const rhs;
+};
+
+/// @param op the operator
+/// @returns true if the op is an arithmetic operation
+inline bool IsArithmetic(BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      return true;
+    default:
+      return false;
+  }
+}
+
+/// @param op the operator
+/// @returns true if the op is a comparison operation
+inline bool IsComparison(BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+      return true;
+    default:
+      return false;
+  }
+}
+
+/// @param op the operator
+/// @returns true if the op is a bitwise operation
+inline bool IsBitwise(BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      return true;
+    default:
+      return false;
+  }
+}
+
+/// @param op the operator
+/// @returns true if the op is a bit shift operation
+inline bool IsBitshift(BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return true;
+    default:
+      return false;
+  }
+}
+
+inline bool BinaryExpression::IsLogical() const {
+  switch (op) {
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      return true;
+    default:
+      return false;
+  }
+}
+
+inline bool BinaryExpression::IsArithmetic() const {
+  return ast::IsArithmetic(op);
+}
+
+inline bool BinaryExpression::IsComparison() const {
+  return ast::IsComparison(op);
+}
+
+inline bool BinaryExpression::IsBitwise() const {
+  return ast::IsBitwise(op);
+}
+
+inline bool BinaryExpression::IsBitshift() const {
+  return ast::IsBitshift(op);
+}
+
+/// @returns the human readable name of the given BinaryOp
+/// @param op the BinaryOp
+constexpr const char* FriendlyName(BinaryOp op) {
+  switch (op) {
+    case BinaryOp::kNone:
+      return "none";
+    case BinaryOp::kAnd:
+      return "and";
+    case BinaryOp::kOr:
+      return "or";
+    case BinaryOp::kXor:
+      return "xor";
+    case BinaryOp::kLogicalAnd:
+      return "logical_and";
+    case BinaryOp::kLogicalOr:
+      return "logical_or";
+    case BinaryOp::kEqual:
+      return "equal";
+    case BinaryOp::kNotEqual:
+      return "not_equal";
+    case BinaryOp::kLessThan:
+      return "less_than";
+    case BinaryOp::kGreaterThan:
+      return "greater_than";
+    case BinaryOp::kLessThanEqual:
+      return "less_than_equal";
+    case BinaryOp::kGreaterThanEqual:
+      return "greater_than_equal";
+    case BinaryOp::kShiftLeft:
+      return "shift_left";
+    case BinaryOp::kShiftRight:
+      return "shift_right";
+    case BinaryOp::kAdd:
+      return "add";
+    case BinaryOp::kSubtract:
+      return "subtract";
+    case BinaryOp::kMultiply:
+      return "multiply";
+    case BinaryOp::kDivide:
+      return "divide";
+    case BinaryOp::kModulo:
+      return "modulo";
+  }
+  return "INVALID";
+}
+
+/// @param out the std::ostream to write to
+/// @param op the BinaryOp
+/// @return the std::ostream so calls can be chained
+inline std::ostream& operator<<(std::ostream& out, BinaryOp op) {
+  out << FriendlyName(op);
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BINARY_EXPRESSION_H_
diff --git a/src/tint/ast/binary_expression_test.cc b/src/tint/ast/binary_expression_test.cc
new file mode 100644
index 0000000..20b8f8f
--- /dev/null
+++ b/src/tint/ast/binary_expression_test.cc
@@ -0,0 +1,95 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BinaryExpressionTest = TestHelper;
+
+TEST_F(BinaryExpressionTest, Creation) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* r = create<BinaryExpression>(BinaryOp::kEqual, lhs, rhs);
+  EXPECT_EQ(r->lhs, lhs);
+  EXPECT_EQ(r->rhs, rhs);
+  EXPECT_EQ(r->op, BinaryOp::kEqual);
+}
+
+TEST_F(BinaryExpressionTest, Creation_WithSource) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* r = create<BinaryExpression>(Source{Source::Location{20, 2}},
+                                     BinaryOp::kEqual, lhs, rhs);
+  auto src = r->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(BinaryExpressionTest, IsBinary) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+
+  auto* r = create<BinaryExpression>(BinaryOp::kEqual, lhs, rhs);
+  EXPECT_TRUE(r->Is<BinaryExpression>());
+}
+
+TEST_F(BinaryExpressionTest, Assert_Null_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<BinaryExpression>(BinaryOp::kEqual, nullptr, b.Expr("rhs"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(BinaryExpressionTest, Assert_Null_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<BinaryExpression>(BinaryOp::kEqual, b.Expr("lhs"), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(BinaryExpressionTest, Assert_DifferentProgramID_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<BinaryExpression>(BinaryOp::kEqual, b2.Expr("lhs"),
+                                    b1.Expr("rhs"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(BinaryExpressionTest, Assert_DifferentProgramID_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<BinaryExpression>(BinaryOp::kEqual, b1.Expr("lhs"),
+                                    b2.Expr("rhs"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/binding_attribute.cc b/src/tint/ast/binding_attribute.cc
new file mode 100644
index 0000000..bc1a74a
--- /dev/null
+++ b/src/tint/ast/binding_attribute.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/binding_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BindingAttribute);
+
+namespace tint {
+namespace ast {
+
+BindingAttribute::BindingAttribute(ProgramID pid,
+                                   const Source& src,
+                                   uint32_t val)
+    : Base(pid, src), value(val) {}
+
+BindingAttribute::~BindingAttribute() = default;
+
+std::string BindingAttribute::Name() const {
+  return "binding";
+}
+
+const BindingAttribute* BindingAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<BindingAttribute>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/binding_attribute.h b/src/tint/ast/binding_attribute.h
new file mode 100644
index 0000000..8f3214d
--- /dev/null
+++ b/src/tint/ast/binding_attribute.h
@@ -0,0 +1,51 @@
+// Copyright 2020 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_AST_BINDING_ATTRIBUTE_H_
+#define SRC_TINT_AST_BINDING_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A binding attribute
+class BindingAttribute final : public Castable<BindingAttribute, Attribute> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the binding value
+  BindingAttribute(ProgramID pid, const Source& src, uint32_t value);
+  ~BindingAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BindingAttribute* Clone(CloneContext* ctx) const override;
+
+  /// the binding value
+  const uint32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BINDING_ATTRIBUTE_H_
diff --git a/src/tint/ast/binding_attribute_test.cc b/src/tint/ast/binding_attribute_test.cc
new file mode 100644
index 0000000..c4c7e39
--- /dev/null
+++ b/src/tint/ast/binding_attribute_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BindingAttributeTest = TestHelper;
+
+TEST_F(BindingAttributeTest, Creation) {
+  auto* d = create<BindingAttribute>(2);
+  EXPECT_EQ(2u, d->value);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bitcast_expression.cc b/src/tint/ast/bitcast_expression.cc
new file mode 100644
index 0000000..8de3afc
--- /dev/null
+++ b/src/tint/ast/bitcast_expression.cc
@@ -0,0 +1,46 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/bitcast_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BitcastExpression);
+
+namespace tint {
+namespace ast {
+
+BitcastExpression::BitcastExpression(ProgramID pid,
+                                     const Source& src,
+                                     const Type* t,
+                                     const Expression* e)
+    : Base(pid, src), type(t), expr(e) {
+  TINT_ASSERT(AST, type);
+  TINT_ASSERT(AST, expr);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
+}
+
+BitcastExpression::BitcastExpression(BitcastExpression&&) = default;
+BitcastExpression::~BitcastExpression() = default;
+
+const BitcastExpression* BitcastExpression::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* t = ctx->Clone(type);
+  auto* e = ctx->Clone(expr);
+  return ctx->dst->create<BitcastExpression>(src, t, e);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bitcast_expression.h b/src/tint/ast/bitcast_expression.h
new file mode 100644
index 0000000..e16ae22
--- /dev/null
+++ b/src/tint/ast/bitcast_expression.h
@@ -0,0 +1,57 @@
+// Copyright 2020 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_AST_BITCAST_EXPRESSION_H_
+#define SRC_TINT_AST_BITCAST_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declaration
+class Type;
+
+/// A bitcast expression
+class BitcastExpression final : public Castable<BitcastExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the bitcast expression source
+  /// @param type the type
+  /// @param expr the expr
+  BitcastExpression(ProgramID program_id,
+                    const Source& source,
+                    const Type* type,
+                    const Expression* expr);
+  /// Move constructor
+  BitcastExpression(BitcastExpression&&);
+  ~BitcastExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BitcastExpression* Clone(CloneContext* ctx) const override;
+
+  /// the target cast type
+  const Type* const type;
+  /// the expression
+  const Expression* const expr;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BITCAST_EXPRESSION_H_
diff --git a/src/tint/ast/bitcast_expression_test.cc b/src/tint/ast/bitcast_expression_test.cc
new file mode 100644
index 0000000..5803003
--- /dev/null
+++ b/src/tint/ast/bitcast_expression_test.cc
@@ -0,0 +1,81 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/bitcast_expression.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BitcastExpressionTest = TestHelper;
+
+TEST_F(BitcastExpressionTest, Create) {
+  auto* expr = Expr("expr");
+
+  auto* exp = create<BitcastExpression>(ty.f32(), expr);
+  EXPECT_TRUE(exp->type->Is<ast::F32>());
+  ASSERT_EQ(exp->expr, expr);
+}
+
+TEST_F(BitcastExpressionTest, CreateWithSource) {
+  auto* expr = Expr("expr");
+
+  auto* exp = create<BitcastExpression>(Source{Source::Location{20, 2}},
+                                        ty.f32(), expr);
+  auto src = exp->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(BitcastExpressionTest, IsBitcast) {
+  auto* expr = Expr("expr");
+
+  auto* exp = create<BitcastExpression>(ty.f32(), expr);
+  EXPECT_TRUE(exp->Is<BitcastExpression>());
+}
+
+TEST_F(BitcastExpressionTest, Assert_Null_Type) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<BitcastExpression>(nullptr, b.Expr("idx"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(BitcastExpressionTest, Assert_Null_Expr) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<BitcastExpression>(b.ty.f32(), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(BitcastExpressionTest, Assert_DifferentProgramID_Expr) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<BitcastExpression>(b1.ty.f32(), b2.Expr("idx"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/block_statement.cc b/src/tint/ast/block_statement.cc
new file mode 100644
index 0000000..5a7dcba
--- /dev/null
+++ b/src/tint/ast/block_statement.cc
@@ -0,0 +1,46 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/block_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BlockStatement);
+
+namespace tint {
+namespace ast {
+
+BlockStatement::BlockStatement(ProgramID pid,
+                               const Source& src,
+                               const StatementList& stmts)
+    : Base(pid, src), statements(std::move(stmts)) {
+  for (auto* stmt : statements) {
+    TINT_ASSERT(AST, stmt);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, stmt, program_id);
+  }
+}
+
+BlockStatement::BlockStatement(BlockStatement&&) = default;
+
+BlockStatement::~BlockStatement() = default;
+
+const BlockStatement* BlockStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto stmts = ctx->Clone(statements);
+  return ctx->dst->create<BlockStatement>(src, stmts);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/block_statement.h b/src/tint/ast/block_statement.h
new file mode 100644
index 0000000..31b2ef1
--- /dev/null
+++ b/src/tint/ast/block_statement.h
@@ -0,0 +1,60 @@
+// Copyright 2020 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_AST_BLOCK_STATEMENT_H_
+#define SRC_TINT_AST_BLOCK_STATEMENT_H_
+
+#include <utility>
+
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A block statement
+class BlockStatement final : public Castable<BlockStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the block statement source
+  /// @param statements the statements
+  BlockStatement(ProgramID program_id,
+                 const Source& source,
+                 const StatementList& statements);
+  /// Move constructor
+  BlockStatement(BlockStatement&&);
+  ~BlockStatement() override;
+
+  /// @returns true if the block has no statements
+  bool Empty() const { return statements.empty(); }
+
+  /// @returns the last statement in the block or nullptr if block empty
+  const Statement* Last() const {
+    return statements.empty() ? nullptr : statements.back();
+  }
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BlockStatement* Clone(CloneContext* ctx) const override;
+
+  /// the statement list
+  const StatementList statements;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BLOCK_STATEMENT_H_
diff --git a/src/tint/ast/block_statement_test.cc b/src/tint/ast/block_statement_test.cc
new file mode 100644
index 0000000..1cc8f38
--- /dev/null
+++ b/src/tint/ast/block_statement_test.cc
@@ -0,0 +1,71 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BlockStatementTest = TestHelper;
+
+TEST_F(BlockStatementTest, Creation) {
+  auto* d = create<DiscardStatement>();
+  auto* ptr = d;
+
+  auto* b = create<BlockStatement>(StatementList{d});
+
+  ASSERT_EQ(b->statements.size(), 1u);
+  EXPECT_EQ(b->statements[0], ptr);
+}
+
+TEST_F(BlockStatementTest, Creation_WithSource) {
+  auto* b = create<BlockStatement>(Source{Source::Location{20, 2}},
+                                   ast::StatementList{});
+  auto src = b->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(BlockStatementTest, IsBlock) {
+  auto* b = create<BlockStatement>(ast::StatementList{});
+  EXPECT_TRUE(b->Is<BlockStatement>());
+}
+
+TEST_F(BlockStatementTest, Assert_Null_Statement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<BlockStatement>(ast::StatementList{nullptr});
+      },
+      "internal compiler error");
+}
+
+TEST_F(BlockStatementTest, Assert_DifferentProgramID_Statement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<BlockStatement>(
+            ast::StatementList{b2.create<DiscardStatement>()});
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bool.cc b/src/tint/ast/bool.cc
new file mode 100644
index 0000000..79596a3
--- /dev/null
+++ b/src/tint/ast/bool.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/bool.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Bool);
+
+namespace tint {
+namespace ast {
+
+Bool::Bool(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Bool::Bool(Bool&&) = default;
+
+Bool::~Bool() = default;
+
+std::string Bool::FriendlyName(const SymbolTable&) const {
+  return "bool";
+}
+
+const Bool* Bool::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<Bool>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bool.h b/src/tint/ast/bool.h
new file mode 100644
index 0000000..fa326da
--- /dev/null
+++ b/src/tint/ast/bool.h
@@ -0,0 +1,56 @@
+// Copyright 2020 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_AST_BOOL_H_
+#define SRC_TINT_AST_BOOL_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+// X11 likes to #define Bool leading to confusing error messages.
+// If its defined, undefine it.
+#ifdef Bool
+#undef Bool
+#endif
+
+namespace tint {
+namespace ast {
+
+/// A boolean type
+class Bool final : public Castable<Bool, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Bool(ProgramID pid, const Source& src);
+  /// Move constructor
+  Bool(Bool&&);
+  ~Bool() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Bool* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BOOL_H_
diff --git a/src/tint/ast/bool_literal_expression.cc b/src/tint/ast/bool_literal_expression.cc
new file mode 100644
index 0000000..5c961b4
--- /dev/null
+++ b/src/tint/ast/bool_literal_expression.cc
@@ -0,0 +1,39 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/bool_literal_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BoolLiteralExpression);
+
+namespace tint {
+namespace ast {
+
+BoolLiteralExpression::BoolLiteralExpression(ProgramID pid,
+                                             const Source& src,
+                                             bool val)
+    : Base(pid, src), value(val) {}
+
+BoolLiteralExpression::~BoolLiteralExpression() = default;
+
+const BoolLiteralExpression* BoolLiteralExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<BoolLiteralExpression>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bool_literal_expression.h b/src/tint/ast/bool_literal_expression.h
new file mode 100644
index 0000000..421453e
--- /dev/null
+++ b/src/tint/ast/bool_literal_expression.h
@@ -0,0 +1,49 @@
+// Copyright 2020 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_AST_BOOL_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_BOOL_LITERAL_EXPRESSION_H_
+
+#include <string>
+
+#include "src/tint/ast/literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A boolean literal
+class BoolLiteralExpression final
+    : public Castable<BoolLiteralExpression, LiteralExpression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the bool literals value
+  BoolLiteralExpression(ProgramID pid, const Source& src, bool value);
+  ~BoolLiteralExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BoolLiteralExpression* Clone(CloneContext* ctx) const override;
+
+  /// The boolean literal value
+  const bool value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BOOL_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/bool_literal_expression_test.cc b/src/tint/ast/bool_literal_expression_test.cc
new file mode 100644
index 0000000..78cd632
--- /dev/null
+++ b/src/tint/ast/bool_literal_expression_test.cc
@@ -0,0 +1,37 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BoolLiteralExpressionTest = TestHelper;
+
+TEST_F(BoolLiteralExpressionTest, True) {
+  auto* b = create<BoolLiteralExpression>(true);
+  ASSERT_TRUE(b->Is<BoolLiteralExpression>());
+  ASSERT_TRUE(b->value);
+}
+
+TEST_F(BoolLiteralExpressionTest, False) {
+  auto* b = create<BoolLiteralExpression>(false);
+  ASSERT_TRUE(b->Is<BoolLiteralExpression>());
+  ASSERT_FALSE(b->value);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/bool_test.cc b/src/tint/ast/bool_test.cc
new file mode 100644
index 0000000..3defbd4
--- /dev/null
+++ b/src/tint/ast/bool_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/bool.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstBoolTest = TestHelper;
+
+TEST_F(AstBoolTest, FriendlyName) {
+  auto* b = create<Bool>();
+  EXPECT_EQ(b->FriendlyName(Symbols()), "bool");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/break_statement.cc b/src/tint/ast/break_statement.cc
new file mode 100644
index 0000000..0c78c73
--- /dev/null
+++ b/src/tint/ast/break_statement.cc
@@ -0,0 +1,38 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/break_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BreakStatement);
+
+namespace tint {
+namespace ast {
+
+BreakStatement::BreakStatement(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+BreakStatement::BreakStatement(BreakStatement&&) = default;
+
+BreakStatement::~BreakStatement() = default;
+
+const BreakStatement* BreakStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<BreakStatement>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/break_statement.h b/src/tint/ast/break_statement.h
new file mode 100644
index 0000000..cf50c74
--- /dev/null
+++ b/src/tint/ast/break_statement.h
@@ -0,0 +1,44 @@
+// Copyright 2020 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_AST_BREAK_STATEMENT_H_
+#define SRC_TINT_AST_BREAK_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An break statement
+class BreakStatement final : public Castable<BreakStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  BreakStatement(ProgramID pid, const Source& src);
+  /// Move constructor
+  BreakStatement(BreakStatement&&);
+  ~BreakStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BreakStatement* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BREAK_STATEMENT_H_
diff --git a/src/tint/ast/break_statement_test.cc b/src/tint/ast/break_statement_test.cc
new file mode 100644
index 0000000..ce419ae
--- /dev/null
+++ b/src/tint/ast/break_statement_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/break_statement.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BreakStatementTest = TestHelper;
+
+TEST_F(BreakStatementTest, Creation_WithSource) {
+  auto* stmt = create<BreakStatement>(Source{Source::Location{20, 2}});
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(BreakStatementTest, IsBreak) {
+  auto* stmt = create<BreakStatement>();
+  EXPECT_TRUE(stmt->Is<BreakStatement>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/builtin.cc b/src/tint/ast/builtin.cc
new file mode 100644
index 0000000..48744e0
--- /dev/null
+++ b/src/tint/ast/builtin.cc
@@ -0,0 +1,82 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/builtin.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, Builtin builtin) {
+  switch (builtin) {
+    case Builtin::kNone: {
+      out << "none";
+      break;
+    }
+    case Builtin::kPosition: {
+      out << "position";
+      break;
+    }
+    case Builtin::kVertexIndex: {
+      out << "vertex_index";
+      break;
+    }
+    case Builtin::kInstanceIndex: {
+      out << "instance_index";
+      break;
+    }
+    case Builtin::kFrontFacing: {
+      out << "front_facing";
+      break;
+    }
+    case Builtin::kFragDepth: {
+      out << "frag_depth";
+      break;
+    }
+    case Builtin::kLocalInvocationId: {
+      out << "local_invocation_id";
+      break;
+    }
+    case Builtin::kLocalInvocationIndex: {
+      out << "local_invocation_index";
+      break;
+    }
+    case Builtin::kGlobalInvocationId: {
+      out << "global_invocation_id";
+      break;
+    }
+    case Builtin::kWorkgroupId: {
+      out << "workgroup_id";
+      break;
+    }
+    case Builtin::kNumWorkgroups: {
+      out << "num_workgroups";
+      break;
+    }
+    case Builtin::kSampleIndex: {
+      out << "sample_index";
+      break;
+    }
+    case Builtin::kSampleMask: {
+      out << "sample_mask";
+      break;
+    }
+    case Builtin::kPointSize: {
+      out << "pointsize";
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/builtin.h b/src/tint/ast/builtin.h
new file mode 100644
index 0000000..913eba2
--- /dev/null
+++ b/src/tint/ast/builtin.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_AST_BUILTIN_H_
+#define SRC_TINT_AST_BUILTIN_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The builtin identifiers
+enum class Builtin {
+  kNone = -1,
+  kPosition,
+  kVertexIndex,
+  kInstanceIndex,
+  kFrontFacing,
+  kFragDepth,
+  kLocalInvocationId,
+  kLocalInvocationIndex,
+  kGlobalInvocationId,
+  kWorkgroupId,
+  kNumWorkgroups,
+  kSampleIndex,
+  kSampleMask,
+
+  // Below are not currently WGSL builtins, but are included in this enum as
+  // they are used by certain backends.
+  kPointSize,
+};
+
+/// @param out the std::ostream to write to
+/// @param builtin the Builtin
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, Builtin builtin);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BUILTIN_H_
diff --git a/src/tint/ast/builtin_attribute.cc b/src/tint/ast/builtin_attribute.cc
new file mode 100644
index 0000000..0591b91
--- /dev/null
+++ b/src/tint/ast/builtin_attribute.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/builtin_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::BuiltinAttribute);
+
+namespace tint {
+namespace ast {
+
+BuiltinAttribute::BuiltinAttribute(ProgramID pid, const Source& src, Builtin b)
+    : Base(pid, src), builtin(b) {}
+
+BuiltinAttribute::~BuiltinAttribute() = default;
+
+std::string BuiltinAttribute::Name() const {
+  return "builtin";
+}
+
+const BuiltinAttribute* BuiltinAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<BuiltinAttribute>(src, builtin);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/builtin_attribute.h b/src/tint/ast/builtin_attribute.h
new file mode 100644
index 0000000..4366739
--- /dev/null
+++ b/src/tint/ast/builtin_attribute.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_AST_BUILTIN_ATTRIBUTE_H_
+#define SRC_TINT_AST_BUILTIN_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/builtin.h"
+
+namespace tint {
+namespace ast {
+
+/// A builtin attribute
+class BuiltinAttribute final : public Castable<BuiltinAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param builtin the builtin value
+  BuiltinAttribute(ProgramID pid, const Source& src, Builtin builtin);
+  ~BuiltinAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const BuiltinAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The builtin value
+  const Builtin builtin;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BUILTIN_ATTRIBUTE_H_
diff --git a/src/tint/ast/builtin_attribute_test.cc b/src/tint/ast/builtin_attribute_test.cc
new file mode 100644
index 0000000..e5a91ea
--- /dev/null
+++ b/src/tint/ast/builtin_attribute_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied->
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using BuiltinAttributeTest = TestHelper;
+
+TEST_F(BuiltinAttributeTest, Creation) {
+  auto* d = create<BuiltinAttribute>(Builtin::kFragDepth);
+  EXPECT_EQ(Builtin::kFragDepth, d->builtin);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/builtin_texture_helper_test.cc b/src/tint/ast/builtin_texture_helper_test.cc
new file mode 100644
index 0000000..2f80558
--- /dev/null
+++ b/src/tint/ast/builtin_texture_helper_test.cc
@@ -0,0 +1,2286 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/builtin_texture_helper_test.h"
+
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+
+namespace tint {
+namespace ast {
+namespace builtin {
+namespace test {
+
+using u32 = ProgramBuilder::u32;
+using i32 = ProgramBuilder::i32;
+using f32 = ProgramBuilder::f32;
+
+TextureOverloadCase::TextureOverloadCase(
+    ValidTextureOverload o,
+    const char* desc,
+    TextureKind tk,
+    ast::SamplerKind sk,
+    ast::TextureDimension dims,
+    TextureDataType datatype,
+    const char* f,
+    std::function<ExpressionList(ProgramBuilder*)> a)
+    : overload(o),
+      description(desc),
+      texture_kind(tk),
+      sampler_kind(sk),
+      texture_dimension(dims),
+      texture_data_type(datatype),
+      function(f),
+      args(std::move(a)) {}
+TextureOverloadCase::TextureOverloadCase(
+    ValidTextureOverload o,
+    const char* desc,
+    TextureKind tk,
+    ast::TextureDimension dims,
+    TextureDataType datatype,
+    const char* f,
+    std::function<ExpressionList(ProgramBuilder*)> a)
+    : overload(o),
+      description(desc),
+      texture_kind(tk),
+      texture_dimension(dims),
+      texture_data_type(datatype),
+      function(f),
+      args(std::move(a)) {}
+TextureOverloadCase::TextureOverloadCase(
+    ValidTextureOverload o,
+    const char* d,
+    Access acc,
+    ast::TexelFormat fmt,
+    ast::TextureDimension dims,
+    TextureDataType datatype,
+    const char* f,
+    std::function<ExpressionList(ProgramBuilder*)> a)
+    : overload(o),
+      description(d),
+      texture_kind(TextureKind::kStorage),
+      access(acc),
+      texel_format(fmt),
+      texture_dimension(dims),
+      texture_data_type(datatype),
+      function(f),
+      args(std::move(a)) {}
+TextureOverloadCase::TextureOverloadCase(const TextureOverloadCase&) = default;
+TextureOverloadCase::~TextureOverloadCase() = default;
+
+std::ostream& operator<<(std::ostream& out, const TextureKind& kind) {
+  switch (kind) {
+    case TextureKind::kRegular:
+      out << "regular";
+      break;
+    case TextureKind::kDepth:
+      out << "depth";
+      break;
+    case TextureKind::kDepthMultisampled:
+      out << "depth-multisampled";
+      break;
+    case TextureKind::kMultisampled:
+      out << "multisampled";
+      break;
+    case TextureKind::kStorage:
+      out << "storage";
+      break;
+  }
+  return out;
+}
+
+std::ostream& operator<<(std::ostream& out, const TextureDataType& ty) {
+  switch (ty) {
+    case TextureDataType::kF32:
+      out << "f32";
+      break;
+    case TextureDataType::kU32:
+      out << "u32";
+      break;
+    case TextureDataType::kI32:
+      out << "i32";
+      break;
+  }
+  return out;
+}
+
+std::ostream& operator<<(std::ostream& out, const TextureOverloadCase& data) {
+  out << "TextureOverloadCase " << static_cast<int>(data.overload) << "\n";
+  out << data.description << "\n";
+  out << "texture_kind:      " << data.texture_kind << "\n";
+  out << "sampler_kind:      ";
+  if (data.texture_kind != TextureKind::kStorage) {
+    out << data.sampler_kind;
+  } else {
+    out << "<unused>";
+  }
+  out << "\n";
+  out << "access:            " << data.access << "\n";
+  out << "texel_format:      " << data.texel_format << "\n";
+  out << "texture_dimension: " << data.texture_dimension << "\n";
+  out << "texture_data_type: " << data.texture_data_type << "\n";
+  return out;
+}
+
+const ast::Type* TextureOverloadCase::BuildResultVectorComponentType(
+    ProgramBuilder* b) const {
+  switch (texture_data_type) {
+    case ast::builtin::test::TextureDataType::kF32:
+      return b->ty.f32();
+    case ast::builtin::test::TextureDataType::kU32:
+      return b->ty.u32();
+    case ast::builtin::test::TextureDataType::kI32:
+      return b->ty.i32();
+  }
+
+  TINT_UNREACHABLE(AST, b->Diagnostics());
+  return {};
+}
+
+const ast::Variable* TextureOverloadCase::BuildTextureVariable(
+    ProgramBuilder* b) const {
+  AttributeList attrs = {
+      b->create<ast::GroupAttribute>(0),
+      b->create<ast::BindingAttribute>(0),
+  };
+  switch (texture_kind) {
+    case ast::builtin::test::TextureKind::kRegular:
+      return b->Global("texture",
+                       b->ty.sampled_texture(texture_dimension,
+                                             BuildResultVectorComponentType(b)),
+                       attrs);
+
+    case ast::builtin::test::TextureKind::kDepth:
+      return b->Global("texture", b->ty.depth_texture(texture_dimension),
+                       attrs);
+
+    case ast::builtin::test::TextureKind::kDepthMultisampled:
+      return b->Global("texture",
+                       b->ty.depth_multisampled_texture(texture_dimension),
+                       attrs);
+
+    case ast::builtin::test::TextureKind::kMultisampled:
+      return b->Global(
+          "texture",
+          b->ty.multisampled_texture(texture_dimension,
+                                     BuildResultVectorComponentType(b)),
+          attrs);
+
+    case ast::builtin::test::TextureKind::kStorage: {
+      auto* st = b->ty.storage_texture(texture_dimension, texel_format, access);
+      return b->Global("texture", st, attrs);
+    }
+  }
+
+  TINT_UNREACHABLE(AST, b->Diagnostics());
+  return nullptr;
+}
+
+const ast::Variable* TextureOverloadCase::BuildSamplerVariable(
+    ProgramBuilder* b) const {
+  AttributeList attrs = {
+      b->create<ast::GroupAttribute>(0),
+      b->create<ast::BindingAttribute>(1),
+  };
+  return b->Global("sampler", b->ty.sampler(sampler_kind), attrs);
+}
+
+std::vector<TextureOverloadCase> TextureOverloadCase::ValidCases() {
+  return {
+      {
+          ValidTextureOverload::kDimensions1d,
+          "textureDimensions(t : texture_1d<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k1d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensions2d,
+          "textureDimensions(t : texture_2d<f32>) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensions2dLevel,
+          "textureDimensions(t     : texture_2d<f32>,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensions2dArray,
+          "textureDimensions(t : texture_2d_array<f32>) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensions2dArrayLevel,
+          "textureDimensions(t     : texture_2d_array<f32>,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensions3d,
+          "textureDimensions(t : texture_3d<f32>) -> vec3<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensions3dLevel,
+          "textureDimensions(t     : texture_3d<f32>,\n"
+          "                  level : i32) -> vec3<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsCube,
+          "textureDimensions(t : texture_cube<f32>) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsCubeLevel,
+          "textureDimensions(t     : texture_cube<f32>,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsCubeArray,
+          "textureDimensions(t : texture_cube_array<f32>) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsCubeArrayLevel,
+          "textureDimensions(t     : texture_cube_array<f32>,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsMultisampled2d,
+          "textureDimensions(t : texture_multisampled_2d<f32>)-> vec2<i32>",
+          TextureKind::kMultisampled,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepth2d,
+          "textureDimensions(t : texture_depth_2d) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepth2dLevel,
+          "textureDimensions(t     : texture_depth_2d,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepth2dArray,
+          "textureDimensions(t : texture_depth_2d_array) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepth2dArrayLevel,
+          "textureDimensions(t     : texture_depth_2d_array,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepthCube,
+          "textureDimensions(t : texture_depth_cube) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepthCubeLevel,
+          "textureDimensions(t     : texture_depth_cube,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepthCubeArray,
+          "textureDimensions(t : texture_depth_cube_array) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepthCubeArrayLevel,
+          "textureDimensions(t     : texture_depth_cube_array,\n"
+          "                  level : i32) -> vec2<i32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture", 1); },
+      },
+      {
+          ValidTextureOverload::kDimensionsDepthMultisampled2d,
+          "textureDimensions(t : texture_depth_multisampled_2d) -> vec2<i32>",
+          TextureKind::kDepthMultisampled,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsStorageWO1d,
+          "textureDimensions(t : texture_storage_1d<rgba32float>) -> i32",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k1d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsStorageWO2d,
+          "textureDimensions(t : texture_storage_2d<rgba32float>) -> "
+          "vec2<i32>",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsStorageWO2dArray,
+          "textureDimensions(t : texture_storage_2d_array<rgba32float>) -> "
+          "vec2<i32>",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kDimensionsStorageWO3d,
+          "textureDimensions(t : texture_storage_3d<rgba32float>) -> "
+          "vec3<i32>",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureDimensions",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+
+      {
+          ValidTextureOverload::kGather2dF32,
+          "textureGather(component : i32,\n"
+          "              t         : texture_2d<T>,\n"
+          "              s         : sampler,\n"
+          "              coords    : vec2<f32>) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                        // component
+                               "texture",                // t
+                               "sampler",                // s
+                               b->vec2<f32>(1.f, 2.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kGather2dOffsetF32,
+          "textureGather(component : i32,\n"
+          "              t         : texture_2d<T>,\n"
+          "              s         : sampler,\n"
+          "              coords    : vec2<f32>,\n"
+          "              offset    : vec2<i32>) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                       // component
+                               "texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               b->vec2<i32>(3, 4));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGather2dArrayF32,
+          "textureGather(component   : i32,\n"
+          "              t           : texture_2d_array<T>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                       // component
+                               "texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3);                      // array index
+          },
+      },
+      {
+          ValidTextureOverload::kGather2dArrayOffsetF32,
+          "textureGather(component   : i32,\n"
+          "              t           : texture_2d_array<T>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32,\n"
+          "              offset      : vec2<i32>) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                       // component
+                               "texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCubeF32,
+          "textureGather(component : i32,\n"
+          "              t         : texture_cube<T>,\n"
+          "              s         : sampler,\n"
+          "              coords    : vec3<f32>) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                             // component
+                               "texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCubeArrayF32,
+          "textureGather(component   : i32,\n"
+          "              t           : texture_cube_array<T>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec3<f32>,\n"
+          "              array_index : i32) -> vec4<T>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList(0,                            // component
+                               "texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4);                           // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepth2dF32,
+          "textureGather(t      : texture_depth_2d,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                // t
+                               "sampler",                // s
+                               b->vec2<f32>(1.f, 2.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepth2dOffsetF32,
+          "textureGather(t      : texture_depth_2d,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>,\n"
+          "              offset : vec2<i32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               b->vec2<i32>(3, 4));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepth2dArrayF32,
+          "textureGather(t           : texture_depth_2d_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3);                      // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepth2dArrayOffsetF32,
+          "textureGather(t           : texture_depth_2d_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32,\n"
+          "              offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepthCubeF32,
+          "textureGather(t      : texture_depth_cube,\n"
+          "              s      : sampler,\n"
+          "              coords : vec3<f32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kGatherDepthCubeArrayF32,
+          "textureGather(t           : texture_depth_cube_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec3<f32>,\n"
+          "              array_index : i32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureGather",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4);                           // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepth2dF32,
+          "textureGatherCompare(t         : texture_depth_2d,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec2<f32>,\n"
+          "                     depth_ref : f32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f);                    // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepth2dOffsetF32,
+          "textureGatherCompare(t         : texture_depth_2d,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec2<f32>,\n"
+          "                     depth_ref : f32,\n"
+          "                     offset    : vec2<i32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f,                     // depth_ref
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepth2dArrayF32,
+          "textureGatherCompare(t           : texture_depth_2d_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec2<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4.f);                    // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepth2dArrayOffsetF32,
+          "textureGatherCompare(t           : texture_depth_2d_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec2<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32,\n"
+          "                     offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4.f,                     // depth_ref
+                               b->vec2<i32>(5, 6));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepthCubeF32,
+          "textureGatherCompare(t         : texture_depth_cube,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec3<f32>,\n"
+          "                     depth_ref : f32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kGatherCompareDepthCubeArrayF32,
+          "textureGatherCompare(t           : texture_depth_cube_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec3<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32) -> vec4<f32>",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureGatherCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4,                            // array_index
+                               5.f);                         // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kNumLayers2dArray,
+          "textureNumLayers(t : texture_2d_array<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLayers",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLayersCubeArray,
+          "textureNumLayers(t : texture_cube_array<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLayers",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLayersDepth2dArray,
+          "textureNumLayers(t : texture_depth_2d_array) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLayers",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLayersDepthCubeArray,
+          "textureNumLayers(t : texture_depth_cube_array) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLayers",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLayersStorageWO2dArray,
+          "textureNumLayers(t : texture_storage_2d_array<rgba32float>) -> i32",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLayers",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevels2d,
+          "textureNumLevels(t : texture_2d<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevels2dArray,
+          "textureNumLevels(t : texture_2d_array<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevels3d,
+          "textureNumLevels(t : texture_3d<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsCube,
+          "textureNumLevels(t : texture_cube<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsCubeArray,
+          "textureNumLevels(t : texture_cube_array<f32>) -> i32",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepth2d,
+          "textureNumLevels(t : texture_depth_2d) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepth2dArray,
+          "textureNumLevels(t : texture_depth_2d_array) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepthCube,
+          "textureNumLevels(t : texture_depth_cube) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumLevelsDepthCubeArray,
+          "textureNumLevels(t : texture_depth_cube_array) -> i32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureNumLevels",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kNumSamplesMultisampled2d,
+          "textureNumSamples(t : texture_multisampled_2d<f32>) -> i32",
+          TextureKind::kMultisampled,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureNumSamples",
+          [](ProgramBuilder* b) { return b->ExprList("texture"); },
+      },
+      {
+          ValidTextureOverload::kSample1dF32,
+          "textureSample(t      : texture_1d<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k1d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",  // t
+                               "sampler",  // s
+                               1.0f);      // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSample2dF32,
+          "textureSample(t      : texture_2d<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                // t
+                               "sampler",                // s
+                               b->vec2<f32>(1.f, 2.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSample2dOffsetF32,
+          "textureSample(t      : texture_2d<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>\n"
+          "              offset : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               b->vec2<i32>(3, 4));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSample2dArrayF32,
+          "textureSample(t           : texture_2d_array<f32>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3);                      // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kSample2dArrayOffsetF32,
+          "textureSample(t           : texture_2d_array<f32>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32\n"
+          "              offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSample3dF32,
+          "textureSample(t      : texture_3d<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : vec3<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSample3dOffsetF32,
+          "textureSample(t      : texture_3d<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : vec3<f32>\n"
+          "              offset : vec3<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               b->vec3<i32>(4, 5, 6));       // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCubeF32,
+          "textureSample(t      : texture_cube<f32>,\n"
+          "              s      : sampler,\n"
+          "              coords : vec3<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCubeArrayF32,
+          "textureSample(t           : texture_cube_array<f32>,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec3<f32>,\n"
+          "              array_index : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4);                           // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepth2dF32,
+          "textureSample(t      : texture_depth_2d,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                // t
+                               "sampler",                // s
+                               b->vec2<f32>(1.f, 2.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepth2dOffsetF32,
+          "textureSample(t      : texture_depth_2d,\n"
+          "              s      : sampler,\n"
+          "              coords : vec2<f32>\n"
+          "              offset : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               b->vec2<i32>(3, 4));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepth2dArrayF32,
+          "textureSample(t           : texture_depth_2d_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3);                      // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepth2dArrayOffsetF32,
+          "textureSample(t           : texture_depth_2d_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec2<f32>,\n"
+          "              array_index : i32\n"
+          "              offset      : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepthCubeF32,
+          "textureSample(t      : texture_depth_cube,\n"
+          "              s      : sampler,\n"
+          "              coords : vec3<f32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f));  // coords
+          },
+      },
+      {
+          ValidTextureOverload::kSampleDepthCubeArrayF32,
+          "textureSample(t           : texture_depth_cube_array,\n"
+          "              s           : sampler,\n"
+          "              coords      : vec3<f32>,\n"
+          "              array_index : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSample",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4);                           // array_index
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias2dF32,
+          "textureSampleBias(t      : texture_2d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec2<f32>,\n"
+          "                  bias   : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f);                    // bias
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias2dOffsetF32,
+          "textureSampleBias(t      : texture_2d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec2<f32>,\n"
+          "                  bias   : f32,\n"
+          "                  offset : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f,                     // bias
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias2dArrayF32,
+          "textureSampleBias(t           : texture_2d_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec2<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  bias        : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               4,                       // array_index
+                               3.f);                    // bias
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias2dArrayOffsetF32,
+          "textureSampleBias(t           : texture_2d_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec2<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  bias        : f32,\n"
+          "                  offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4.f,                     // bias
+                               b->vec2<i32>(5, 6));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias3dF32,
+          "textureSampleBias(t      : texture_3d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  bias   : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // bias
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBias3dOffsetF32,
+          "textureSampleBias(t      : texture_3d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  bias   : f32,\n"
+          "                  offset : vec3<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f,                          // bias
+                               b->vec3<i32>(5, 6, 7));       // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBiasCubeF32,
+          "textureSampleBias(t      : texture_cube<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  bias   : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // bias
+          },
+      },
+      {
+          ValidTextureOverload::kSampleBiasCubeArrayF32,
+          "textureSampleBias(t           : texture_cube_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec3<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  bias        : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSampleBias",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               3,                            // array_index
+                               4.f);                         // bias
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel2dF32,
+          "textureSampleLevel(t      : texture_2d<f32>,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec2<f32>,\n"
+          "                   level  : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f);                    // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel2dOffsetF32,
+          "textureSampleLevel(t      : texture_2d<f32>,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec2<f32>,\n"
+          "                   level  : f32,\n"
+          "                   offset : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f,                     // level
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel2dArrayF32,
+          "textureSampleLevel(t           : texture_2d_array<f32>,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec2<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4.f);                    // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel2dArrayOffsetF32,
+          "textureSampleLevel(t           : texture_2d_array<f32>,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec2<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : f32,\n"
+          "                   offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4.f,                     // level
+                               b->vec2<i32>(5, 6));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel3dF32,
+          "textureSampleLevel(t      : texture_3d<f32>,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec3<f32>,\n"
+          "                   level  : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevel3dOffsetF32,
+          "textureSampleLevel(t      : texture_3d<f32>,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec3<f32>,\n"
+          "                   level  : f32,\n"
+          "                   offset : vec3<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f,                          // level
+                               b->vec3<i32>(5, 6, 7));       // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelCubeF32,
+          "textureSampleLevel(t      : texture_cube<f32>,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec3<f32>,\n"
+          "                   level  : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelCubeArrayF32,
+          "textureSampleLevel(t           : texture_cube_array<f32>,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec3<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : f32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4,                            // array_index
+                               5.f);                         // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepth2dF32,
+          "textureSampleLevel(t      : texture_depth_2d,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec2<f32>,\n"
+          "                   level  : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3);                      // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepth2dOffsetF32,
+          "textureSampleLevel(t      : texture_depth_2d,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec2<f32>,\n"
+          "                   level  : i32,\n"
+          "                   offset : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // level
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepth2dArrayF32,
+          "textureSampleLevel(t           : texture_depth_2d_array,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec2<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4);                      // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepth2dArrayOffsetF32,
+          "textureSampleLevel(t           : texture_depth_2d_array,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec2<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : i32,\n"
+          "                   offset      : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               4,                       // level
+                               b->vec2<i32>(5, 6));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepthCubeF32,
+          "textureSampleLevel(t      : texture_depth_cube,\n"
+          "                   s      : sampler,\n"
+          "                   coords : vec3<f32>,\n"
+          "                   level  : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4);                           // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleLevelDepthCubeArrayF32,
+          "textureSampleLevel(t           : texture_depth_cube_array,\n"
+          "                   s           : sampler,\n"
+          "                   coords      : vec3<f32>,\n"
+          "                   array_index : i32,\n"
+          "                   level       : i32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSampleLevel",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4,                            // array_index
+                               5);                           // level
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad2dF32,
+          "textureSampleGrad(t      : texture_2d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec2<f32>\n"
+          "                  ddx    : vec2<f32>,\n"
+          "                  ddy    : vec2<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                  // t
+                               "sampler",                  // s
+                               b->vec2<f32>(1.0f, 2.0f),   // coords
+                               b->vec2<f32>(3.0f, 4.0f),   // ddx
+                               b->vec2<f32>(5.0f, 6.0f));  // ddy
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad2dOffsetF32,
+          "textureSampleGrad(t      : texture_2d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec2<f32>,\n"
+          "                  ddx    : vec2<f32>,\n"
+          "                  ddy    : vec2<f32>,\n"
+          "                  offset : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               b->vec2<f32>(3.f, 4.f),  // ddx
+                               b->vec2<f32>(5.f, 6.f),  // ddy
+                               b->vec2<i32>(7, 7));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad2dArrayF32,
+          "textureSampleGrad(t           : texture_2d_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec2<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  ddx         : vec2<f32>,\n"
+          "                  ddy         : vec2<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                // t
+                               "sampler",                // s
+                               b->vec2<f32>(1.f, 2.f),   // coords
+                               3,                        // array_index
+                               b->vec2<f32>(4.f, 5.f),   // ddx
+                               b->vec2<f32>(6.f, 7.f));  // ddy
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad2dArrayOffsetF32,
+          "textureSampleGrad(t           : texture_2d_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec2<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  ddx         : vec2<f32>,\n"
+          "                  ddy         : vec2<f32>,\n"
+          "                  offset      : vec2<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3,                       // array_index
+                               b->vec2<f32>(4.f, 5.f),  // ddx
+                               b->vec2<f32>(6.f, 7.f),  // ddy
+                               b->vec2<i32>(6, 7));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad3dF32,
+          "textureSampleGrad(t      : texture_3d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  ddx    : vec3<f32>,\n"
+          "                  ddy    : vec3<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),   // coords
+                               b->vec3<f32>(4.f, 5.f, 6.f),   // ddx
+                               b->vec3<f32>(7.f, 8.f, 9.f));  // ddy
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGrad3dOffsetF32,
+          "textureSampleGrad(t      : texture_3d<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  ddx    : vec3<f32>,\n"
+          "                  ddy    : vec3<f32>,\n"
+          "                  offset : vec3<i32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               b->vec3<f32>(4.f, 5.f, 6.f),  // ddx
+                               b->vec3<f32>(7.f, 8.f, 9.f),  // ddy
+                               b->vec3<i32>(0, 1, 2));       // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGradCubeF32,
+          "textureSampleGrad(t      : texture_cube<f32>,\n"
+          "                  s      : sampler,\n"
+          "                  coords : vec3<f32>,\n"
+          "                  ddx    : vec3<f32>,\n"
+          "                  ddy    : vec3<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                     // t
+                               "sampler",                     // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),   // coords
+                               b->vec3<f32>(4.f, 5.f, 6.f),   // ddx
+                               b->vec3<f32>(7.f, 8.f, 9.f));  // ddy
+          },
+      },
+      {
+          ValidTextureOverload::kSampleGradCubeArrayF32,
+          "textureSampleGrad(t           : texture_cube_array<f32>,\n"
+          "                  s           : sampler,\n"
+          "                  coords      : vec3<f32>,\n"
+          "                  array_index : i32,\n"
+          "                  ddx         : vec3<f32>,\n"
+          "                  ddy         : vec3<f32>) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::SamplerKind::kSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSampleGrad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                      // t
+                               "sampler",                      // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),    // coords
+                               4,                              // array_index
+                               b->vec3<f32>(5.f, 6.f, 7.f),    // ddx
+                               b->vec3<f32>(8.f, 9.f, 10.f));  // ddy
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepth2dF32,
+          "textureSampleCompare(t         : texture_depth_2d,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec2<f32>,\n"
+          "                     depth_ref : f32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f);                    // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepth2dOffsetF32,
+          "textureSampleCompare(t         : texture_depth_2d,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec2<f32>,\n"
+          "                     depth_ref : f32,\n"
+          "                     offset    : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               3.f,                     // depth_ref
+                               b->vec2<i32>(4, 5));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepth2dArrayF32,
+          "textureSampleCompare(t           : texture_depth_2d_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec2<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               4,                       // array_index
+                               3.f);                    // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepth2dArrayOffsetF32,
+          "textureSampleCompare(t           : texture_depth_2d_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec2<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32,\n"
+          "                     offset      : vec2<i32>) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",               // t
+                               "sampler",               // s
+                               b->vec2<f32>(1.f, 2.f),  // coords
+                               4,                       // array_index
+                               3.f,                     // depth_ref
+                               b->vec2<i32>(5, 6));     // offset
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepthCubeF32,
+          "textureSampleCompare(t         : texture_depth_cube,\n"
+          "                     s         : sampler_comparison,\n"
+          "                     coords    : vec3<f32>,\n"
+          "                     depth_ref : f32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::kCube,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4.f);                         // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kSampleCompareDepthCubeArrayF32,
+          "textureSampleCompare(t           : texture_depth_cube_array,\n"
+          "                     s           : sampler_comparison,\n"
+          "                     coords      : vec3<f32>,\n"
+          "                     array_index : i32,\n"
+          "                     depth_ref   : f32) -> f32",
+          TextureKind::kDepth,
+          ast::SamplerKind::kComparisonSampler,
+          ast::TextureDimension::kCubeArray,
+          TextureDataType::kF32,
+          "textureSampleCompare",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                    // t
+                               "sampler",                    // s
+                               b->vec3<f32>(1.f, 2.f, 3.f),  // coords
+                               4,                            // array_index
+                               5.f);                         // depth_ref
+          },
+      },
+      {
+          ValidTextureOverload::kLoad1dLevelF32,
+          "textureLoad(t      : texture_1d<f32>,\n"
+          "            coords : i32,\n"
+          "            level  : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k1d,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",  // t
+                               1,          // coords
+                               3);         // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad1dLevelU32,
+          "textureLoad(t      : texture_1d<u32>,\n"
+          "            coords : i32,\n"
+          "            level  : i32) -> vec4<u32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k1d,
+          TextureDataType::kU32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",  // t
+                               1,          // coords
+                               3);         // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad1dLevelI32,
+          "textureLoad(t      : texture_1d<i32>,\n"
+          "            coords : i32,\n"
+          "            level  : i32) -> vec4<i32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k1d,
+          TextureDataType::kI32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",  // t
+                               1,          // coords
+                               3);         // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dLevelF32,
+          "textureLoad(t      : texture_2d<f32>,\n"
+          "            coords : vec2<i32>,\n"
+          "            level  : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dLevelU32,
+          "textureLoad(t      : texture_2d<u32>,\n"
+          "            coords : vec2<i32>,\n"
+          "            level  : i32) -> vec4<u32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2d,
+          TextureDataType::kU32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dLevelI32,
+          "textureLoad(t      : texture_2d<i32>,\n"
+          "            coords : vec2<i32>,\n"
+          "            level  : i32) -> vec4<i32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2d,
+          TextureDataType::kI32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dArrayLevelF32,
+          "textureLoad(t           : texture_2d_array<f32>,\n"
+          "            coords      : vec2<i32>,\n"
+          "            array_index : i32,\n"
+          "            level       : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3,                   // array_index
+                               4);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dArrayLevelU32,
+          "textureLoad(t           : texture_2d_array<u32>,\n"
+          "            coords      : vec2<i32>,\n"
+          "            array_index : i32,\n"
+          "            level       : i32) -> vec4<u32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kU32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3,                   // array_index
+                               4);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad2dArrayLevelI32,
+          "textureLoad(t           : texture_2d_array<i32>,\n"
+          "            coords      : vec2<i32>,\n"
+          "            array_index : i32,\n"
+          "            level       : i32) -> vec4<i32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kI32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3,                   // array_index
+                               4);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad3dLevelF32,
+          "textureLoad(t      : texture_3d<f32>,\n"
+          "            coords : vec3<i32>,\n"
+          "            level  : i32) -> vec4<f32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",              // t
+                               b->vec3<i32>(1, 2, 3),  // coords
+                               4);                     // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad3dLevelU32,
+          "textureLoad(t      : texture_3d<u32>,\n"
+          "            coords : vec3<i32>,\n"
+          "            level  : i32) -> vec4<u32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k3d,
+          TextureDataType::kU32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",              // t
+                               b->vec3<i32>(1, 2, 3),  // coords
+                               4);                     // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoad3dLevelI32,
+          "textureLoad(t      : texture_3d<i32>,\n"
+          "            coords : vec3<i32>,\n"
+          "            level  : i32) -> vec4<i32>",
+          TextureKind::kRegular,
+          ast::TextureDimension::k3d,
+          TextureDataType::kI32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",              // t
+                               b->vec3<i32>(1, 2, 3),  // coords
+                               4);                     // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoadMultisampled2dF32,
+          "textureLoad(t            : texture_multisampled_2d<f32>,\n"
+          "            coords       : vec2<i32>,\n"
+          "            sample_index : i32) -> vec4<f32>",
+          TextureKind::kMultisampled,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // sample_index
+          },
+      },
+      {
+          ValidTextureOverload::kLoadMultisampled2dU32,
+          "textureLoad(t            : texture_multisampled_2d<u32>,\n"
+          "            coords       : vec2<i32>,\n"
+          "            sample_index : i32) -> vec4<u32>",
+          TextureKind::kMultisampled,
+          ast::TextureDimension::k2d,
+          TextureDataType::kU32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // sample_index
+          },
+      },
+      {
+          ValidTextureOverload::kLoadMultisampled2dI32,
+          "textureLoad(t            : texture_multisampled_2d<i32>,\n"
+          "            coords       : vec2<i32>,\n"
+          "            sample_index : i32) -> vec4<i32>",
+          TextureKind::kMultisampled,
+          ast::TextureDimension::k2d,
+          TextureDataType::kI32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // sample_index
+          },
+      },
+      {
+          ValidTextureOverload::kLoadDepth2dLevelF32,
+          "textureLoad(t      : texture_depth_2d,\n"
+          "            coords : vec2<i32>,\n"
+          "            level  : i32) -> f32",
+          TextureKind::kDepth,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kLoadDepth2dArrayLevelF32,
+          "textureLoad(t           : texture_depth_2d_array,\n"
+          "            coords      : vec2<i32>,\n"
+          "            array_index : i32,\n"
+          "            level       : i32) -> f32",
+          TextureKind::kDepth,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureLoad",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3,                   // array_index
+                               4);                  // level
+          },
+      },
+      {
+          ValidTextureOverload::kStoreWO1dRgba32float,
+          "textureStore(t      : texture_storage_1d<rgba32float>,\n"
+          "             coords : i32,\n"
+          "             value  : vec4<T>)",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k1d,
+          TextureDataType::kF32,
+          "textureStore",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                          // t
+                               1,                                  // coords
+                               b->vec4<f32>(2.f, 3.f, 4.f, 5.f));  // value
+          },
+      },
+      {
+          ValidTextureOverload::kStoreWO2dRgba32float,
+          "textureStore(t      : texture_storage_2d<rgba32float>,\n"
+          "             coords : vec2<i32>,\n"
+          "             value  : vec4<T>)",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k2d,
+          TextureDataType::kF32,
+          "textureStore",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                          // t
+                               b->vec2<i32>(1, 2),                 // coords
+                               b->vec4<f32>(3.f, 4.f, 5.f, 6.f));  // value
+          },
+      },
+      {
+          ValidTextureOverload::kStoreWO2dArrayRgba32float,
+          "textureStore(t           : texture_storage_2d_array<rgba32float>,\n"
+          "             coords      : vec2<i32>,\n"
+          "             array_index : i32,\n"
+          "             value       : vec4<T>)",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k2dArray,
+          TextureDataType::kF32,
+          "textureStore",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",           // t
+                               b->vec2<i32>(1, 2),  // coords
+                               3,                   // array_index
+                               b->vec4<f32>(4.f, 5.f, 6.f, 7.f));  // value
+          },
+      },
+      {
+          ValidTextureOverload::kStoreWO3dRgba32float,
+          "textureStore(t      : texture_storage_3d<rgba32float>,\n"
+          "             coords : vec3<i32>,\n"
+          "             value  : vec4<T>)",
+          ast::Access::kWrite,
+          ast::TexelFormat::kRgba32Float,
+          ast::TextureDimension::k3d,
+          TextureDataType::kF32,
+          "textureStore",
+          [](ProgramBuilder* b) {
+            return b->ExprList("texture",                          // t
+                               b->vec3<i32>(1, 2, 3),              // coords
+                               b->vec4<f32>(4.f, 5.f, 6.f, 7.f));  // value
+          },
+      },
+  };
+}
+
+bool ReturnsVoid(ValidTextureOverload texture_overload) {
+  switch (texture_overload) {
+    case ValidTextureOverload::kStoreWO1dRgba32float:
+    case ValidTextureOverload::kStoreWO2dRgba32float:
+    case ValidTextureOverload::kStoreWO2dArrayRgba32float:
+    case ValidTextureOverload::kStoreWO3dRgba32float:
+      return true;
+    default:
+      return false;
+  }
+}
+
+}  // namespace test
+}  // namespace builtin
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/builtin_texture_helper_test.h b/src/tint/ast/builtin_texture_helper_test.h
new file mode 100644
index 0000000..751f51b
--- /dev/null
+++ b/src/tint/ast/builtin_texture_helper_test.h
@@ -0,0 +1,269 @@
+// Copyright 2020 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_AST_BUILTIN_TEXTURE_HELPER_TEST_H_
+#define SRC_TINT_AST_BUILTIN_TEXTURE_HELPER_TEST_H_
+
+#include <vector>
+
+#include "src/tint/ast/access.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/storage_texture_type.h"
+
+namespace tint {
+namespace ast {
+namespace builtin {
+namespace test {
+
+enum class TextureKind {
+  kRegular,
+  kDepth,
+  kDepthMultisampled,
+  kMultisampled,
+  kStorage
+};
+enum class TextureDataType { kF32, kU32, kI32 };
+
+std::ostream& operator<<(std::ostream& out, const TextureKind& kind);
+std::ostream& operator<<(std::ostream& out, const TextureDataType& ty);
+
+/// Non-exhaustive list of valid texture overloads
+enum class ValidTextureOverload {
+  kDimensions1d,
+  kDimensions2d,
+  kDimensions2dLevel,
+  kDimensions2dArray,
+  kDimensions2dArrayLevel,
+  kDimensions3d,
+  kDimensions3dLevel,
+  kDimensionsCube,
+  kDimensionsCubeLevel,
+  kDimensionsCubeArray,
+  kDimensionsCubeArrayLevel,
+  kDimensionsMultisampled2d,
+  kDimensionsDepth2d,
+  kDimensionsDepth2dLevel,
+  kDimensionsDepth2dArray,
+  kDimensionsDepth2dArrayLevel,
+  kDimensionsDepthCube,
+  kDimensionsDepthCubeLevel,
+  kDimensionsDepthCubeArray,
+  kDimensionsDepthCubeArrayLevel,
+  kDimensionsDepthMultisampled2d,
+  kDimensionsStorageWO1d,
+  kDimensionsStorageWO2d,
+  kDimensionsStorageWO2dArray,
+  kDimensionsStorageWO3d,
+  kGather2dF32,
+  kGather2dOffsetF32,
+  kGather2dArrayF32,
+  kGather2dArrayOffsetF32,
+  kGatherCubeF32,
+  kGatherCubeArrayF32,
+  kGatherDepth2dF32,
+  kGatherDepth2dOffsetF32,
+  kGatherDepth2dArrayF32,
+  kGatherDepth2dArrayOffsetF32,
+  kGatherDepthCubeF32,
+  kGatherDepthCubeArrayF32,
+  kGatherCompareDepth2dF32,
+  kGatherCompareDepth2dOffsetF32,
+  kGatherCompareDepth2dArrayF32,
+  kGatherCompareDepth2dArrayOffsetF32,
+  kGatherCompareDepthCubeF32,
+  kGatherCompareDepthCubeArrayF32,
+  kNumLayers2dArray,
+  kNumLayersCubeArray,
+  kNumLayersDepth2dArray,
+  kNumLayersDepthCubeArray,
+  kNumLayersStorageWO2dArray,
+  kNumLevels2d,
+  kNumLevels2dArray,
+  kNumLevels3d,
+  kNumLevelsCube,
+  kNumLevelsCubeArray,
+  kNumLevelsDepth2d,
+  kNumLevelsDepth2dArray,
+  kNumLevelsDepthCube,
+  kNumLevelsDepthCubeArray,
+  kNumSamplesMultisampled2d,
+  kNumSamplesDepthMultisampled2d,
+  kSample1dF32,
+  kSample2dF32,
+  kSample2dOffsetF32,
+  kSample2dArrayF32,
+  kSample2dArrayOffsetF32,
+  kSample3dF32,
+  kSample3dOffsetF32,
+  kSampleCubeF32,
+  kSampleCubeArrayF32,
+  kSampleDepth2dF32,
+  kSampleDepth2dOffsetF32,
+  kSampleDepth2dArrayF32,
+  kSampleDepth2dArrayOffsetF32,
+  kSampleDepthCubeF32,
+  kSampleDepthCubeArrayF32,
+  kSampleBias2dF32,
+  kSampleBias2dOffsetF32,
+  kSampleBias2dArrayF32,
+  kSampleBias2dArrayOffsetF32,
+  kSampleBias3dF32,
+  kSampleBias3dOffsetF32,
+  kSampleBiasCubeF32,
+  kSampleBiasCubeArrayF32,
+  kSampleLevel2dF32,
+  kSampleLevel2dOffsetF32,
+  kSampleLevel2dArrayF32,
+  kSampleLevel2dArrayOffsetF32,
+  kSampleLevel3dF32,
+  kSampleLevel3dOffsetF32,
+  kSampleLevelCubeF32,
+  kSampleLevelCubeArrayF32,
+  kSampleLevelDepth2dF32,
+  kSampleLevelDepth2dOffsetF32,
+  kSampleLevelDepth2dArrayF32,
+  kSampleLevelDepth2dArrayOffsetF32,
+  kSampleLevelDepthCubeF32,
+  kSampleLevelDepthCubeArrayF32,
+  kSampleGrad2dF32,
+  kSampleGrad2dOffsetF32,
+  kSampleGrad2dArrayF32,
+  kSampleGrad2dArrayOffsetF32,
+  kSampleGrad3dF32,
+  kSampleGrad3dOffsetF32,
+  kSampleGradCubeF32,
+  kSampleGradCubeArrayF32,
+  kSampleCompareDepth2dF32,
+  kSampleCompareDepth2dOffsetF32,
+  kSampleCompareDepth2dArrayF32,
+  kSampleCompareDepth2dArrayOffsetF32,
+  kSampleCompareDepthCubeF32,
+  kSampleCompareDepthCubeArrayF32,
+  kSampleCompareLevelDepth2dF32,
+  kSampleCompareLevelDepth2dOffsetF32,
+  kSampleCompareLevelDepth2dArrayF32,
+  kSampleCompareLevelDepth2dArrayOffsetF32,
+  kSampleCompareLevelDepthCubeF32,
+  kSampleCompareLevelDepthCubeArrayF32,
+  kLoad1dLevelF32,
+  kLoad1dLevelU32,
+  kLoad1dLevelI32,
+  kLoad2dLevelF32,
+  kLoad2dLevelU32,
+  kLoad2dLevelI32,
+  kLoad2dArrayLevelF32,
+  kLoad2dArrayLevelU32,
+  kLoad2dArrayLevelI32,
+  kLoad3dLevelF32,
+  kLoad3dLevelU32,
+  kLoad3dLevelI32,
+  kLoadMultisampled2dF32,
+  kLoadMultisampled2dU32,
+  kLoadMultisampled2dI32,
+  kLoadDepth2dLevelF32,
+  kLoadDepth2dArrayLevelF32,
+  kLoadDepthMultisampled2dF32,
+  kStoreWO1dRgba32float,       // Not permutated for all texel formats
+  kStoreWO2dRgba32float,       // Not permutated for all texel formats
+  kStoreWO2dArrayRgba32float,  // Not permutated for all texel formats
+  kStoreWO3dRgba32float,       // Not permutated for all texel formats
+};
+
+/// @param texture_overload the ValidTextureOverload
+/// @returns true if the ValidTextureOverload builtin returns no value.
+bool ReturnsVoid(ValidTextureOverload texture_overload);
+
+/// Describes a texture builtin overload
+struct TextureOverloadCase {
+  /// Constructor for textureSample...() functions
+  TextureOverloadCase(ValidTextureOverload,
+                      const char*,
+                      TextureKind,
+                      ast::SamplerKind,
+                      ast::TextureDimension,
+                      TextureDataType,
+                      const char*,
+                      std::function<ExpressionList(ProgramBuilder*)>);
+  /// Constructor for textureLoad() functions with non-storage textures
+  TextureOverloadCase(ValidTextureOverload,
+                      const char*,
+                      TextureKind,
+                      ast::TextureDimension,
+                      TextureDataType,
+                      const char*,
+                      std::function<ExpressionList(ProgramBuilder*)>);
+  /// Constructor for textureLoad() with storage textures
+  TextureOverloadCase(ValidTextureOverload,
+                      const char*,
+                      Access,
+                      ast::TexelFormat,
+                      ast::TextureDimension,
+                      TextureDataType,
+                      const char*,
+                      std::function<ExpressionList(ProgramBuilder*)>);
+  /// Copy constructor
+  TextureOverloadCase(const TextureOverloadCase&);
+  /// Destructor
+  ~TextureOverloadCase();
+
+  /// @return a vector containing a large number (non-exhaustive) of valid
+  /// texture overloads.
+  static std::vector<TextureOverloadCase> ValidCases();
+
+  /// @param builder the AST builder used for the test
+  /// @returns the vector component type of the texture function return value
+  const ast::Type* BuildResultVectorComponentType(
+      ProgramBuilder* builder) const;
+  /// @param builder the AST builder used for the test
+  /// @returns a variable holding the test texture, automatically registered as
+  /// a global variable.
+  const ast::Variable* BuildTextureVariable(ProgramBuilder* builder) const;
+  /// @param builder the AST builder used for the test
+  /// @returns a Variable holding the test sampler, automatically registered as
+  /// a global variable.
+  const ast::Variable* BuildSamplerVariable(ProgramBuilder* builder) const;
+
+  /// The enumerator for this overload
+  const ValidTextureOverload overload;
+  /// A human readable description of the overload
+  const char* const description;
+  /// The texture kind for the texture parameter
+  const TextureKind texture_kind;
+  /// The sampler kind for the sampler parameter
+  /// Used only when texture_kind is not kStorage
+  ast::SamplerKind const sampler_kind = ast::SamplerKind::kSampler;
+  /// The access control for the storage texture
+  /// Used only when texture_kind is kStorage
+  Access const access = Access::kReadWrite;
+  /// The image format for the storage texture
+  /// Used only when texture_kind is kStorage
+  ast::TexelFormat const texel_format = ast::TexelFormat::kNone;
+  /// The dimensions of the texture parameter
+  ast::TextureDimension const texture_dimension;
+  /// The data type of the texture parameter
+  const TextureDataType texture_data_type;
+  /// Name of the function. e.g. `textureSample`, `textureSampleGrad`, etc
+  const char* const function;
+  /// A function that builds the AST arguments for the overload
+  std::function<ExpressionList(ProgramBuilder*)> const args;
+};
+
+std::ostream& operator<<(std::ostream& out, const TextureOverloadCase& data);
+
+}  // namespace test
+}  // namespace builtin
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_BUILTIN_TEXTURE_HELPER_TEST_H_
diff --git a/src/tint/ast/call_expression.cc b/src/tint/ast/call_expression.cc
new file mode 100644
index 0000000..7abf4d7
--- /dev/null
+++ b/src/tint/ast/call_expression.cc
@@ -0,0 +1,78 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/call_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::CallExpression);
+
+namespace tint {
+namespace ast {
+
+namespace {
+CallExpression::Target ToTarget(const IdentifierExpression* name) {
+  CallExpression::Target target;
+  target.name = name;
+  return target;
+}
+CallExpression::Target ToTarget(const Type* type) {
+  CallExpression::Target target;
+  target.type = type;
+  return target;
+}
+}  // namespace
+
+CallExpression::CallExpression(ProgramID pid,
+                               const Source& src,
+                               const IdentifierExpression* name,
+                               ExpressionList a)
+    : Base(pid, src), target(ToTarget(name)), args(a) {
+  TINT_ASSERT(AST, name);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
+  for (auto* arg : args) {
+    TINT_ASSERT(AST, arg);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, arg, program_id);
+  }
+}
+
+CallExpression::CallExpression(ProgramID pid,
+                               const Source& src,
+                               const Type* type,
+                               ExpressionList a)
+    : Base(pid, src), target(ToTarget(type)), args(a) {
+  TINT_ASSERT(AST, type);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, type, program_id);
+  for (auto* arg : args) {
+    TINT_ASSERT(AST, arg);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, arg, program_id);
+  }
+}
+
+CallExpression::CallExpression(CallExpression&&) = default;
+
+CallExpression::~CallExpression() = default;
+
+const CallExpression* CallExpression::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto p = ctx->Clone(args);
+  return target.name
+             ? ctx->dst->create<CallExpression>(src, ctx->Clone(target.name), p)
+             : ctx->dst->create<CallExpression>(src, ctx->Clone(target.type),
+                                                p);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/call_expression.h b/src/tint/ast/call_expression.h
new file mode 100644
index 0000000..efb1841
--- /dev/null
+++ b/src/tint/ast/call_expression.h
@@ -0,0 +1,84 @@
+// Copyright 2020 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_AST_CALL_EXPRESSION_H_
+#define SRC_TINT_AST_CALL_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declarations.
+class Type;
+class IdentifierExpression;
+
+/// A call expression - represents either a:
+/// * sem::Function
+/// * sem::Builtin
+/// * sem::TypeConstructor
+/// * sem::TypeConversion
+class CallExpression final : public Castable<CallExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the call expression source
+  /// @param name the function or type name
+  /// @param args the arguments
+  CallExpression(ProgramID program_id,
+                 const Source& source,
+                 const IdentifierExpression* name,
+                 ExpressionList args);
+
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the call expression source
+  /// @param type the type
+  /// @param args the arguments
+  CallExpression(ProgramID program_id,
+                 const Source& source,
+                 const Type* type,
+                 ExpressionList args);
+
+  /// Move constructor
+  CallExpression(CallExpression&&);
+  ~CallExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const CallExpression* Clone(CloneContext* ctx) const override;
+
+  /// Target is either an identifier, or a Type.
+  /// One of these must be nullptr and the other a non-nullptr.
+  struct Target {
+    /// name is a function or builtin to call, or type name to construct or
+    /// cast-to
+    const IdentifierExpression* name = nullptr;
+    /// type to construct or cast-to
+    const Type* type = nullptr;
+  };
+
+  /// The target function
+  const Target target;
+
+  /// The arguments
+  const ExpressionList args;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_CALL_EXPRESSION_H_
diff --git a/src/tint/ast/call_expression_test.cc b/src/tint/ast/call_expression_test.cc
new file mode 100644
index 0000000..a150af6
--- /dev/null
+++ b/src/tint/ast/call_expression_test.cc
@@ -0,0 +1,149 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using CallExpressionTest = TestHelper;
+
+TEST_F(CallExpressionTest, CreationIdentifier) {
+  auto* func = Expr("func");
+  ExpressionList params;
+  params.push_back(Expr("param1"));
+  params.push_back(Expr("param2"));
+
+  auto* stmt = create<CallExpression>(func, params);
+  EXPECT_EQ(stmt->target.name, func);
+  EXPECT_EQ(stmt->target.type, nullptr);
+
+  const auto& vec = stmt->args;
+  ASSERT_EQ(vec.size(), 2u);
+  EXPECT_EQ(vec[0], params[0]);
+  EXPECT_EQ(vec[1], params[1]);
+}
+
+TEST_F(CallExpressionTest, CreationIdentifier_WithSource) {
+  auto* func = Expr("func");
+  auto* stmt = create<CallExpression>(Source{{20, 2}}, func, ExpressionList{});
+  EXPECT_EQ(stmt->target.name, func);
+  EXPECT_EQ(stmt->target.type, nullptr);
+
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(CallExpressionTest, CreationType) {
+  auto* type = ty.f32();
+  ExpressionList params;
+  params.push_back(Expr("param1"));
+  params.push_back(Expr("param2"));
+
+  auto* stmt = create<CallExpression>(type, params);
+  EXPECT_EQ(stmt->target.name, nullptr);
+  EXPECT_EQ(stmt->target.type, type);
+
+  const auto& vec = stmt->args;
+  ASSERT_EQ(vec.size(), 2u);
+  EXPECT_EQ(vec[0], params[0]);
+  EXPECT_EQ(vec[1], params[1]);
+}
+
+TEST_F(CallExpressionTest, CreationType_WithSource) {
+  auto* type = ty.f32();
+  auto* stmt = create<CallExpression>(Source{{20, 2}}, type, ExpressionList{});
+  EXPECT_EQ(stmt->target.name, nullptr);
+  EXPECT_EQ(stmt->target.type, type);
+
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(CallExpressionTest, IsCall) {
+  auto* func = Expr("func");
+  auto* stmt = create<CallExpression>(func, ExpressionList{});
+  EXPECT_TRUE(stmt->Is<CallExpression>());
+}
+
+TEST_F(CallExpressionTest, Assert_Null_Identifier) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CallExpression>(static_cast<IdentifierExpression*>(nullptr),
+                                 ExpressionList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallExpressionTest, Assert_Null_Type) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CallExpression>(static_cast<Type*>(nullptr), ExpressionList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallExpressionTest, Assert_Null_Param) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        ExpressionList params;
+        params.push_back(b.Expr("param1"));
+        params.push_back(nullptr);
+        params.push_back(b.Expr("param2"));
+        b.create<CallExpression>(b.Expr("func"), params);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallExpressionTest, Assert_DifferentProgramID_Identifier) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CallExpression>(b2.Expr("func"), ExpressionList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallExpressionTest, Assert_DifferentProgramID_Type) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CallExpression>(b2.ty.f32(), ExpressionList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallExpressionTest, Assert_DifferentProgramID_Param) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CallExpression>(b1.Expr("func"),
+                                  ExpressionList{b2.Expr("param1")});
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/call_statement.cc b/src/tint/ast/call_statement.cc
new file mode 100644
index 0000000..be2e97c
--- /dev/null
+++ b/src/tint/ast/call_statement.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/call_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::CallStatement);
+
+namespace tint {
+namespace ast {
+
+CallStatement::CallStatement(ProgramID pid,
+                             const Source& src,
+                             const CallExpression* call)
+    : Base(pid, src), expr(call) {
+  TINT_ASSERT(AST, expr);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
+}
+
+CallStatement::CallStatement(CallStatement&&) = default;
+
+CallStatement::~CallStatement() = default;
+
+const CallStatement* CallStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* call = ctx->Clone(expr);
+  return ctx->dst->create<CallStatement>(src, call);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/call_statement.h b/src/tint/ast/call_statement.h
new file mode 100644
index 0000000..b9e0c4c
--- /dev/null
+++ b/src/tint/ast/call_statement.h
@@ -0,0 +1,49 @@
+// Copyright 2020 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_AST_CALL_STATEMENT_H_
+#define SRC_TINT_AST_CALL_STATEMENT_H_
+
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A call expression
+class CallStatement final : public Castable<CallStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node for the statement
+  /// @param call the function
+  CallStatement(ProgramID pid, const Source& src, const CallExpression* call);
+  /// Move constructor
+  CallStatement(CallStatement&&);
+  ~CallStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const CallStatement* Clone(CloneContext* ctx) const override;
+
+  /// The call expression
+  const CallExpression* const expr;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_CALL_STATEMENT_H_
diff --git a/src/tint/ast/call_statement_test.cc b/src/tint/ast/call_statement_test.cc
new file mode 100644
index 0000000..1267a6d
--- /dev/null
+++ b/src/tint/ast/call_statement_test.cc
@@ -0,0 +1,60 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/call_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using CallStatementTest = TestHelper;
+
+TEST_F(CallStatementTest, Creation) {
+  auto* expr = create<CallExpression>(Expr("func"), ExpressionList{});
+
+  auto* c = create<CallStatement>(expr);
+  EXPECT_EQ(c->expr, expr);
+}
+
+TEST_F(CallStatementTest, IsCall) {
+  auto* c = create<CallStatement>(Call("f"));
+  EXPECT_TRUE(c->Is<CallStatement>());
+}
+
+TEST_F(CallStatementTest, Assert_Null_Call) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CallStatement>(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CallStatementTest, Assert_DifferentProgramID_Call) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CallStatement>(
+            b2.create<CallExpression>(b2.Expr("func"), ExpressionList{}));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/case_statement.cc b/src/tint/ast/case_statement.cc
new file mode 100644
index 0000000..98a2277
--- /dev/null
+++ b/src/tint/ast/case_statement.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/case_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::CaseStatement);
+
+namespace tint {
+namespace ast {
+
+CaseStatement::CaseStatement(ProgramID pid,
+                             const Source& src,
+                             CaseSelectorList s,
+                             const BlockStatement* b)
+    : Base(pid, src), selectors(s), body(b) {
+  TINT_ASSERT(AST, body);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+  for (auto* selector : selectors) {
+    TINT_ASSERT(AST, selector);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, selector, program_id);
+  }
+}
+
+CaseStatement::CaseStatement(CaseStatement&&) = default;
+
+CaseStatement::~CaseStatement() = default;
+
+const CaseStatement* CaseStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto sel = ctx->Clone(selectors);
+  auto* b = ctx->Clone(body);
+  return ctx->dst->create<CaseStatement>(src, sel, b);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/case_statement.h b/src/tint/ast/case_statement.h
new file mode 100644
index 0000000..99fd81d5
--- /dev/null
+++ b/src/tint/ast/case_statement.h
@@ -0,0 +1,67 @@
+// Copyright 2020 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_AST_CASE_STATEMENT_H_
+#define SRC_TINT_AST_CASE_STATEMENT_H_
+
+#include <vector>
+
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/int_literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A list of case literals
+using CaseSelectorList = std::vector<const IntLiteralExpression*>;
+
+/// A case statement
+class CaseStatement final : public Castable<CaseStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param selectors the case selectors
+  /// @param body the case body
+  CaseStatement(ProgramID pid,
+                const Source& src,
+                CaseSelectorList selectors,
+                const BlockStatement* body);
+  /// Move constructor
+  CaseStatement(CaseStatement&&);
+  ~CaseStatement() override;
+
+  /// @returns true if this is a default statement
+  bool IsDefault() const { return selectors.empty(); }
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const CaseStatement* Clone(CloneContext* ctx) const override;
+
+  /// The case selectors, empty if none set
+  const CaseSelectorList selectors;
+
+  /// The case body
+  const BlockStatement* const body;
+};
+
+/// A list of case statements
+using CaseStatementList = std::vector<const CaseStatement*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_CASE_STATEMENT_H_
diff --git a/src/tint/ast/case_statement_test.cc b/src/tint/ast/case_statement_test.cc
new file mode 100644
index 0000000..b716f9f
--- /dev/null
+++ b/src/tint/ast/case_statement_test.cc
@@ -0,0 +1,137 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/case_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using CaseStatementTest = TestHelper;
+
+TEST_F(CaseStatementTest, Creation_i32) {
+  CaseSelectorList b;
+  auto* selector = create<SintLiteralExpression>(2);
+  b.push_back(selector);
+
+  auto* discard = create<DiscardStatement>();
+  auto* body = create<BlockStatement>(StatementList{discard});
+
+  auto* c = create<CaseStatement>(b, body);
+  ASSERT_EQ(c->selectors.size(), 1u);
+  EXPECT_EQ(c->selectors[0], selector);
+  ASSERT_EQ(c->body->statements.size(), 1u);
+  EXPECT_EQ(c->body->statements[0], discard);
+}
+
+TEST_F(CaseStatementTest, Creation_u32) {
+  CaseSelectorList b;
+  auto* selector = create<UintLiteralExpression>(2u);
+  b.push_back(selector);
+
+  auto* discard = create<DiscardStatement>();
+  auto* body = create<BlockStatement>(StatementList{discard});
+
+  auto* c = create<CaseStatement>(b, body);
+  ASSERT_EQ(c->selectors.size(), 1u);
+  EXPECT_EQ(c->selectors[0], selector);
+  ASSERT_EQ(c->body->statements.size(), 1u);
+  EXPECT_EQ(c->body->statements[0], discard);
+}
+
+TEST_F(CaseStatementTest, Creation_WithSource) {
+  CaseSelectorList b;
+  b.push_back(create<SintLiteralExpression>(2));
+
+  auto* body = create<BlockStatement>(StatementList{
+      create<DiscardStatement>(),
+  });
+  auto* c = create<CaseStatement>(Source{Source::Location{20, 2}}, b, body);
+  auto src = c->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(CaseStatementTest, IsDefault_WithoutSelectors) {
+  auto* body = create<BlockStatement>(StatementList{
+      create<DiscardStatement>(),
+  });
+  auto* c = create<CaseStatement>(CaseSelectorList{}, body);
+  EXPECT_TRUE(c->IsDefault());
+}
+
+TEST_F(CaseStatementTest, IsDefault_WithSelectors) {
+  CaseSelectorList b;
+  b.push_back(create<SintLiteralExpression>(2));
+
+  auto* c = create<CaseStatement>(b, create<BlockStatement>(StatementList{}));
+  EXPECT_FALSE(c->IsDefault());
+}
+
+TEST_F(CaseStatementTest, IsCase) {
+  auto* c = create<CaseStatement>(CaseSelectorList{},
+                                  create<BlockStatement>(StatementList{}));
+  EXPECT_TRUE(c->Is<CaseStatement>());
+}
+
+TEST_F(CaseStatementTest, Assert_Null_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CaseStatement>(CaseSelectorList{}, nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CaseStatementTest, Assert_Null_Selector) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CaseStatement>(CaseSelectorList{nullptr},
+                                b.create<BlockStatement>(StatementList{}));
+      },
+      "internal compiler error");
+}
+
+TEST_F(CaseStatementTest, Assert_DifferentProgramID_Call) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CaseStatement>(CaseSelectorList{},
+                                 b2.create<BlockStatement>(StatementList{}));
+      },
+      "internal compiler error");
+}
+
+TEST_F(CaseStatementTest, Assert_DifferentProgramID_Selector) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CaseStatement>(
+            CaseSelectorList{b2.create<SintLiteralExpression>(2)},
+            b1.create<BlockStatement>(StatementList{}));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/compound_assignment_statement.cc b/src/tint/ast/compound_assignment_statement.cc
new file mode 100644
index 0000000..cc1f07c
--- /dev/null
+++ b/src/tint/ast/compound_assignment_statement.cc
@@ -0,0 +1,51 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/compound_assignment_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::CompoundAssignmentStatement);
+
+namespace tint {
+namespace ast {
+
+CompoundAssignmentStatement::CompoundAssignmentStatement(ProgramID pid,
+                                                         const Source& src,
+                                                         const Expression* l,
+                                                         const Expression* r,
+                                                         BinaryOp o)
+    : Base(pid, src), lhs(l), rhs(r), op(o) {
+  TINT_ASSERT(AST, lhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, lhs, program_id);
+  TINT_ASSERT(AST, rhs);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, rhs, program_id);
+}
+
+CompoundAssignmentStatement::CompoundAssignmentStatement(
+    CompoundAssignmentStatement&&) = default;
+
+CompoundAssignmentStatement::~CompoundAssignmentStatement() = default;
+
+const CompoundAssignmentStatement* CompoundAssignmentStatement::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* l = ctx->Clone(lhs);
+  auto* r = ctx->Clone(rhs);
+  return ctx->dst->create<CompoundAssignmentStatement>(src, l, r, op);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/compound_assignment_statement.h b/src/tint/ast/compound_assignment_statement.h
new file mode 100644
index 0000000..49a2004
--- /dev/null
+++ b/src/tint/ast/compound_assignment_statement.h
@@ -0,0 +1,63 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_COMPOUND_ASSIGNMENT_STATEMENT_H_
+#define SRC_TINT_AST_COMPOUND_ASSIGNMENT_STATEMENT_H_
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A compound assignment statement
+class CompoundAssignmentStatement final
+    : public Castable<CompoundAssignmentStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the compound assignment statement source
+  /// @param lhs the left side of the expression
+  /// @param rhs the right side of the expression
+  /// @param op the binary operator
+  CompoundAssignmentStatement(ProgramID program_id,
+                              const Source& source,
+                              const Expression* lhs,
+                              const Expression* rhs,
+                              BinaryOp op);
+  /// Move constructor
+  CompoundAssignmentStatement(CompoundAssignmentStatement&&);
+  ~CompoundAssignmentStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const CompoundAssignmentStatement* Clone(CloneContext* ctx) const override;
+
+  /// left side expression
+  const Expression* const lhs;
+
+  /// right side expression
+  const Expression* const rhs;
+
+  /// the binary operator
+  const BinaryOp op;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_COMPOUND_ASSIGNMENT_STATEMENT_H_
diff --git a/src/tint/ast/compound_assignment_statement_test.cc b/src/tint/ast/compound_assignment_statement_test.cc
new file mode 100644
index 0000000..c84faf0
--- /dev/null
+++ b/src/tint/ast/compound_assignment_statement_test.cc
@@ -0,0 +1,102 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/compound_assignment_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using CompoundAssignmentStatementTest = TestHelper;
+
+TEST_F(CompoundAssignmentStatementTest, Creation) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+  auto op = BinaryOp::kAdd;
+
+  auto* stmt = create<CompoundAssignmentStatement>(lhs, rhs, op);
+  EXPECT_EQ(stmt->lhs, lhs);
+  EXPECT_EQ(stmt->rhs, rhs);
+  EXPECT_EQ(stmt->op, op);
+}
+
+TEST_F(CompoundAssignmentStatementTest, CreationWithSource) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+  auto op = BinaryOp::kMultiply;
+
+  auto* stmt = create<CompoundAssignmentStatement>(
+      Source{Source::Location{20, 2}}, lhs, rhs, op);
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(CompoundAssignmentStatementTest, IsCompoundAssign) {
+  auto* lhs = Expr("lhs");
+  auto* rhs = Expr("rhs");
+  auto op = BinaryOp::kSubtract;
+
+  auto* stmt = create<CompoundAssignmentStatement>(lhs, rhs, op);
+  EXPECT_TRUE(stmt->Is<CompoundAssignmentStatement>());
+}
+
+TEST_F(CompoundAssignmentStatementTest, Assert_Null_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CompoundAssignmentStatement>(nullptr, b.Expr(1),
+                                              BinaryOp::kAdd);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CompoundAssignmentStatementTest, Assert_Null_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<CompoundAssignmentStatement>(b.Expr(1), nullptr,
+                                              BinaryOp::kAdd);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CompoundAssignmentStatementTest, Assert_DifferentProgramID_LHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CompoundAssignmentStatement>(b2.Expr("lhs"), b1.Expr("rhs"),
+                                               BinaryOp::kAdd);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CompoundAssignmentStatementTest, Assert_DifferentProgramID_RHS) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<CompoundAssignmentStatement>(b1.Expr("lhs"), b2.Expr("rhs"),
+                                               BinaryOp::kAdd);
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/continue_statement.cc b/src/tint/ast/continue_statement.cc
new file mode 100644
index 0000000..764c912
--- /dev/null
+++ b/src/tint/ast/continue_statement.cc
@@ -0,0 +1,38 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/continue_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ContinueStatement);
+
+namespace tint {
+namespace ast {
+
+ContinueStatement::ContinueStatement(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+ContinueStatement::ContinueStatement(ContinueStatement&&) = default;
+
+ContinueStatement::~ContinueStatement() = default;
+
+const ContinueStatement* ContinueStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<ContinueStatement>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/continue_statement.h b/src/tint/ast/continue_statement.h
new file mode 100644
index 0000000..ab0b12e
--- /dev/null
+++ b/src/tint/ast/continue_statement.h
@@ -0,0 +1,44 @@
+// Copyright 2020 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_AST_CONTINUE_STATEMENT_H_
+#define SRC_TINT_AST_CONTINUE_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An continue statement
+class ContinueStatement final : public Castable<ContinueStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  ContinueStatement(ProgramID pid, const Source& src);
+  /// Move constructor
+  ContinueStatement(ContinueStatement&&);
+  ~ContinueStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const ContinueStatement* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_CONTINUE_STATEMENT_H_
diff --git a/src/tint/ast/continue_statement_test.cc b/src/tint/ast/continue_statement_test.cc
new file mode 100644
index 0000000..8f78852
--- /dev/null
+++ b/src/tint/ast/continue_statement_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/continue_statement.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ContinueStatementTest = TestHelper;
+
+TEST_F(ContinueStatementTest, Creation_WithSource) {
+  auto* stmt = create<ContinueStatement>(Source{Source::Location{20, 2}});
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(ContinueStatementTest, IsContinue) {
+  auto* stmt = create<ContinueStatement>();
+  EXPECT_TRUE(stmt->Is<ContinueStatement>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/depth_multisampled_texture.cc b/src/tint/ast/depth_multisampled_texture.cc
new file mode 100644
index 0000000..bda191d
--- /dev/null
+++ b/src/tint/ast/depth_multisampled_texture.cc
@@ -0,0 +1,56 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/depth_multisampled_texture.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DepthMultisampledTexture);
+
+namespace tint {
+namespace ast {
+namespace {
+
+bool IsValidDepthDimension(TextureDimension dim) {
+  return dim == TextureDimension::k2d;
+}
+
+}  // namespace
+
+DepthMultisampledTexture::DepthMultisampledTexture(ProgramID pid,
+                                                   const Source& src,
+                                                   TextureDimension d)
+    : Base(pid, src, d) {
+  TINT_ASSERT(AST, IsValidDepthDimension(dim));
+}
+
+DepthMultisampledTexture::DepthMultisampledTexture(DepthMultisampledTexture&&) =
+    default;
+
+DepthMultisampledTexture::~DepthMultisampledTexture() = default;
+
+std::string DepthMultisampledTexture::FriendlyName(const SymbolTable&) const {
+  std::ostringstream out;
+  out << "texture_depth_multisampled_" << dim;
+  return out.str();
+}
+
+const DepthMultisampledTexture* DepthMultisampledTexture::Clone(
+    CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<DepthMultisampledTexture>(src, dim);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/depth_multisampled_texture.h b/src/tint/ast/depth_multisampled_texture.h
new file mode 100644
index 0000000..95051a4
--- /dev/null
+++ b/src/tint/ast/depth_multisampled_texture.h
@@ -0,0 +1,54 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_DEPTH_MULTISAMPLED_TEXTURE_H_
+#define SRC_TINT_AST_DEPTH_MULTISAMPLED_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// A multisampled depth texture type.
+class DepthMultisampledTexture final
+    : public Castable<DepthMultisampledTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  DepthMultisampledTexture(ProgramID pid,
+                           const Source& src,
+                           TextureDimension dim);
+  /// Move constructor
+  DepthMultisampledTexture(DepthMultisampledTexture&&);
+  ~DepthMultisampledTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const DepthMultisampledTexture* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_DEPTH_MULTISAMPLED_TEXTURE_H_
diff --git a/src/tint/ast/depth_multisampled_texture_test.cc b/src/tint/ast/depth_multisampled_texture_test.cc
new file mode 100644
index 0000000..540a667
--- /dev/null
+++ b/src/tint/ast/depth_multisampled_texture_test.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/depth_multisampled_texture.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstDepthMultisampledTextureTest = TestHelper;
+
+TEST_F(AstDepthMultisampledTextureTest, Dim) {
+  auto* d = create<DepthMultisampledTexture>(TextureDimension::k2d);
+  EXPECT_EQ(d->dim, TextureDimension::k2d);
+}
+
+TEST_F(AstDepthMultisampledTextureTest, FriendlyName) {
+  auto* d = create<DepthMultisampledTexture>(TextureDimension::k2d);
+  EXPECT_EQ(d->FriendlyName(Symbols()), "texture_depth_multisampled_2d");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/depth_texture.cc b/src/tint/ast/depth_texture.cc
new file mode 100644
index 0000000..5abfa76
--- /dev/null
+++ b/src/tint/ast/depth_texture.cc
@@ -0,0 +1,53 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/depth_texture.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DepthTexture);
+
+namespace tint {
+namespace ast {
+namespace {
+
+bool IsValidDepthDimension(TextureDimension dim) {
+  return dim == TextureDimension::k2d || dim == TextureDimension::k2dArray ||
+         dim == TextureDimension::kCube || dim == TextureDimension::kCubeArray;
+}
+
+}  // namespace
+
+DepthTexture::DepthTexture(ProgramID pid, const Source& src, TextureDimension d)
+    : Base(pid, src, d) {
+  TINT_ASSERT(AST, IsValidDepthDimension(dim));
+}
+
+DepthTexture::DepthTexture(DepthTexture&&) = default;
+
+DepthTexture::~DepthTexture() = default;
+
+std::string DepthTexture::FriendlyName(const SymbolTable&) const {
+  std::ostringstream out;
+  out << "texture_depth_" << dim;
+  return out.str();
+}
+
+const DepthTexture* DepthTexture::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<DepthTexture>(src, dim);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/depth_texture.h b/src/tint/ast/depth_texture.h
new file mode 100644
index 0000000..62cd09d
--- /dev/null
+++ b/src/tint/ast/depth_texture.h
@@ -0,0 +1,51 @@
+// Copyright 2020 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_AST_DEPTH_TEXTURE_H_
+#define SRC_TINT_AST_DEPTH_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// A depth texture type.
+class DepthTexture final : public Castable<DepthTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  DepthTexture(ProgramID pid, const Source& src, TextureDimension dim);
+  /// Move constructor
+  DepthTexture(DepthTexture&&);
+  ~DepthTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const DepthTexture* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_DEPTH_TEXTURE_H_
diff --git a/src/tint/ast/depth_texture_test.cc b/src/tint/ast/depth_texture_test.cc
new file mode 100644
index 0000000..2c4de5e
--- /dev/null
+++ b/src/tint/ast/depth_texture_test.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/depth_texture.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstDepthTextureTest = TestHelper;
+
+TEST_F(AstDepthTextureTest, IsTexture) {
+  Texture* ty = create<DepthTexture>(TextureDimension::kCube);
+  EXPECT_TRUE(ty->Is<DepthTexture>());
+  EXPECT_FALSE(ty->Is<SampledTexture>());
+  EXPECT_FALSE(ty->Is<StorageTexture>());
+}
+
+TEST_F(AstDepthTextureTest, Dim) {
+  auto* d = create<DepthTexture>(TextureDimension::kCube);
+  EXPECT_EQ(d->dim, TextureDimension::kCube);
+}
+
+TEST_F(AstDepthTextureTest, FriendlyName) {
+  auto* d = create<DepthTexture>(TextureDimension::kCube);
+  EXPECT_EQ(d->FriendlyName(Symbols()), "texture_depth_cube");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/disable_validation_attribute.cc b/src/tint/ast/disable_validation_attribute.cc
new file mode 100644
index 0000000..e474b8e
--- /dev/null
+++ b/src/tint/ast/disable_validation_attribute.cc
@@ -0,0 +1,57 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/disable_validation_attribute.h"
+#include "src/tint/clone_context.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DisableValidationAttribute);
+
+namespace tint {
+namespace ast {
+
+DisableValidationAttribute::DisableValidationAttribute(ProgramID pid,
+                                                       DisabledValidation val)
+    : Base(pid), validation(val) {}
+
+DisableValidationAttribute::~DisableValidationAttribute() = default;
+
+std::string DisableValidationAttribute::InternalName() const {
+  switch (validation) {
+    case DisabledValidation::kFunctionHasNoBody:
+      return "disable_validation__function_has_no_body";
+    case DisabledValidation::kBindingPointCollision:
+      return "disable_validation__binding_point_collision";
+    case DisabledValidation::kIgnoreStorageClass:
+      return "disable_validation__ignore_storage_class";
+    case DisabledValidation::kEntryPointParameter:
+      return "disable_validation__entry_point_parameter";
+    case DisabledValidation::kIgnoreConstructibleFunctionParameter:
+      return "disable_validation__ignore_constructible_function_parameter";
+    case DisabledValidation::kIgnoreStrideAttribute:
+      return "disable_validation__ignore_stride";
+    case DisabledValidation::kIgnoreInvalidPointerArgument:
+      return "disable_validation__ignore_invalid_pointer_argument";
+  }
+  return "<invalid>";
+}
+
+const DisableValidationAttribute* DisableValidationAttribute::Clone(
+    CloneContext* ctx) const {
+  return ctx->dst->ASTNodes().Create<DisableValidationAttribute>(ctx->dst->ID(),
+                                                                 validation);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/disable_validation_attribute.h b/src/tint/ast/disable_validation_attribute.h
new file mode 100644
index 0000000..d46edb8
--- /dev/null
+++ b/src/tint/ast/disable_validation_attribute.h
@@ -0,0 +1,83 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_DISABLE_VALIDATION_ATTRIBUTE_H_
+#define SRC_TINT_AST_DISABLE_VALIDATION_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/internal_attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// Enumerator of validation features that can be disabled with a
+/// DisableValidationAttribute attribute.
+enum class DisabledValidation {
+  /// When applied to a function, the validator will not complain there is no
+  /// body to a function.
+  kFunctionHasNoBody,
+  /// When applied to a module-scoped variable, the validator will not complain
+  /// if two resource variables have the same binding points.
+  kBindingPointCollision,
+  /// When applied to a variable, the validator will not complain about the
+  /// declared storage class.
+  kIgnoreStorageClass,
+  /// When applied to an entry-point function parameter, the validator will not
+  /// check for entry IO attributes.
+  kEntryPointParameter,
+  /// When applied to a function parameter, the validator will not
+  /// check if parameter type is constructible
+  kIgnoreConstructibleFunctionParameter,
+  /// When applied to a member attribute, a stride attribute may be applied to
+  /// non-array types.
+  kIgnoreStrideAttribute,
+  /// When applied to a pointer function parameter, the validator will not
+  /// require a function call argument passed for that parameter to have a
+  /// certain form.
+  kIgnoreInvalidPointerArgument,
+};
+
+/// An internal attribute used to tell the validator to ignore specific
+/// violations. Typically generated by transforms that need to produce ASTs that
+/// would otherwise cause validation errors.
+class DisableValidationAttribute final
+    : public Castable<DisableValidationAttribute, InternalAttribute> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param validation the validation to disable
+  explicit DisableValidationAttribute(ProgramID program_id,
+                                      DisabledValidation validation);
+
+  /// Destructor
+  ~DisableValidationAttribute() override;
+
+  /// @return a short description of the internal attribute which will be
+  /// displayed in WGSL as `@internal(<name>)` (but is not parsable).
+  std::string InternalName() const override;
+
+  /// Performs a deep clone of this object using the CloneContext `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned object
+  const DisableValidationAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The validation that this attribute disables
+  const DisabledValidation validation;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_DISABLE_VALIDATION_ATTRIBUTE_H_
diff --git a/src/tint/ast/discard_statement.cc b/src/tint/ast/discard_statement.cc
new file mode 100644
index 0000000..a8f9cf6
--- /dev/null
+++ b/src/tint/ast/discard_statement.cc
@@ -0,0 +1,38 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/discard_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DiscardStatement);
+
+namespace tint {
+namespace ast {
+
+DiscardStatement::DiscardStatement(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+DiscardStatement::DiscardStatement(DiscardStatement&&) = default;
+
+DiscardStatement::~DiscardStatement() = default;
+
+const DiscardStatement* DiscardStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<DiscardStatement>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/discard_statement.h b/src/tint/ast/discard_statement.h
new file mode 100644
index 0000000..7e4fcf1
--- /dev/null
+++ b/src/tint/ast/discard_statement.h
@@ -0,0 +1,44 @@
+// Copyright 2020 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_AST_DISCARD_STATEMENT_H_
+#define SRC_TINT_AST_DISCARD_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A discard statement
+class DiscardStatement final : public Castable<DiscardStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  DiscardStatement(ProgramID pid, const Source& src);
+  /// Move constructor
+  DiscardStatement(DiscardStatement&&);
+  ~DiscardStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const DiscardStatement* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_DISCARD_STATEMENT_H_
diff --git a/src/tint/ast/discard_statement_test.cc b/src/tint/ast/discard_statement_test.cc
new file mode 100644
index 0000000..08a0cc3
--- /dev/null
+++ b/src/tint/ast/discard_statement_test.cc
@@ -0,0 +1,49 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/discard_statement.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using DiscardStatementTest = TestHelper;
+
+TEST_F(DiscardStatementTest, Creation) {
+  auto* stmt = create<DiscardStatement>();
+  EXPECT_EQ(stmt->source.range.begin.line, 0u);
+  EXPECT_EQ(stmt->source.range.begin.column, 0u);
+  EXPECT_EQ(stmt->source.range.end.line, 0u);
+  EXPECT_EQ(stmt->source.range.end.column, 0u);
+}
+
+TEST_F(DiscardStatementTest, Creation_WithSource) {
+  auto* stmt = create<DiscardStatement>(
+      Source{Source::Range{Source::Location{20, 2}, Source::Location{20, 5}}});
+  EXPECT_EQ(stmt->source.range.begin.line, 20u);
+  EXPECT_EQ(stmt->source.range.begin.column, 2u);
+  EXPECT_EQ(stmt->source.range.end.line, 20u);
+  EXPECT_EQ(stmt->source.range.end.column, 5u);
+}
+
+TEST_F(DiscardStatementTest, IsDiscard) {
+  auto* stmt = create<DiscardStatement>();
+  EXPECT_TRUE(stmt->Is<DiscardStatement>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/else_statement.cc b/src/tint/ast/else_statement.cc
new file mode 100644
index 0000000..62908b5
--- /dev/null
+++ b/src/tint/ast/else_statement.cc
@@ -0,0 +1,47 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/else_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ElseStatement);
+
+namespace tint {
+namespace ast {
+
+ElseStatement::ElseStatement(ProgramID pid,
+                             const Source& src,
+                             const Expression* cond,
+                             const BlockStatement* b)
+    : Base(pid, src), condition(cond), body(b) {
+  TINT_ASSERT(AST, body);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
+}
+
+ElseStatement::ElseStatement(ElseStatement&&) = default;
+
+ElseStatement::~ElseStatement() = default;
+
+const ElseStatement* ElseStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* cond = ctx->Clone(condition);
+  auto* b = ctx->Clone(body);
+  return ctx->dst->create<ElseStatement>(src, cond, b);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/else_statement.h b/src/tint/ast/else_statement.h
new file mode 100644
index 0000000..6f641c7
--- /dev/null
+++ b/src/tint/ast/else_statement.h
@@ -0,0 +1,61 @@
+// Copyright 2020 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_AST_ELSE_STATEMENT_H_
+#define SRC_TINT_AST_ELSE_STATEMENT_H_
+
+#include <vector>
+
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// An else statement
+class ElseStatement final : public Castable<ElseStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param condition the else condition
+  /// @param body the else body
+  ElseStatement(ProgramID pid,
+                const Source& src,
+                const Expression* condition,
+                const BlockStatement* body);
+  /// Move constructor
+  ElseStatement(ElseStatement&&);
+  ~ElseStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const ElseStatement* Clone(CloneContext* ctx) const override;
+
+  /// The else condition or nullptr if none set
+  const Expression* const condition;
+
+  /// The else body
+  const BlockStatement* const body;
+};
+
+/// A list of else statements
+using ElseStatementList = std::vector<const ElseStatement*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ELSE_STATEMENT_H_
diff --git a/src/tint/ast/else_statement_test.cc b/src/tint/ast/else_statement_test.cc
new file mode 100644
index 0000000..27fe9ec
--- /dev/null
+++ b/src/tint/ast/else_statement_test.cc
@@ -0,0 +1,94 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ElseStatementTest = TestHelper;
+
+TEST_F(ElseStatementTest, Creation) {
+  auto* cond = Expr(true);
+  auto* body = create<BlockStatement>(StatementList{
+      create<DiscardStatement>(),
+  });
+  auto* discard = body->statements[0];
+
+  auto* e = create<ElseStatement>(cond, body);
+  EXPECT_EQ(e->condition, cond);
+  ASSERT_EQ(e->body->statements.size(), 1u);
+  EXPECT_EQ(e->body->statements[0], discard);
+}
+
+TEST_F(ElseStatementTest, Creation_WithSource) {
+  auto* e = create<ElseStatement>(Source{Source::Location{20, 2}}, Expr(true),
+                                  Block());
+  auto src = e->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(ElseStatementTest, IsElse) {
+  auto* e = create<ElseStatement>(nullptr, Block());
+  EXPECT_TRUE(e->Is<ElseStatement>());
+}
+
+TEST_F(ElseStatementTest, HasCondition) {
+  auto* cond = Expr(true);
+  auto* e = create<ElseStatement>(cond, Block());
+  EXPECT_TRUE(e->condition);
+}
+
+TEST_F(ElseStatementTest, HasContition_NullCondition) {
+  auto* e = create<ElseStatement>(nullptr, Block());
+  EXPECT_FALSE(e->condition);
+}
+
+TEST_F(ElseStatementTest, Assert_Null_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<ElseStatement>(b.Expr(true), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ElseStatementTest, Assert_DifferentProgramID_Condition) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<ElseStatement>(b2.Expr(true), b1.Block());
+      },
+      "internal compiler error");
+}
+
+TEST_F(ElseStatementTest, Assert_DifferentProgramID_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<ElseStatement>(b1.Expr(true), b2.Block());
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/expression.cc b/src/tint/ast/expression.cc
new file mode 100644
index 0000000..a432326
--- /dev/null
+++ b/src/tint/ast/expression.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/expression.h"
+
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/info.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Expression);
+
+namespace tint {
+namespace ast {
+
+Expression::Expression(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Expression::Expression(Expression&&) = default;
+
+Expression::~Expression() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/expression.h b/src/tint/ast/expression.h
new file mode 100644
index 0000000..15bdacc
--- /dev/null
+++ b/src/tint/ast/expression.h
@@ -0,0 +1,47 @@
+// Copyright 2020 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_AST_EXPRESSION_H_
+#define SRC_TINT_AST_EXPRESSION_H_
+
+#include <string>
+#include <vector>
+
+#include "src/tint/ast/node.h"
+#include "src/tint/sem/type.h"
+
+namespace tint {
+namespace ast {
+
+/// Base expression class
+class Expression : public Castable<Expression, Node> {
+ public:
+  ~Expression() override;
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Expression(ProgramID pid, const Source& src);
+  /// Move constructor
+  Expression(Expression&&);
+};
+
+/// A list of expressions
+using ExpressionList = std::vector<const Expression*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_EXPRESSION_H_
diff --git a/src/tint/ast/external_texture.cc b/src/tint/ast/external_texture.cc
new file mode 100644
index 0000000..a5a703d
--- /dev/null
+++ b/src/tint/ast/external_texture.cc
@@ -0,0 +1,41 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/external_texture.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ExternalTexture);
+
+namespace tint {
+namespace ast {
+
+// ExternalTexture::ExternalTexture() : Base(ast::TextureDimension::k2d) {}
+ExternalTexture::ExternalTexture(ProgramID pid, const Source& src)
+    : Base(pid, src, ast::TextureDimension::k2d) {}
+
+ExternalTexture::ExternalTexture(ExternalTexture&&) = default;
+
+ExternalTexture::~ExternalTexture() = default;
+
+std::string ExternalTexture::FriendlyName(const SymbolTable&) const {
+  return "texture_external";
+}
+
+const ExternalTexture* ExternalTexture::Clone(CloneContext* ctx) const {
+  return ctx->dst->create<ExternalTexture>();
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/external_texture.h b/src/tint/ast/external_texture.h
new file mode 100644
index 0000000..cf9f60d
--- /dev/null
+++ b/src/tint/ast/external_texture.h
@@ -0,0 +1,51 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_EXTERNAL_TEXTURE_H_
+#define SRC_TINT_AST_EXTERNAL_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// An external texture type
+class ExternalTexture final : public Castable<ExternalTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  ExternalTexture(ProgramID pid, const Source& src);
+
+  /// Move constructor
+  ExternalTexture(ExternalTexture&&);
+  ~ExternalTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const ExternalTexture* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_EXTERNAL_TEXTURE_H_
diff --git a/src/tint/ast/external_texture_test.cc b/src/tint/ast/external_texture_test.cc
new file mode 100644
index 0000000..af25ac2
--- /dev/null
+++ b/src/tint/ast/external_texture_test.cc
@@ -0,0 +1,46 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/external_texture.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstExternalTextureTest = TestHelper;
+
+TEST_F(AstExternalTextureTest, IsTexture) {
+  Texture* ty = create<ExternalTexture>();
+  EXPECT_FALSE(ty->Is<DepthTexture>());
+  EXPECT_TRUE(ty->Is<ExternalTexture>());
+  EXPECT_FALSE(ty->Is<MultisampledTexture>());
+  EXPECT_FALSE(ty->Is<SampledTexture>());
+  EXPECT_FALSE(ty->Is<StorageTexture>());
+}
+
+TEST_F(AstExternalTextureTest, Dim) {
+  auto* ty = create<ExternalTexture>();
+  EXPECT_EQ(ty->dim, ast::TextureDimension::k2d);
+}
+
+TEST_F(AstExternalTextureTest, FriendlyName) {
+  auto* ty = create<ExternalTexture>();
+  EXPECT_EQ(ty->FriendlyName(Symbols()), "texture_external");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/f32.cc b/src/tint/ast/f32.cc
new file mode 100644
index 0000000..319822c
--- /dev/null
+++ b/src/tint/ast/f32.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/f32.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::F32);
+
+namespace tint {
+namespace ast {
+
+F32::F32(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+F32::F32(F32&&) = default;
+
+F32::~F32() = default;
+
+std::string F32::FriendlyName(const SymbolTable&) const {
+  return "f32";
+}
+
+const F32* F32::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<F32>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/f32.h b/src/tint/ast/f32.h
new file mode 100644
index 0000000..58019c9
--- /dev/null
+++ b/src/tint/ast/f32.h
@@ -0,0 +1,50 @@
+// Copyright 2020 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_AST_F32_H_
+#define SRC_TINT_AST_F32_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A float 32 type
+class F32 final : public Castable<F32, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  F32(ProgramID pid, const Source& src);
+  /// Move constructor
+  F32(F32&&);
+  ~F32() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const F32* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_F32_H_
diff --git a/src/tint/ast/f32_test.cc b/src/tint/ast/f32_test.cc
new file mode 100644
index 0000000..ec6d62e
--- /dev/null
+++ b/src/tint/ast/f32_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/f32.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstF32Test = TestHelper;
+
+TEST_F(AstF32Test, FriendlyName) {
+  auto* f = create<F32>();
+  EXPECT_EQ(f->FriendlyName(Symbols()), "f32");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/fallthrough_statement.cc b/src/tint/ast/fallthrough_statement.cc
new file mode 100644
index 0000000..463d2ad
--- /dev/null
+++ b/src/tint/ast/fallthrough_statement.cc
@@ -0,0 +1,39 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/fallthrough_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::FallthroughStatement);
+
+namespace tint {
+namespace ast {
+
+FallthroughStatement::FallthroughStatement(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+FallthroughStatement::FallthroughStatement(FallthroughStatement&&) = default;
+
+FallthroughStatement::~FallthroughStatement() = default;
+
+const FallthroughStatement* FallthroughStatement::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<FallthroughStatement>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/fallthrough_statement.h b/src/tint/ast/fallthrough_statement.h
new file mode 100644
index 0000000..f1cc716
--- /dev/null
+++ b/src/tint/ast/fallthrough_statement.h
@@ -0,0 +1,45 @@
+// Copyright 2020 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_AST_FALLTHROUGH_STATEMENT_H_
+#define SRC_TINT_AST_FALLTHROUGH_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An fallthrough statement
+class FallthroughStatement final
+    : public Castable<FallthroughStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  FallthroughStatement(ProgramID pid, const Source& src);
+  /// Move constructor
+  FallthroughStatement(FallthroughStatement&&);
+  ~FallthroughStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const FallthroughStatement* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_FALLTHROUGH_STATEMENT_H_
diff --git a/src/tint/ast/fallthrough_statement_test.cc b/src/tint/ast/fallthrough_statement_test.cc
new file mode 100644
index 0000000..5adda3d
--- /dev/null
+++ b/src/tint/ast/fallthrough_statement_test.cc
@@ -0,0 +1,47 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/fallthrough_statement.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using FallthroughStatementTest = TestHelper;
+
+TEST_F(FallthroughStatementTest, Creation) {
+  auto* stmt = create<FallthroughStatement>();
+  EXPECT_EQ(stmt->source.range.begin.line, 0u);
+  EXPECT_EQ(stmt->source.range.begin.column, 0u);
+  EXPECT_EQ(stmt->source.range.end.line, 0u);
+  EXPECT_EQ(stmt->source.range.end.column, 0u);
+}
+
+TEST_F(FallthroughStatementTest, Creation_WithSource) {
+  auto* stmt = create<FallthroughStatement>(Source{Source::Location{20, 2}});
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(FallthroughStatementTest, IsFallthrough) {
+  auto* stmt = create<FallthroughStatement>();
+  EXPECT_TRUE(stmt->Is<FallthroughStatement>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/float_literal_expression.cc b/src/tint/ast/float_literal_expression.cc
new file mode 100644
index 0000000..b505f67
--- /dev/null
+++ b/src/tint/ast/float_literal_expression.cc
@@ -0,0 +1,41 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/float_literal_expression.h"
+
+#include <limits>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::FloatLiteralExpression);
+
+namespace tint {
+namespace ast {
+
+FloatLiteralExpression::FloatLiteralExpression(ProgramID pid,
+                                               const Source& src,
+                                               float val)
+    : Base(pid, src), value(val) {}
+
+FloatLiteralExpression::~FloatLiteralExpression() = default;
+
+const FloatLiteralExpression* FloatLiteralExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<FloatLiteralExpression>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/float_literal_expression.h b/src/tint/ast/float_literal_expression.h
new file mode 100644
index 0000000..ffa3c2f
--- /dev/null
+++ b/src/tint/ast/float_literal_expression.h
@@ -0,0 +1,49 @@
+// Copyright 2020 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_AST_FLOAT_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_FLOAT_LITERAL_EXPRESSION_H_
+
+#include <string>
+
+#include "src/tint/ast/literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A float literal
+class FloatLiteralExpression final
+    : public Castable<FloatLiteralExpression, LiteralExpression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the float literals value
+  FloatLiteralExpression(ProgramID pid, const Source& src, float value);
+  ~FloatLiteralExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const FloatLiteralExpression* Clone(CloneContext* ctx) const override;
+
+  /// The float literal value
+  const float value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_FLOAT_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/float_literal_expression_test.cc b/src/tint/ast/float_literal_expression_test.cc
new file mode 100644
index 0000000..7a91da9
--- /dev/null
+++ b/src/tint/ast/float_literal_expression_test.cc
@@ -0,0 +1,31 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using FloatLiteralExpressionTest = TestHelper;
+
+TEST_F(FloatLiteralExpressionTest, Value) {
+  auto* f = create<FloatLiteralExpression>(47.2f);
+  ASSERT_TRUE(f->Is<FloatLiteralExpression>());
+  EXPECT_EQ(f->value, 47.2f);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/for_loop_statement.cc b/src/tint/ast/for_loop_statement.cc
new file mode 100644
index 0000000..7bb9389
--- /dev/null
+++ b/src/tint/ast/for_loop_statement.cc
@@ -0,0 +1,59 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/for_loop_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ForLoopStatement);
+
+namespace tint {
+namespace ast {
+
+ForLoopStatement::ForLoopStatement(ProgramID pid,
+                                   const Source& src,
+                                   const Statement* init,
+                                   const Expression* cond,
+                                   const Statement* cont,
+                                   const BlockStatement* b)
+    : Base(pid, src),
+      initializer(init),
+      condition(cond),
+      continuing(cont),
+      body(b) {
+  TINT_ASSERT(AST, body);
+
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, initializer, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, continuing, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+}
+
+ForLoopStatement::ForLoopStatement(ForLoopStatement&&) = default;
+
+ForLoopStatement::~ForLoopStatement() = default;
+
+const ForLoopStatement* ForLoopStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+
+  auto* init = ctx->Clone(initializer);
+  auto* cond = ctx->Clone(condition);
+  auto* cont = ctx->Clone(continuing);
+  auto* b = ctx->Clone(body);
+  return ctx->dst->create<ForLoopStatement>(src, init, cond, cont, b);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/for_loop_statement.h b/src/tint/ast/for_loop_statement.h
new file mode 100644
index 0000000..040ba52
--- /dev/null
+++ b/src/tint/ast/for_loop_statement.h
@@ -0,0 +1,67 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_FOR_LOOP_STATEMENT_H_
+#define SRC_TINT_AST_FOR_LOOP_STATEMENT_H_
+
+#include "src/tint/ast/block_statement.h"
+
+namespace tint {
+namespace ast {
+
+class Expression;
+
+/// A for loop statement
+class ForLoopStatement final : public Castable<ForLoopStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the for loop statement source
+  /// @param initializer the optional loop initializer statement
+  /// @param condition the optional loop condition expression
+  /// @param continuing the optional continuing statement
+  /// @param body the loop body
+  ForLoopStatement(ProgramID program_id,
+                   Source const& source,
+                   const Statement* initializer,
+                   const Expression* condition,
+                   const Statement* continuing,
+                   const BlockStatement* body);
+  /// Move constructor
+  ForLoopStatement(ForLoopStatement&&);
+  ~ForLoopStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const ForLoopStatement* Clone(CloneContext* ctx) const override;
+
+  /// The initializer statement
+  const Statement* const initializer;
+
+  /// The condition expression
+  const Expression* const condition;
+
+  /// The continuing statement
+  const Statement* const continuing;
+
+  /// The loop body block
+  const BlockStatement* const body;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_FOR_LOOP_STATEMENT_H_
diff --git a/src/tint/ast/for_loop_statement_test.cc b/src/tint/ast/for_loop_statement_test.cc
new file mode 100644
index 0000000..3e5e585
--- /dev/null
+++ b/src/tint/ast/for_loop_statement_test.cc
@@ -0,0 +1,104 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ForLoopStatementTest = TestHelper;
+
+TEST_F(ForLoopStatementTest, Creation) {
+  auto* init = Decl(Var("i", ty.u32()));
+  auto* cond =
+      create<BinaryExpression>(BinaryOp::kLessThan, Expr("i"), Expr(5u));
+  auto* cont = Assign("i", Add("i", 1));
+  auto* body = Block(Return());
+  auto* l = For(init, cond, cont, body);
+
+  EXPECT_EQ(l->initializer, init);
+  EXPECT_EQ(l->condition, cond);
+  EXPECT_EQ(l->continuing, cont);
+  EXPECT_EQ(l->body, body);
+}
+
+TEST_F(ForLoopStatementTest, Creation_WithSource) {
+  auto* body = Block(Return());
+  auto* l = For(Source{{20u, 2u}}, nullptr, nullptr, nullptr, body);
+  auto src = l->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(ForLoopStatementTest, Creation_Null_InitCondCont) {
+  auto* body = Block(Return());
+  auto* l = For(nullptr, nullptr, nullptr, body);
+  EXPECT_EQ(l->body, body);
+}
+
+TEST_F(ForLoopStatementTest, Assert_Null_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.For(nullptr, nullptr, nullptr, nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ForLoopStatementTest, Assert_DifferentProgramID_Initializer) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.For(b2.Block(), nullptr, nullptr, b1.Block());
+      },
+      "internal compiler error");
+}
+
+TEST_F(ForLoopStatementTest, Assert_DifferentProgramID_Condition) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.For(nullptr, b2.Expr(true), nullptr, b1.Block());
+      },
+      "internal compiler error");
+}
+
+TEST_F(ForLoopStatementTest, Assert_DifferentProgramID_Continuing) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.For(nullptr, nullptr, b2.Block(), b1.Block());
+      },
+      "internal compiler error");
+}
+
+TEST_F(ForLoopStatementTest, Assert_DifferentProgramID_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.For(nullptr, nullptr, nullptr, b2.Block());
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/function.cc b/src/tint/ast/function.cc
new file mode 100644
index 0000000..cba8091
--- /dev/null
+++ b/src/tint/ast/function.cc
@@ -0,0 +1,108 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/function.h"
+
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/workgroup_attribute.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Function);
+
+namespace tint {
+namespace ast {
+
+Function::Function(ProgramID pid,
+                   const Source& src,
+                   Symbol sym,
+                   VariableList parameters,
+                   const Type* return_ty,
+                   const BlockStatement* b,
+                   AttributeList attrs,
+                   AttributeList return_type_attrs)
+    : Base(pid, src),
+      symbol(sym),
+      params(std::move(parameters)),
+      return_type(return_ty),
+      body(b),
+      attributes(std::move(attrs)),
+      return_type_attributes(std::move(return_type_attrs)) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+  for (auto* param : params) {
+    TINT_ASSERT(AST, param && param->is_const);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, param, program_id);
+  }
+  TINT_ASSERT(AST, symbol.IsValid());
+  TINT_ASSERT(AST, return_type);
+  for (auto* attr : attributes) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, attr, program_id);
+  }
+  for (auto* attr : return_type_attributes) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, attr, program_id);
+  }
+}
+
+Function::Function(Function&&) = default;
+
+Function::~Function() = default;
+
+PipelineStage Function::PipelineStage() const {
+  if (auto* stage = GetAttribute<StageAttribute>(attributes)) {
+    return stage->stage;
+  }
+  return PipelineStage::kNone;
+}
+
+const Function* Function::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto sym = ctx->Clone(symbol);
+  auto p = ctx->Clone(params);
+  auto* ret = ctx->Clone(return_type);
+  auto* b = ctx->Clone(body);
+  auto attrs = ctx->Clone(attributes);
+  auto ret_attrs = ctx->Clone(return_type_attributes);
+  return ctx->dst->create<Function>(src, sym, p, ret, b, attrs, ret_attrs);
+}
+
+const Function* FunctionList::Find(Symbol sym) const {
+  for (auto* func : *this) {
+    if (func->symbol == sym) {
+      return func;
+    }
+  }
+  return nullptr;
+}
+
+const Function* FunctionList::Find(Symbol sym, PipelineStage stage) const {
+  for (auto* func : *this) {
+    if (func->symbol == sym && func->PipelineStage() == stage) {
+      return func;
+    }
+  }
+  return nullptr;
+}
+
+bool FunctionList::HasStage(ast::PipelineStage stage) const {
+  for (auto* func : *this) {
+    if (func->PipelineStage() == stage) {
+      return true;
+    }
+  }
+  return false;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/function.h b/src/tint/ast/function.h
new file mode 100644
index 0000000..10ad7a6
--- /dev/null
+++ b/src/tint/ast/function.h
@@ -0,0 +1,118 @@
+// Copyright 2020 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_AST_FUNCTION_H_
+#define SRC_TINT_AST_FUNCTION_H_
+
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/binding_attribute.h"
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/builtin_attribute.h"
+#include "src/tint/ast/group_attribute.h"
+#include "src/tint/ast/location_attribute.h"
+#include "src/tint/ast/pipeline_stage.h"
+#include "src/tint/ast/variable.h"
+
+namespace tint {
+namespace ast {
+
+/// A Function statement.
+class Function final : public Castable<Function, Node> {
+ public:
+  /// Create a function
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the variable source
+  /// @param symbol the function symbol
+  /// @param params the function parameters
+  /// @param return_type the return type
+  /// @param body the function body
+  /// @param attributes the function attributes
+  /// @param return_type_attributes the return type attributes
+  Function(ProgramID program_id,
+           const Source& source,
+           Symbol symbol,
+           VariableList params,
+           const Type* return_type,
+           const BlockStatement* body,
+           AttributeList attributes,
+           AttributeList return_type_attributes);
+  /// Move constructor
+  Function(Function&&);
+
+  ~Function() override;
+
+  /// @returns the functions pipeline stage or None if not set
+  ast::PipelineStage PipelineStage() const;
+
+  /// @returns true if this function is an entry point
+  bool IsEntryPoint() const { return PipelineStage() != PipelineStage::kNone; }
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const Function* Clone(CloneContext* ctx) const override;
+
+  /// The function symbol
+  const Symbol symbol;
+
+  /// The function params
+  const VariableList params;
+
+  /// The function return type
+  const Type* const return_type;
+
+  /// The function body
+  const BlockStatement* const body;
+
+  /// The attributes attached to this function
+  const AttributeList attributes;
+
+  /// The attributes attached to the function return type.
+  const AttributeList return_type_attributes;
+};
+
+/// A list of functions
+class FunctionList : public std::vector<const Function*> {
+ public:
+  /// Appends f to the end of the list
+  /// @param f the function to append to this list
+  void Add(const Function* f) { this->emplace_back(f); }
+
+  /// Returns the function with the given name
+  /// @param sym the function symbol to search for
+  /// @returns the associated function or nullptr if none exists
+  const Function* Find(Symbol sym) const;
+
+  /// Returns the function with the given name
+  /// @param sym the function symbol to search for
+  /// @param stage the pipeline stage
+  /// @returns the associated function or nullptr if none exists
+  const Function* Find(Symbol sym, PipelineStage stage) const;
+
+  /// @param stage the pipeline stage
+  /// @returns true if the Builder contains an entrypoint function with
+  /// the given stage
+  bool HasStage(PipelineStage stage) const;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_FUNCTION_H_
diff --git a/src/tint/ast/function_test.cc b/src/tint/ast/function_test.cc
new file mode 100644
index 0000000..d4077bd
--- /dev/null
+++ b/src/tint/ast/function_test.cc
@@ -0,0 +1,196 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/workgroup_attribute.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using FunctionTest = TestHelper;
+
+TEST_F(FunctionTest, Creation) {
+  VariableList params;
+  params.push_back(Param("var", ty.i32()));
+  auto* var = params[0];
+
+  auto* f = Func("func", params, ty.void_(), StatementList{}, AttributeList{});
+  EXPECT_EQ(f->symbol, Symbols().Get("func"));
+  ASSERT_EQ(f->params.size(), 1u);
+  EXPECT_TRUE(f->return_type->Is<ast::Void>());
+  EXPECT_EQ(f->params[0], var);
+}
+
+TEST_F(FunctionTest, Creation_WithSource) {
+  VariableList params;
+  params.push_back(Param("var", ty.i32()));
+
+  auto* f = Func(Source{Source::Location{20, 2}}, "func", params, ty.void_(),
+                 StatementList{}, AttributeList{});
+  auto src = f->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(FunctionTest, Assert_InvalidName) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Func("", VariableList{}, b.ty.void_(), StatementList{},
+               AttributeList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_Null_ReturnType) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Func("f", VariableList{}, nullptr, StatementList{}, AttributeList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_Null_Param) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        VariableList params;
+        params.push_back(b.Param("var", b.ty.i32()));
+        params.push_back(nullptr);
+
+        b.Func("f", params, b.ty.void_(), StatementList{}, AttributeList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_DifferentProgramID_Symbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Func(b2.Sym("func"), VariableList{}, b1.ty.void_(), StatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_DifferentProgramID_Param) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Func("func", VariableList{b2.Param("var", b2.ty.i32())},
+                b1.ty.void_(), StatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_DifferentProgramID_Attr) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Func("func", VariableList{}, b1.ty.void_(), StatementList{},
+                AttributeList{
+                    b2.WorkgroupSize(2, 4, 6),
+                });
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_DifferentProgramID_ReturnAttr) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Func("func", VariableList{}, b1.ty.void_(), StatementList{},
+                AttributeList{},
+                AttributeList{
+                    b2.WorkgroupSize(2, 4, 6),
+                });
+      },
+      "internal compiler error");
+}
+
+TEST_F(FunctionTest, Assert_NonConstParam) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        VariableList params;
+        params.push_back(b.Var("var", b.ty.i32(), ast::StorageClass::kNone));
+
+        b.Func("f", params, b.ty.void_(), StatementList{}, AttributeList{});
+      },
+      "internal compiler error");
+}
+
+using FunctionListTest = TestHelper;
+
+TEST_F(FunctionListTest, FindSymbol) {
+  auto* func = Func("main", VariableList{}, ty.f32(), StatementList{},
+                    ast::AttributeList{});
+  FunctionList list;
+  list.Add(func);
+  EXPECT_EQ(func, list.Find(Symbols().Register("main")));
+}
+
+TEST_F(FunctionListTest, FindSymbolMissing) {
+  FunctionList list;
+  EXPECT_EQ(nullptr, list.Find(Symbols().Register("Missing")));
+}
+
+TEST_F(FunctionListTest, FindSymbolStage) {
+  auto* fs = Func("main", VariableList{}, ty.f32(), StatementList{},
+                  ast::AttributeList{
+                      Stage(PipelineStage::kFragment),
+                  });
+  auto* vs = Func("main", VariableList{}, ty.f32(), StatementList{},
+                  ast::AttributeList{
+                      Stage(PipelineStage::kVertex),
+                  });
+  FunctionList list;
+  list.Add(fs);
+  list.Add(vs);
+  EXPECT_EQ(fs,
+            list.Find(Symbols().Register("main"), PipelineStage::kFragment));
+  EXPECT_EQ(vs, list.Find(Symbols().Register("main"), PipelineStage::kVertex));
+}
+
+TEST_F(FunctionListTest, FindSymbolStageMissing) {
+  FunctionList list;
+  list.Add(Func("main", VariableList{}, ty.f32(), StatementList{},
+                ast::AttributeList{
+                    Stage(PipelineStage::kFragment),
+                }));
+  EXPECT_EQ(nullptr,
+            list.Find(Symbols().Register("main"), PipelineStage::kVertex));
+}
+
+TEST_F(FunctionListTest, HasStage) {
+  FunctionList list;
+  list.Add(Func("main", VariableList{}, ty.f32(), StatementList{},
+                ast::AttributeList{
+                    Stage(PipelineStage::kFragment),
+                }));
+  EXPECT_TRUE(list.HasStage(PipelineStage::kFragment));
+  EXPECT_FALSE(list.HasStage(PipelineStage::kVertex));
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/group_attribute.cc b/src/tint/ast/group_attribute.cc
new file mode 100644
index 0000000..58714e8
--- /dev/null
+++ b/src/tint/ast/group_attribute.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/group_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::GroupAttribute);
+
+namespace tint {
+namespace ast {
+
+GroupAttribute::GroupAttribute(ProgramID pid, const Source& src, uint32_t val)
+    : Base(pid, src), value(val) {}
+
+GroupAttribute::~GroupAttribute() = default;
+
+std::string GroupAttribute::Name() const {
+  return "group";
+}
+
+const GroupAttribute* GroupAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<GroupAttribute>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/group_attribute.h b/src/tint/ast/group_attribute.h
new file mode 100644
index 0000000..be5c61e
--- /dev/null
+++ b/src/tint/ast/group_attribute.h
@@ -0,0 +1,51 @@
+// Copyright 2020 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_AST_GROUP_ATTRIBUTE_H_
+#define SRC_TINT_AST_GROUP_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A group attribute
+class GroupAttribute final : public Castable<GroupAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the group value
+  GroupAttribute(ProgramID pid, const Source& src, uint32_t value);
+  ~GroupAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const GroupAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The group value
+  const uint32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_GROUP_ATTRIBUTE_H_
diff --git a/src/tint/ast/group_attribute_test.cc b/src/tint/ast/group_attribute_test.cc
new file mode 100644
index 0000000..fd7c18b
--- /dev/null
+++ b/src/tint/ast/group_attribute_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using GroupAttributeTest = TestHelper;
+
+TEST_F(GroupAttributeTest, Creation) {
+  auto* d = create<GroupAttribute>(2);
+  EXPECT_EQ(2u, d->value);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/i32.cc b/src/tint/ast/i32.cc
new file mode 100644
index 0000000..83e4524
--- /dev/null
+++ b/src/tint/ast/i32.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/i32.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::I32);
+
+namespace tint {
+namespace ast {
+
+I32::I32(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+I32::I32(I32&&) = default;
+
+I32::~I32() = default;
+
+std::string I32::FriendlyName(const SymbolTable&) const {
+  return "i32";
+}
+
+const I32* I32::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<I32>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/i32.h b/src/tint/ast/i32.h
new file mode 100644
index 0000000..335bc98
--- /dev/null
+++ b/src/tint/ast/i32.h
@@ -0,0 +1,50 @@
+// Copyright 2020 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_AST_I32_H_
+#define SRC_TINT_AST_I32_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A signed int 32 type.
+class I32 final : public Castable<I32, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  I32(ProgramID pid, const Source& src);
+  /// Move constructor
+  I32(I32&&);
+  ~I32() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const I32* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_I32_H_
diff --git a/src/tint/ast/i32_test.cc b/src/tint/ast/i32_test.cc
new file mode 100644
index 0000000..7e1c265
--- /dev/null
+++ b/src/tint/ast/i32_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/i32.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstI32Test = TestHelper;
+
+TEST_F(AstI32Test, FriendlyName) {
+  auto* i = create<I32>();
+  EXPECT_EQ(i->FriendlyName(Symbols()), "i32");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/id_attribute.cc b/src/tint/ast/id_attribute.cc
new file mode 100644
index 0000000..b94b450
--- /dev/null
+++ b/src/tint/ast/id_attribute.cc
@@ -0,0 +1,42 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/id_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IdAttribute);
+
+namespace tint {
+namespace ast {
+
+IdAttribute::IdAttribute(ProgramID pid, const Source& src, uint32_t val)
+    : Base(pid, src), value(val) {}
+
+IdAttribute::~IdAttribute() = default;
+
+std::string IdAttribute::Name() const {
+  return "id";
+}
+
+const IdAttribute* IdAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<IdAttribute>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/id_attribute.h b/src/tint/ast/id_attribute.h
new file mode 100644
index 0000000..7fbac0e
--- /dev/null
+++ b/src/tint/ast/id_attribute.h
@@ -0,0 +1,51 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_ID_ATTRIBUTE_H_
+#define SRC_TINT_AST_ID_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// An id attribute for pipeline-overridable constants
+class IdAttribute final : public Castable<IdAttribute, Attribute> {
+ public:
+  /// Create an id attribute.
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param val the numeric id value
+  IdAttribute(ProgramID pid, const Source& src, uint32_t val);
+  ~IdAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const IdAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The id value
+  const uint32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_ID_ATTRIBUTE_H_
diff --git a/src/tint/ast/id_attribute_test.cc b/src/tint/ast/id_attribute_test.cc
new file mode 100644
index 0000000..c3c372b
--- /dev/null
+++ b/src/tint/ast/id_attribute_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/id_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IdAttributeTest = TestHelper;
+
+TEST_F(IdAttributeTest, Creation) {
+  auto* d = create<IdAttribute>(12);
+  EXPECT_EQ(12u, d->value);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/identifier_expression.cc b/src/tint/ast/identifier_expression.cc
new file mode 100644
index 0000000..1f73a45
--- /dev/null
+++ b/src/tint/ast/identifier_expression.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/identifier_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IdentifierExpression);
+
+namespace tint {
+namespace ast {
+
+IdentifierExpression::IdentifierExpression(ProgramID pid,
+                                           const Source& src,
+                                           Symbol sym)
+    : Base(pid, src), symbol(sym) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
+  TINT_ASSERT(AST, symbol.IsValid());
+}
+
+IdentifierExpression::IdentifierExpression(IdentifierExpression&&) = default;
+
+IdentifierExpression::~IdentifierExpression() = default;
+
+const IdentifierExpression* IdentifierExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto sym = ctx->Clone(symbol);
+  return ctx->dst->create<IdentifierExpression>(src, sym);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/identifier_expression.h b/src/tint/ast/identifier_expression.h
new file mode 100644
index 0000000..6c3c8b3
--- /dev/null
+++ b/src/tint/ast/identifier_expression.h
@@ -0,0 +1,49 @@
+// Copyright 2020 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_AST_IDENTIFIER_EXPRESSION_H_
+#define SRC_TINT_AST_IDENTIFIER_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// An identifier expression
+class IdentifierExpression final
+    : public Castable<IdentifierExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param sym the symbol for the identifier
+  IdentifierExpression(ProgramID pid, const Source& src, Symbol sym);
+  /// Move constructor
+  IdentifierExpression(IdentifierExpression&&);
+  ~IdentifierExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const IdentifierExpression* Clone(CloneContext* ctx) const override;
+
+  /// The symbol for the identifier
+  const Symbol symbol;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_IDENTIFIER_EXPRESSION_H_
diff --git a/src/tint/ast/identifier_expression_test.cc b/src/tint/ast/identifier_expression_test.cc
new file mode 100644
index 0000000..b8c3a2a
--- /dev/null
+++ b/src/tint/ast/identifier_expression_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IdentifierExpressionTest = TestHelper;
+
+TEST_F(IdentifierExpressionTest, Creation) {
+  auto* i = Expr("ident");
+  EXPECT_EQ(i->symbol, Symbol(1, ID()));
+}
+
+TEST_F(IdentifierExpressionTest, Creation_WithSource) {
+  auto* i = Expr(Source{Source::Location{20, 2}}, "ident");
+  EXPECT_EQ(i->symbol, Symbol(1, ID()));
+
+  auto src = i->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(IdentifierExpressionTest, IsIdentifier) {
+  auto* i = Expr("ident");
+  EXPECT_TRUE(i->Is<IdentifierExpression>());
+}
+
+TEST_F(IdentifierExpressionTest, Assert_InvalidSymbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Expr("");
+      },
+      "internal compiler error");
+}
+
+TEST_F(IdentifierExpressionTest, Assert_DifferentProgramID_Symbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Expr(b2.Sym("b2"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/if_statement.cc b/src/tint/ast/if_statement.cc
new file mode 100644
index 0000000..276d5d2
--- /dev/null
+++ b/src/tint/ast/if_statement.cc
@@ -0,0 +1,57 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/if_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IfStatement);
+
+namespace tint {
+namespace ast {
+
+IfStatement::IfStatement(ProgramID pid,
+                         const Source& src,
+                         const Expression* cond,
+                         const BlockStatement* b,
+                         ElseStatementList else_stmts)
+    : Base(pid, src),
+      condition(cond),
+      body(b),
+      else_statements(std::move(else_stmts)) {
+  TINT_ASSERT(AST, condition);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
+  TINT_ASSERT(AST, body);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+  for (auto* el : else_statements) {
+    TINT_ASSERT(AST, el);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, el, program_id);
+  }
+}
+
+IfStatement::IfStatement(IfStatement&&) = default;
+
+IfStatement::~IfStatement() = default;
+
+const IfStatement* IfStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* cond = ctx->Clone(condition);
+  auto* b = ctx->Clone(body);
+  auto el = ctx->Clone(else_statements);
+  return ctx->dst->create<IfStatement>(src, cond, b, el);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/if_statement.h b/src/tint/ast/if_statement.h
new file mode 100644
index 0000000..dae9fd7
--- /dev/null
+++ b/src/tint/ast/if_statement.h
@@ -0,0 +1,62 @@
+// Copyright 2020 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_AST_IF_STATEMENT_H_
+#define SRC_TINT_AST_IF_STATEMENT_H_
+
+#include <utility>
+
+#include "src/tint/ast/else_statement.h"
+
+namespace tint {
+namespace ast {
+
+/// An if statement
+class IfStatement final : public Castable<IfStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param condition the if condition
+  /// @param body the if body
+  /// @param else_stmts the else statements
+  IfStatement(ProgramID pid,
+              const Source& src,
+              const Expression* condition,
+              const BlockStatement* body,
+              ElseStatementList else_stmts);
+  /// Move constructor
+  IfStatement(IfStatement&&);
+  ~IfStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const IfStatement* Clone(CloneContext* ctx) const override;
+
+  /// The if condition or nullptr if none set
+  const Expression* const condition;
+
+  /// The if body
+  const BlockStatement* const body;
+
+  /// The else statements
+  const ElseStatementList else_statements;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_IF_STATEMENT_H_
diff --git a/src/tint/ast/if_statement_test.cc b/src/tint/ast/if_statement_test.cc
new file mode 100644
index 0000000..6090eca
--- /dev/null
+++ b/src/tint/ast/if_statement_test.cc
@@ -0,0 +1,106 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/if_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IfStatementTest = TestHelper;
+
+TEST_F(IfStatementTest, Creation) {
+  auto* cond = Expr("cond");
+  auto* stmt = create<IfStatement>(Source{Source::Location{20, 2}}, cond,
+                                   Block(create<DiscardStatement>()),
+                                   ElseStatementList{});
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(IfStatementTest, IsIf) {
+  auto* stmt = create<IfStatement>(Expr(true), Block(), ElseStatementList{});
+  EXPECT_TRUE(stmt->Is<IfStatement>());
+}
+
+TEST_F(IfStatementTest, Assert_Null_Condition) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<IfStatement>(nullptr, b.Block(), ElseStatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(IfStatementTest, Assert_Null_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<IfStatement>(b.Expr(true), nullptr, ElseStatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(IfStatementTest, Assert_Null_ElseStatement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        auto* body = b.create<BlockStatement>(StatementList{});
+        b.create<IfStatement>(b.Expr(true), body, ElseStatementList{nullptr});
+      },
+      "internal compiler error");
+}
+
+TEST_F(IfStatementTest, Assert_DifferentProgramID_Cond) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<IfStatement>(b2.Expr(true), b1.Block(), ElseStatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(IfStatementTest, Assert_DifferentProgramID_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<IfStatement>(b1.Expr(true), b2.Block(), ElseStatementList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(IfStatementTest, Assert_DifferentProgramID_ElseStatement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<IfStatement>(
+            b1.Expr(true), b1.Block(),
+            ElseStatementList{
+                b2.create<ElseStatement>(b2.Expr("ident"), b2.Block()),
+            });
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/index_accessor_expression.cc b/src/tint/ast/index_accessor_expression.cc
new file mode 100644
index 0000000..afd73fd
--- /dev/null
+++ b/src/tint/ast/index_accessor_expression.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/index_accessor_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IndexAccessorExpression);
+
+namespace tint {
+namespace ast {
+
+IndexAccessorExpression::IndexAccessorExpression(ProgramID pid,
+                                                 const Source& src,
+                                                 const Expression* obj,
+                                                 const Expression* idx)
+    : Base(pid, src), object(obj), index(idx) {
+  TINT_ASSERT(AST, object);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, object, program_id);
+  TINT_ASSERT(AST, idx);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, idx, program_id);
+}
+
+IndexAccessorExpression::IndexAccessorExpression(IndexAccessorExpression&&) =
+    default;
+
+IndexAccessorExpression::~IndexAccessorExpression() = default;
+
+const IndexAccessorExpression* IndexAccessorExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* obj = ctx->Clone(object);
+  auto* idx = ctx->Clone(index);
+  return ctx->dst->create<IndexAccessorExpression>(src, obj, idx);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/index_accessor_expression.h b/src/tint/ast/index_accessor_expression.h
new file mode 100644
index 0000000..6fabada
--- /dev/null
+++ b/src/tint/ast/index_accessor_expression.h
@@ -0,0 +1,56 @@
+// Copyright 2020 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_AST_INDEX_ACCESSOR_EXPRESSION_H_
+#define SRC_TINT_AST_INDEX_ACCESSOR_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// An index accessor expression
+class IndexAccessorExpression final
+    : public Castable<IndexAccessorExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the index accessor source
+  /// @param obj the object
+  /// @param idx the index expression
+  IndexAccessorExpression(ProgramID program_id,
+                          const Source& source,
+                          const Expression* obj,
+                          const Expression* idx);
+  /// Move constructor
+  IndexAccessorExpression(IndexAccessorExpression&&);
+  ~IndexAccessorExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const IndexAccessorExpression* Clone(CloneContext* ctx) const override;
+
+  /// the array, vector or matrix
+  const Expression* const object;
+
+  /// the index expression
+  const Expression* const index;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_INDEX_ACCESSOR_EXPRESSION_H_
diff --git a/src/tint/ast/index_accessor_expression_test.cc b/src/tint/ast/index_accessor_expression_test.cc
new file mode 100644
index 0000000..8b91239
--- /dev/null
+++ b/src/tint/ast/index_accessor_expression_test.cc
@@ -0,0 +1,91 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IndexAccessorExpressionTest = TestHelper;
+
+TEST_F(IndexAccessorExpressionTest, Create) {
+  auto* obj = Expr("obj");
+  auto* idx = Expr("idx");
+
+  auto* exp = IndexAccessor(obj, idx);
+  ASSERT_EQ(exp->object, obj);
+  ASSERT_EQ(exp->index, idx);
+}
+
+TEST_F(IndexAccessorExpressionTest, CreateWithSource) {
+  auto* obj = Expr("obj");
+  auto* idx = Expr("idx");
+
+  auto* exp = IndexAccessor(Source{{20, 2}}, obj, idx);
+  auto src = exp->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(IndexAccessorExpressionTest, IsIndexAccessor) {
+  auto* obj = Expr("obj");
+  auto* idx = Expr("idx");
+
+  auto* exp = IndexAccessor(obj, idx);
+  EXPECT_TRUE(exp->Is<IndexAccessorExpression>());
+}
+
+TEST_F(IndexAccessorExpressionTest, Assert_Null_Array) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.IndexAccessor(nullptr, b.Expr("idx"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(IndexAccessorExpressionTest, Assert_Null_Index) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.IndexAccessor(b.Expr("arr"), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(IndexAccessorExpressionTest, Assert_DifferentProgramID_Array) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.IndexAccessor(b2.Expr("arr"), b1.Expr("idx"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(IndexAccessorExpressionTest, Assert_DifferentProgramID_Index) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.IndexAccessor(b1.Expr("arr"), b2.Expr("idx"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/int_literal_expression.cc b/src/tint/ast/int_literal_expression.cc
new file mode 100644
index 0000000..05c9f2d
--- /dev/null
+++ b/src/tint/ast/int_literal_expression.cc
@@ -0,0 +1,28 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/int_literal_expression.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::IntLiteralExpression);
+
+namespace tint {
+namespace ast {
+
+IntLiteralExpression::IntLiteralExpression(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+IntLiteralExpression::~IntLiteralExpression() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/int_literal_expression.h b/src/tint/ast/int_literal_expression.h
new file mode 100644
index 0000000..9da44bb
--- /dev/null
+++ b/src/tint/ast/int_literal_expression.h
@@ -0,0 +1,45 @@
+// Copyright 2020 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_AST_INT_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_INT_LITERAL_EXPRESSION_H_
+
+#include "src/tint/ast/literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// An integer literal. This could be either signed or unsigned.
+class IntLiteralExpression
+    : public Castable<IntLiteralExpression, LiteralExpression> {
+ public:
+  ~IntLiteralExpression() override;
+
+  /// @returns the literal value as a u32
+  virtual uint32_t ValueAsU32() const = 0;
+
+  /// @returns the literal value as an i32
+  int32_t ValueAsI32() const { return static_cast<int32_t>(ValueAsU32()); }
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  IntLiteralExpression(ProgramID pid, const Source& src);
+};  // namespace ast
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_INT_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/int_literal_expression_test.cc b/src/tint/ast/int_literal_expression_test.cc
new file mode 100644
index 0000000..e64d456
--- /dev/null
+++ b/src/tint/ast/int_literal_expression_test.cc
@@ -0,0 +1,35 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IntLiteralExpressionTest = TestHelper;
+
+TEST_F(IntLiteralExpressionTest, Sint_IsInt) {
+  auto* i = create<SintLiteralExpression>(47);
+  ASSERT_TRUE(i->Is<IntLiteralExpression>());
+}
+
+TEST_F(IntLiteralExpressionTest, Uint_IsInt) {
+  auto* i = create<UintLiteralExpression>(42);
+  EXPECT_TRUE(i->Is<IntLiteralExpression>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/internal_attribute.cc b/src/tint/ast/internal_attribute.cc
new file mode 100644
index 0000000..7f8c84be
--- /dev/null
+++ b/src/tint/ast/internal_attribute.cc
@@ -0,0 +1,31 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/internal_attribute.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::InternalAttribute);
+
+namespace tint {
+namespace ast {
+
+InternalAttribute::InternalAttribute(ProgramID pid) : Base(pid, Source{}) {}
+
+InternalAttribute::~InternalAttribute() = default;
+
+std::string InternalAttribute::Name() const {
+  return "internal";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/internal_attribute.h b/src/tint/ast/internal_attribute.h
new file mode 100644
index 0000000..6a9fd4a
--- /dev/null
+++ b/src/tint/ast/internal_attribute.h
@@ -0,0 +1,48 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_INTERNAL_ATTRIBUTE_H_
+#define SRC_TINT_AST_INTERNAL_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// An attribute used to indicate that a function is tint-internal.
+/// These attributes are not produced by generators, but instead are usually
+/// created by transforms for consumption by a particular backend.
+class InternalAttribute : public Castable<InternalAttribute, Attribute> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  explicit InternalAttribute(ProgramID program_id);
+
+  /// Destructor
+  ~InternalAttribute() override;
+
+  /// @return a short description of the internal attribute which will be
+  /// displayed in WGSL as `@internal(<name>)` (but is not parsable).
+  virtual std::string InternalName() const = 0;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_INTERNAL_ATTRIBUTE_H_
diff --git a/src/tint/ast/interpolate_attribute.cc b/src/tint/ast/interpolate_attribute.cc
new file mode 100644
index 0000000..1cf62c6
--- /dev/null
+++ b/src/tint/ast/interpolate_attribute.cc
@@ -0,0 +1,86 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/interpolate_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::InterpolateAttribute);
+
+namespace tint {
+namespace ast {
+
+InterpolateAttribute::InterpolateAttribute(ProgramID pid,
+                                           const Source& src,
+                                           InterpolationType ty,
+                                           InterpolationSampling smpl)
+    : Base(pid, src), type(ty), sampling(smpl) {}
+
+InterpolateAttribute::~InterpolateAttribute() = default;
+
+std::string InterpolateAttribute::Name() const {
+  return "interpolate";
+}
+
+const InterpolateAttribute* InterpolateAttribute::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<InterpolateAttribute>(src, type, sampling);
+}
+
+std::ostream& operator<<(std::ostream& out, InterpolationType type) {
+  switch (type) {
+    case InterpolationType::kPerspective: {
+      out << "perspective";
+      break;
+    }
+    case InterpolationType::kLinear: {
+      out << "linear";
+      break;
+    }
+    case InterpolationType::kFlat: {
+      out << "flat";
+      break;
+    }
+  }
+  return out;
+}
+
+std::ostream& operator<<(std::ostream& out, InterpolationSampling sampling) {
+  switch (sampling) {
+    case InterpolationSampling::kNone: {
+      out << "none";
+      break;
+    }
+    case InterpolationSampling::kCenter: {
+      out << "center";
+      break;
+    }
+    case InterpolationSampling::kCentroid: {
+      out << "centroid";
+      break;
+    }
+    case InterpolationSampling::kSample: {
+      out << "sample";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/interpolate_attribute.h b/src/tint/ast/interpolate_attribute.h
new file mode 100644
index 0000000..ac3b49d
--- /dev/null
+++ b/src/tint/ast/interpolate_attribute.h
@@ -0,0 +1,76 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
+#define SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
+
+#include <ostream>
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// The interpolation type.
+enum class InterpolationType { kPerspective, kLinear, kFlat };
+
+/// The interpolation sampling.
+enum class InterpolationSampling { kNone = -1, kCenter, kCentroid, kSample };
+
+/// An interpolate attribute
+class InterpolateAttribute final
+    : public Castable<InterpolateAttribute, Attribute> {
+ public:
+  /// Create an interpolate attribute.
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param type the interpolation type
+  /// @param sampling the interpolation sampling
+  InterpolateAttribute(ProgramID pid,
+                       const Source& src,
+                       InterpolationType type,
+                       InterpolationSampling sampling);
+  ~InterpolateAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const InterpolateAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The interpolation type
+  const InterpolationType type;
+
+  /// The interpolation sampling
+  const InterpolationSampling sampling;
+};
+
+/// @param out the std::ostream to write to
+/// @param type the interpolation type
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, InterpolationType type);
+
+/// @param out the std::ostream to write to
+/// @param sampling the interpolation sampling
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, InterpolationSampling sampling);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_INTERPOLATE_ATTRIBUTE_H_
diff --git a/src/tint/ast/interpolate_attribute_test.cc b/src/tint/ast/interpolate_attribute_test.cc
new file mode 100644
index 0000000..e8417b2
--- /dev/null
+++ b/src/tint/ast/interpolate_attribute_test.cc
@@ -0,0 +1,34 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/interpolate_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using InterpolateAttributeTest = TestHelper;
+
+TEST_F(InterpolateAttributeTest, Creation) {
+  auto* d = create<InterpolateAttribute>(InterpolationType::kLinear,
+                                         InterpolationSampling::kCenter);
+  EXPECT_EQ(InterpolationType::kLinear, d->type);
+  EXPECT_EQ(InterpolationSampling::kCenter, d->sampling);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/invariant_attribute.cc b/src/tint/ast/invariant_attribute.cc
new file mode 100644
index 0000000..f893b8a
--- /dev/null
+++ b/src/tint/ast/invariant_attribute.cc
@@ -0,0 +1,40 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/invariant_attribute.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::InvariantAttribute);
+
+namespace tint {
+namespace ast {
+
+InvariantAttribute::InvariantAttribute(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+InvariantAttribute::~InvariantAttribute() = default;
+
+std::string InvariantAttribute::Name() const {
+  return "invariant";
+}
+
+const InvariantAttribute* InvariantAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<InvariantAttribute>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/invariant_attribute.h b/src/tint/ast/invariant_attribute.h
new file mode 100644
index 0000000..08a58e8
--- /dev/null
+++ b/src/tint/ast/invariant_attribute.h
@@ -0,0 +1,48 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_INVARIANT_ATTRIBUTE_H_
+#define SRC_TINT_AST_INVARIANT_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// The invariant attribute
+class InvariantAttribute final
+    : public Castable<InvariantAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  InvariantAttribute(ProgramID pid, const Source& src);
+  ~InvariantAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const InvariantAttribute* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_INVARIANT_ATTRIBUTE_H_
diff --git a/src/tint/ast/invariant_attribute_test.cc b/src/tint/ast/invariant_attribute_test.cc
new file mode 100644
index 0000000..7b09ed6
--- /dev/null
+++ b/src/tint/ast/invariant_attribute_test.cc
@@ -0,0 +1,27 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/invariant_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using InvariantAttributeTest = TestHelper;
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/literal_expression.cc b/src/tint/ast/literal_expression.cc
new file mode 100644
index 0000000..25aa908
--- /dev/null
+++ b/src/tint/ast/literal_expression.cc
@@ -0,0 +1,28 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/literal_expression.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::LiteralExpression);
+
+namespace tint {
+namespace ast {
+
+LiteralExpression::LiteralExpression(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+LiteralExpression::~LiteralExpression() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/literal_expression.h b/src/tint/ast/literal_expression.h
new file mode 100644
index 0000000..0b1c708
--- /dev/null
+++ b/src/tint/ast/literal_expression.h
@@ -0,0 +1,40 @@
+// Copyright 2020 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_AST_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_LITERAL_EXPRESSION_H_
+
+#include <string>
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// Base class for a literal value expressions
+class LiteralExpression : public Castable<LiteralExpression, Expression> {
+ public:
+  ~LiteralExpression() override;
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the input source
+  LiteralExpression(ProgramID pid, const Source& src);
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/location_attribute.cc b/src/tint/ast/location_attribute.cc
new file mode 100644
index 0000000..9f3a53d
--- /dev/null
+++ b/src/tint/ast/location_attribute.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/location_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::LocationAttribute);
+
+namespace tint {
+namespace ast {
+
+LocationAttribute::LocationAttribute(ProgramID pid,
+                                     const Source& src,
+                                     uint32_t val)
+    : Base(pid, src), value(val) {}
+
+LocationAttribute::~LocationAttribute() = default;
+
+std::string LocationAttribute::Name() const {
+  return "location";
+}
+
+const LocationAttribute* LocationAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<LocationAttribute>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/location_attribute.h b/src/tint/ast/location_attribute.h
new file mode 100644
index 0000000..3c8078d
--- /dev/null
+++ b/src/tint/ast/location_attribute.h
@@ -0,0 +1,51 @@
+// Copyright 2020 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_AST_LOCATION_ATTRIBUTE_H_
+#define SRC_TINT_AST_LOCATION_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A location attribute
+class LocationAttribute final : public Castable<LocationAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the location value
+  LocationAttribute(ProgramID pid, const Source& src, uint32_t value);
+  ~LocationAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const LocationAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The location value
+  const uint32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_LOCATION_ATTRIBUTE_H_
diff --git a/src/tint/ast/location_attribute_test.cc b/src/tint/ast/location_attribute_test.cc
new file mode 100644
index 0000000..e826a1c
--- /dev/null
+++ b/src/tint/ast/location_attribute_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using LocationAttributeTest = TestHelper;
+
+TEST_F(LocationAttributeTest, Creation) {
+  auto* d = create<LocationAttribute>(2);
+  EXPECT_EQ(2u, d->value);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/loop_statement.cc b/src/tint/ast/loop_statement.cc
new file mode 100644
index 0000000..4e490fd2
--- /dev/null
+++ b/src/tint/ast/loop_statement.cc
@@ -0,0 +1,47 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/loop_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::LoopStatement);
+
+namespace tint {
+namespace ast {
+
+LoopStatement::LoopStatement(ProgramID pid,
+                             const Source& src,
+                             const BlockStatement* b,
+                             const BlockStatement* cont)
+    : Base(pid, src), body(b), continuing(cont) {
+  TINT_ASSERT(AST, body);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, body, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, continuing, program_id);
+}
+
+LoopStatement::LoopStatement(LoopStatement&&) = default;
+
+LoopStatement::~LoopStatement() = default;
+
+const LoopStatement* LoopStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* b = ctx->Clone(body);
+  auto* cont = ctx->Clone(continuing);
+  return ctx->dst->create<LoopStatement>(src, b, cont);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/loop_statement.h b/src/tint/ast/loop_statement.h
new file mode 100644
index 0000000..be29fc0
--- /dev/null
+++ b/src/tint/ast/loop_statement.h
@@ -0,0 +1,55 @@
+// Copyright 2020 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_AST_LOOP_STATEMENT_H_
+#define SRC_TINT_AST_LOOP_STATEMENT_H_
+
+#include "src/tint/ast/block_statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A loop statement
+class LoopStatement final : public Castable<LoopStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the loop statement source
+  /// @param body the body statements
+  /// @param continuing the continuing statements
+  LoopStatement(ProgramID program_id,
+                const Source& source,
+                const BlockStatement* body,
+                const BlockStatement* continuing);
+  /// Move constructor
+  LoopStatement(LoopStatement&&);
+  ~LoopStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const LoopStatement* Clone(CloneContext* ctx) const override;
+
+  /// The loop body
+  const BlockStatement* const body;
+
+  /// The continuing statements
+  const BlockStatement* const continuing;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_LOOP_STATEMENT_H_
diff --git a/src/tint/ast/loop_statement_test.cc b/src/tint/ast/loop_statement_test.cc
new file mode 100644
index 0000000..17cf7e9
--- /dev/null
+++ b/src/tint/ast/loop_statement_test.cc
@@ -0,0 +1,105 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/loop_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using LoopStatementTest = TestHelper;
+
+TEST_F(LoopStatementTest, Creation) {
+  auto* body = Block(create<DiscardStatement>());
+  auto* b = body->Last();
+
+  auto* continuing = Block(create<DiscardStatement>());
+
+  auto* l = create<LoopStatement>(body, continuing);
+  ASSERT_EQ(l->body->statements.size(), 1u);
+  EXPECT_EQ(l->body->statements[0], b);
+  ASSERT_EQ(l->continuing->statements.size(), 1u);
+  EXPECT_EQ(l->continuing->statements[0], continuing->Last());
+}
+
+TEST_F(LoopStatementTest, Creation_WithSource) {
+  auto* body = Block(create<DiscardStatement>());
+
+  auto* continuing = Block(create<DiscardStatement>());
+
+  auto* l =
+      create<LoopStatement>(Source{Source::Location{20, 2}}, body, continuing);
+  auto src = l->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(LoopStatementTest, IsLoop) {
+  auto* l = create<LoopStatement>(Block(), Block());
+  EXPECT_TRUE(l->Is<LoopStatement>());
+}
+
+TEST_F(LoopStatementTest, HasContinuing_WithoutContinuing) {
+  auto* body = Block(create<DiscardStatement>());
+
+  auto* l = create<LoopStatement>(body, nullptr);
+  EXPECT_FALSE(l->continuing);
+}
+
+TEST_F(LoopStatementTest, HasContinuing_WithContinuing) {
+  auto* body = Block(create<DiscardStatement>());
+
+  auto* continuing = Block(create<DiscardStatement>());
+
+  auto* l = create<LoopStatement>(body, continuing);
+  EXPECT_TRUE(l->continuing);
+}
+
+TEST_F(LoopStatementTest, Assert_Null_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<LoopStatement>(nullptr, nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(LoopStatementTest, Assert_DifferentProgramID_Body) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<LoopStatement>(b2.Block(), b1.Block());
+      },
+      "internal compiler error");
+}
+
+TEST_F(LoopStatementTest, Assert_DifferentProgramID_Continuing) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<LoopStatement>(b1.Block(), b2.Block());
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/matrix.cc b/src/tint/ast/matrix.cc
new file mode 100644
index 0000000..ce65483
--- /dev/null
+++ b/src/tint/ast/matrix.cc
@@ -0,0 +1,56 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/matrix.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Matrix);
+
+namespace tint {
+namespace ast {
+
+Matrix::Matrix(ProgramID pid,
+               const Source& src,
+               const Type* subtype,
+               uint32_t r,
+               uint32_t c)
+    : Base(pid, src), type(subtype), rows(r), columns(c) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, subtype, program_id);
+  TINT_ASSERT(AST, rows > 1);
+  TINT_ASSERT(AST, rows < 5);
+  TINT_ASSERT(AST, columns > 1);
+  TINT_ASSERT(AST, columns < 5);
+}
+
+Matrix::Matrix(Matrix&&) = default;
+
+Matrix::~Matrix() = default;
+
+std::string Matrix::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "mat" << columns << "x" << rows << "<" << type->FriendlyName(symbols)
+      << ">";
+  return out.str();
+}
+
+const Matrix* Matrix::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<Matrix>(src, ty, rows, columns);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/matrix.h b/src/tint/ast/matrix.h
new file mode 100644
index 0000000..0ba418c
--- /dev/null
+++ b/src/tint/ast/matrix.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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_AST_MATRIX_H_
+#define SRC_TINT_AST_MATRIX_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A matrix type
+class Matrix final : public Castable<Matrix, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param subtype the declared type of the matrix components. May be null for
+  ///        matrix constructors, where the element type will be inferred from
+  ///        the constructor arguments
+  /// @param rows the number of rows in the matrix
+  /// @param columns the number of columns in the matrix
+  Matrix(ProgramID pid,
+         const Source& src,
+         const Type* subtype,
+         uint32_t rows,
+         uint32_t columns);
+  /// Move constructor
+  Matrix(Matrix&&);
+  ~Matrix() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Matrix* Clone(CloneContext* ctx) const override;
+
+  /// The declared type of the matrix components. May be null for matrix
+  /// constructors, where the element type will be inferred from the constructor
+  /// arguments
+  const Type* const type;
+
+  /// The number of rows in the matrix
+  const uint32_t rows;
+
+  /// The number of columns in the matrix
+  const uint32_t columns;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_MATRIX_H_
diff --git a/src/tint/ast/matrix_test.cc b/src/tint/ast/matrix_test.cc
new file mode 100644
index 0000000..9bb4b8c
--- /dev/null
+++ b/src/tint/ast/matrix_test.cc
@@ -0,0 +1,52 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/texture.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstMatrixTest = TestHelper;
+
+TEST_F(AstMatrixTest, Creation) {
+  auto* i32 = create<I32>();
+  auto* m = create<Matrix>(i32, 2, 4);
+  EXPECT_EQ(m->type, i32);
+  EXPECT_EQ(m->rows, 2u);
+  EXPECT_EQ(m->columns, 4u);
+}
+
+TEST_F(AstMatrixTest, FriendlyName) {
+  auto* i32 = create<I32>();
+  auto* m = create<Matrix>(i32, 3, 2);
+  EXPECT_EQ(m->FriendlyName(Symbols()), "mat2x3<i32>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/member_accessor_expression.cc b/src/tint/ast/member_accessor_expression.cc
new file mode 100644
index 0000000..1b5a724
--- /dev/null
+++ b/src/tint/ast/member_accessor_expression.cc
@@ -0,0 +1,51 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/member_accessor_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::MemberAccessorExpression);
+
+namespace tint {
+namespace ast {
+
+MemberAccessorExpression::MemberAccessorExpression(
+    ProgramID pid,
+    const Source& src,
+    const Expression* str,
+    const IdentifierExpression* mem)
+    : Base(pid, src), structure(str), member(mem) {
+  TINT_ASSERT(AST, structure);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, structure, program_id);
+  TINT_ASSERT(AST, member);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, member, program_id);
+}
+
+MemberAccessorExpression::MemberAccessorExpression(MemberAccessorExpression&&) =
+    default;
+
+MemberAccessorExpression::~MemberAccessorExpression() = default;
+
+const MemberAccessorExpression* MemberAccessorExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* str = ctx->Clone(structure);
+  auto* mem = ctx->Clone(member);
+  return ctx->dst->create<MemberAccessorExpression>(src, str, mem);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/member_accessor_expression.h b/src/tint/ast/member_accessor_expression.h
new file mode 100644
index 0000000..8a82ba1
--- /dev/null
+++ b/src/tint/ast/member_accessor_expression.h
@@ -0,0 +1,56 @@
+// Copyright 2020 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_AST_MEMBER_ACCESSOR_EXPRESSION_H_
+#define SRC_TINT_AST_MEMBER_ACCESSOR_EXPRESSION_H_
+
+#include "src/tint/ast/identifier_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A member accessor expression
+class MemberAccessorExpression final
+    : public Castable<MemberAccessorExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the member accessor expression source
+  /// @param structure the structure
+  /// @param member the member
+  MemberAccessorExpression(ProgramID program_id,
+                           const Source& source,
+                           const Expression* structure,
+                           const IdentifierExpression* member);
+  /// Move constructor
+  MemberAccessorExpression(MemberAccessorExpression&&);
+  ~MemberAccessorExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const MemberAccessorExpression* Clone(CloneContext* ctx) const override;
+
+  /// The structure
+  const Expression* const structure;
+
+  /// The member expression
+  const IdentifierExpression* const member;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_MEMBER_ACCESSOR_EXPRESSION_H_
diff --git a/src/tint/ast/member_accessor_expression_test.cc b/src/tint/ast/member_accessor_expression_test.cc
new file mode 100644
index 0000000..12c4e1f
--- /dev/null
+++ b/src/tint/ast/member_accessor_expression_test.cc
@@ -0,0 +1,89 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using MemberAccessorExpressionTest = TestHelper;
+
+TEST_F(MemberAccessorExpressionTest, Creation) {
+  auto* str = Expr("structure");
+  auto* mem = Expr("member");
+
+  auto* stmt = create<MemberAccessorExpression>(str, mem);
+  EXPECT_EQ(stmt->structure, str);
+  EXPECT_EQ(stmt->member, mem);
+}
+
+TEST_F(MemberAccessorExpressionTest, Creation_WithSource) {
+  auto* stmt = create<MemberAccessorExpression>(
+      Source{Source::Location{20, 2}}, Expr("structure"), Expr("member"));
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(MemberAccessorExpressionTest, IsMemberAccessor) {
+  auto* stmt =
+      create<MemberAccessorExpression>(Expr("structure"), Expr("member"));
+  EXPECT_TRUE(stmt->Is<MemberAccessorExpression>());
+}
+
+TEST_F(MemberAccessorExpressionTest, Assert_Null_Struct) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<MemberAccessorExpression>(nullptr, b.Expr("member"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(MemberAccessorExpressionTest, Assert_Null_Member) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<MemberAccessorExpression>(b.Expr("struct"), nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(MemberAccessorExpressionTest, Assert_DifferentProgramID_Struct) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<MemberAccessorExpression>(b2.Expr("structure"),
+                                            b1.Expr("member"));
+      },
+      "internal compiler error");
+}
+
+TEST_F(MemberAccessorExpressionTest, Assert_DifferentProgramID_Member) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<MemberAccessorExpression>(b1.Expr("structure"),
+                                            b2.Expr("member"));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/module.cc b/src/tint/ast/module.cc
new file mode 100644
index 0000000..49634f8
--- /dev/null
+++ b/src/tint/ast/module.cc
@@ -0,0 +1,127 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/module.h"
+
+#include <utility>
+
+#include "src/tint/ast/type_decl.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Module);
+
+namespace tint {
+namespace ast {
+
+Module::Module(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Module::Module(ProgramID pid,
+               const Source& src,
+               std::vector<const ast::Node*> global_decls)
+    : Base(pid, src), global_declarations_(std::move(global_decls)) {
+  for (auto* decl : global_declarations_) {
+    if (decl == nullptr) {
+      continue;
+    }
+    diag::List diags;
+    BinGlobalDeclaration(decl, diags);
+  }
+}
+
+Module::~Module() = default;
+
+const ast::TypeDecl* Module::LookupType(Symbol name) const {
+  for (auto* ty : TypeDecls()) {
+    if (ty->name == name) {
+      return ty;
+    }
+  }
+  return nullptr;
+}
+
+void Module::AddGlobalDeclaration(const tint::ast::Node* decl) {
+  diag::List diags;
+  BinGlobalDeclaration(decl, diags);
+  global_declarations_.emplace_back(decl);
+}
+
+void Module::BinGlobalDeclaration(const tint::ast::Node* decl,
+                                  diag::List& diags) {
+  Switch(
+      decl,  //
+      [&](const ast::TypeDecl* type) {
+        TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, type, program_id);
+        type_decls_.push_back(type);
+      },
+      [&](const Function* func) {
+        TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, func, program_id);
+        functions_.push_back(func);
+      },
+      [&](const Variable* var) {
+        TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, var, program_id);
+        global_variables_.push_back(var);
+      },
+      [&](Default) {
+        TINT_ICE(AST, diags) << "Unknown global declaration type";
+      });
+}
+
+void Module::AddGlobalVariable(const ast::Variable* var) {
+  TINT_ASSERT(AST, var);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, var, program_id);
+  global_variables_.push_back(var);
+  global_declarations_.push_back(var);
+}
+
+void Module::AddTypeDecl(const ast::TypeDecl* type) {
+  TINT_ASSERT(AST, type);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, type, program_id);
+  type_decls_.push_back(type);
+  global_declarations_.push_back(type);
+}
+
+void Module::AddFunction(const ast::Function* func) {
+  TINT_ASSERT(AST, func);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, func, program_id);
+  functions_.push_back(func);
+  global_declarations_.push_back(func);
+}
+
+const Module* Module::Clone(CloneContext* ctx) const {
+  auto* out = ctx->dst->create<Module>();
+  out->Copy(ctx, this);
+  return out;
+}
+
+void Module::Copy(CloneContext* ctx, const Module* src) {
+  ctx->Clone(global_declarations_, src->global_declarations_);
+
+  // During the clone, declarations may have been placed into the module.
+  // Clear everything out, as we're about to re-bin the declarations.
+  type_decls_.clear();
+  functions_.clear();
+  global_variables_.clear();
+
+  for (auto* decl : global_declarations_) {
+    if (!decl) {
+      TINT_ICE(AST, ctx->dst->Diagnostics())
+          << "src global declaration was nullptr";
+      continue;
+    }
+    BinGlobalDeclaration(decl, ctx->dst->Diagnostics());
+  }
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/module.h b/src/tint/ast/module.h
new file mode 100644
index 0000000..b98013d
--- /dev/null
+++ b/src/tint/ast/module.h
@@ -0,0 +1,125 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_MODULE_H_
+#define SRC_TINT_AST_MODULE_H_
+
+#include <string>
+#include <vector>
+
+#include "src/tint/ast/function.h"
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+class TypeDecl;
+
+/// Module holds the top-level AST types, functions and global variables used by
+/// a Program.
+class Module final : public Castable<Module, Node> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Module(ProgramID pid, const Source& src);
+
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param global_decls the list of global types, functions, and variables, in
+  /// the order they were declared in the source program
+  Module(ProgramID pid,
+         const Source& src,
+         std::vector<const Node*> global_decls);
+
+  /// Destructor
+  ~Module() override;
+
+  /// @returns the declaration-ordered global declarations for the module
+  const std::vector<const Node*>& GlobalDeclarations() const {
+    return global_declarations_;
+  }
+
+  /// Add a global variable to the Builder
+  /// @param var the variable to add
+  void AddGlobalVariable(const Variable* var);
+
+  /// @returns true if the module has the global declaration `decl`
+  /// @param decl the declaration to check
+  bool HasGlobalDeclaration(Node* decl) const {
+    for (auto* d : global_declarations_) {
+      if (d == decl) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /// Adds a global declaration to the Builder.
+  /// @param decl the declaration to add
+  void AddGlobalDeclaration(const tint::ast::Node* decl);
+
+  /// @returns the global variables for the module
+  const VariableList& GlobalVariables() const { return global_variables_; }
+
+  /// @returns the global variables for the module
+  VariableList& GlobalVariables() { return global_variables_; }
+
+  /// Adds a type declaration to the Builder.
+  /// @param decl the type declaration to add
+  void AddTypeDecl(const TypeDecl* decl);
+
+  /// @returns the TypeDecl registered as a TypeDecl()
+  /// @param name the name of the type to search for
+  const TypeDecl* LookupType(Symbol name) const;
+
+  /// @returns the declared types in the module
+  const std::vector<const TypeDecl*>& TypeDecls() const { return type_decls_; }
+
+  /// Add a function to the Builder
+  /// @param func the function to add
+  void AddFunction(const Function* func);
+
+  /// @returns the functions declared in the module
+  const FunctionList& Functions() const { return functions_; }
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const Module* Clone(CloneContext* ctx) const override;
+
+  /// Copy copies the content of the Module src into this module.
+  /// @param ctx the clone context
+  /// @param src the module to copy into this module
+  void Copy(CloneContext* ctx, const Module* src);
+
+ private:
+  /// Adds `decl` to either:
+  /// * #global_declarations_
+  /// * #type_decls_
+  /// * #functions_
+  void BinGlobalDeclaration(const tint::ast::Node* decl, diag::List& diags);
+
+  std::vector<const Node*> global_declarations_;
+  std::vector<const TypeDecl*> type_decls_;
+  FunctionList functions_;
+  VariableList global_variables_;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_MODULE_H_
diff --git a/src/tint/ast/module_clone_test.cc b/src/tint/ast/module_clone_test.cc
new file mode 100644
index 0000000..4e92f25
--- /dev/null
+++ b/src/tint/ast/module_clone_test.cc
@@ -0,0 +1,181 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <unordered_set>
+
+#include "gtest/gtest.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+TEST(ModuleCloneTest, Clone) {
+#if TINT_BUILD_WGSL_READER && TINT_BUILD_WGSL_WRITER
+  // Shader that exercises the bulk of the AST nodes and types.
+  // See also fuzzers/tint_ast_clone_fuzzer.cc for further coverage of cloning.
+  Source::File file("test.wgsl", R"(struct S0 {
+  @size(4)
+  m0 : u32,
+  m1 : array<u32>,
+};
+
+struct S1 {
+  @size(4)
+  m0 : u32,
+  m1 : array<u32, 6>,
+};
+
+let c0 : i32 = 10;
+let c1 : bool = true;
+
+type t0 = array<vec4<f32>>;
+type t1 = array<vec4<f32>>;
+
+var<private> g0 : u32 = 20u;
+var<private> g1 : f32 = 123.0;
+@group(0) @binding(0) var g2 : texture_2d<f32>;
+@group(1) @binding(0) var g3 : texture_depth_2d;
+@group(2) @binding(0) var g4 : texture_storage_2d<rg32float, write>;
+@group(3) @binding(0) var g5 : texture_depth_cube_array;
+@group(4) @binding(0) var g6 : texture_external;
+
+var<private> g7 : vec3<f32>;
+@group(0) @binding(1) var<storage, write> g8 : S0;
+@group(1) @binding(1) var<storage, read> g9 : S0;
+@group(2) @binding(1) var<storage, read_write> g10 : S0;
+
+fn f0(p0 : bool) -> f32 {
+  if (p0) {
+    return 1.0;
+  }
+  return 0.0;
+}
+
+fn f1(p0 : f32, p1 : i32) -> f32 {
+  var l0 : i32 = 3;
+  var l1 : f32 = 8.0;
+  var l2 : u32 = bitcast<u32>(4);
+  var l3 : vec2<u32> = vec2<u32>(u32(l0), u32(l1));
+  var l4 : S1;
+  var l5 : u32 = l4.m1[5];
+  let l6 : ptr<private, u32> = &g0;
+  loop {
+    l0 = (p1 + 2);
+    if (((l0 % 4) == 0)) {
+      break;
+    }
+
+    continuing {
+      if (1 == 2) {
+        l0 = l0 - 1;
+      } else {
+        l0 = l0 - 2;
+      }
+    }
+  }
+  switch(l2) {
+    case 0u: {
+      break;
+    }
+    case 1u: {
+      return f0(true);
+    }
+    default: {
+      discard;
+    }
+  }
+  return 1.0;
+}
+
+@stage(fragment)
+fn main() {
+  f1(1.0, 2);
+}
+
+let declaration_order_check_0 : i32 = 1;
+
+type declaration_order_check_1 = f32;
+
+fn declaration_order_check_2() {}
+
+type declaration_order_check_3 = f32;
+
+let declaration_order_check_4 : i32 = 1;
+
+)");
+
+  // Parse the wgsl, create the src program
+  auto src = reader::wgsl::Parse(&file);
+
+  ASSERT_TRUE(src.IsValid()) << diag::Formatter().format(src.Diagnostics());
+
+  // Clone the src program to dst
+  Program dst(src.Clone());
+
+  ASSERT_TRUE(dst.IsValid()) << diag::Formatter().format(dst.Diagnostics());
+
+  // Expect the printed strings to match
+  EXPECT_EQ(Program::printer(&src), Program::printer(&dst));
+
+  // Check that none of the AST nodes or type pointers in dst are found in src
+  std::unordered_set<const ast::Node*> src_nodes;
+  for (auto* src_node : src.ASTNodes().Objects()) {
+    src_nodes.emplace(src_node);
+  }
+  std::unordered_set<const sem::Type*> src_types;
+  for (auto* src_type : src.Types()) {
+    src_types.emplace(src_type);
+  }
+  for (auto* dst_node : dst.ASTNodes().Objects()) {
+    ASSERT_EQ(src_nodes.count(dst_node), 0u);
+  }
+  for (auto* dst_type : dst.Types()) {
+    ASSERT_EQ(src_types.count(dst_type), 0u);
+  }
+
+  // Regenerate the wgsl for the src program. We use this instead of the
+  // original source so that reformatting doesn't impact the final wgsl
+  // comparison.
+  writer::wgsl::Options options;
+  std::string src_wgsl;
+  {
+    auto result = writer::wgsl::Generate(&src, options);
+    ASSERT_TRUE(result.success) << result.error;
+    src_wgsl = result.wgsl;
+
+    // Move the src program to a temporary that'll be dropped, so that the src
+    // program is released before we attempt to print the dst program. This
+    // guarantee that all the source program nodes and types are destructed and
+    // freed. ASAN should error if there's any remaining references in dst when
+    // we try to reconstruct the WGSL.
+    auto tmp = std::move(src);
+  }
+
+  // Print the dst module, check it matches the original source
+  auto result = writer::wgsl::Generate(&dst, options);
+  ASSERT_TRUE(result.success);
+  auto dst_wgsl = result.wgsl;
+  ASSERT_EQ(src_wgsl, dst_wgsl);
+
+#else  // #if TINT_BUILD_WGSL_READER && TINT_BUILD_WGSL_WRITER
+  GTEST_SKIP() << "ModuleCloneTest requires TINT_BUILD_WGSL_READER and "
+                  "TINT_BUILD_WGSL_WRITER to be enabled";
+#endif
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/module_test.cc b/src/tint/ast/module_test.cc
new file mode 100644
index 0000000..21b48dc
--- /dev/null
+++ b/src/tint/ast/module_test.cc
@@ -0,0 +1,143 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/clone_context.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ModuleTest = TestHelper;
+
+TEST_F(ModuleTest, Creation) {
+  EXPECT_EQ(Program(std::move(*this)).AST().Functions().size(), 0u);
+}
+
+TEST_F(ModuleTest, LookupFunction) {
+  auto* func = Func("main", VariableList{}, ty.f32(), StatementList{},
+                    ast::AttributeList{});
+
+  Program program(std::move(*this));
+  EXPECT_EQ(func,
+            program.AST().Functions().Find(program.Symbols().Get("main")));
+}
+
+TEST_F(ModuleTest, LookupFunctionMissing) {
+  Program program(std::move(*this));
+  EXPECT_EQ(nullptr,
+            program.AST().Functions().Find(program.Symbols().Get("Missing")));
+}
+
+TEST_F(ModuleTest, Assert_Null_GlobalVariable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder builder;
+        builder.AST().AddGlobalVariable(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ModuleTest, Assert_Null_TypeDecl) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder builder;
+        builder.AST().AddTypeDecl(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ModuleTest, Assert_DifferentProgramID_Function) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.AST().AddFunction(b2.create<ast::Function>(
+            b2.Symbols().Register("func"), VariableList{}, b2.ty.f32(),
+            b2.Block(), AttributeList{}, AttributeList{}));
+      },
+      "internal compiler error");
+}
+
+TEST_F(ModuleTest, Assert_DifferentProgramID_GlobalVariable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.AST().AddGlobalVariable(
+            b2.Var("var", b2.ty.i32(), ast::StorageClass::kPrivate));
+      },
+      "internal compiler error");
+}
+
+TEST_F(ModuleTest, Assert_Null_Function) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder builder;
+        builder.AST().AddFunction(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ModuleTest, CloneOrder) {
+  // Create a program with a function, alias decl and var decl.
+  Program p = [] {
+    ProgramBuilder b;
+    b.Func("F", {}, b.ty.void_(), {});
+    b.Alias("A", b.ty.u32());
+    b.Global("V", b.ty.i32(), ast::StorageClass::kPrivate);
+    return Program(std::move(b));
+  }();
+
+  // Clone the program, using ReplaceAll() to create new module-scope
+  // declarations. We want to test that these are added just before the
+  // declaration that triggered the ReplaceAll().
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &p);
+  ctx.ReplaceAll([&](const ast::Function*) -> const ast::Function* {
+    ctx.dst->Alias("inserted_before_F", cloned.ty.u32());
+    return nullptr;
+  });
+  ctx.ReplaceAll([&](const ast::Alias*) -> const ast::Alias* {
+    ctx.dst->Alias("inserted_before_A", cloned.ty.u32());
+    return nullptr;
+  });
+  ctx.ReplaceAll([&](const ast::Variable*) -> const ast::Variable* {
+    ctx.dst->Alias("inserted_before_V", cloned.ty.u32());
+    return nullptr;
+  });
+  ctx.Clone();
+
+  auto& decls = cloned.AST().GlobalDeclarations();
+  ASSERT_EQ(decls.size(), 6u);
+  EXPECT_TRUE(decls[1]->Is<ast::Function>());
+  EXPECT_TRUE(decls[3]->Is<ast::Alias>());
+  EXPECT_TRUE(decls[5]->Is<ast::Variable>());
+
+  ASSERT_TRUE(decls[0]->Is<ast::Alias>());
+  ASSERT_TRUE(decls[2]->Is<ast::Alias>());
+  ASSERT_TRUE(decls[4]->Is<ast::Alias>());
+
+  ASSERT_EQ(cloned.Symbols().NameFor(decls[0]->As<ast::Alias>()->name),
+            "inserted_before_F");
+  ASSERT_EQ(cloned.Symbols().NameFor(decls[2]->As<ast::Alias>()->name),
+            "inserted_before_A");
+  ASSERT_EQ(cloned.Symbols().NameFor(decls[4]->As<ast::Alias>()->name),
+            "inserted_before_V");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/multisampled_texture.cc b/src/tint/ast/multisampled_texture.cc
new file mode 100644
index 0000000..f216a3c
--- /dev/null
+++ b/src/tint/ast/multisampled_texture.cc
@@ -0,0 +1,52 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/multisampled_texture.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::MultisampledTexture);
+
+namespace tint {
+namespace ast {
+
+MultisampledTexture::MultisampledTexture(ProgramID pid,
+                                         const Source& src,
+                                         TextureDimension d,
+                                         const Type* ty)
+    : Base(pid, src, d), type(ty) {
+  TINT_ASSERT(AST, type);
+}
+
+MultisampledTexture::MultisampledTexture(MultisampledTexture&&) = default;
+
+MultisampledTexture::~MultisampledTexture() = default;
+
+std::string MultisampledTexture::FriendlyName(
+    const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "texture_multisampled_" << dim << "<" << type->FriendlyName(symbols)
+      << ">";
+  return out.str();
+}
+
+const MultisampledTexture* MultisampledTexture::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<MultisampledTexture>(src, dim, ty);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/multisampled_texture.h b/src/tint/ast/multisampled_texture.h
new file mode 100644
index 0000000..fbc3db0
--- /dev/null
+++ b/src/tint/ast/multisampled_texture.h
@@ -0,0 +1,59 @@
+// Copyright 2020 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_AST_MULTISAMPLED_TEXTURE_H_
+#define SRC_TINT_AST_MULTISAMPLED_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// A multisampled texture type.
+class MultisampledTexture final
+    : public Castable<MultisampledTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  /// @param type the data type of the multisampled texture
+  MultisampledTexture(ProgramID pid,
+                      const Source& src,
+                      TextureDimension dim,
+                      const Type* type);
+  /// Move constructor
+  MultisampledTexture(MultisampledTexture&&);
+  ~MultisampledTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const MultisampledTexture* Clone(CloneContext* ctx) const override;
+
+  /// The subtype of the multisampled texture
+  const Type* const type;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_MULTISAMPLED_TEXTURE_H_
diff --git a/src/tint/ast/multisampled_texture_test.cc b/src/tint/ast/multisampled_texture_test.cc
new file mode 100644
index 0000000..ba7ffb5
--- /dev/null
+++ b/src/tint/ast/multisampled_texture_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/multisampled_texture.h"
+
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/depth_texture.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampled_texture.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/storage_texture.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/texture.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstMultisampledTextureTest = TestHelper;
+
+TEST_F(AstMultisampledTextureTest, IsTexture) {
+  auto* f32 = create<F32>();
+  Texture* ty = create<MultisampledTexture>(TextureDimension::kCube, f32);
+  EXPECT_FALSE(ty->Is<DepthTexture>());
+  EXPECT_TRUE(ty->Is<MultisampledTexture>());
+  EXPECT_FALSE(ty->Is<SampledTexture>());
+  EXPECT_FALSE(ty->Is<StorageTexture>());
+}
+
+TEST_F(AstMultisampledTextureTest, Dim) {
+  auto* f32 = create<F32>();
+  auto* s = create<MultisampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->dim, TextureDimension::k3d);
+}
+
+TEST_F(AstMultisampledTextureTest, Type) {
+  auto* f32 = create<F32>();
+  auto* s = create<MultisampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->type, f32);
+}
+
+TEST_F(AstMultisampledTextureTest, FriendlyName) {
+  auto* f32 = create<F32>();
+  auto* s = create<MultisampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->FriendlyName(Symbols()), "texture_multisampled_3d<f32>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/node.cc b/src/tint/ast/node.cc
new file mode 100644
index 0000000..b90f0ef
--- /dev/null
+++ b/src/tint/ast/node.cc
@@ -0,0 +1,29 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/node.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Node);
+
+namespace tint {
+namespace ast {
+
+Node::Node(ProgramID pid, const Source& src) : program_id(pid), source(src) {}
+
+Node::Node(Node&&) = default;
+
+Node::~Node() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/node.h b/src/tint/ast/node.h
new file mode 100644
index 0000000..01b017b5
--- /dev/null
+++ b/src/tint/ast/node.h
@@ -0,0 +1,68 @@
+// Copyright 2020 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_AST_NODE_H_
+#define SRC_TINT_AST_NODE_H_
+
+#include <string>
+
+#include "src/tint/clone_context.h"
+
+namespace tint {
+
+// Forward declarations
+class CloneContext;
+namespace sem {
+class Type;
+}
+namespace sem {
+class Info;
+}
+
+namespace ast {
+
+/// AST base class node
+class Node : public Castable<Node, Cloneable> {
+ public:
+  ~Node() override;
+
+  /// The identifier of the program that owns this node
+  const ProgramID program_id;
+
+  /// The node source data
+  const Source source;
+
+ protected:
+  /// Create a new node
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the input source for the node
+  Node(ProgramID pid, const Source& src);
+  /// Move constructor
+  Node(Node&&);
+
+ private:
+  Node(const Node&) = delete;
+};
+
+}  // namespace ast
+
+/// @param node a pointer to an AST node
+/// @returns the ProgramID of the given AST node.
+inline ProgramID ProgramIDOf(const ast::Node* node) {
+  return node ? node->program_id : ProgramID();
+}
+
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_NODE_H_
diff --git a/src/tint/ast/phony_expression.cc b/src/tint/ast/phony_expression.cc
new file mode 100644
index 0000000..c05fb1d
--- /dev/null
+++ b/src/tint/ast/phony_expression.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/phony_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::PhonyExpression);
+
+namespace tint {
+namespace ast {
+
+PhonyExpression::PhonyExpression(ProgramID pid, const Source& src)
+    : Base(pid, src) {}
+
+PhonyExpression::PhonyExpression(PhonyExpression&&) = default;
+
+PhonyExpression::~PhonyExpression() = default;
+
+const PhonyExpression* PhonyExpression::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<PhonyExpression>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/phony_expression.h b/src/tint/ast/phony_expression.h
new file mode 100644
index 0000000..e021846
--- /dev/null
+++ b/src/tint/ast/phony_expression.h
@@ -0,0 +1,45 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_PHONY_EXPRESSION_H_
+#define SRC_TINT_AST_PHONY_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// Represents the `_` of a phony assignment `_ = <expr>`
+/// @see https://www.w3.org/TR/WGSL/#phony-assignment-section
+class PhonyExpression final : public Castable<PhonyExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  PhonyExpression(ProgramID pid, const Source& src);
+  /// Move constructor
+  PhonyExpression(PhonyExpression&&);
+  ~PhonyExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const PhonyExpression* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_PHONY_EXPRESSION_H_
diff --git a/src/tint/ast/phony_expression_test.cc b/src/tint/ast/phony_expression_test.cc
new file mode 100644
index 0000000..568a2d0
--- /dev/null
+++ b/src/tint/ast/phony_expression_test.cc
@@ -0,0 +1,42 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using IdentifierExpressionTest = TestHelper;
+
+TEST_F(IdentifierExpressionTest, Creation) {
+  EXPECT_NE(Phony(), nullptr);
+}
+
+TEST_F(IdentifierExpressionTest, Creation_WithSource) {
+  auto* p = Phony(Source{{20, 2}});
+
+  auto src = p->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(IdentifierExpressionTest, IsPhony) {
+  auto* p = Phony();
+  EXPECT_TRUE(p->Is<PhonyExpression>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/pipeline_stage.cc b/src/tint/ast/pipeline_stage.cc
new file mode 100644
index 0000000..9f5203d
--- /dev/null
+++ b/src/tint/ast/pipeline_stage.cc
@@ -0,0 +1,43 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/pipeline_stage.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, PipelineStage stage) {
+  switch (stage) {
+    case PipelineStage::kNone: {
+      out << "none";
+      break;
+    }
+    case PipelineStage::kVertex: {
+      out << "vertex";
+      break;
+    }
+    case PipelineStage::kFragment: {
+      out << "fragment";
+      break;
+    }
+    case PipelineStage::kCompute: {
+      out << "compute";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/pipeline_stage.h b/src/tint/ast/pipeline_stage.h
new file mode 100644
index 0000000..37e5030
--- /dev/null
+++ b/src/tint/ast/pipeline_stage.h
@@ -0,0 +1,34 @@
+// Copyright 2020 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_AST_PIPELINE_STAGE_H_
+#define SRC_TINT_AST_PIPELINE_STAGE_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The pipeline stage
+enum class PipelineStage { kNone = -1, kVertex, kFragment, kCompute };
+
+/// @param out the std::ostream to write to
+/// @param stage the PipelineStage
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, PipelineStage stage);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_PIPELINE_STAGE_H_
diff --git a/src/tint/ast/pointer.cc b/src/tint/ast/pointer.cc
new file mode 100644
index 0000000..1db6fc0
--- /dev/null
+++ b/src/tint/ast/pointer.cc
@@ -0,0 +1,57 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/pointer.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Pointer);
+
+namespace tint {
+namespace ast {
+
+Pointer::Pointer(ProgramID pid,
+                 const Source& src,
+                 const Type* const subtype,
+                 ast::StorageClass sc,
+                 ast::Access ac)
+    : Base(pid, src), type(subtype), storage_class(sc), access(ac) {}
+
+std::string Pointer::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "ptr<";
+  if (storage_class != ast::StorageClass::kNone) {
+    out << storage_class << ", ";
+  }
+  out << type->FriendlyName(symbols);
+  if (access != ast::Access::kUndefined) {
+    out << ", " << access;
+  }
+  out << ">";
+  return out.str();
+}
+
+Pointer::Pointer(Pointer&&) = default;
+
+Pointer::~Pointer() = default;
+
+const Pointer* Pointer::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<Pointer>(src, ty, storage_class, access);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/pointer.h b/src/tint/ast/pointer.h
new file mode 100644
index 0000000..c73eb40
--- /dev/null
+++ b/src/tint/ast/pointer.h
@@ -0,0 +1,68 @@
+// Copyright 2020 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_AST_POINTER_H_
+#define SRC_TINT_AST_POINTER_H_
+
+#include <string>
+
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/storage_class.h"
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A pointer type.
+class Pointer final : public Castable<Pointer, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param subtype the pointee type
+  /// @param storage_class the storage class of the pointer
+  /// @param access the access control of the pointer
+  Pointer(ProgramID pid,
+          const Source& src,
+          const Type* const subtype,
+          ast::StorageClass storage_class,
+          ast::Access access);
+  /// Move constructor
+  Pointer(Pointer&&);
+  ~Pointer() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Pointer* Clone(CloneContext* ctx) const override;
+
+  /// The pointee type
+  const Type* const type;
+
+  /// The storage class of the pointer
+  ast::StorageClass const storage_class;
+
+  /// The access control of the pointer
+  ast::Access const access;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_POINTER_H_
diff --git a/src/tint/ast/pointer_test.cc b/src/tint/ast/pointer_test.cc
new file mode 100644
index 0000000..ec5520c
--- /dev/null
+++ b/src/tint/ast/pointer_test.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/pointer.h"
+
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstPointerTest = TestHelper;
+
+TEST_F(AstPointerTest, Creation) {
+  auto* i32 = create<I32>();
+  auto* p = create<Pointer>(i32, ast::StorageClass::kStorage, Access::kRead);
+  EXPECT_EQ(p->type, i32);
+  EXPECT_EQ(p->storage_class, ast::StorageClass::kStorage);
+  EXPECT_EQ(p->access, Access::kRead);
+}
+
+TEST_F(AstPointerTest, FriendlyName) {
+  auto* i32 = create<I32>();
+  auto* p =
+      create<Pointer>(i32, ast::StorageClass::kWorkgroup, Access::kUndefined);
+  EXPECT_EQ(p->FriendlyName(Symbols()), "ptr<workgroup, i32>");
+}
+
+TEST_F(AstPointerTest, FriendlyNameWithAccess) {
+  auto* i32 = create<I32>();
+  auto* p =
+      create<Pointer>(i32, ast::StorageClass::kStorage, Access::kReadWrite);
+  EXPECT_EQ(p->FriendlyName(Symbols()), "ptr<storage, i32, read_write>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/return_statement.cc b/src/tint/ast/return_statement.cc
new file mode 100644
index 0000000..61b6d45
--- /dev/null
+++ b/src/tint/ast/return_statement.cc
@@ -0,0 +1,46 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/return_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::ReturnStatement);
+
+namespace tint {
+namespace ast {
+
+ReturnStatement::ReturnStatement(ProgramID pid, const Source& src)
+    : Base(pid, src), value(nullptr) {}
+
+ReturnStatement::ReturnStatement(ProgramID pid,
+                                 const Source& src,
+                                 const Expression* val)
+    : Base(pid, src), value(val) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, value, program_id);
+}
+
+ReturnStatement::ReturnStatement(ReturnStatement&&) = default;
+
+ReturnStatement::~ReturnStatement() = default;
+
+const ReturnStatement* ReturnStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ret = ctx->Clone(value);
+  return ctx->dst->create<ReturnStatement>(src, ret);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/return_statement.h b/src/tint/ast/return_statement.h
new file mode 100644
index 0000000..5a20ff4
--- /dev/null
+++ b/src/tint/ast/return_statement.h
@@ -0,0 +1,54 @@
+// Copyright 2020 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_AST_RETURN_STATEMENT_H_
+#define SRC_TINT_AST_RETURN_STATEMENT_H_
+
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/statement.h"
+
+namespace tint {
+namespace ast {
+
+/// A return statement
+class ReturnStatement final : public Castable<ReturnStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  ReturnStatement(ProgramID pid, const Source& src);
+
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the return value
+  ReturnStatement(ProgramID pid, const Source& src, const Expression* value);
+  /// Move constructor
+  ReturnStatement(ReturnStatement&&);
+  ~ReturnStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const ReturnStatement* Clone(CloneContext* ctx) const override;
+
+  /// The value returned. May be null.
+  const Expression* const value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_RETURN_STATEMENT_H_
diff --git a/src/tint/ast/return_statement_test.cc b/src/tint/ast/return_statement_test.cc
new file mode 100644
index 0000000..93c75a7
--- /dev/null
+++ b/src/tint/ast/return_statement_test.cc
@@ -0,0 +1,68 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/return_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ReturnStatementTest = TestHelper;
+
+TEST_F(ReturnStatementTest, Creation) {
+  auto* expr = Expr("expr");
+
+  auto* r = create<ReturnStatement>(expr);
+  EXPECT_EQ(r->value, expr);
+}
+
+TEST_F(ReturnStatementTest, Creation_WithSource) {
+  auto* r = create<ReturnStatement>(Source{Source::Location{20, 2}});
+  auto src = r->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(ReturnStatementTest, IsReturn) {
+  auto* r = create<ReturnStatement>();
+  EXPECT_TRUE(r->Is<ReturnStatement>());
+}
+
+TEST_F(ReturnStatementTest, WithoutValue) {
+  auto* r = create<ReturnStatement>();
+  EXPECT_EQ(r->value, nullptr);
+}
+
+TEST_F(ReturnStatementTest, WithValue) {
+  auto* expr = Expr("expr");
+  auto* r = create<ReturnStatement>(expr);
+  EXPECT_NE(r->value, nullptr);
+}
+
+TEST_F(ReturnStatementTest, Assert_DifferentProgramID_Expr) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<ReturnStatement>(b2.Expr(true));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sampled_texture.cc b/src/tint/ast/sampled_texture.cc
new file mode 100644
index 0000000..7937ad5
--- /dev/null
+++ b/src/tint/ast/sampled_texture.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/sampled_texture.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::SampledTexture);
+
+namespace tint {
+namespace ast {
+
+SampledTexture::SampledTexture(ProgramID pid,
+                               const Source& src,
+                               TextureDimension d,
+                               const Type* ty)
+    : Base(pid, src, d), type(ty) {
+  TINT_ASSERT(AST, type);
+}
+
+SampledTexture::SampledTexture(SampledTexture&&) = default;
+
+SampledTexture::~SampledTexture() = default;
+
+std::string SampledTexture::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "texture_" << dim << "<" << type->FriendlyName(symbols) << ">";
+  return out.str();
+}
+
+const SampledTexture* SampledTexture::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<SampledTexture>(src, dim, ty);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sampled_texture.h b/src/tint/ast/sampled_texture.h
new file mode 100644
index 0000000..6d0820c
--- /dev/null
+++ b/src/tint/ast/sampled_texture.h
@@ -0,0 +1,58 @@
+// Copyright 2020 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_AST_SAMPLED_TEXTURE_H_
+#define SRC_TINT_AST_SAMPLED_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// A sampled texture type.
+class SampledTexture final : public Castable<SampledTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  /// @param type the data type of the sampled texture
+  SampledTexture(ProgramID pid,
+                 const Source& src,
+                 TextureDimension dim,
+                 const Type* type);
+  /// Move constructor
+  SampledTexture(SampledTexture&&);
+  ~SampledTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const SampledTexture* Clone(CloneContext* ctx) const override;
+
+  /// The subtype of the sampled texture
+  const Type* const type;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_SAMPLED_TEXTURE_H_
diff --git a/src/tint/ast/sampled_texture_test.cc b/src/tint/ast/sampled_texture_test.cc
new file mode 100644
index 0000000..033751d
--- /dev/null
+++ b/src/tint/ast/sampled_texture_test.cc
@@ -0,0 +1,54 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/sampled_texture.h"
+
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstSampledTextureTest = TestHelper;
+
+TEST_F(AstSampledTextureTest, IsTexture) {
+  auto* f32 = create<F32>();
+  Texture* ty = create<SampledTexture>(TextureDimension::kCube, f32);
+  EXPECT_FALSE(ty->Is<DepthTexture>());
+  EXPECT_TRUE(ty->Is<SampledTexture>());
+  EXPECT_FALSE(ty->Is<StorageTexture>());
+}
+
+TEST_F(AstSampledTextureTest, Dim) {
+  auto* f32 = create<F32>();
+  auto* s = create<SampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->dim, TextureDimension::k3d);
+}
+
+TEST_F(AstSampledTextureTest, Type) {
+  auto* f32 = create<F32>();
+  auto* s = create<SampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->type, f32);
+}
+
+TEST_F(AstSampledTextureTest, FriendlyName) {
+  auto* f32 = create<F32>();
+  auto* s = create<SampledTexture>(TextureDimension::k3d, f32);
+  EXPECT_EQ(s->FriendlyName(Symbols()), "texture_3d<f32>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sampler.cc b/src/tint/ast/sampler.cc
new file mode 100644
index 0000000..2db571d
--- /dev/null
+++ b/src/tint/ast/sampler.cc
@@ -0,0 +1,53 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/sampler.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Sampler);
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, SamplerKind kind) {
+  switch (kind) {
+    case SamplerKind::kSampler:
+      out << "sampler";
+      break;
+    case SamplerKind::kComparisonSampler:
+      out << "comparison_sampler";
+      break;
+  }
+  return out;
+}
+
+Sampler::Sampler(ProgramID pid, const Source& src, SamplerKind k)
+    : Base(pid, src), kind(k) {}
+
+Sampler::Sampler(Sampler&&) = default;
+
+Sampler::~Sampler() = default;
+
+std::string Sampler::FriendlyName(const SymbolTable&) const {
+  return kind == SamplerKind::kSampler ? "sampler" : "sampler_comparison";
+}
+
+const Sampler* Sampler::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<Sampler>(src, kind);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sampler.h b/src/tint/ast/sampler.h
new file mode 100644
index 0000000..8724384
--- /dev/null
+++ b/src/tint/ast/sampler.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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_AST_SAMPLER_H_
+#define SRC_TINT_AST_SAMPLER_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// The different kinds of samplers
+enum class SamplerKind {
+  /// A regular sampler
+  kSampler,
+  /// A comparison sampler
+  kComparisonSampler
+};
+
+/// @param out the std::ostream to write to
+/// @param kind the SamplerKind
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, SamplerKind kind);
+
+/// A sampler type.
+class Sampler final : public Castable<Sampler, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param kind the kind of sampler
+  Sampler(ProgramID pid, const Source& src, SamplerKind kind);
+  /// Move constructor
+  Sampler(Sampler&&);
+  ~Sampler() override;
+
+  /// @returns true if this is a comparison sampler
+  bool IsComparison() const { return kind == SamplerKind::kComparisonSampler; }
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Sampler* Clone(CloneContext* ctx) const override;
+
+  /// The sampler type
+  const SamplerKind kind;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_SAMPLER_H_
diff --git a/src/tint/ast/sampler_test.cc b/src/tint/ast/sampler_test.cc
new file mode 100644
index 0000000..2ae7d42
--- /dev/null
+++ b/src/tint/ast/sampler_test.cc
@@ -0,0 +1,48 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/sampler.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstSamplerTest = TestHelper;
+
+TEST_F(AstSamplerTest, Creation) {
+  auto* s = create<Sampler>(SamplerKind::kSampler);
+  EXPECT_EQ(s->kind, SamplerKind::kSampler);
+}
+
+TEST_F(AstSamplerTest, Creation_ComparisonSampler) {
+  auto* s = create<Sampler>(SamplerKind::kComparisonSampler);
+  EXPECT_EQ(s->kind, SamplerKind::kComparisonSampler);
+  EXPECT_TRUE(s->IsComparison());
+}
+
+TEST_F(AstSamplerTest, FriendlyNameSampler) {
+  auto* s = create<Sampler>(SamplerKind::kSampler);
+  EXPECT_EQ(s->FriendlyName(Symbols()), "sampler");
+}
+
+TEST_F(AstSamplerTest, FriendlyNameComparisonSampler) {
+  auto* s = create<Sampler>(SamplerKind::kComparisonSampler);
+  EXPECT_EQ(s->FriendlyName(Symbols()), "sampler_comparison");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sint_literal_expression.cc b/src/tint/ast/sint_literal_expression.cc
new file mode 100644
index 0000000..fc0a4b3
--- /dev/null
+++ b/src/tint/ast/sint_literal_expression.cc
@@ -0,0 +1,43 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/sint_literal_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::SintLiteralExpression);
+
+namespace tint {
+namespace ast {
+
+SintLiteralExpression::SintLiteralExpression(ProgramID pid,
+                                             const Source& src,
+                                             int32_t val)
+    : Base(pid, src), value(val) {}
+
+SintLiteralExpression::~SintLiteralExpression() = default;
+
+uint32_t SintLiteralExpression::ValueAsU32() const {
+  return static_cast<uint32_t>(value);
+}
+
+const SintLiteralExpression* SintLiteralExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<SintLiteralExpression>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/sint_literal_expression.h b/src/tint/ast/sint_literal_expression.h
new file mode 100644
index 0000000..1431dd2
--- /dev/null
+++ b/src/tint/ast/sint_literal_expression.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_AST_SINT_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_SINT_LITERAL_EXPRESSION_H_
+
+#include <string>
+
+#include "src/tint/ast/int_literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A signed int literal
+class SintLiteralExpression final
+    : public Castable<SintLiteralExpression, IntLiteralExpression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the signed int literals value
+  SintLiteralExpression(ProgramID pid, const Source& src, int32_t value);
+  ~SintLiteralExpression() override;
+
+  /// @returns the literal value as a u32
+  uint32_t ValueAsU32() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const SintLiteralExpression* Clone(CloneContext* ctx) const override;
+
+  /// The int literal value
+  const int32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_SINT_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/sint_literal_expression_test.cc b/src/tint/ast/sint_literal_expression_test.cc
new file mode 100644
index 0000000..f19fff9
--- /dev/null
+++ b/src/tint/ast/sint_literal_expression_test.cc
@@ -0,0 +1,31 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using SintLiteralExpressionTest = TestHelper;
+
+TEST_F(SintLiteralExpressionTest, Value) {
+  auto* i = create<SintLiteralExpression>(47);
+  ASSERT_TRUE(i->Is<SintLiteralExpression>());
+  EXPECT_EQ(i->value, 47);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/stage_attribute.cc b/src/tint/ast/stage_attribute.cc
new file mode 100644
index 0000000..92cc802
--- /dev/null
+++ b/src/tint/ast/stage_attribute.cc
@@ -0,0 +1,44 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/stage_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StageAttribute);
+
+namespace tint {
+namespace ast {
+
+StageAttribute::StageAttribute(ProgramID pid,
+                               const Source& src,
+                               PipelineStage s)
+    : Base(pid, src), stage(s) {}
+
+StageAttribute::~StageAttribute() = default;
+
+std::string StageAttribute::Name() const {
+  return "stage";
+}
+
+const StageAttribute* StageAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<StageAttribute>(src, stage);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/stage_attribute.h b/src/tint/ast/stage_attribute.h
new file mode 100644
index 0000000..7bf918c
--- /dev/null
+++ b/src/tint/ast/stage_attribute.h
@@ -0,0 +1,54 @@
+// Copyright 2020 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_AST_STAGE_ATTRIBUTE_H_
+#define SRC_TINT_AST_STAGE_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/pipeline_stage.h"
+
+namespace tint {
+namespace ast {
+
+/// A workgroup attribute
+class StageAttribute final : public Castable<StageAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param stage the pipeline stage
+  /// @param source the source of this attribute
+  StageAttribute(ProgramID program_id,
+                 const Source& source,
+                 PipelineStage stage);
+  ~StageAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StageAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The pipeline stage
+  const PipelineStage stage;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STAGE_ATTRIBUTE_H_
diff --git a/src/tint/ast/stage_attribute_test.cc b/src/tint/ast/stage_attribute_test.cc
new file mode 100644
index 0000000..e1cc93a
--- /dev/null
+++ b/src/tint/ast/stage_attribute_test.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/stage_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/workgroup_attribute.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StageAttributeTest = TestHelper;
+
+TEST_F(StageAttributeTest, Creation_1param) {
+  auto* d = create<StageAttribute>(PipelineStage::kFragment);
+  EXPECT_EQ(d->stage, PipelineStage::kFragment);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/statement.cc b/src/tint/ast/statement.cc
new file mode 100644
index 0000000..de02bbd
--- /dev/null
+++ b/src/tint/ast/statement.cc
@@ -0,0 +1,87 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/statement.h"
+
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/continue_statement.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/fallthrough_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/variable_decl_statement.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Statement);
+
+namespace tint {
+namespace ast {
+
+Statement::Statement(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Statement::Statement(Statement&&) = default;
+
+Statement::~Statement() = default;
+
+const char* Statement::Name() const {
+  if (Is<AssignmentStatement>()) {
+    return "assignment statement";
+  }
+  if (Is<BlockStatement>()) {
+    return "block statement";
+  }
+  if (Is<BreakStatement>()) {
+    return "break statement";
+  }
+  if (Is<CaseStatement>()) {
+    return "case statement";
+  }
+  if (Is<CallStatement>()) {
+    return "function call";
+  }
+  if (Is<ContinueStatement>()) {
+    return "continue statement";
+  }
+  if (Is<DiscardStatement>()) {
+    return "discard statement";
+  }
+  if (Is<ElseStatement>()) {
+    return "else statement";
+  }
+  if (Is<FallthroughStatement>()) {
+    return "fallthrough statement";
+  }
+  if (Is<IfStatement>()) {
+    return "if statement";
+  }
+  if (Is<LoopStatement>()) {
+    return "loop statement";
+  }
+  if (Is<ReturnStatement>()) {
+    return "return statement";
+  }
+  if (Is<SwitchStatement>()) {
+    return "switch statement";
+  }
+  if (Is<VariableDeclStatement>()) {
+    return "variable declaration";
+  }
+  return "statement";
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/statement.h b/src/tint/ast/statement.h
new file mode 100644
index 0000000..e6c9a1c
--- /dev/null
+++ b/src/tint/ast/statement.h
@@ -0,0 +1,48 @@
+// Copyright 2020 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_AST_STATEMENT_H_
+#define SRC_TINT_AST_STATEMENT_H_
+
+#include <vector>
+
+#include "src/tint/ast/node.h"
+
+namespace tint {
+namespace ast {
+
+/// Base statement class
+class Statement : public Castable<Statement, Node> {
+ public:
+  ~Statement() override;
+
+  /// @returns the human readable name for the statement type.
+  const char* Name() const;
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of the expression
+  Statement(ProgramID pid, const Source& src);
+  /// Move constructor
+  Statement(Statement&&);
+};
+
+/// A list of statements
+using StatementList = std::vector<const Statement*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STATEMENT_H_
diff --git a/src/tint/ast/storage_class.cc b/src/tint/ast/storage_class.cc
new file mode 100644
index 0000000..b760647
--- /dev/null
+++ b/src/tint/ast/storage_class.cc
@@ -0,0 +1,51 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/storage_class.h"
+
+namespace tint {
+namespace ast {
+
+const char* ToString(StorageClass sc) {
+  switch (sc) {
+    case StorageClass::kInvalid:
+      return "invalid";
+    case StorageClass::kNone:
+      return "none";
+    case StorageClass::kInput:
+      return "in";
+    case StorageClass::kOutput:
+      return "out";
+    case StorageClass::kUniform:
+      return "uniform";
+    case StorageClass::kWorkgroup:
+      return "workgroup";
+    case StorageClass::kUniformConstant:
+      return "uniform_constant";
+    case StorageClass::kStorage:
+      return "storage";
+    case StorageClass::kPrivate:
+      return "private";
+    case StorageClass::kFunction:
+      return "function";
+  }
+  return "<unknown>";
+}
+std::ostream& operator<<(std::ostream& out, StorageClass sc) {
+  out << ToString(sc);
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/storage_class.h b/src/tint/ast/storage_class.h
new file mode 100644
index 0000000..bc22468
--- /dev/null
+++ b/src/tint/ast/storage_class.h
@@ -0,0 +1,56 @@
+// Copyright 2020 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_AST_STORAGE_CLASS_H_
+#define SRC_TINT_AST_STORAGE_CLASS_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// Storage class of a given pointer.
+enum class StorageClass {
+  kInvalid = -1,
+  kNone,
+  kInput,
+  kOutput,
+  kUniform,
+  kWorkgroup,
+  kUniformConstant,
+  kStorage,
+  kPrivate,
+  kFunction
+};
+
+/// @returns true if the StorageClass is host-shareable
+/// @param sc the StorageClass
+/// @see https://gpuweb.github.io/gpuweb/wgsl.html#host-shareable
+inline bool IsHostShareable(StorageClass sc) {
+  return sc == ast::StorageClass::kUniform || sc == ast::StorageClass::kStorage;
+}
+
+/// @param sc the StorageClass
+/// @return the name of the given storage class
+const char* ToString(StorageClass sc);
+
+/// @param out the std::ostream to write to
+/// @param sc the StorageClass
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, StorageClass sc);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STORAGE_CLASS_H_
diff --git a/src/tint/ast/storage_texture.cc b/src/tint/ast/storage_texture.cc
new file mode 100644
index 0000000..9f818ec
--- /dev/null
+++ b/src/tint/ast/storage_texture.cc
@@ -0,0 +1,146 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/storage_texture.h"
+
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StorageTexture);
+
+namespace tint {
+namespace ast {
+
+// Note, these names match the names in the WGSL spec. This behaviour is used
+// in the WGSL writer to emit the texture format names.
+std::ostream& operator<<(std::ostream& out, TexelFormat format) {
+  switch (format) {
+    case TexelFormat::kNone:
+      out << "none";
+      break;
+    case TexelFormat::kR32Uint:
+      out << "r32uint";
+      break;
+    case TexelFormat::kR32Sint:
+      out << "r32sint";
+      break;
+    case TexelFormat::kR32Float:
+      out << "r32float";
+      break;
+    case TexelFormat::kRgba8Unorm:
+      out << "rgba8unorm";
+      break;
+    case TexelFormat::kRgba8Snorm:
+      out << "rgba8snorm";
+      break;
+    case TexelFormat::kRgba8Uint:
+      out << "rgba8uint";
+      break;
+    case TexelFormat::kRgba8Sint:
+      out << "rgba8sint";
+      break;
+    case TexelFormat::kRg32Uint:
+      out << "rg32uint";
+      break;
+    case TexelFormat::kRg32Sint:
+      out << "rg32sint";
+      break;
+    case TexelFormat::kRg32Float:
+      out << "rg32float";
+      break;
+    case TexelFormat::kRgba16Uint:
+      out << "rgba16uint";
+      break;
+    case TexelFormat::kRgba16Sint:
+      out << "rgba16sint";
+      break;
+    case TexelFormat::kRgba16Float:
+      out << "rgba16float";
+      break;
+    case TexelFormat::kRgba32Uint:
+      out << "rgba32uint";
+      break;
+    case TexelFormat::kRgba32Sint:
+      out << "rgba32sint";
+      break;
+    case TexelFormat::kRgba32Float:
+      out << "rgba32float";
+      break;
+  }
+  return out;
+}
+
+StorageTexture::StorageTexture(ProgramID pid,
+                               const Source& src,
+                               TextureDimension d,
+                               TexelFormat fmt,
+                               const Type* subtype,
+                               Access ac)
+    : Base(pid, src, d), format(fmt), type(subtype), access(ac) {}
+
+StorageTexture::StorageTexture(StorageTexture&&) = default;
+
+StorageTexture::~StorageTexture() = default;
+
+std::string StorageTexture::FriendlyName(const SymbolTable&) const {
+  std::ostringstream out;
+  out << "texture_storage_" << dim << "<" << format << ", " << access << ">";
+  return out.str();
+}
+
+const StorageTexture* StorageTexture::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<StorageTexture>(src, dim, format, ty, access);
+}
+
+Type* StorageTexture::SubtypeFor(TexelFormat format, ProgramBuilder& builder) {
+  switch (format) {
+    case TexelFormat::kR32Uint:
+    case TexelFormat::kRgba8Uint:
+    case TexelFormat::kRg32Uint:
+    case TexelFormat::kRgba16Uint:
+    case TexelFormat::kRgba32Uint: {
+      return builder.create<U32>();
+    }
+
+    case TexelFormat::kR32Sint:
+    case TexelFormat::kRgba8Sint:
+    case TexelFormat::kRg32Sint:
+    case TexelFormat::kRgba16Sint:
+    case TexelFormat::kRgba32Sint: {
+      return builder.create<I32>();
+    }
+
+    case TexelFormat::kRgba8Unorm:
+    case TexelFormat::kRgba8Snorm:
+    case TexelFormat::kR32Float:
+    case TexelFormat::kRg32Float:
+    case TexelFormat::kRgba16Float:
+    case TexelFormat::kRgba32Float: {
+      return builder.create<F32>();
+    }
+
+    case TexelFormat::kNone:
+      break;
+  }
+
+  return nullptr;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/storage_texture.h b/src/tint/ast/storage_texture.h
new file mode 100644
index 0000000..9af93af
--- /dev/null
+++ b/src/tint/ast/storage_texture.h
@@ -0,0 +1,101 @@
+// Copyright 2020 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_AST_STORAGE_TEXTURE_H_
+#define SRC_TINT_AST_STORAGE_TEXTURE_H_
+
+#include <string>
+
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace ast {
+
+/// The texel format in the storage texture
+enum class TexelFormat {
+  kNone = -1,
+  kRgba8Unorm,
+  kRgba8Snorm,
+  kRgba8Uint,
+  kRgba8Sint,
+  kRgba16Uint,
+  kRgba16Sint,
+  kRgba16Float,
+  kR32Uint,
+  kR32Sint,
+  kR32Float,
+  kRg32Uint,
+  kRg32Sint,
+  kRg32Float,
+  kRgba32Uint,
+  kRgba32Sint,
+  kRgba32Float,
+};
+
+/// @param out the std::ostream to write to
+/// @param format the TexelFormat
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, TexelFormat format);
+
+/// A storage texture type.
+class StorageTexture final : public Castable<StorageTexture, Texture> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  /// @param format the image format of the texture
+  /// @param subtype the storage subtype. Use SubtypeFor() to calculate this.
+  /// @param access_control the access control for the texture.
+  StorageTexture(ProgramID pid,
+                 const Source& src,
+                 TextureDimension dim,
+                 TexelFormat format,
+                 const Type* subtype,
+                 Access access_control);
+
+  /// Move constructor
+  StorageTexture(StorageTexture&&);
+  ~StorageTexture() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const StorageTexture* Clone(CloneContext* ctx) const override;
+
+  /// @param format the storage texture image format
+  /// @param builder the ProgramBuilder used to build the returned type
+  /// @returns the storage texture subtype for the given TexelFormat
+  static Type* SubtypeFor(TexelFormat format, ProgramBuilder& builder);
+
+  /// The image format
+  const TexelFormat format;
+
+  /// The storage subtype
+  const Type* const type;
+
+  /// The access control
+  const Access access;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STORAGE_TEXTURE_H_
diff --git a/src/tint/ast/storage_texture_test.cc b/src/tint/ast/storage_texture_test.cc
new file mode 100644
index 0000000..5186b7e
--- /dev/null
+++ b/src/tint/ast/storage_texture_test.cc
@@ -0,0 +1,95 @@
+// Copyright 2020 The Tint Authors->
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/storage_texture.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstStorageTextureTest = TestHelper;
+
+TEST_F(AstStorageTextureTest, IsTexture) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Float, *this);
+  Texture* ty =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Float, subtype, Access::kRead);
+  EXPECT_FALSE(ty->Is<DepthTexture>());
+  EXPECT_FALSE(ty->Is<SampledTexture>());
+  EXPECT_TRUE(ty->Is<StorageTexture>());
+}
+
+TEST_F(AstStorageTextureTest, Dim) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Float, *this);
+  auto* s =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Float, subtype, Access::kRead);
+  EXPECT_EQ(s->dim, TextureDimension::k2dArray);
+}
+
+TEST_F(AstStorageTextureTest, Format) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Float, *this);
+  auto* s =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Float, subtype, Access::kRead);
+  EXPECT_EQ(s->format, TexelFormat::kRgba32Float);
+}
+
+TEST_F(AstStorageTextureTest, FriendlyName) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Float, *this);
+  auto* s =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Float, subtype, Access::kRead);
+  EXPECT_EQ(s->FriendlyName(Symbols()),
+            "texture_storage_2d_array<rgba32float, read>");
+}
+
+TEST_F(AstStorageTextureTest, F32) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Float, *this);
+  Type* s =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Float, subtype, Access::kRead);
+
+  ASSERT_TRUE(s->Is<Texture>());
+  ASSERT_TRUE(s->Is<StorageTexture>());
+  EXPECT_TRUE(s->As<StorageTexture>()->type->Is<F32>());
+}
+
+TEST_F(AstStorageTextureTest, U32) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRg32Uint, *this);
+  Type* s =
+      create<StorageTexture>(TextureDimension::k2dArray, TexelFormat::kRg32Uint,
+                             subtype, Access::kRead);
+
+  ASSERT_TRUE(s->Is<Texture>());
+  ASSERT_TRUE(s->Is<StorageTexture>());
+  EXPECT_TRUE(s->As<StorageTexture>()->type->Is<U32>());
+}
+
+TEST_F(AstStorageTextureTest, I32) {
+  auto* subtype = StorageTexture::SubtypeFor(TexelFormat::kRgba32Sint, *this);
+  Type* s =
+      create<StorageTexture>(TextureDimension::k2dArray,
+                             TexelFormat::kRgba32Sint, subtype, Access::kRead);
+
+  ASSERT_TRUE(s->Is<Texture>());
+  ASSERT_TRUE(s->Is<StorageTexture>());
+  EXPECT_TRUE(s->As<StorageTexture>()->type->Is<I32>());
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/stride_attribute.cc b/src/tint/ast/stride_attribute.cc
new file mode 100644
index 0000000..1c763ac
--- /dev/null
+++ b/src/tint/ast/stride_attribute.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/stride_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StrideAttribute);
+
+namespace tint {
+namespace ast {
+
+StrideAttribute::StrideAttribute(ProgramID pid, const Source& src, uint32_t s)
+    : Base(pid, src), stride(s) {}
+
+StrideAttribute::~StrideAttribute() = default;
+
+std::string StrideAttribute::Name() const {
+  return "stride";
+}
+
+const StrideAttribute* StrideAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<StrideAttribute>(src, stride);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/stride_attribute.h b/src/tint/ast/stride_attribute.h
new file mode 100644
index 0000000..0aa3baf
--- /dev/null
+++ b/src/tint/ast/stride_attribute.h
@@ -0,0 +1,53 @@
+// Copyright 2020 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_AST_STRIDE_ATTRIBUTE_H_
+#define SRC_TINT_AST_STRIDE_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/internal_attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A stride attribute used by the SPIR-V reader for strided arrays and
+/// matrices.
+class StrideAttribute final : public Castable<StrideAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param stride the stride value
+  StrideAttribute(ProgramID pid, const Source& src, uint32_t stride);
+  ~StrideAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StrideAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The stride value
+  const uint32_t stride;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRIDE_ATTRIBUTE_H_
diff --git a/src/tint/ast/stride_attribute_test.cc b/src/tint/ast/stride_attribute_test.cc
new file mode 100644
index 0000000..f8549ec
--- /dev/null
+++ b/src/tint/ast/stride_attribute_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StrideAttributeTest = TestHelper;
+
+TEST_F(StrideAttributeTest, Creation) {
+  auto* d = create<StrideAttribute>(2);
+  EXPECT_EQ(2u, d->stride);
+}
+
+TEST_F(StrideAttributeTest, Source) {
+  auto* d = create<StrideAttribute>(
+      Source{Source::Range{Source::Location{1, 2}, Source::Location{3, 4}}}, 2);
+  EXPECT_EQ(d->source.range.begin.line, 1u);
+  EXPECT_EQ(d->source.range.begin.column, 2u);
+  EXPECT_EQ(d->source.range.end.line, 3u);
+  EXPECT_EQ(d->source.range.end.column, 4u);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct.cc b/src/tint/ast/struct.cc
new file mode 100644
index 0000000..c33c07d
--- /dev/null
+++ b/src/tint/ast/struct.cc
@@ -0,0 +1,56 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Struct);
+
+namespace tint {
+namespace ast {
+
+Struct::Struct(ProgramID pid,
+               const Source& src,
+               Symbol n,
+               StructMemberList m,
+               AttributeList attrs)
+    : Base(pid, src, n), members(std::move(m)), attributes(std::move(attrs)) {
+  for (auto* mem : members) {
+    TINT_ASSERT(AST, mem);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, mem, program_id);
+  }
+  for (auto* attr : attributes) {
+    TINT_ASSERT(AST, attr);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, attr, program_id);
+  }
+}
+
+Struct::Struct(Struct&&) = default;
+
+Struct::~Struct() = default;
+
+const Struct* Struct::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto n = ctx->Clone(name);
+  auto mem = ctx->Clone(members);
+  auto attrs = ctx->Clone(attributes);
+  return ctx->dst->create<Struct>(src, n, mem, attrs);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct.h b/src/tint/ast/struct.h
new file mode 100644
index 0000000..076c46d
--- /dev/null
+++ b/src/tint/ast/struct.h
@@ -0,0 +1,63 @@
+// Copyright 2020 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_AST_STRUCT_H_
+#define SRC_TINT_AST_STRUCT_H_
+
+#include <string>
+#include <utility>
+
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/struct_member.h"
+#include "src/tint/ast/type_decl.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct statement.
+class Struct final : public Castable<Struct, TypeDecl> {
+ public:
+  /// Create a new struct statement
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node for the import statement
+  /// @param name The name of the structure
+  /// @param members The struct members
+  /// @param attributes The struct attributes
+  Struct(ProgramID pid,
+         const Source& src,
+         Symbol name,
+         StructMemberList members,
+         AttributeList attributes);
+  /// Move constructor
+  Struct(Struct&&);
+
+  ~Struct() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const Struct* Clone(CloneContext* ctx) const override;
+
+  /// The members
+  const StructMemberList members;
+
+  /// The struct attributes
+  const AttributeList attributes;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRUCT_H_
diff --git a/src/tint/ast/struct_member.cc b/src/tint/ast/struct_member.cc
new file mode 100644
index 0000000..2afb10d
--- /dev/null
+++ b/src/tint/ast/struct_member.cc
@@ -0,0 +1,53 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMember);
+
+namespace tint {
+namespace ast {
+
+StructMember::StructMember(ProgramID pid,
+                           const Source& src,
+                           const Symbol& sym,
+                           const ast::Type* ty,
+                           AttributeList attrs)
+    : Base(pid, src), symbol(sym), type(ty), attributes(std::move(attrs)) {
+  TINT_ASSERT(AST, type);
+  TINT_ASSERT(AST, symbol.IsValid());
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
+  for (auto* attr : attributes) {
+    TINT_ASSERT(AST, attr);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, attr, program_id);
+  }
+}
+
+StructMember::StructMember(StructMember&&) = default;
+
+StructMember::~StructMember() = default;
+
+const StructMember* StructMember::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto sym = ctx->Clone(symbol);
+  auto* ty = ctx->Clone(type);
+  auto attrs = ctx->Clone(attributes);
+  return ctx->dst->create<StructMember>(src, sym, ty, attrs);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member.h b/src/tint/ast/struct_member.h
new file mode 100644
index 0000000..4be9256
--- /dev/null
+++ b/src/tint/ast/struct_member.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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_AST_STRUCT_MEMBER_H_
+#define SRC_TINT_AST_STRUCT_MEMBER_H_
+
+#include <utility>
+#include <vector>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declaration
+class Type;
+
+/// A struct member statement.
+class StructMember final : public Castable<StructMember, Node> {
+ public:
+  /// Create a new struct member statement
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node for the struct member statement
+  /// @param sym The struct member symbol
+  /// @param type The struct member type
+  /// @param attributes The struct member attributes
+  StructMember(ProgramID pid,
+               const Source& src,
+               const Symbol& sym,
+               const ast::Type* type,
+               AttributeList attributes);
+  /// Move constructor
+  StructMember(StructMember&&);
+
+  ~StructMember() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StructMember* Clone(CloneContext* ctx) const override;
+
+  /// The symbol
+  const Symbol symbol;
+
+  /// The type
+  const ast::Type* const type;
+
+  /// The attributes
+  const AttributeList attributes;
+};
+
+/// A list of struct members
+using StructMemberList = std::vector<const StructMember*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRUCT_MEMBER_H_
diff --git a/src/tint/ast/struct_member_align_attribute.cc b/src/tint/ast/struct_member_align_attribute.cc
new file mode 100644
index 0000000..7790800
--- /dev/null
+++ b/src/tint/ast/struct_member_align_attribute.cc
@@ -0,0 +1,46 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member_align_attribute.h"
+
+#include <string>
+
+#include "src/tint/clone_context.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberAlignAttribute);
+
+namespace tint {
+namespace ast {
+
+StructMemberAlignAttribute::StructMemberAlignAttribute(ProgramID pid,
+                                                       const Source& src,
+                                                       uint32_t a)
+    : Base(pid, src), align(a) {}
+
+StructMemberAlignAttribute::~StructMemberAlignAttribute() = default;
+
+std::string StructMemberAlignAttribute::Name() const {
+  return "align";
+}
+
+const StructMemberAlignAttribute* StructMemberAlignAttribute::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<StructMemberAlignAttribute>(src, align);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_align_attribute.h b/src/tint/ast/struct_member_align_attribute.h
new file mode 100644
index 0000000..a1b455b
--- /dev/null
+++ b/src/tint/ast/struct_member_align_attribute.h
@@ -0,0 +1,53 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_STRUCT_MEMBER_ALIGN_ATTRIBUTE_H_
+#define SRC_TINT_AST_STRUCT_MEMBER_ALIGN_ATTRIBUTE_H_
+
+#include <stddef.h>
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct member align attribute
+class StructMemberAlignAttribute final
+    : public Castable<StructMemberAlignAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param align the align value
+  StructMemberAlignAttribute(ProgramID pid, const Source& src, uint32_t align);
+  ~StructMemberAlignAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StructMemberAlignAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The align value
+  const uint32_t align;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRUCT_MEMBER_ALIGN_ATTRIBUTE_H_
diff --git a/src/tint/ast/struct_member_align_attribute_test.cc b/src/tint/ast/struct_member_align_attribute_test.cc
new file mode 100644
index 0000000..9dcddd4
--- /dev/null
+++ b/src/tint/ast/struct_member_align_attribute_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member_align_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StructMemberAlignAttributeTest = TestHelper;
+
+TEST_F(StructMemberAlignAttributeTest, Creation) {
+  auto* d = create<StructMemberAlignAttribute>(2);
+  EXPECT_EQ(2u, d->align);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_offset_attribute.cc b/src/tint/ast/struct_member_offset_attribute.cc
new file mode 100644
index 0000000..a854f8c
--- /dev/null
+++ b/src/tint/ast/struct_member_offset_attribute.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member_offset_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberOffsetAttribute);
+
+namespace tint {
+namespace ast {
+
+StructMemberOffsetAttribute::StructMemberOffsetAttribute(ProgramID pid,
+                                                         const Source& src,
+                                                         uint32_t o)
+    : Base(pid, src), offset(o) {}
+
+StructMemberOffsetAttribute::~StructMemberOffsetAttribute() = default;
+
+std::string StructMemberOffsetAttribute::Name() const {
+  return "offset";
+}
+
+const StructMemberOffsetAttribute* StructMemberOffsetAttribute::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<StructMemberOffsetAttribute>(src, offset);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_offset_attribute.h b/src/tint/ast/struct_member_offset_attribute.h
new file mode 100644
index 0000000..63db959
--- /dev/null
+++ b/src/tint/ast/struct_member_offset_attribute.h
@@ -0,0 +1,63 @@
+// Copyright 2020 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_AST_STRUCT_MEMBER_OFFSET_ATTRIBUTE_H_
+#define SRC_TINT_AST_STRUCT_MEMBER_OFFSET_ATTRIBUTE_H_
+
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct member offset attribute
+/// @note The WGSL spec removed the `@offset(n)` attribute for `@size(n)`
+/// and `@align(n)` in https://github.com/gpuweb/gpuweb/pull/1447. However
+/// this attribute is kept because the SPIR-V reader has to deal with absolute
+/// offsets, and transforming these to size / align is complex and can be done
+/// in a number of ways. The Resolver is responsible for consuming the size and
+/// align attributes and transforming these into absolute offsets. It is
+/// trivial for the Resolver to handle `@offset(n)` or `@size(n)` /
+/// `@align(n)` attributes, so this is what we do, keeping all the layout
+/// logic in one place.
+class StructMemberOffsetAttribute final
+    : public Castable<StructMemberOffsetAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param offset the offset value
+  StructMemberOffsetAttribute(ProgramID pid,
+                              const Source& src,
+                              uint32_t offset);
+  ~StructMemberOffsetAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StructMemberOffsetAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The offset value
+  const uint32_t offset;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRUCT_MEMBER_OFFSET_ATTRIBUTE_H_
diff --git a/src/tint/ast/struct_member_offset_attribute_test.cc b/src/tint/ast/struct_member_offset_attribute_test.cc
new file mode 100644
index 0000000..022821a
--- /dev/null
+++ b/src/tint/ast/struct_member_offset_attribute_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StructMemberOffsetAttributeTest = TestHelper;
+
+TEST_F(StructMemberOffsetAttributeTest, Creation) {
+  auto* d = create<StructMemberOffsetAttribute>(2);
+  EXPECT_EQ(2u, d->offset);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_size_attribute.cc b/src/tint/ast/struct_member_size_attribute.cc
new file mode 100644
index 0000000..d76820a
--- /dev/null
+++ b/src/tint/ast/struct_member_size_attribute.cc
@@ -0,0 +1,46 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member_size_attribute.h"
+
+#include <string>
+
+#include "src/tint/clone_context.h"
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::StructMemberSizeAttribute);
+
+namespace tint {
+namespace ast {
+
+StructMemberSizeAttribute::StructMemberSizeAttribute(ProgramID pid,
+                                                     const Source& src,
+                                                     uint32_t sz)
+    : Base(pid, src), size(sz) {}
+
+StructMemberSizeAttribute::~StructMemberSizeAttribute() = default;
+
+std::string StructMemberSizeAttribute::Name() const {
+  return "size";
+}
+
+const StructMemberSizeAttribute* StructMemberSizeAttribute::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<StructMemberSizeAttribute>(src, size);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_size_attribute.h b/src/tint/ast/struct_member_size_attribute.h
new file mode 100644
index 0000000..c2a6e50
--- /dev/null
+++ b/src/tint/ast/struct_member_size_attribute.h
@@ -0,0 +1,53 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_STRUCT_MEMBER_SIZE_ATTRIBUTE_H_
+#define SRC_TINT_AST_STRUCT_MEMBER_SIZE_ATTRIBUTE_H_
+
+#include <stddef.h>
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+/// A struct member size attribute
+class StructMemberSizeAttribute final
+    : public Castable<StructMemberSizeAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param size the size value
+  StructMemberSizeAttribute(ProgramID pid, const Source& src, uint32_t size);
+  ~StructMemberSizeAttribute() override;
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const StructMemberSizeAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The size value
+  const uint32_t size;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_STRUCT_MEMBER_SIZE_ATTRIBUTE_H_
diff --git a/src/tint/ast/struct_member_size_attribute_test.cc b/src/tint/ast/struct_member_size_attribute_test.cc
new file mode 100644
index 0000000..346535d
--- /dev/null
+++ b/src/tint/ast/struct_member_size_attribute_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct_member_size_attribute.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StructMemberSizeAttributeTest = TestHelper;
+
+TEST_F(StructMemberSizeAttributeTest, Creation) {
+  auto* d = create<StructMemberSizeAttribute>(2);
+  EXPECT_EQ(2u, d->size);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_member_test.cc b/src/tint/ast/struct_member_test.cc
new file mode 100644
index 0000000..c675a47
--- /dev/null
+++ b/src/tint/ast/struct_member_test.cc
@@ -0,0 +1,98 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using StructMemberTest = TestHelper;
+
+TEST_F(StructMemberTest, Creation) {
+  auto* st = Member("a", ty.i32(), {MemberSize(4)});
+  EXPECT_EQ(st->symbol, Symbol(1, ID()));
+  EXPECT_TRUE(st->type->Is<ast::I32>());
+  EXPECT_EQ(st->attributes.size(), 1u);
+  EXPECT_TRUE(st->attributes[0]->Is<StructMemberSizeAttribute>());
+  EXPECT_EQ(st->source.range.begin.line, 0u);
+  EXPECT_EQ(st->source.range.begin.column, 0u);
+  EXPECT_EQ(st->source.range.end.line, 0u);
+  EXPECT_EQ(st->source.range.end.column, 0u);
+}
+
+TEST_F(StructMemberTest, CreationWithSource) {
+  auto* st = Member(
+      Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 8}}},
+      "a", ty.i32());
+  EXPECT_EQ(st->symbol, Symbol(1, ID()));
+  EXPECT_TRUE(st->type->Is<ast::I32>());
+  EXPECT_EQ(st->attributes.size(), 0u);
+  EXPECT_EQ(st->source.range.begin.line, 27u);
+  EXPECT_EQ(st->source.range.begin.column, 4u);
+  EXPECT_EQ(st->source.range.end.line, 27u);
+  EXPECT_EQ(st->source.range.end.column, 8u);
+}
+
+TEST_F(StructMemberTest, Assert_Empty_Symbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Member("", b.ty.i32());
+      },
+      "internal compiler error");
+}
+
+TEST_F(StructMemberTest, Assert_Null_Type) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Member("a", nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(StructMemberTest, Assert_Null_Attribute) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Member("a", b.ty.i32(), {b.MemberSize(4), nullptr});
+      },
+      "internal compiler error");
+}
+
+TEST_F(StructMemberTest, Assert_DifferentProgramID_Symbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Member(b2.Sym("a"), b1.ty.i32(), {b1.MemberSize(4)});
+      },
+      "internal compiler error");
+}
+
+TEST_F(StructMemberTest, Assert_DifferentProgramID_Attribute) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Member("a", b1.ty.i32(), {b2.MemberSize(4)});
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/struct_test.cc b/src/tint/ast/struct_test.cc
new file mode 100644
index 0000000..3b02767
--- /dev/null
+++ b/src/tint/ast/struct_test.cc
@@ -0,0 +1,133 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/struct.h"
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/texture.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+#include "src/tint/transform/add_spirv_block_attribute.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstStructTest = TestHelper;
+using SpirvBlockAttribute =
+    transform::AddSpirvBlockAttribute::SpirvBlockAttribute;
+
+TEST_F(AstStructTest, Creation) {
+  auto name = Sym("s");
+  auto* s = create<Struct>(name, StructMemberList{Member("a", ty.i32())},
+                           AttributeList{});
+  EXPECT_EQ(s->name, name);
+  EXPECT_EQ(s->members.size(), 1u);
+  EXPECT_TRUE(s->attributes.empty());
+  EXPECT_EQ(s->source.range.begin.line, 0u);
+  EXPECT_EQ(s->source.range.begin.column, 0u);
+  EXPECT_EQ(s->source.range.end.line, 0u);
+  EXPECT_EQ(s->source.range.end.column, 0u);
+}
+
+TEST_F(AstStructTest, Creation_WithAttributes) {
+  auto name = Sym("s");
+  AttributeList attrs;
+  attrs.push_back(ASTNodes().Create<SpirvBlockAttribute>(ID()));
+
+  auto* s =
+      create<Struct>(name, StructMemberList{Member("a", ty.i32())}, attrs);
+  EXPECT_EQ(s->name, name);
+  EXPECT_EQ(s->members.size(), 1u);
+  ASSERT_EQ(s->attributes.size(), 1u);
+  EXPECT_TRUE(s->attributes[0]->Is<SpirvBlockAttribute>());
+  EXPECT_EQ(s->source.range.begin.line, 0u);
+  EXPECT_EQ(s->source.range.begin.column, 0u);
+  EXPECT_EQ(s->source.range.end.line, 0u);
+  EXPECT_EQ(s->source.range.end.column, 0u);
+}
+
+TEST_F(AstStructTest, CreationWithSourceAndAttributes) {
+  auto name = Sym("s");
+  auto* s = create<Struct>(
+      Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 8}}},
+      name, StructMemberList{Member("a", ty.i32())},
+      AttributeList{ASTNodes().Create<SpirvBlockAttribute>(ID())});
+  EXPECT_EQ(s->name, name);
+  EXPECT_EQ(s->members.size(), 1u);
+  ASSERT_EQ(s->attributes.size(), 1u);
+  EXPECT_TRUE(s->attributes[0]->Is<SpirvBlockAttribute>());
+  EXPECT_EQ(s->source.range.begin.line, 27u);
+  EXPECT_EQ(s->source.range.begin.column, 4u);
+  EXPECT_EQ(s->source.range.end.line, 27u);
+  EXPECT_EQ(s->source.range.end.column, 8u);
+}
+
+TEST_F(AstStructTest, Assert_Null_StructMember) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<Struct>(b.Sym("S"),
+                         StructMemberList{b.Member("a", b.ty.i32()), nullptr},
+                         AttributeList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(AstStructTest, Assert_Null_Attribute) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<Struct>(b.Sym("S"),
+                         StructMemberList{b.Member("a", b.ty.i32())},
+                         AttributeList{nullptr});
+      },
+      "internal compiler error");
+}
+
+TEST_F(AstStructTest, Assert_DifferentProgramID_StructMember) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<Struct>(b1.Sym("S"),
+                          StructMemberList{b2.Member("a", b2.ty.i32())},
+                          AttributeList{});
+      },
+      "internal compiler error");
+}
+
+TEST_F(AstStructTest, Assert_DifferentProgramID_Attribute) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<Struct>(
+            b1.Sym("S"), StructMemberList{b1.Member("a", b1.ty.i32())},
+            AttributeList{b2.ASTNodes().Create<SpirvBlockAttribute>(b2.ID())});
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/switch_statement.cc b/src/tint/ast/switch_statement.cc
new file mode 100644
index 0000000..02cbb61
--- /dev/null
+++ b/src/tint/ast/switch_statement.cc
@@ -0,0 +1,50 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/switch_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::SwitchStatement);
+
+namespace tint {
+namespace ast {
+
+SwitchStatement::SwitchStatement(ProgramID pid,
+                                 const Source& src,
+                                 const Expression* cond,
+                                 CaseStatementList b)
+    : Base(pid, src), condition(cond), body(b) {
+  TINT_ASSERT(AST, condition);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, condition, program_id);
+  for (auto* stmt : body) {
+    TINT_ASSERT(AST, stmt);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, stmt, program_id);
+  }
+}
+
+SwitchStatement::SwitchStatement(SwitchStatement&&) = default;
+
+SwitchStatement::~SwitchStatement() = default;
+
+const SwitchStatement* SwitchStatement::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* cond = ctx->Clone(condition);
+  auto b = ctx->Clone(body);
+  return ctx->dst->create<SwitchStatement>(src, cond, b);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/switch_statement.h b/src/tint/ast/switch_statement.h
new file mode 100644
index 0000000..aa0cc77
--- /dev/null
+++ b/src/tint/ast/switch_statement.h
@@ -0,0 +1,60 @@
+// Copyright 2020 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_AST_SWITCH_STATEMENT_H_
+#define SRC_TINT_AST_SWITCH_STATEMENT_H_
+
+#include "src/tint/ast/case_statement.h"
+#include "src/tint/ast/expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A switch statement
+class SwitchStatement final : public Castable<SwitchStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param condition the switch condition
+  /// @param body the switch body
+  SwitchStatement(ProgramID pid,
+                  const Source& src,
+                  const Expression* condition,
+                  CaseStatementList body);
+  /// Move constructor
+  SwitchStatement(SwitchStatement&&);
+  ~SwitchStatement() override;
+
+  /// @returns true if this is a default statement
+  bool IsDefault() const { return condition == nullptr; }
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const SwitchStatement* Clone(CloneContext* ctx) const override;
+
+  /// The switch condition or nullptr if none set
+  const Expression* const condition;
+
+  /// The Switch body
+  const CaseStatementList body;
+  SwitchStatement(const SwitchStatement&) = delete;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_SWITCH_STATEMENT_H_
diff --git a/src/tint/ast/switch_statement_test.cc b/src/tint/ast/switch_statement_test.cc
new file mode 100644
index 0000000..ecbf68c
--- /dev/null
+++ b/src/tint/ast/switch_statement_test.cc
@@ -0,0 +1,118 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/switch_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using SwitchStatementTest = TestHelper;
+
+TEST_F(SwitchStatementTest, Creation) {
+  CaseSelectorList lit;
+  lit.push_back(create<SintLiteralExpression>(1));
+
+  auto* ident = Expr("ident");
+  CaseStatementList body;
+  auto* case_stmt = create<CaseStatement>(lit, Block());
+  body.push_back(case_stmt);
+
+  auto* stmt = create<SwitchStatement>(ident, body);
+  EXPECT_EQ(stmt->condition, ident);
+  ASSERT_EQ(stmt->body.size(), 1u);
+  EXPECT_EQ(stmt->body[0], case_stmt);
+}
+
+TEST_F(SwitchStatementTest, Creation_WithSource) {
+  auto* ident = Expr("ident");
+
+  auto* stmt = create<SwitchStatement>(Source{Source::Location{20, 2}}, ident,
+                                       CaseStatementList());
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(SwitchStatementTest, IsSwitch) {
+  CaseSelectorList lit;
+  lit.push_back(create<SintLiteralExpression>(2));
+
+  auto* ident = Expr("ident");
+  CaseStatementList body;
+  body.push_back(create<CaseStatement>(lit, Block()));
+
+  auto* stmt = create<SwitchStatement>(ident, body);
+  EXPECT_TRUE(stmt->Is<SwitchStatement>());
+}
+
+TEST_F(SwitchStatementTest, Assert_Null_Condition) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        CaseStatementList cases;
+        cases.push_back(
+            b.create<CaseStatement>(CaseSelectorList{b.Expr(1)}, b.Block()));
+        b.create<SwitchStatement>(nullptr, cases);
+      },
+      "internal compiler error");
+}
+
+TEST_F(SwitchStatementTest, Assert_Null_CaseStatement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<SwitchStatement>(b.Expr(true), CaseStatementList{nullptr});
+      },
+      "internal compiler error");
+}
+
+TEST_F(SwitchStatementTest, Assert_DifferentProgramID_Condition) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<SwitchStatement>(b2.Expr(true), CaseStatementList{
+                                                      b1.create<CaseStatement>(
+                                                          CaseSelectorList{
+                                                              b1.Expr(1),
+                                                          },
+                                                          b1.Block()),
+                                                  });
+      },
+      "internal compiler error");
+}
+
+TEST_F(SwitchStatementTest, Assert_DifferentProgramID_CaseStatement) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<SwitchStatement>(b1.Expr(true), CaseStatementList{
+                                                      b2.create<CaseStatement>(
+                                                          CaseSelectorList{
+                                                              b2.Expr(1),
+                                                          },
+                                                          b2.Block()),
+                                                  });
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/test_helper.h b/src/tint/ast/test_helper.h
new file mode 100644
index 0000000..2c3be98
--- /dev/null
+++ b/src/tint/ast/test_helper.h
@@ -0,0 +1,38 @@
+// Copyright 2020 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_AST_TEST_HELPER_H_
+#define SRC_TINT_AST_TEST_HELPER_H_
+
+#include "gtest/gtest.h"
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace ast {
+
+/// Helper base class for testing
+template <typename BASE>
+class TestHelperBase : public BASE, public ProgramBuilder {};
+
+/// Helper class for testing that derives from testing::Test.
+using TestHelper = TestHelperBase<testing::Test>;
+
+/// Helper class for testing that derives from `T`.
+template <typename T>
+using TestParamHelper = TestHelperBase<testing::TestWithParam<T>>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TEST_HELPER_H_
diff --git a/src/tint/ast/texture.cc b/src/tint/ast/texture.cc
new file mode 100644
index 0000000..38d16e4
--- /dev/null
+++ b/src/tint/ast/texture.cc
@@ -0,0 +1,89 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/texture.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Texture);
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, TextureDimension dim) {
+  switch (dim) {
+    case TextureDimension::kNone:
+      out << "None";
+      break;
+    case TextureDimension::k1d:
+      out << "1d";
+      break;
+    case TextureDimension::k2d:
+      out << "2d";
+      break;
+    case TextureDimension::k2dArray:
+      out << "2d_array";
+      break;
+    case TextureDimension::k3d:
+      out << "3d";
+      break;
+    case TextureDimension::kCube:
+      out << "cube";
+      break;
+    case TextureDimension::kCubeArray:
+      out << "cube_array";
+      break;
+  }
+  return out;
+}
+
+bool IsTextureArray(TextureDimension dim) {
+  switch (dim) {
+    case TextureDimension::k2dArray:
+    case TextureDimension::kCubeArray:
+      return true;
+    case TextureDimension::k2d:
+    case TextureDimension::kNone:
+    case TextureDimension::k1d:
+    case TextureDimension::k3d:
+    case TextureDimension::kCube:
+      return false;
+  }
+  return false;
+}
+
+int NumCoordinateAxes(TextureDimension dim) {
+  switch (dim) {
+    case TextureDimension::kNone:
+      return 0;
+    case TextureDimension::k1d:
+      return 1;
+    case TextureDimension::k2d:
+    case TextureDimension::k2dArray:
+      return 2;
+    case TextureDimension::k3d:
+    case TextureDimension::kCube:
+    case TextureDimension::kCubeArray:
+      return 3;
+  }
+  return 0;
+}
+
+Texture::Texture(ProgramID pid, const Source& src, TextureDimension d)
+    : Base(pid, src), dim(d) {}
+
+Texture::Texture(Texture&&) = default;
+
+Texture::~Texture() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/texture.h b/src/tint/ast/texture.h
new file mode 100644
index 0000000..41e893a
--- /dev/null
+++ b/src/tint/ast/texture.h
@@ -0,0 +1,83 @@
+// Copyright 2020 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_AST_TEXTURE_H_
+#define SRC_TINT_AST_TEXTURE_H_
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// The dimensionality of the texture
+enum class TextureDimension {
+  /// Invalid texture
+  kNone = -1,
+  /// 1 dimensional texture
+  k1d,
+  /// 2 dimensional texture
+  k2d,
+  /// 2 dimensional array texture
+  k2dArray,
+  /// 3 dimensional texture
+  k3d,
+  /// cube texture
+  kCube,
+  /// cube array texture
+  kCubeArray,
+};
+
+/// @param out the std::ostream to write to
+/// @param dim the TextureDimension
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, TextureDimension dim);
+
+/// @param dim the TextureDimension to query
+/// @return true if the given TextureDimension is an array texture
+bool IsTextureArray(TextureDimension dim);
+
+/// Returns the number of axes in the coordinate used for accessing
+/// the texture, where an access is one of: sampling, fetching, load,
+/// or store.
+///  None -> 0
+///  1D -> 1
+///  2D, 2DArray -> 2
+///  3D, Cube, CubeArray -> 3
+/// Note: To sample a cube texture, the coordinate has 3 dimensions,
+/// but textureDimensions on a cube or cube array returns a 2-element
+/// size, representing the (x,y) size of each cube face, in texels.
+/// @param dim the TextureDimension to query
+/// @return number of dimensions in a coordinate for the dimensionality
+int NumCoordinateAxes(TextureDimension dim);
+
+/// A texture type.
+class Texture : public Castable<Texture, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param dim the dimensionality of the texture
+  Texture(ProgramID pid, const Source& src, TextureDimension dim);
+  /// Move constructor
+  Texture(Texture&&);
+  ~Texture() override;
+
+  /// The texture dimension
+  const TextureDimension dim;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TEXTURE_H_
diff --git a/src/tint/ast/texture_test.cc b/src/tint/ast/texture_test.cc
new file mode 100644
index 0000000..017d56e
--- /dev/null
+++ b/src/tint/ast/texture_test.cc
@@ -0,0 +1,58 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/texture.h"
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/struct.h"
+#include "src/tint/ast/test_helper.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/vector.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstTextureTypeTest = TestHelper;
+
+TEST_F(AstTextureTypeTest, IsTextureArray) {
+  EXPECT_EQ(false, IsTextureArray(TextureDimension::kNone));
+  EXPECT_EQ(false, IsTextureArray(TextureDimension::k1d));
+  EXPECT_EQ(false, IsTextureArray(TextureDimension::k2d));
+  EXPECT_EQ(true, IsTextureArray(TextureDimension::k2dArray));
+  EXPECT_EQ(false, IsTextureArray(TextureDimension::k3d));
+  EXPECT_EQ(false, IsTextureArray(TextureDimension::kCube));
+  EXPECT_EQ(true, IsTextureArray(TextureDimension::kCubeArray));
+}
+
+TEST_F(AstTextureTypeTest, NumCoordinateAxes) {
+  EXPECT_EQ(0, NumCoordinateAxes(TextureDimension::kNone));
+  EXPECT_EQ(1, NumCoordinateAxes(TextureDimension::k1d));
+  EXPECT_EQ(2, NumCoordinateAxes(TextureDimension::k2d));
+  EXPECT_EQ(2, NumCoordinateAxes(TextureDimension::k2dArray));
+  EXPECT_EQ(3, NumCoordinateAxes(TextureDimension::k3d));
+  EXPECT_EQ(3, NumCoordinateAxes(TextureDimension::kCube));
+  EXPECT_EQ(3, NumCoordinateAxes(TextureDimension::kCubeArray));
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/traverse_expressions.h b/src/tint/ast/traverse_expressions.h
new file mode 100644
index 0000000..084a201
--- /dev/null
+++ b/src/tint/ast/traverse_expressions.h
@@ -0,0 +1,154 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_TRAVERSE_EXPRESSIONS_H_
+#define SRC_TINT_AST_TRAVERSE_EXPRESSIONS_H_
+
+#include <vector>
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/bitcast_expression.h"
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/index_accessor_expression.h"
+#include "src/tint/ast/literal_expression.h"
+#include "src/tint/ast/member_accessor_expression.h"
+#include "src/tint/ast/phony_expression.h"
+#include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/utils/reverse.h"
+
+namespace tint {
+namespace ast {
+
+/// The action to perform after calling the TraverseExpressions() callback
+/// function.
+enum class TraverseAction {
+  /// Stop traversal immediately.
+  Stop,
+  /// Descend into this expression.
+  Descend,
+  /// Do not descend into this expression.
+  Skip,
+};
+
+/// The order TraverseExpressions() will traverse expressions
+enum class TraverseOrder {
+  /// Expressions will be traversed from left to right
+  LeftToRight,
+  /// Expressions will be traversed from right to left
+  RightToLeft,
+};
+
+/// TraverseExpressions performs a depth-first traversal of the expression nodes
+/// from `root`, calling `callback` for each of the visited expressions that
+/// match the predicate parameter type, in pre-ordering (root first).
+/// @param root the root expression node
+/// @param diags the diagnostics used for error messages
+/// @param callback the callback function. Must be of the signature:
+///        `TraverseAction(const T*)` where T is an ast::Expression type.
+/// @return true on success, false on error
+template <TraverseOrder ORDER = TraverseOrder::LeftToRight, typename CALLBACK>
+bool TraverseExpressions(const ast::Expression* root,
+                         diag::List& diags,
+                         CALLBACK&& callback) {
+  using EXPR_TYPE = std::remove_pointer_t<traits::ParameterType<CALLBACK, 0>>;
+  std::vector<const ast::Expression*> to_visit{root};
+
+  auto push_pair = [&](const ast::Expression* left,
+                       const ast::Expression* right) {
+    if (ORDER == TraverseOrder::LeftToRight) {
+      to_visit.push_back(right);
+      to_visit.push_back(left);
+    } else {
+      to_visit.push_back(left);
+      to_visit.push_back(right);
+    }
+  };
+  auto push_list = [&](const std::vector<const ast::Expression*>& exprs) {
+    if (ORDER == TraverseOrder::LeftToRight) {
+      for (auto* expr : utils::Reverse(exprs)) {
+        to_visit.push_back(expr);
+      }
+    } else {
+      for (auto* expr : exprs) {
+        to_visit.push_back(expr);
+      }
+    }
+  };
+
+  while (!to_visit.empty()) {
+    auto* expr = to_visit.back();
+    to_visit.pop_back();
+
+    if (auto* filtered = expr->As<EXPR_TYPE>()) {
+      switch (callback(filtered)) {
+        case TraverseAction::Stop:
+          return true;
+        case TraverseAction::Skip:
+          continue;
+        case TraverseAction::Descend:
+          break;
+      }
+    }
+
+    bool ok = Switch(
+        expr,
+        [&](const IndexAccessorExpression* idx) {
+          push_pair(idx->object, idx->index);
+          return true;
+        },
+        [&](const BinaryExpression* bin_op) {
+          push_pair(bin_op->lhs, bin_op->rhs);
+          return true;
+        },
+        [&](const BitcastExpression* bitcast) {
+          to_visit.push_back(bitcast->expr);
+          return true;
+        },
+        [&](const CallExpression* call) {
+          // TODO(crbug.com/tint/1257): Resolver breaks if we actually include
+          // the function name in the traversal. to_visit.push_back(call->func);
+          push_list(call->args);
+          return true;
+        },
+        [&](const MemberAccessorExpression* member) {
+          // TODO(crbug.com/tint/1257): Resolver breaks if we actually include
+          // the member name in the traversal. push_pair(member->structure,
+          // member->member);
+          to_visit.push_back(member->structure);
+          return true;
+        },
+        [&](const UnaryOpExpression* unary) {
+          to_visit.push_back(unary->expr);
+          return true;
+        },
+        [&](Default) {
+          if (expr->IsAnyOf<LiteralExpression, IdentifierExpression,
+                            PhonyExpression>()) {
+            return true;  // Leaf expression
+          }
+          TINT_ICE(AST, diags)
+              << "unhandled expression type: " << expr->TypeInfo().name;
+          return false;
+        });
+    if (!ok) {
+      return false;
+    }
+  }
+  return true;
+}
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TRAVERSE_EXPRESSIONS_H_
diff --git a/src/tint/ast/traverse_expressions_test.cc b/src/tint/ast/traverse_expressions_test.cc
new file mode 100644
index 0000000..ae839ba
--- /dev/null
+++ b/src/tint/ast/traverse_expressions_test.cc
@@ -0,0 +1,237 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/traverse_expressions.h"
+#include "gmock/gmock.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using ::testing::ElementsAre;
+
+using TraverseExpressionsTest = TestHelper;
+
+TEST_F(TraverseExpressionsTest, DescendIndexAccessor) {
+  std::vector<const ast::Expression*> e = {Expr(1), Expr(1), Expr(1), Expr(1)};
+  std::vector<const ast::Expression*> i = {IndexAccessor(e[0], e[1]),
+                                           IndexAccessor(e[2], e[3])};
+  auto* root = IndexAccessor(i[0], i[1]);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, i[0], e[0], e[1], i[1], e[2], e[3]));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, i[1], e[3], e[2], i[0], e[1], e[0]));
+  }
+}
+
+TEST_F(TraverseExpressionsTest, DescendBinaryExpression) {
+  std::vector<const ast::Expression*> e = {Expr(1), Expr(1), Expr(1), Expr(1)};
+  std::vector<const ast::Expression*> i = {Add(e[0], e[1]), Sub(e[2], e[3])};
+  auto* root = Mul(i[0], i[1]);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, i[0], e[0], e[1], i[1], e[2], e[3]));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, i[1], e[3], e[2], i[0], e[1], e[0]));
+  }
+}
+
+TEST_F(TraverseExpressionsTest, DescendBitcastExpression) {
+  auto* e = Expr(1);
+  auto* b0 = Bitcast<i32>(e);
+  auto* b1 = Bitcast<i32>(b0);
+  auto* b2 = Bitcast<i32>(b1);
+  auto* root = Bitcast<i32>(b2);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, b2, b1, b0, e));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, b2, b1, b0, e));
+  }
+}
+
+TEST_F(TraverseExpressionsTest, DescendCallExpression) {
+  std::vector<const ast::Expression*> e = {Expr(1), Expr(1), Expr(1), Expr(1)};
+  std::vector<const ast::Expression*> c = {Call("a", e[0], e[1]),
+                                           Call("b", e[2], e[3])};
+  auto* root = Call("c", c[0], c[1]);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, c[0], e[0], e[1], c[1], e[2], e[3]));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, c[1], e[3], e[2], c[0], e[1], e[0]));
+  }
+}
+
+// TODO(crbug.com/tint/1257): Test ignores member accessor 'member' field.
+// Replace with the test below when fixed.
+TEST_F(TraverseExpressionsTest, DescendMemberIndexExpression) {
+  auto* e = Expr(1);
+  auto* m = MemberAccessor(e, Expr("a"));
+  auto* root = MemberAccessor(m, Expr("b"));
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, m, e));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, m, e));
+  }
+}
+
+// TODO(crbug.com/tint/1257): The correct test for DescendMemberIndexExpression.
+TEST_F(TraverseExpressionsTest, DISABLED_DescendMemberIndexExpression) {
+  auto* e = Expr(1);
+  std::vector<const ast::IdentifierExpression*> i = {Expr("a"), Expr("b")};
+  auto* m = MemberAccessor(e, i[0]);
+  auto* root = MemberAccessor(m, i[1]);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, m, e, i[0], i[1]));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, i[1], m, i[0], e));
+  }
+}
+
+TEST_F(TraverseExpressionsTest, DescendUnaryExpression) {
+  auto* e = Expr(1);
+  auto* u0 = AddressOf(e);
+  auto* u1 = Deref(u0);
+  auto* u2 = AddressOf(u1);
+  auto* root = Deref(u2);
+  {
+    std::vector<const ast::Expression*> l2r;
+    TraverseExpressions<TraverseOrder::LeftToRight>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          l2r.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(l2r, ElementsAre(root, u2, u1, u0, e));
+  }
+  {
+    std::vector<const ast::Expression*> r2l;
+    TraverseExpressions<TraverseOrder::RightToLeft>(
+        root, Diagnostics(), [&](const ast::Expression* expr) {
+          r2l.push_back(expr);
+          return ast::TraverseAction::Descend;
+        });
+    EXPECT_THAT(r2l, ElementsAre(root, u2, u1, u0, e));
+  }
+}
+
+TEST_F(TraverseExpressionsTest, Skip) {
+  std::vector<const ast::Expression*> e = {Expr(1), Expr(1), Expr(1), Expr(1)};
+  std::vector<const ast::Expression*> i = {IndexAccessor(e[0], e[1]),
+                                           IndexAccessor(e[2], e[3])};
+  auto* root = IndexAccessor(i[0], i[1]);
+  std::vector<const ast::Expression*> order;
+  TraverseExpressions<TraverseOrder::LeftToRight>(
+      root, Diagnostics(), [&](const ast::Expression* expr) {
+        order.push_back(expr);
+        return expr == i[0] ? ast::TraverseAction::Skip
+                            : ast::TraverseAction::Descend;
+      });
+  EXPECT_THAT(order, ElementsAre(root, i[0], i[1], e[2], e[3]));
+}
+
+TEST_F(TraverseExpressionsTest, Stop) {
+  std::vector<const ast::Expression*> e = {Expr(1), Expr(1), Expr(1), Expr(1)};
+  std::vector<const ast::Expression*> i = {IndexAccessor(e[0], e[1]),
+                                           IndexAccessor(e[2], e[3])};
+  auto* root = IndexAccessor(i[0], i[1]);
+  std::vector<const ast::Expression*> order;
+  TraverseExpressions<TraverseOrder::LeftToRight>(
+      root, Diagnostics(), [&](const ast::Expression* expr) {
+        order.push_back(expr);
+        return expr == i[0] ? ast::TraverseAction::Stop
+                            : ast::TraverseAction::Descend;
+      });
+  EXPECT_THAT(order, ElementsAre(root, i[0]));
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/type.h b/src/tint/ast/type.h
new file mode 100644
index 0000000..8154988
--- /dev/null
+++ b/src/tint/ast/type.h
@@ -0,0 +1,53 @@
+// Copyright 2020 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_AST_TYPE_H_
+#define SRC_TINT_AST_TYPE_H_
+
+#include <string>
+
+#include "src/tint/ast/node.h"
+#include "src/tint/clone_context.h"
+
+namespace tint {
+
+// Forward declarations
+class ProgramBuilder;
+class SymbolTable;
+
+namespace ast {
+
+/// Base class for a type in the system
+class Type : public Castable<Type, Node> {
+ public:
+  /// Move constructor
+  Type(Type&&);
+  ~Type() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  virtual std::string FriendlyName(const SymbolTable& symbols) const = 0;
+
+ protected:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Type(ProgramID pid, const Source& src);
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TYPE_H_
diff --git a/src/tint/ast/type_decl.cc b/src/tint/ast/type_decl.cc
new file mode 100644
index 0000000..6d0b301
--- /dev/null
+++ b/src/tint/ast/type_decl.cc
@@ -0,0 +1,34 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/type_decl.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::TypeDecl);
+
+namespace tint {
+namespace ast {
+
+TypeDecl::TypeDecl(ProgramID pid, const Source& src, Symbol n)
+    : Base(pid, src), name(n) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
+}
+
+TypeDecl::TypeDecl(TypeDecl&&) = default;
+
+TypeDecl::~TypeDecl() = default;
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/type_decl.h b/src/tint/ast/type_decl.h
new file mode 100644
index 0000000..0e290cd
--- /dev/null
+++ b/src/tint/ast/type_decl.h
@@ -0,0 +1,45 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_TYPE_DECL_H_
+#define SRC_TINT_AST_TYPE_DECL_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// The base class for type declarations.
+class TypeDecl : public Castable<TypeDecl, Node> {
+ public:
+  /// Create a new struct statement
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node for the import statement
+  /// @param name The name of the structure
+  TypeDecl(ProgramID pid, const Source& src, Symbol name);
+  /// Move constructor
+  TypeDecl(TypeDecl&&);
+
+  ~TypeDecl() override;
+
+  /// The name of the type declaration
+  const Symbol name;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TYPE_DECL_H_
diff --git a/src/tint/ast/type_name.cc b/src/tint/ast/type_name.cc
new file mode 100644
index 0000000..1f58b9e
--- /dev/null
+++ b/src/tint/ast/type_name.cc
@@ -0,0 +1,42 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/type_name.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::TypeName);
+
+namespace tint {
+namespace ast {
+
+TypeName::TypeName(ProgramID pid, const Source& src, Symbol n)
+    : Base(pid, src), name(n) {}
+
+TypeName::~TypeName() = default;
+
+TypeName::TypeName(TypeName&&) = default;
+
+std::string TypeName::FriendlyName(const SymbolTable& symbols) const {
+  return symbols.NameFor(name);
+}
+
+const TypeName* TypeName::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  auto n = ctx->Clone(name);
+  return ctx->dst->create<TypeName>(src, n);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/type_name.h b/src/tint/ast/type_name.h
new file mode 100644
index 0000000..9bae5d0
--- /dev/null
+++ b/src/tint/ast/type_name.h
@@ -0,0 +1,55 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_TYPE_NAME_H_
+#define SRC_TINT_AST_TYPE_NAME_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A named type (i.e. struct or alias)
+class TypeName final : public Castable<TypeName, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param name the type name
+  TypeName(ProgramID pid, const Source& src, Symbol name);
+  /// Move constructor
+  TypeName(TypeName&&);
+  /// Destructor
+  ~TypeName() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const TypeName* Clone(CloneContext* ctx) const override;
+
+  /// The type name
+  Symbol name;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_TYPE_NAME_H_
diff --git a/src/tint/ast/u32.cc b/src/tint/ast/u32.cc
new file mode 100644
index 0000000..892289a
--- /dev/null
+++ b/src/tint/ast/u32.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/u32.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::U32);
+
+namespace tint {
+namespace ast {
+
+U32::U32(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+U32::~U32() = default;
+
+U32::U32(U32&&) = default;
+
+std::string U32::FriendlyName(const SymbolTable&) const {
+  return "u32";
+}
+
+const U32* U32::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<U32>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/u32.h b/src/tint/ast/u32.h
new file mode 100644
index 0000000..a31222b
--- /dev/null
+++ b/src/tint/ast/u32.h
@@ -0,0 +1,50 @@
+// Copyright 2020 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_AST_U32_H_
+#define SRC_TINT_AST_U32_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A unsigned int 32 type.
+class U32 final : public Castable<U32, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  U32(ProgramID pid, const Source& src);
+  /// Move constructor
+  U32(U32&&);
+  ~U32() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const U32* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_U32_H_
diff --git a/src/tint/ast/u32_test.cc b/src/tint/ast/u32_test.cc
new file mode 100644
index 0000000..9e8e5a6
--- /dev/null
+++ b/src/tint/ast/u32_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/u32.h"
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstU32Test = TestHelper;
+
+TEST_F(AstU32Test, FriendlyName) {
+  auto* u = create<U32>();
+  EXPECT_EQ(u->FriendlyName(Symbols()), "u32");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/uint_literal_expression.cc b/src/tint/ast/uint_literal_expression.cc
new file mode 100644
index 0000000..d53e3eb
--- /dev/null
+++ b/src/tint/ast/uint_literal_expression.cc
@@ -0,0 +1,43 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/uint_literal_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::UintLiteralExpression);
+
+namespace tint {
+namespace ast {
+
+UintLiteralExpression::UintLiteralExpression(ProgramID pid,
+                                             const Source& src,
+                                             uint32_t val)
+    : Base(pid, src), value(val) {}
+
+UintLiteralExpression::~UintLiteralExpression() = default;
+
+uint32_t UintLiteralExpression::ValueAsU32() const {
+  return value;
+}
+
+const UintLiteralExpression* UintLiteralExpression::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<UintLiteralExpression>(src, value);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/uint_literal_expression.h b/src/tint/ast/uint_literal_expression.h
new file mode 100644
index 0000000..58fdb96
--- /dev/null
+++ b/src/tint/ast/uint_literal_expression.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_AST_UINT_LITERAL_EXPRESSION_H_
+#define SRC_TINT_AST_UINT_LITERAL_EXPRESSION_H_
+
+#include <string>
+
+#include "src/tint/ast/int_literal_expression.h"
+
+namespace tint {
+namespace ast {
+
+/// A uint literal
+class UintLiteralExpression final
+    : public Castable<UintLiteralExpression, IntLiteralExpression> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param value the uint literals value
+  UintLiteralExpression(ProgramID pid, const Source& src, uint32_t value);
+  ~UintLiteralExpression() override;
+
+  /// @returns the literal value as a u32
+  uint32_t ValueAsU32() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const UintLiteralExpression* Clone(CloneContext* ctx) const override;
+
+  /// The int literal value
+  const uint32_t value;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_UINT_LITERAL_EXPRESSION_H_
diff --git a/src/tint/ast/uint_literal_expression_test.cc b/src/tint/ast/uint_literal_expression_test.cc
new file mode 100644
index 0000000..1732dde
--- /dev/null
+++ b/src/tint/ast/uint_literal_expression_test.cc
@@ -0,0 +1,31 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using UintLiteralExpressionTest = TestHelper;
+
+TEST_F(UintLiteralExpressionTest, Value) {
+  auto* u = create<UintLiteralExpression>(47);
+  ASSERT_TRUE(u->Is<UintLiteralExpression>());
+  EXPECT_EQ(u->value, 47u);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/unary_op.cc b/src/tint/ast/unary_op.cc
new file mode 100644
index 0000000..4b363f5
--- /dev/null
+++ b/src/tint/ast/unary_op.cc
@@ -0,0 +1,47 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/unary_op.h"
+
+namespace tint {
+namespace ast {
+
+std::ostream& operator<<(std::ostream& out, UnaryOp mod) {
+  switch (mod) {
+    case UnaryOp::kAddressOf: {
+      out << "address-of";
+      break;
+    }
+    case UnaryOp::kComplement: {
+      out << "complement";
+      break;
+    }
+    case UnaryOp::kIndirection: {
+      out << "indirection";
+      break;
+    }
+    case UnaryOp::kNegation: {
+      out << "negation";
+      break;
+    }
+    case UnaryOp::kNot: {
+      out << "not";
+      break;
+    }
+  }
+  return out;
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/unary_op.h b/src/tint/ast/unary_op.h
new file mode 100644
index 0000000..33fdbbf
--- /dev/null
+++ b/src/tint/ast/unary_op.h
@@ -0,0 +1,40 @@
+// Copyright 2020 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_AST_UNARY_OP_H_
+#define SRC_TINT_AST_UNARY_OP_H_
+
+#include <ostream>
+
+namespace tint {
+namespace ast {
+
+/// The unary op
+enum class UnaryOp {
+  kAddressOf,    // &EXPR
+  kComplement,   // ~EXPR
+  kIndirection,  // *EXPR
+  kNegation,     // -EXPR
+  kNot,          // !EXPR
+};
+
+/// @param out the std::ostream to write to
+/// @param mod the UnaryOp
+/// @return the std::ostream so calls can be chained
+std::ostream& operator<<(std::ostream& out, UnaryOp mod);
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_UNARY_OP_H_
diff --git a/src/tint/ast/unary_op_expression.cc b/src/tint/ast/unary_op_expression.cc
new file mode 100644
index 0000000..032fa02
--- /dev/null
+++ b/src/tint/ast/unary_op_expression.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/unary_op_expression.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::UnaryOpExpression);
+
+namespace tint {
+namespace ast {
+
+UnaryOpExpression::UnaryOpExpression(ProgramID pid,
+                                     const Source& src,
+                                     UnaryOp o,
+                                     const Expression* e)
+    : Base(pid, src), op(o), expr(e) {
+  TINT_ASSERT(AST, expr);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, expr, program_id);
+}
+
+UnaryOpExpression::UnaryOpExpression(UnaryOpExpression&&) = default;
+
+UnaryOpExpression::~UnaryOpExpression() = default;
+
+const UnaryOpExpression* UnaryOpExpression::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* e = ctx->Clone(expr);
+  return ctx->dst->create<UnaryOpExpression>(src, op, e);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/unary_op_expression.h b/src/tint/ast/unary_op_expression.h
new file mode 100644
index 0000000..9b19a77
--- /dev/null
+++ b/src/tint/ast/unary_op_expression.h
@@ -0,0 +1,56 @@
+// Copyright 2020 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_AST_UNARY_OP_EXPRESSION_H_
+#define SRC_TINT_AST_UNARY_OP_EXPRESSION_H_
+
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/unary_op.h"
+
+namespace tint {
+namespace ast {
+
+/// A unary op expression
+class UnaryOpExpression final : public Castable<UnaryOpExpression, Expression> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the unary op expression source
+  /// @param op the op
+  /// @param expr the expr
+  UnaryOpExpression(ProgramID program_id,
+                    const Source& source,
+                    UnaryOp op,
+                    const Expression* expr);
+  /// Move constructor
+  UnaryOpExpression(UnaryOpExpression&&);
+  ~UnaryOpExpression() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const UnaryOpExpression* Clone(CloneContext* ctx) const override;
+
+  /// The op
+  const UnaryOp op;
+
+  /// The expression
+  const Expression* const expr;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_UNARY_OP_EXPRESSION_H_
diff --git a/src/tint/ast/unary_op_expression_test.cc b/src/tint/ast/unary_op_expression_test.cc
new file mode 100644
index 0000000..3f1b1cf
--- /dev/null
+++ b/src/tint/ast/unary_op_expression_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/unary_op_expression.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using UnaryOpExpressionTest = TestHelper;
+
+TEST_F(UnaryOpExpressionTest, Creation) {
+  auto* ident = Expr("ident");
+
+  auto* u = create<UnaryOpExpression>(UnaryOp::kNot, ident);
+  EXPECT_EQ(u->op, UnaryOp::kNot);
+  EXPECT_EQ(u->expr, ident);
+}
+
+TEST_F(UnaryOpExpressionTest, Creation_WithSource) {
+  auto* ident = Expr("ident");
+  auto* u = create<UnaryOpExpression>(Source{Source::Location{20, 2}},
+                                      UnaryOp::kNot, ident);
+  auto src = u->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(UnaryOpExpressionTest, IsUnaryOp) {
+  auto* ident = Expr("ident");
+  auto* u = create<UnaryOpExpression>(UnaryOp::kNot, ident);
+  EXPECT_TRUE(u->Is<UnaryOpExpression>());
+}
+
+TEST_F(UnaryOpExpressionTest, Assert_Null_Expression) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<UnaryOpExpression>(UnaryOp::kNot, nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(UnaryOpExpressionTest, Assert_DifferentProgramID_Expression) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<UnaryOpExpression>(UnaryOp::kNot, b2.Expr(true));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/variable.cc b/src/tint/ast/variable.cc
new file mode 100644
index 0000000..62d8dd9
--- /dev/null
+++ b/src/tint/ast/variable.cc
@@ -0,0 +1,79 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/variable.h"
+
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/variable.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Variable);
+
+namespace tint {
+namespace ast {
+
+Variable::Variable(ProgramID pid,
+                   const Source& src,
+                   const Symbol& sym,
+                   StorageClass dsc,
+                   Access da,
+                   const ast::Type* ty,
+                   bool constant,
+                   bool overridable,
+                   const Expression* ctor,
+                   AttributeList attrs)
+    : Base(pid, src),
+      symbol(sym),
+      type(ty),
+      is_const(constant),
+      is_overridable(overridable),
+      constructor(ctor),
+      attributes(std::move(attrs)),
+      declared_storage_class(dsc),
+      declared_access(da) {
+  TINT_ASSERT(AST, symbol.IsValid());
+  TINT_ASSERT(AST, is_overridable ? is_const : true);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, symbol, program_id);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, constructor, program_id);
+}
+
+Variable::Variable(Variable&&) = default;
+
+Variable::~Variable() = default;
+
+VariableBindingPoint Variable::BindingPoint() const {
+  const GroupAttribute* group = nullptr;
+  const BindingAttribute* binding = nullptr;
+  for (auto* attr : attributes) {
+    if (auto* g = attr->As<GroupAttribute>()) {
+      group = g;
+    } else if (auto* b = attr->As<BindingAttribute>()) {
+      binding = b;
+    }
+  }
+  return VariableBindingPoint{group, binding};
+}
+
+const Variable* Variable::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  auto sym = ctx->Clone(symbol);
+  auto* ty = ctx->Clone(type);
+  auto* ctor = ctx->Clone(constructor);
+  auto attrs = ctx->Clone(attributes);
+  return ctx->dst->create<Variable>(src, sym, declared_storage_class,
+                                    declared_access, ty, is_const,
+                                    is_overridable, ctor, attrs);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/variable.h b/src/tint/ast/variable.h
new file mode 100644
index 0000000..88b6ad2
--- /dev/null
+++ b/src/tint/ast/variable.h
@@ -0,0 +1,186 @@
+// Copyright 2020 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_AST_VARIABLE_H_
+#define SRC_TINT_AST_VARIABLE_H_
+
+#include <utility>
+#include <vector>
+
+#include "src/tint/ast/access.h"
+#include "src/tint/ast/attribute.h"
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/storage_class.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declarations
+class BindingAttribute;
+class GroupAttribute;
+class LocationAttribute;
+class Type;
+
+/// VariableBindingPoint holds a group and binding attribute.
+struct VariableBindingPoint {
+  /// The `@group` part of the binding point
+  const GroupAttribute* group = nullptr;
+  /// The `@binding` part of the binding point
+  const BindingAttribute* binding = nullptr;
+
+  /// @returns true if the BindingPoint has a valid group and binding
+  /// attribute.
+  inline operator bool() const { return group && binding; }
+};
+
+/// A Variable statement.
+///
+/// An instance of this class represents one of four constructs in WGSL: "var"
+/// declaration, "let" declaration, "override" declaration, or formal parameter
+/// to a function.
+///
+/// 1. A "var" declaration is a name for typed storage.  Examples:
+///
+///       // Declared outside a function, i.e. at module scope, requires
+///       // a storage class.
+///       var<workgroup> width : i32;     // no initializer
+///       var<private> height : i32 = 3;  // with initializer
+///
+///       // A variable declared inside a function doesn't take a storage class,
+///       // and maps to SPIR-V Function storage.
+///       var computed_depth : i32;
+///       var area : i32 = compute_area(width, height);
+///
+/// 2. A "let" declaration is a name for a typed value.  Examples:
+///
+///       let twice_depth : i32 = width + width;  // Must have initializer
+///
+/// 3. An "override" declaration is a name for a pipeline-overridable constant.
+/// Examples:
+///
+///       override radius : i32 = 2;       // Can be overridden by name.
+///       @id(5) override width : i32 = 2; // Can be overridden by ID.
+///       override scale : f32;            // No default - must be overridden.
+///
+/// 4. A formal parameter to a function is a name for a typed value to
+///    be passed into a function.  Example:
+///
+///       fn twice(a: i32) -> i32 {  // "a:i32" is the formal parameter
+///         return a + a;
+///       }
+///
+/// From the WGSL draft, about "var"::
+///
+///   A variable is a named reference to storage that can contain a value of a
+///   particular type.
+///
+///   Two types are associated with a variable: its store type (the type of
+///   value that may be placed in the referenced storage) and its reference
+///   type (the type of the variable itself).  If a variable has store type T
+///   and storage class S, then its reference type is pointer-to-T-in-S.
+///
+/// This class uses the term "type" to refer to:
+///     the value type of a "let",
+///     the value type of an "override",
+///     the value type of the formal parameter,
+///     or the store type of the "var".
+//
+/// Setting is_const:
+///   - "var" gets false
+///   - "let" gets true
+///   - "override" gets true
+///   - formal parameter gets true
+///
+/// Setting is_overrideable:
+///   - "var" gets false
+///   - "let" gets false
+///   - "override" gets true
+///   - formal parameter gets false
+///
+/// Setting storage class:
+///   - "var" is StorageClass::kNone when using the
+///     defaulting syntax for a "var" declared inside a function.
+///   - "let" is always StorageClass::kNone.
+///   - formal parameter is always StorageClass::kNone.
+class Variable final : public Castable<Variable, Node> {
+ public:
+  /// Create a variable
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the variable source
+  /// @param sym the variable symbol
+  /// @param declared_storage_class the declared storage class
+  /// @param declared_access the declared access control
+  /// @param type the declared variable type
+  /// @param is_const true if the variable is const
+  /// @param is_overridable true if the variable is pipeline-overridable
+  /// @param constructor the constructor expression
+  /// @param attributes the variable attributes
+  Variable(ProgramID program_id,
+           const Source& source,
+           const Symbol& sym,
+           StorageClass declared_storage_class,
+           Access declared_access,
+           const ast::Type* type,
+           bool is_const,
+           bool is_overridable,
+           const Expression* constructor,
+           AttributeList attributes);
+  /// Move constructor
+  Variable(Variable&&);
+
+  ~Variable() override;
+
+  /// @returns the binding point information for the variable
+  VariableBindingPoint BindingPoint() const;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const Variable* Clone(CloneContext* ctx) const override;
+
+  /// The variable symbol
+  const Symbol symbol;
+
+  /// The declared variable type. This is null if the type is inferred, e.g.:
+  ///   let f = 1.0;
+  ///   var i = 1;
+  const ast::Type* const type;
+
+  /// True if this is a constant, false otherwise
+  const bool is_const;
+
+  /// True if this is a pipeline-overridable constant, false otherwise
+  const bool is_overridable;
+
+  /// The constructor expression or nullptr if none set
+  const Expression* const constructor;
+
+  /// The attributes attached to this variable
+  const AttributeList attributes;
+
+  /// The declared storage class
+  const StorageClass declared_storage_class;
+
+  /// The declared access control
+  const Access declared_access;
+};
+
+/// A list of variables
+using VariableList = std::vector<const Variable*>;
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_VARIABLE_H_
diff --git a/src/tint/ast/variable_decl_statement.cc b/src/tint/ast/variable_decl_statement.cc
new file mode 100644
index 0000000..6adf183
--- /dev/null
+++ b/src/tint/ast/variable_decl_statement.cc
@@ -0,0 +1,45 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/variable_decl_statement.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::VariableDeclStatement);
+
+namespace tint {
+namespace ast {
+
+VariableDeclStatement::VariableDeclStatement(ProgramID pid,
+                                             const Source& src,
+                                             const Variable* var)
+    : Base(pid, src), variable(var) {
+  TINT_ASSERT(AST, variable);
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, variable, program_id);
+}
+
+VariableDeclStatement::VariableDeclStatement(VariableDeclStatement&&) = default;
+
+VariableDeclStatement::~VariableDeclStatement() = default;
+
+const VariableDeclStatement* VariableDeclStatement::Clone(
+    CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* var = ctx->Clone(variable);
+  return ctx->dst->create<VariableDeclStatement>(src, var);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/variable_decl_statement.h b/src/tint/ast/variable_decl_statement.h
new file mode 100644
index 0000000..47fb8ab
--- /dev/null
+++ b/src/tint/ast/variable_decl_statement.h
@@ -0,0 +1,52 @@
+// Copyright 2020 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_AST_VARIABLE_DECL_STATEMENT_H_
+#define SRC_TINT_AST_VARIABLE_DECL_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+#include "src/tint/ast/variable.h"
+
+namespace tint {
+namespace ast {
+
+/// A variable declaration statement
+class VariableDeclStatement final
+    : public Castable<VariableDeclStatement, Statement> {
+ public:
+  /// Constructor
+  /// @param program_id the identifier of the program that owns this node
+  /// @param source the variable statement source
+  /// @param variable the variable
+  VariableDeclStatement(ProgramID program_id,
+                        const Source& source,
+                        const Variable* variable);
+  /// Move constructor
+  VariableDeclStatement(VariableDeclStatement&&);
+  ~VariableDeclStatement() override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const VariableDeclStatement* Clone(CloneContext* ctx) const override;
+
+  /// The variable
+  const Variable* const variable;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_VARIABLE_DECL_STATEMENT_H_
diff --git a/src/tint/ast/variable_decl_statement_test.cc b/src/tint/ast/variable_decl_statement_test.cc
new file mode 100644
index 0000000..9881c66
--- /dev/null
+++ b/src/tint/ast/variable_decl_statement_test.cc
@@ -0,0 +1,72 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/variable_decl_statement.h"
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using VariableDeclStatementTest = TestHelper;
+
+TEST_F(VariableDeclStatementTest, Creation) {
+  auto* var = Var("a", ty.f32(), StorageClass::kNone);
+
+  auto* stmt = create<VariableDeclStatement>(var);
+  EXPECT_EQ(stmt->variable, var);
+}
+
+TEST_F(VariableDeclStatementTest, Creation_WithSource) {
+  auto* var = Var("a", ty.f32(), StorageClass::kNone);
+
+  auto* stmt =
+      create<VariableDeclStatement>(Source{Source::Location{20, 2}}, var);
+  auto src = stmt->source;
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
+}
+
+TEST_F(VariableDeclStatementTest, IsVariableDecl) {
+  auto* var = Var("a", ty.f32(), StorageClass::kNone);
+
+  auto* stmt = create<VariableDeclStatement>(var);
+  EXPECT_TRUE(stmt->Is<VariableDeclStatement>());
+}
+
+TEST_F(VariableDeclStatementTest, Assert_Null_Variable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.create<VariableDeclStatement>(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(VariableDeclStatementTest, Assert_DifferentProgramID_Variable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.create<VariableDeclStatement>(
+            b2.Var("a", b2.ty.f32(), StorageClass::kNone));
+      },
+      "internal compiler error");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/variable_test.cc b/src/tint/ast/variable_test.cc
new file mode 100644
index 0000000..b053c0fa
--- /dev/null
+++ b/src/tint/ast/variable_test.cc
@@ -0,0 +1,156 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using VariableTest = TestHelper;
+
+TEST_F(VariableTest, Creation) {
+  auto* v = Var("my_var", ty.i32(), StorageClass::kFunction);
+
+  EXPECT_EQ(v->symbol, Symbol(1, ID()));
+  EXPECT_EQ(v->declared_storage_class, StorageClass::kFunction);
+  EXPECT_TRUE(v->type->Is<ast::I32>());
+  EXPECT_EQ(v->source.range.begin.line, 0u);
+  EXPECT_EQ(v->source.range.begin.column, 0u);
+  EXPECT_EQ(v->source.range.end.line, 0u);
+  EXPECT_EQ(v->source.range.end.column, 0u);
+}
+
+TEST_F(VariableTest, CreationWithSource) {
+  auto* v = Var(
+      Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 5}}},
+      "i", ty.f32(), StorageClass::kPrivate, nullptr, AttributeList{});
+
+  EXPECT_EQ(v->symbol, Symbol(1, ID()));
+  EXPECT_EQ(v->declared_storage_class, StorageClass::kPrivate);
+  EXPECT_TRUE(v->type->Is<ast::F32>());
+  EXPECT_EQ(v->source.range.begin.line, 27u);
+  EXPECT_EQ(v->source.range.begin.column, 4u);
+  EXPECT_EQ(v->source.range.end.line, 27u);
+  EXPECT_EQ(v->source.range.end.column, 5u);
+}
+
+TEST_F(VariableTest, CreationEmpty) {
+  auto* v = Var(
+      Source{Source::Range{Source::Location{27, 4}, Source::Location{27, 7}}},
+      "a_var", ty.i32(), StorageClass::kWorkgroup, nullptr, AttributeList{});
+
+  EXPECT_EQ(v->symbol, Symbol(1, ID()));
+  EXPECT_EQ(v->declared_storage_class, StorageClass::kWorkgroup);
+  EXPECT_TRUE(v->type->Is<ast::I32>());
+  EXPECT_EQ(v->source.range.begin.line, 27u);
+  EXPECT_EQ(v->source.range.begin.column, 4u);
+  EXPECT_EQ(v->source.range.end.line, 27u);
+  EXPECT_EQ(v->source.range.end.column, 7u);
+}
+
+TEST_F(VariableTest, Assert_MissingSymbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.Var("", b.ty.i32(), StorageClass::kNone);
+      },
+      "internal compiler error");
+}
+
+TEST_F(VariableTest, Assert_DifferentProgramID_Symbol) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Var(b2.Sym("x"), b1.ty.f32(), StorageClass::kNone);
+      },
+      "internal compiler error");
+}
+
+TEST_F(VariableTest, Assert_DifferentProgramID_Constructor) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b1;
+        ProgramBuilder b2;
+        b1.Var("x", b1.ty.f32(), StorageClass::kNone, b2.Expr(1.2f));
+      },
+      "internal compiler error");
+}
+
+TEST_F(VariableTest, WithAttributes) {
+  auto* var = Var("my_var", ty.i32(), StorageClass::kFunction, nullptr,
+                  AttributeList{
+                      create<LocationAttribute>(1),
+                      create<BuiltinAttribute>(Builtin::kPosition),
+                      create<IdAttribute>(1200),
+                  });
+
+  auto& attributes = var->attributes;
+  EXPECT_TRUE(ast::HasAttribute<ast::LocationAttribute>(attributes));
+  EXPECT_TRUE(ast::HasAttribute<ast::BuiltinAttribute>(attributes));
+  EXPECT_TRUE(ast::HasAttribute<ast::IdAttribute>(attributes));
+
+  auto* location = ast::GetAttribute<ast::LocationAttribute>(attributes);
+  ASSERT_NE(nullptr, location);
+  EXPECT_EQ(1u, location->value);
+}
+
+TEST_F(VariableTest, BindingPoint) {
+  auto* var = Var("my_var", ty.i32(), StorageClass::kFunction, nullptr,
+                  AttributeList{
+                      create<BindingAttribute>(2),
+                      create<GroupAttribute>(1),
+                  });
+  EXPECT_TRUE(var->BindingPoint());
+  ASSERT_NE(var->BindingPoint().binding, nullptr);
+  ASSERT_NE(var->BindingPoint().group, nullptr);
+  EXPECT_EQ(var->BindingPoint().binding->value, 2u);
+  EXPECT_EQ(var->BindingPoint().group->value, 1u);
+}
+
+TEST_F(VariableTest, BindingPointAttributes) {
+  auto* var = Var("my_var", ty.i32(), StorageClass::kFunction, nullptr,
+                  AttributeList{});
+  EXPECT_FALSE(var->BindingPoint());
+  EXPECT_EQ(var->BindingPoint().group, nullptr);
+  EXPECT_EQ(var->BindingPoint().binding, nullptr);
+}
+
+TEST_F(VariableTest, BindingPointMissingGroupAttribute) {
+  auto* var = Var("my_var", ty.i32(), StorageClass::kFunction, nullptr,
+                  AttributeList{
+                      create<BindingAttribute>(2),
+                  });
+  EXPECT_FALSE(var->BindingPoint());
+  ASSERT_NE(var->BindingPoint().binding, nullptr);
+  EXPECT_EQ(var->BindingPoint().binding->value, 2u);
+  EXPECT_EQ(var->BindingPoint().group, nullptr);
+}
+
+TEST_F(VariableTest, BindingPointMissingBindingAttribute) {
+  auto* var = Var("my_var", ty.i32(), StorageClass::kFunction, nullptr,
+                  AttributeList{create<GroupAttribute>(1)});
+  EXPECT_FALSE(var->BindingPoint());
+  ASSERT_NE(var->BindingPoint().group, nullptr);
+  EXPECT_EQ(var->BindingPoint().group->value, 1u);
+  EXPECT_EQ(var->BindingPoint().binding, nullptr);
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/vector.cc b/src/tint/ast/vector.cc
new file mode 100644
index 0000000..d63a61b
--- /dev/null
+++ b/src/tint/ast/vector.cc
@@ -0,0 +1,55 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/vector.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Vector);
+
+namespace tint {
+namespace ast {
+
+Vector::Vector(ProgramID pid,
+               Source const& src,
+               const Type* subtype,
+               uint32_t w)
+    : Base(pid, src), type(subtype), width(w) {
+  TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, subtype, program_id);
+  TINT_ASSERT(AST, width > 1);
+  TINT_ASSERT(AST, width < 5);
+}
+
+Vector::Vector(Vector&&) = default;
+
+Vector::~Vector() = default;
+
+std::string Vector::FriendlyName(const SymbolTable& symbols) const {
+  std::ostringstream out;
+  out << "vec" << width;
+  if (type) {
+    out << "<" << type->FriendlyName(symbols) << ">";
+  }
+  return out.str();
+}
+
+const Vector* Vector::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* ty = ctx->Clone(type);
+  return ctx->dst->create<Vector>(src, ty, width);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/vector.h b/src/tint/ast/vector.h
new file mode 100644
index 0000000..bfc4908
--- /dev/null
+++ b/src/tint/ast/vector.h
@@ -0,0 +1,62 @@
+// Copyright 2020 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_AST_VECTOR_H_
+#define SRC_TINT_AST_VECTOR_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A vector type.
+class Vector final : public Castable<Vector, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param subtype the declared type of the vector components. May be null
+  ///        for vector constructors, where the element type will be inferred
+  ///        from the constructor arguments
+  /// @param width the number of elements in the vector
+  Vector(ProgramID pid, Source const& src, const Type* subtype, uint32_t width);
+  /// Move constructor
+  Vector(Vector&&);
+  ~Vector() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Vector* Clone(CloneContext* ctx) const override;
+
+  /// The declared type of the vector components. May be null for vector
+  /// constructors, where the element type will be inferred from the constructor
+  /// arguments
+  const Type* const type;
+
+  /// The number of elements in the vector
+  const uint32_t width;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_VECTOR_H_
diff --git a/src/tint/ast/vector_test.cc b/src/tint/ast/vector_test.cc
new file mode 100644
index 0000000..a029e73
--- /dev/null
+++ b/src/tint/ast/vector_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/vector.h"
+
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using AstVectorTest = TestHelper;
+
+TEST_F(AstVectorTest, Creation) {
+  auto* i32 = create<I32>();
+  auto* v = create<Vector>(i32, 2);
+  EXPECT_EQ(v->type, i32);
+  EXPECT_EQ(v->width, 2u);
+}
+
+TEST_F(AstVectorTest, FriendlyName) {
+  auto* f32 = create<F32>();
+  auto* v = create<Vector>(f32, 3);
+  EXPECT_EQ(v->FriendlyName(Symbols()), "vec3<f32>");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/void.cc b/src/tint/ast/void.cc
new file mode 100644
index 0000000..1abb83c
--- /dev/null
+++ b/src/tint/ast/void.cc
@@ -0,0 +1,40 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/void.h"
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::Void);
+
+namespace tint {
+namespace ast {
+
+Void::Void(ProgramID pid, const Source& src) : Base(pid, src) {}
+
+Void::Void(Void&&) = default;
+
+Void::~Void() = default;
+
+std::string Void::FriendlyName(const SymbolTable&) const {
+  return "void";
+}
+
+const Void* Void::Clone(CloneContext* ctx) const {
+  auto src = ctx->Clone(source);
+  return ctx->dst->create<Void>(src);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/void.h b/src/tint/ast/void.h
new file mode 100644
index 0000000..94d382f
--- /dev/null
+++ b/src/tint/ast/void.h
@@ -0,0 +1,50 @@
+// Copyright 2020 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_AST_VOID_H_
+#define SRC_TINT_AST_VOID_H_
+
+#include <string>
+
+#include "src/tint/ast/type.h"
+
+namespace tint {
+namespace ast {
+
+/// A void type
+class Void final : public Castable<Void, Type> {
+ public:
+  /// Constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  Void(ProgramID pid, const Source& src);
+  /// Move constructor
+  Void(Void&&);
+  ~Void() override;
+
+  /// @param symbols the program's symbol table
+  /// @returns the name for this type that closely resembles how it would be
+  /// declared in WGSL.
+  std::string FriendlyName(const SymbolTable& symbols) const override;
+
+  /// Clones this type and all transitive types using the `CloneContext` `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned type
+  const Void* Clone(CloneContext* ctx) const override;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_VOID_H_
diff --git a/src/tint/ast/workgroup_attribute.cc b/src/tint/ast/workgroup_attribute.cc
new file mode 100644
index 0000000..7ff6954
--- /dev/null
+++ b/src/tint/ast/workgroup_attribute.cc
@@ -0,0 +1,49 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/workgroup_attribute.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::WorkgroupAttribute);
+
+namespace tint {
+namespace ast {
+
+WorkgroupAttribute::WorkgroupAttribute(ProgramID pid,
+                                       const Source& src,
+                                       const ast::Expression* x_,
+                                       const ast::Expression* y_,
+                                       const ast::Expression* z_)
+    : Base(pid, src), x(x_), y(y_), z(z_) {}
+
+WorkgroupAttribute::~WorkgroupAttribute() = default;
+
+std::string WorkgroupAttribute::Name() const {
+  return "workgroup_size";
+}
+
+const WorkgroupAttribute* WorkgroupAttribute::Clone(CloneContext* ctx) const {
+  // Clone arguments outside of create() call to have deterministic ordering
+  auto src = ctx->Clone(source);
+  auto* x_ = ctx->Clone(x);
+  auto* y_ = ctx->Clone(y);
+  auto* z_ = ctx->Clone(z);
+  return ctx->dst->create<WorkgroupAttribute>(src, x_, y_, z_);
+}
+
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/ast/workgroup_attribute.h b/src/tint/ast/workgroup_attribute.h
new file mode 100644
index 0000000..5ffc7c64
--- /dev/null
+++ b/src/tint/ast/workgroup_attribute.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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_AST_WORKGROUP_ATTRIBUTE_H_
+#define SRC_TINT_AST_WORKGROUP_ATTRIBUTE_H_
+
+#include <array>
+#include <string>
+
+#include "src/tint/ast/attribute.h"
+
+namespace tint {
+namespace ast {
+
+// Forward declaration
+class Expression;
+
+/// A workgroup attribute
+class WorkgroupAttribute final
+    : public Castable<WorkgroupAttribute, Attribute> {
+ public:
+  /// constructor
+  /// @param pid the identifier of the program that owns this node
+  /// @param src the source of this node
+  /// @param x the workgroup x dimension expression
+  /// @param y the optional workgroup y dimension expression
+  /// @param z the optional workgroup z dimension expression
+  WorkgroupAttribute(ProgramID pid,
+                     const Source& src,
+                     const ast::Expression* x,
+                     const ast::Expression* y = nullptr,
+                     const ast::Expression* z = nullptr);
+
+  ~WorkgroupAttribute() override;
+
+  /// @returns the workgroup dimensions
+  std::array<const ast::Expression*, 3> Values() const { return {x, y, z}; }
+
+  /// @returns the WGSL name for the attribute
+  std::string Name() const override;
+
+  /// Clones this node and all transitive child nodes using the `CloneContext`
+  /// `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned node
+  const WorkgroupAttribute* Clone(CloneContext* ctx) const override;
+
+  /// The workgroup x dimension.
+  const ast::Expression* const x;
+  /// The optional workgroup y dimension. May be null.
+  const ast::Expression* const y = nullptr;
+  /// The optional workgroup z dimension. May be null.
+  const ast::Expression* const z = nullptr;
+};
+
+}  // namespace ast
+}  // namespace tint
+
+#endif  // SRC_TINT_AST_WORKGROUP_ATTRIBUTE_H_
diff --git a/src/tint/ast/workgroup_attribute_test.cc b/src/tint/ast/workgroup_attribute_test.cc
new file mode 100644
index 0000000..ecf7186
--- /dev/null
+++ b/src/tint/ast/workgroup_attribute_test.cc
@@ -0,0 +1,80 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/workgroup_attribute.h"
+
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace ast {
+namespace {
+
+using WorkgroupAttributeTest = TestHelper;
+
+TEST_F(WorkgroupAttributeTest, Creation_1param) {
+  auto* d = WorkgroupSize(2);
+  auto values = d->Values();
+
+  ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->ValueAsU32(), 2u);
+
+  EXPECT_EQ(values[1], nullptr);
+  EXPECT_EQ(values[2], nullptr);
+}
+TEST_F(WorkgroupAttributeTest, Creation_2param) {
+  auto* d = WorkgroupSize(2, 4);
+  auto values = d->Values();
+
+  ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->ValueAsU32(), 2u);
+
+  ASSERT_TRUE(values[1]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->ValueAsU32(), 4u);
+
+  EXPECT_EQ(values[2], nullptr);
+}
+
+TEST_F(WorkgroupAttributeTest, Creation_3param) {
+  auto* d = WorkgroupSize(2, 4, 6);
+  auto values = d->Values();
+
+  ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->ValueAsU32(), 2u);
+
+  ASSERT_TRUE(values[1]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->ValueAsU32(), 4u);
+
+  ASSERT_TRUE(values[2]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[2]->As<ast::IntLiteralExpression>()->ValueAsU32(), 6u);
+}
+
+TEST_F(WorkgroupAttributeTest, Creation_WithIdentifier) {
+  auto* d = WorkgroupSize(2, 4, "depth");
+  auto values = d->Values();
+
+  ASSERT_TRUE(values[0]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[0]->As<ast::IntLiteralExpression>()->ValueAsU32(), 2u);
+
+  ASSERT_TRUE(values[1]->Is<ast::IntLiteralExpression>());
+  EXPECT_EQ(values[1]->As<ast::IntLiteralExpression>()->ValueAsU32(), 4u);
+
+  auto* z_ident = As<ast::IdentifierExpression>(values[2]);
+  ASSERT_TRUE(z_ident);
+  EXPECT_EQ(Symbols().NameFor(z_ident->symbol), "depth");
+}
+
+}  // namespace
+}  // namespace ast
+}  // namespace tint
diff --git a/src/tint/bench/benchmark.cc b/src/tint/bench/benchmark.cc
new file mode 100644
index 0000000..4fd96e2
--- /dev/null
+++ b/src/tint/bench/benchmark.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/bench/benchmark.h"
+
+#include <filesystem>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+namespace tint::bench {
+namespace {
+
+std::filesystem::path kInputFileDir;
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+std::variant<std::vector<T>, Error> ReadFile(const std::string& input_file) {
+  FILE* file = nullptr;
+#if defined(_MSC_VER)
+  fopen_s(&file, input_file.c_str(), "rb");
+#else
+  file = fopen(input_file.c_str(), "rb");
+#endif
+  if (!file) {
+    return Error{"Failed to open " + input_file};
+  }
+
+  fseek(file, 0, SEEK_END);
+  const auto file_size = static_cast<size_t>(ftell(file));
+  if (0 != (file_size % sizeof(T))) {
+    std::stringstream err;
+    err << "File " << input_file
+        << " does not contain an integral number of objects: " << file_size
+        << " bytes in the file, require " << sizeof(T) << " bytes per object";
+    fclose(file);
+    return Error{err.str()};
+  }
+  fseek(file, 0, SEEK_SET);
+
+  std::vector<T> buffer;
+  buffer.resize(file_size / sizeof(T));
+
+  size_t bytes_read = fread(buffer.data(), 1, file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    return Error{"Failed to read " + input_file};
+  }
+
+  return buffer;
+}
+
+bool FindBenchmarkInputDir() {
+  // Attempt to find the benchmark input files by searching up from the current
+  // working directory.
+  auto path = std::filesystem::current_path();
+  while (std::filesystem::is_directory(path)) {
+    auto test = path / "test" / "tint" / "benchmark";
+    if (std::filesystem::is_directory(test)) {
+      kInputFileDir = test;
+      return true;
+    }
+    auto parent = path.parent_path();
+    if (path == parent) {
+      break;
+    }
+    path = parent;
+  }
+  return false;
+}
+
+}  // namespace
+
+std::variant<tint::Source::File, Error> LoadInputFile(std::string name) {
+  auto path = (kInputFileDir / name).string();
+  auto data = ReadFile<uint8_t>(path);
+  if (auto* buf = std::get_if<std::vector<uint8_t>>(&data)) {
+    return tint::Source::File(path, std::string(buf->begin(), buf->end()));
+  }
+  return std::get<Error>(data);
+}
+
+std::variant<ProgramAndFile, Error> LoadProgram(std::string name) {
+  auto res = bench::LoadInputFile(name);
+  if (auto err = std::get_if<bench::Error>(&res)) {
+    return *err;
+  }
+  auto& file = std::get<Source::File>(res);
+  auto program = reader::wgsl::Parse(&file);
+  if (program.Diagnostics().contains_errors()) {
+    return Error{program.Diagnostics().str()};
+  }
+  return ProgramAndFile{std::move(program), std::move(file)};
+}
+
+}  // namespace tint::bench
+
+int main(int argc, char** argv) {
+  benchmark::Initialize(&argc, argv);
+  if (benchmark::ReportUnrecognizedArguments(argc, argv)) {
+    return 1;
+  }
+  if (!tint::bench::FindBenchmarkInputDir()) {
+    std::cerr << "failed to locate benchmark input files" << std::endl;
+    return 1;
+  }
+  benchmark::RunSpecifiedBenchmarks();
+}
diff --git a/src/tint/bench/benchmark.h b/src/tint/bench/benchmark.h
new file mode 100644
index 0000000..96a935d
--- /dev/null
+++ b/src/tint/bench/benchmark.h
@@ -0,0 +1,76 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_BENCH_BENCHMARK_H_
+#define SRC_TINT_BENCH_BENCHMARK_H_
+
+#include <memory>
+#include <string>
+#include <variant>  // NOLINT: Found C system header after C++ system header.
+
+#include "benchmark/benchmark.h"
+#include "src/tint/utils/concat.h"
+#include "tint/tint.h"
+
+namespace tint::bench {
+
+/// Error indicates an operation did not complete successfully.
+struct Error {
+  /// The error message.
+  std::string msg;
+};
+
+/// ProgramAndFile holds a Program and a Source::File.
+struct ProgramAndFile {
+  /// The tint program parsed from file.
+  Program program;
+  /// The source file
+  Source::File file;
+};
+
+/// LoadInputFile attempts to load a benchmark input file with the given file
+/// name.
+/// @param name the file name
+/// @returns either the loaded Source::File or an Error
+std::variant<Source::File, Error> LoadInputFile(std::string name);
+
+/// LoadInputFile attempts to load a benchmark input program with the given file
+/// name.
+/// @param name the file name
+/// @returns either the loaded Program or an Error
+std::variant<ProgramAndFile, Error> LoadProgram(std::string name);
+
+/// Declares a benchmark with the given function and WGSL file name
+#define TINT_BENCHMARK_WGSL_PROGRAM(FUNC, WGSL_NAME) \
+  BENCHMARK_CAPTURE(FUNC, WGSL_NAME, WGSL_NAME);
+
+/// Declares a set of benchmarks for the given function using a list of WGSL
+/// files in `<tint>/test/benchmark`.
+#define TINT_BENCHMARK_WGSL_PROGRAMS(FUNC)                                 \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "animometer.wgsl");                    \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "bloom-vertical-blur.wgsl");           \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "cluster-lights.wgsl");                \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "empty.wgsl");                         \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "metaball-isosurface.wgsl");           \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "particles.wgsl");                     \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "shadow-fragment.wgsl");               \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple-compute.wgsl");                \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple-fragment.wgsl");               \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple-vertex.wgsl");                 \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "skinned-shadowed-pbr-fragment.wgsl"); \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "skinned-shadowed-pbr-vertex.wgsl");
+
+}  // namespace tint::bench
+
+#endif  // SRC_TINT_BENCH_BENCHMARK_H_
diff --git a/src/tint/builtin_table.cc b/src/tint/builtin_table.cc
new file mode 100644
index 0000000..13e7c5f
--- /dev/null
+++ b/src/tint/builtin_table.cc
@@ -0,0 +1,1169 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/builtin_table.h"
+
+#include <algorithm>
+#include <limits>
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/atomic_type.h"
+#include "src/tint/sem/depth_multisampled_texture_type.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/external_texture_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/pipeline_stage_set.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/storage_texture_type.h"
+#include "src/tint/utils/hash.h"
+#include "src/tint/utils/map.h"
+#include "src/tint/utils/math.h"
+#include "src/tint/utils/scoped_assignment.h"
+
+namespace tint {
+namespace {
+
+// Forward declarations
+struct OverloadInfo;
+class Matchers;
+class NumberMatcher;
+class TypeMatcher;
+
+/// A special type that matches all TypeMatchers
+class Any final : public Castable<Any, sem::Type> {
+ public:
+  Any() = default;
+  ~Any() override = default;
+
+  // Stub implementations for sem::Type conformance.
+  size_t Hash() const override { return 0; }
+  bool Equals(const sem::Type&) const override { return false; }
+  std::string FriendlyName(const SymbolTable&) const override {
+    return "<any>";
+  }
+};
+
+/// Number is an 32 bit unsigned integer, which can be in one of three states:
+/// * Invalid - Number has not been assigned a value
+/// * Valid   - a fixed integer value
+/// * Any     - matches any other non-invalid number
+struct Number {
+  static const Number any;
+  static const Number invalid;
+
+  /// Constructed as a valid number with the value v
+  explicit Number(uint32_t v) : value_(v), state_(kValid) {}
+
+  /// @returns the value of the number
+  inline uint32_t Value() const { return value_; }
+
+  /// @returns the true if the number is valid
+  inline bool IsValid() const { return state_ == kValid; }
+
+  /// @returns the true if the number is any
+  inline bool IsAny() const { return state_ == kAny; }
+
+  /// Assignment operator.
+  /// The number becomes valid, with the value n
+  inline Number& operator=(uint32_t n) {
+    value_ = n;
+    state_ = kValid;
+    return *this;
+  }
+
+ private:
+  enum State {
+    kInvalid,
+    kValid,
+    kAny,
+  };
+
+  constexpr explicit Number(State state) : state_(state) {}
+
+  uint32_t value_ = 0;
+  State state_ = kInvalid;
+};
+
+const Number Number::any{Number::kAny};
+const Number Number::invalid{Number::kInvalid};
+
+/// ClosedState holds the state of the open / closed numbers and types.
+/// Used by the MatchState.
+class ClosedState {
+ public:
+  explicit ClosedState(ProgramBuilder& b) : builder(b) {}
+
+  /// If the type with index `idx` is open, then it is closed with type `ty` and
+  /// Type() returns true. If the type is closed, then `Type()` returns true iff
+  /// it is equal to `ty`.
+  bool Type(uint32_t idx, const sem::Type* ty) {
+    auto res = types_.emplace(idx, ty);
+    return res.second || res.first->second == ty;
+  }
+
+  /// If the number with index `idx` is open, then it is closed with number
+  /// `number` and Num() returns true. If the number is closed, then `Num()`
+  /// returns true iff it is equal to `ty`.
+  bool Num(uint32_t idx, Number number) {
+    auto res = numbers_.emplace(idx, number.Value());
+    return res.second || res.first->second == number.Value();
+  }
+
+  /// Type returns the closed type with index `idx`.
+  /// An ICE is raised if the type is not closed.
+  const sem::Type* Type(uint32_t idx) const {
+    auto it = types_.find(idx);
+    if (it == types_.end()) {
+      TINT_ICE(Resolver, builder.Diagnostics())
+          << "type with index " << idx << " is not closed";
+      return nullptr;
+    }
+    TINT_ASSERT(Resolver, it != types_.end());
+    return it->second;
+  }
+
+  /// Type returns the number type with index `idx`.
+  /// An ICE is raised if the number is not closed.
+  Number Num(uint32_t idx) const {
+    auto it = numbers_.find(idx);
+    if (it == numbers_.end()) {
+      TINT_ICE(Resolver, builder.Diagnostics())
+          << "number with index " << idx << " is not closed";
+      return Number::invalid;
+    }
+    return Number(it->second);
+  }
+
+ private:
+  ProgramBuilder& builder;
+  std::unordered_map<uint32_t, const sem::Type*> types_;
+  std::unordered_map<uint32_t, uint32_t> numbers_;
+};
+
+/// Index type used for matcher indices
+using MatcherIndex = uint8_t;
+
+/// Index value used for open types / numbers that do not have a constraint
+constexpr MatcherIndex kNoMatcher = std::numeric_limits<MatcherIndex>::max();
+
+/// MatchState holds the state used to match an overload.
+class MatchState {
+ public:
+  MatchState(ProgramBuilder& b,
+             ClosedState& c,
+             const Matchers& m,
+             const OverloadInfo& o,
+             MatcherIndex const* matcher_indices)
+      : builder(b),
+        closed(c),
+        matchers(m),
+        overload(o),
+        matcher_indices_(matcher_indices) {}
+
+  /// The program builder
+  ProgramBuilder& builder;
+  /// The open / closed types and numbers
+  ClosedState& closed;
+  /// The type and number matchers
+  Matchers const& matchers;
+  /// The current overload being evaluated
+  OverloadInfo const& overload;
+
+  /// Type uses the next TypeMatcher from the matcher indices to match the type
+  /// `ty`. If the type matches, the canonical expected type is returned. If the
+  /// type `ty` does not match, then nullptr is returned.
+  /// @note: The matcher indices are progressed on calling.
+  const sem::Type* Type(const sem::Type* ty);
+
+  /// Num uses the next NumMatcher from the matcher indices to match the number
+  /// `num`. If the number matches, the canonical expected number is returned.
+  /// If the number `num` does not match, then an invalid number is returned.
+  /// @note: The matcher indices are progressed on calling.
+  Number Num(Number num);
+
+  /// @returns a string representation of the next TypeMatcher from the matcher
+  /// indices.
+  /// @note: The matcher indices are progressed on calling.
+  std::string TypeName();
+
+  /// @returns a string representation of the next NumberMatcher from the
+  /// matcher indices.
+  /// @note: The matcher indices are progressed on calling.
+  std::string NumName();
+
+ private:
+  MatcherIndex const* matcher_indices_ = nullptr;
+};
+
+/// A TypeMatcher is the interface used to match an type used as part of an
+/// overload's parameter or return type.
+class TypeMatcher {
+ public:
+  /// Destructor
+  virtual ~TypeMatcher() = default;
+
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may close open types and numbers in state.
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  virtual const sem::Type* Match(MatchState& state,
+                                 const sem::Type* type) const = 0;
+
+  /// @return a string representation of the matcher. Used for printing error
+  /// messages when no overload is found.
+  virtual std::string String(MatchState& state) const = 0;
+};
+
+/// A NumberMatcher is the interface used to match a number or enumerator used
+/// as part of an overload's parameter or return type.
+class NumberMatcher {
+ public:
+  /// Destructor
+  virtual ~NumberMatcher() = default;
+
+  /// Checks whether the given number matches the matcher rules.
+  /// Match may close open numbers in state.
+  /// @param number the number to match
+  /// @returns true if the argument type is as expected.
+  virtual Number Match(MatchState& state, Number number) const = 0;
+
+  /// @return a string representation of the matcher. Used for printing error
+  /// messages when no overload is found.
+  virtual std::string String(MatchState& state) const = 0;
+};
+
+/// OpenTypeMatcher is a Matcher for an open type.
+/// The OpenTypeMatcher will match against any type (so long as it is consistent
+/// across all uses in the overload)
+class OpenTypeMatcher : public TypeMatcher {
+ public:
+  /// Constructor
+  explicit OpenTypeMatcher(uint32_t index) : index_(index) {}
+
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override {
+    if (type->Is<Any>()) {
+      return state.closed.Type(index_);
+    }
+    return state.closed.Type(index_, type) ? type : nullptr;
+  }
+
+  std::string String(MatchState& state) const override;
+
+ private:
+  uint32_t index_;
+};
+
+/// OpenNumberMatcher is a Matcher for an open number.
+/// The OpenNumberMatcher will match against any number (so long as it is
+/// consistent for the overload)
+class OpenNumberMatcher : public NumberMatcher {
+ public:
+  explicit OpenNumberMatcher(uint32_t index) : index_(index) {}
+
+  Number Match(MatchState& state, Number number) const override {
+    if (number.IsAny()) {
+      return state.closed.Num(index_);
+    }
+    return state.closed.Num(index_, number) ? number : Number::invalid;
+  }
+
+  std::string String(MatchState& state) const override;
+
+ private:
+  uint32_t index_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// 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 TexelFormat = ast::TexelFormat;
+using Access = ast::Access;
+using StorageClass = ast::StorageClass;
+using ParameterUsage = sem::ParameterUsage;
+using PipelineStageSet = sem::PipelineStageSet;
+using PipelineStage = ast::PipelineStage;
+
+bool match_bool(const sem::Type* ty) {
+  return ty->IsAnyOf<Any, sem::Bool>();
+}
+
+const sem::Bool* build_bool(MatchState& state) {
+  return state.builder.create<sem::Bool>();
+}
+
+bool match_f32(const sem::Type* ty) {
+  return ty->IsAnyOf<Any, sem::F32>();
+}
+
+const sem::I32* build_i32(MatchState& state) {
+  return state.builder.create<sem::I32>();
+}
+
+bool match_i32(const sem::Type* ty) {
+  return ty->IsAnyOf<Any, sem::I32>();
+}
+
+const sem::U32* build_u32(MatchState& state) {
+  return state.builder.create<sem::U32>();
+}
+
+bool match_u32(const sem::Type* ty) {
+  return ty->IsAnyOf<Any, sem::U32>();
+}
+
+const sem::F32* build_f32(MatchState& state) {
+  return state.builder.create<sem::F32>();
+}
+
+bool match_vec(const sem::Type* ty, Number& N, const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    N = Number::any;
+    T = ty;
+    return true;
+  }
+
+  if (auto* v = ty->As<sem::Vector>()) {
+    N = v->Width();
+    T = v->type();
+    return true;
+  }
+  return false;
+}
+
+const sem::Vector* build_vec(MatchState& state, Number N, const sem::Type* el) {
+  return state.builder.create<sem::Vector>(el, N.Value());
+}
+
+template <int N>
+bool match_vec(const sem::Type* ty, const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    T = ty;
+    return true;
+  }
+
+  if (auto* v = ty->As<sem::Vector>()) {
+    if (v->Width() == N) {
+      T = v->type();
+      return true;
+    }
+  }
+  return false;
+}
+
+bool match_vec2(const sem::Type* ty, const sem::Type*& T) {
+  return match_vec<2>(ty, T);
+}
+
+const sem::Vector* build_vec2(MatchState& state, const sem::Type* T) {
+  return build_vec(state, Number(2), T);
+}
+
+bool match_vec3(const sem::Type* ty, const sem::Type*& T) {
+  return match_vec<3>(ty, T);
+}
+
+const sem::Vector* build_vec3(MatchState& state, const sem::Type* T) {
+  return build_vec(state, Number(3), T);
+}
+
+bool match_vec4(const sem::Type* ty, const sem::Type*& T) {
+  return match_vec<4>(ty, T);
+}
+
+const sem::Vector* build_vec4(MatchState& state, const sem::Type* T) {
+  return build_vec(state, Number(4), T);
+}
+
+bool match_mat(const sem::Type* ty, Number& M, Number& N, const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    M = Number::any;
+    N = Number::any;
+    T = ty;
+    return true;
+  }
+  if (auto* m = ty->As<sem::Matrix>()) {
+    M = m->columns();
+    N = m->ColumnType()->Width();
+    T = m->type();
+    return true;
+  }
+  return false;
+}
+
+const sem::Matrix* build_mat(MatchState& state,
+                             Number N,
+                             Number M,
+                             const sem::Type* T) {
+  auto* column_type = state.builder.create<sem::Vector>(T, M.Value());
+  return state.builder.create<sem::Matrix>(column_type, N.Value());
+}
+
+bool match_array(const sem::Type* ty, const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    T = ty;
+    return true;
+  }
+
+  if (auto* a = ty->As<sem::Array>()) {
+    if (a->Count() == 0) {
+      T = a->ElemType();
+      return true;
+    }
+  }
+  return false;
+}
+
+const sem::Array* build_array(MatchState& state, const sem::Type* el) {
+  return state.builder.create<sem::Array>(el,
+                                          /* count */ 0u,
+                                          /* align */ 0u,
+                                          /* size */ 0u,
+                                          /* stride */ 0u,
+                                          /* stride_implicit */ 0u);
+}
+
+bool match_ptr(const sem::Type* ty, Number& S, const sem::Type*& T, Number& A) {
+  if (ty->Is<Any>()) {
+    S = Number::any;
+    T = ty;
+    A = Number::any;
+    return true;
+  }
+
+  if (auto* p = ty->As<sem::Pointer>()) {
+    S = Number(static_cast<uint32_t>(p->StorageClass()));
+    T = p->StoreType();
+    A = Number(static_cast<uint32_t>(p->Access()));
+    return true;
+  }
+  return false;
+}
+
+const sem::Pointer* build_ptr(MatchState& state,
+                              Number S,
+                              const sem::Type* T,
+                              Number& A) {
+  return state.builder.create<sem::Pointer>(
+      T, static_cast<ast::StorageClass>(S.Value()),
+      static_cast<ast::Access>(A.Value()));
+}
+
+bool match_atomic(const sem::Type* ty, const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    T = ty;
+    return true;
+  }
+
+  if (auto* a = ty->As<sem::Atomic>()) {
+    T = a->Type();
+    return true;
+  }
+  return false;
+}
+
+const sem::Atomic* build_atomic(MatchState& state, const sem::Type* T) {
+  return state.builder.create<sem::Atomic>(T);
+}
+
+bool match_sampler(const sem::Type* ty) {
+  if (ty->Is<Any>()) {
+    return true;
+  }
+  return ty->Is([](const sem::Sampler* s) {
+    return s->kind() == ast::SamplerKind::kSampler;
+  });
+}
+
+const sem::Sampler* build_sampler(MatchState& state) {
+  return state.builder.create<sem::Sampler>(ast::SamplerKind::kSampler);
+}
+
+bool match_sampler_comparison(const sem::Type* ty) {
+  if (ty->Is<Any>()) {
+    return true;
+  }
+  return ty->Is([](const sem::Sampler* s) {
+    return s->kind() == ast::SamplerKind::kComparisonSampler;
+  });
+}
+
+const sem::Sampler* build_sampler_comparison(MatchState& state) {
+  return state.builder.create<sem::Sampler>(
+      ast::SamplerKind::kComparisonSampler);
+}
+
+bool match_texture(const sem::Type* ty,
+                   ast::TextureDimension dim,
+                   const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    T = ty;
+    return true;
+  }
+  if (auto* v = ty->As<sem::SampledTexture>()) {
+    if (v->dim() == dim) {
+      T = v->type();
+      return true;
+    }
+  }
+  return false;
+}
+
+#define JOIN(a, b) a##b
+
+#define DECLARE_SAMPLED_TEXTURE(suffix, dim)                  \
+  bool JOIN(match_texture_, suffix)(const sem::Type* ty,      \
+                                    const sem::Type*& T) {    \
+    return match_texture(ty, dim, T);                         \
+  }                                                           \
+  const sem::SampledTexture* JOIN(build_texture_, suffix)(    \
+      MatchState & state, const sem::Type* T) {               \
+    return state.builder.create<sem::SampledTexture>(dim, T); \
+  }
+
+DECLARE_SAMPLED_TEXTURE(1d, ast::TextureDimension::k1d)
+DECLARE_SAMPLED_TEXTURE(2d, ast::TextureDimension::k2d)
+DECLARE_SAMPLED_TEXTURE(2d_array, ast::TextureDimension::k2dArray)
+DECLARE_SAMPLED_TEXTURE(3d, ast::TextureDimension::k3d)
+DECLARE_SAMPLED_TEXTURE(cube, ast::TextureDimension::kCube)
+DECLARE_SAMPLED_TEXTURE(cube_array, ast::TextureDimension::kCubeArray)
+#undef DECLARE_SAMPLED_TEXTURE
+
+bool match_texture_multisampled(const sem::Type* ty,
+                                ast::TextureDimension dim,
+                                const sem::Type*& T) {
+  if (ty->Is<Any>()) {
+    T = ty;
+    return true;
+  }
+  if (auto* v = ty->As<sem::MultisampledTexture>()) {
+    if (v->dim() == dim) {
+      T = v->type();
+      return true;
+    }
+  }
+  return false;
+}
+
+#define DECLARE_MULTISAMPLED_TEXTURE(suffix, dim)                            \
+  bool JOIN(match_texture_multisampled_, suffix)(const sem::Type* ty,        \
+                                                 const sem::Type*& T) {      \
+    return match_texture_multisampled(ty, dim, T);                           \
+  }                                                                          \
+  const sem::MultisampledTexture* JOIN(build_texture_multisampled_, suffix)( \
+      MatchState & state, const sem::Type* T) {                              \
+    return state.builder.create<sem::MultisampledTexture>(dim, T);           \
+  }
+
+DECLARE_MULTISAMPLED_TEXTURE(2d, ast::TextureDimension::k2d)
+#undef DECLARE_MULTISAMPLED_TEXTURE
+
+bool match_texture_depth(const sem::Type* ty, ast::TextureDimension dim) {
+  if (ty->Is<Any>()) {
+    return true;
+  }
+  return ty->Is([&](const sem::DepthTexture* t) { return t->dim() == dim; });
+}
+
+#define DECLARE_DEPTH_TEXTURE(suffix, dim)                       \
+  bool JOIN(match_texture_depth_, suffix)(const sem::Type* ty) { \
+    return match_texture_depth(ty, dim);                         \
+  }                                                              \
+  const sem::DepthTexture* JOIN(build_texture_depth_,            \
+                                suffix)(MatchState & state) {    \
+    return state.builder.create<sem::DepthTexture>(dim);         \
+  }
+
+DECLARE_DEPTH_TEXTURE(2d, ast::TextureDimension::k2d)
+DECLARE_DEPTH_TEXTURE(2d_array, ast::TextureDimension::k2dArray)
+DECLARE_DEPTH_TEXTURE(cube, ast::TextureDimension::kCube)
+DECLARE_DEPTH_TEXTURE(cube_array, ast::TextureDimension::kCubeArray)
+#undef DECLARE_DEPTH_TEXTURE
+
+bool match_texture_depth_multisampled_2d(const sem::Type* ty) {
+  if (ty->Is<Any>()) {
+    return true;
+  }
+  return ty->Is([&](const sem::DepthMultisampledTexture* t) {
+    return t->dim() == ast::TextureDimension::k2d;
+  });
+}
+
+sem::DepthMultisampledTexture* build_texture_depth_multisampled_2d(
+    MatchState& state) {
+  return state.builder.create<sem::DepthMultisampledTexture>(
+      ast::TextureDimension::k2d);
+}
+
+bool match_texture_storage(const sem::Type* ty,
+                           ast::TextureDimension dim,
+                           Number& F,
+                           Number& A) {
+  if (ty->Is<Any>()) {
+    F = Number::any;
+    A = Number::any;
+    return true;
+  }
+  if (auto* v = ty->As<sem::StorageTexture>()) {
+    if (v->dim() == dim) {
+      F = Number(static_cast<uint32_t>(v->texel_format()));
+      A = Number(static_cast<uint32_t>(v->access()));
+      return true;
+    }
+  }
+  return false;
+}
+
+#define DECLARE_STORAGE_TEXTURE(suffix, dim)                                  \
+  bool JOIN(match_texture_storage_, suffix)(const sem::Type* ty, Number& F,   \
+                                            Number& A) {                      \
+    return match_texture_storage(ty, dim, F, A);                              \
+  }                                                                           \
+  const sem::StorageTexture* JOIN(build_texture_storage_, suffix)(            \
+      MatchState & state, Number F, Number A) {                               \
+    auto format = static_cast<TexelFormat>(F.Value());                        \
+    auto access = static_cast<Access>(A.Value());                             \
+    auto* T = sem::StorageTexture::SubtypeFor(format, state.builder.Types()); \
+    return state.builder.create<sem::StorageTexture>(dim, format, access, T); \
+  }
+
+DECLARE_STORAGE_TEXTURE(1d, ast::TextureDimension::k1d)
+DECLARE_STORAGE_TEXTURE(2d, ast::TextureDimension::k2d)
+DECLARE_STORAGE_TEXTURE(2d_array, ast::TextureDimension::k2dArray)
+DECLARE_STORAGE_TEXTURE(3d, ast::TextureDimension::k3d)
+#undef DECLARE_STORAGE_TEXTURE
+
+bool match_texture_external(const sem::Type* ty) {
+  return ty->IsAnyOf<Any, sem::ExternalTexture>();
+}
+
+const sem::ExternalTexture* build_texture_external(MatchState& state) {
+  return state.builder.create<sem::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.
+bool match_modf_result(const sem::Type* ty) {
+  return ty->Is<Any>();
+}
+bool match_modf_result_vec(const sem::Type* ty, Number& N) {
+  if (!ty->Is<Any>()) {
+    return false;
+  }
+  N = Number::any;
+  return true;
+}
+bool match_frexp_result(const sem::Type* ty) {
+  return ty->Is<Any>();
+}
+bool match_frexp_result_vec(const sem::Type* ty, Number& N) {
+  if (!ty->Is<Any>()) {
+    return false;
+  }
+  N = Number::any;
+  return true;
+}
+
+struct NameAndType {
+  std::string name;
+  sem::Type* type;
+};
+const sem::Struct* build_struct(
+    MatchState& state,
+    std::string name,
+    std::initializer_list<NameAndType> member_names_and_types) {
+  uint32_t offset = 0;
+  uint32_t max_align = 0;
+  sem::StructMemberList members;
+  for (auto& m : member_names_and_types) {
+    uint32_t align = m.type->Align();
+    uint32_t size = m.type->Size();
+    offset = utils::RoundUp(align, offset);
+    max_align = std::max(max_align, align);
+    members.emplace_back(state.builder.create<sem::StructMember>(
+        /* declaration */ nullptr,
+        /* name */ state.builder.Sym(m.name),
+        /* type */ m.type,
+        /* index */ static_cast<uint32_t>(members.size()),
+        /* offset */ offset,
+        /* align */ align,
+        /* size */ size));
+    offset += size;
+  }
+  uint32_t size_without_padding = offset;
+  uint32_t size_with_padding = utils::RoundUp(max_align, offset);
+  return state.builder.create<sem::Struct>(
+      /* declaration */ nullptr,
+      /* name */ state.builder.Sym(name),
+      /* members */ members,
+      /* align */ max_align,
+      /* size */ size_with_padding,
+      /* size_no_padding */ size_without_padding);
+}
+
+const sem::Struct* build_modf_result(MatchState& state) {
+  auto* f32 = state.builder.create<sem::F32>();
+  return build_struct(state, "__modf_result", {{"fract", f32}, {"whole", f32}});
+}
+const sem::Struct* build_modf_result_vec(MatchState& state, Number& n) {
+  auto* vec_f32 = state.builder.create<sem::Vector>(
+      state.builder.create<sem::F32>(), n.Value());
+  return build_struct(state, "__modf_result_vec" + std::to_string(n.Value()),
+                      {{"fract", vec_f32}, {"whole", vec_f32}});
+}
+const sem::Struct* build_frexp_result(MatchState& state) {
+  auto* f32 = state.builder.create<sem::F32>();
+  auto* i32 = state.builder.create<sem::I32>();
+  return build_struct(state, "__frexp_result", {{"sig", f32}, {"exp", i32}});
+}
+const sem::Struct* build_frexp_result_vec(MatchState& state, Number& n) {
+  auto* vec_f32 = state.builder.create<sem::Vector>(
+      state.builder.create<sem::F32>(), n.Value());
+  auto* vec_i32 = state.builder.create<sem::Vector>(
+      state.builder.create<sem::I32>(), n.Value());
+  return build_struct(state, "__frexp_result_vec" + std::to_string(n.Value()),
+                      {{"sig", vec_f32}, {"exp", vec_i32}});
+}
+
+/// ParameterInfo describes a parameter
+struct ParameterInfo {
+  /// The parameter usage (parameter name in definition file)
+  const ParameterUsage usage;
+
+  /// Pointer to a list of indices that are used to match the parameter type.
+  /// The matcher indices index on Matchers::type and / or Matchers::number.
+  /// These indices are consumed by the matchers themselves.
+  /// The first index is always a TypeMatcher.
+  MatcherIndex const* const matcher_indices;
+};
+
+/// OpenTypeInfo describes an open type
+struct OpenTypeInfo {
+  /// Name of the open type (e.g. 'T')
+  const char* name;
+  /// Optional type matcher constraint.
+  /// Either an index in Matchers::type, or kNoMatcher
+  const MatcherIndex matcher_index;
+};
+
+/// OpenNumberInfo describes an open number
+struct OpenNumberInfo {
+  /// Name of the open number (e.g. 'N')
+  const char* name;
+  /// Optional number matcher constraint.
+  /// Either an index in Matchers::number, or kNoMatcher
+  const MatcherIndex matcher_index;
+};
+
+/// OverloadInfo describes a single function overload
+struct OverloadInfo {
+  /// Total number of parameters for the overload
+  const uint8_t num_parameters;
+  /// Total number of open types for the overload
+  const uint8_t num_open_types;
+  /// Total number of open numbers for the overload
+  const uint8_t num_open_numbers;
+  /// Pointer to the first open type
+  OpenTypeInfo const* const open_types;
+  /// Pointer to the first open number
+  OpenNumberInfo const* const open_numbers;
+  /// Pointer to the first parameter
+  ParameterInfo const* const parameters;
+  /// Pointer to a list of matcher indices that index on Matchers::type and
+  /// Matchers::number, used to build the return type. If the function has no
+  /// return type then this is null
+  MatcherIndex const* const return_matcher_indices;
+  /// The pipeline stages that this overload can be used in
+  PipelineStageSet supported_stages;
+  /// True if the overload is marked as deprecated
+  bool is_deprecated;
+};
+
+/// BuiltinInfo describes a builtin function
+struct BuiltinInfo {
+  /// Number of overloads of the builtin function
+  const uint8_t num_overloads;
+  /// Pointer to the start of the overloads for the function
+  OverloadInfo const* const overloads;
+};
+
+#include "builtin_table.inl"
+
+/// BuiltinPrototype describes a fully matched builtin function, which is
+/// used as a lookup for building unique sem::Builtin instances.
+struct BuiltinPrototype {
+  /// Parameter describes a single parameter
+  struct Parameter {
+    /// Parameter type
+    const sem::Type* const type;
+    /// Parameter usage
+    ParameterUsage const usage = ParameterUsage::kNone;
+  };
+
+  /// Hasher provides a hash function for the BuiltinPrototype
+  struct Hasher {
+    /// @param i the BuiltinPrototype to create a hash for
+    /// @return the hash value
+    inline std::size_t operator()(const BuiltinPrototype& i) const {
+      size_t hash = utils::Hash(i.parameters.size());
+      for (auto& p : i.parameters) {
+        utils::HashCombine(&hash, p.type, p.usage);
+      }
+      return utils::Hash(hash, i.type, i.return_type, i.supported_stages,
+                         i.is_deprecated);
+    }
+  };
+
+  sem::BuiltinType type = sem::BuiltinType::kNone;
+  std::vector<Parameter> parameters;
+  sem::Type const* return_type = nullptr;
+  PipelineStageSet supported_stages;
+  bool is_deprecated = false;
+};
+
+/// Equality operator for BuiltinPrototype
+bool operator==(const BuiltinPrototype& a, const BuiltinPrototype& b) {
+  if (a.type != b.type || a.supported_stages != b.supported_stages ||
+      a.return_type != b.return_type || a.is_deprecated != b.is_deprecated ||
+      a.parameters.size() != b.parameters.size()) {
+    return false;
+  }
+  for (size_t i = 0; i < a.parameters.size(); 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 BuiltinTable interface.
+class Impl : public BuiltinTable {
+ public:
+  explicit Impl(ProgramBuilder& builder);
+
+  const sem::Builtin* Lookup(sem::BuiltinType builtin_type,
+                             const std::vector<const sem::Type*>& args,
+                             const Source& source) override;
+
+ private:
+  const sem::Builtin* Match(sem::BuiltinType builtin_type,
+                            const OverloadInfo& overload,
+                            const std::vector<const sem::Type*>& args,
+                            int& match_score);
+
+  MatchState Match(ClosedState& closed,
+                   const OverloadInfo& overload,
+                   MatcherIndex const* matcher_indices) const;
+
+  void PrintOverload(std::ostream& ss,
+                     const OverloadInfo& overload,
+                     sem::BuiltinType builtin_type) const;
+
+  ProgramBuilder& builder;
+  Matchers matchers;
+  std::unordered_map<BuiltinPrototype, sem::Builtin*, BuiltinPrototype::Hasher>
+      builtins;
+};
+
+/// @return a string representing a call to a builtin with the given argument
+/// types.
+std::string CallSignature(ProgramBuilder& builder,
+                          sem::BuiltinType builtin_type,
+                          const std::vector<const sem::Type*>& args) {
+  std::stringstream ss;
+  ss << sem::str(builtin_type) << "(";
+  {
+    bool first = true;
+    for (auto* arg : args) {
+      if (!first) {
+        ss << ", ";
+      }
+      first = false;
+      ss << arg->UnwrapRef()->FriendlyName(builder.Symbols());
+    }
+  }
+  ss << ")";
+
+  return ss.str();
+}
+
+std::string OpenTypeMatcher::String(MatchState& state) const {
+  return state.overload.open_types[index_].name;
+}
+
+std::string OpenNumberMatcher::String(MatchState& state) const {
+  return state.overload.open_numbers[index_].name;
+}
+
+Impl::Impl(ProgramBuilder& b) : builder(b) {}
+
+const sem::Builtin* Impl::Lookup(sem::BuiltinType builtin_type,
+                                 const std::vector<const sem::Type*>& args,
+                                 const Source& source) {
+  // Candidate holds information about a mismatched overload that could be what
+  // the user intended to call.
+  struct Candidate {
+    const OverloadInfo* overload;
+    int score;
+  };
+
+  // The list of failed matches that had promise.
+  std::vector<Candidate> candidates;
+
+  auto& builtin = kBuiltins[static_cast<uint32_t>(builtin_type)];
+  for (uint32_t o = 0; o < builtin.num_overloads; o++) {
+    int match_score = 1000;
+    auto& overload = builtin.overloads[o];
+    if (auto* match = Match(builtin_type, overload, args, match_score)) {
+      return match;
+    }
+    if (match_score > 0) {
+      candidates.emplace_back(Candidate{&overload, match_score});
+    }
+  }
+
+  // Sort the candidates with the most promising first
+  std::stable_sort(
+      candidates.begin(), candidates.end(),
+      [](const Candidate& a, const Candidate& b) { return a.score > b.score; });
+
+  // Generate an error message
+  std::stringstream ss;
+  ss << "no matching call to " << CallSignature(builder, builtin_type, args)
+     << std::endl;
+  if (!candidates.empty()) {
+    ss << std::endl;
+    ss << candidates.size() << " candidate function"
+       << (candidates.size() > 1 ? "s:" : ":") << std::endl;
+    for (auto& candidate : candidates) {
+      ss << "  ";
+      PrintOverload(ss, *candidate.overload, builtin_type);
+      ss << std::endl;
+    }
+  }
+  builder.Diagnostics().add_error(diag::System::Resolver, ss.str(), source);
+  return nullptr;
+}
+
+const sem::Builtin* Impl::Match(sem::BuiltinType builtin_type,
+                                const OverloadInfo& overload,
+                                const std::vector<const sem::Type*>& args,
+                                int& match_score) {
+  // Score wait for argument <-> parameter count matches / mismatches
+  constexpr int kScorePerParamArgMismatch = -1;
+  constexpr int kScorePerMatchedParam = 2;
+  constexpr int kScorePerMatchedOpenType = 1;
+  constexpr int kScorePerMatchedOpenNumber = 1;
+
+  auto num_parameters = overload.num_parameters;
+  auto num_arguments = static_cast<decltype(num_parameters)>(args.size());
+
+  bool overload_matched = true;
+
+  if (num_parameters != num_arguments) {
+    match_score +=
+        kScorePerParamArgMismatch * (std::max(num_parameters, num_arguments) -
+                                     std::min(num_parameters, num_arguments));
+    overload_matched = false;
+  }
+
+  ClosedState closed(builder);
+
+  std::vector<BuiltinPrototype::Parameter> parameters;
+
+  auto num_params = std::min(num_parameters, num_arguments);
+  for (uint32_t p = 0; p < num_params; p++) {
+    auto& parameter = overload.parameters[p];
+    auto* indices = parameter.matcher_indices;
+    auto* type = Match(closed, overload, indices).Type(args[p]->UnwrapRef());
+    if (type) {
+      parameters.emplace_back(
+          BuiltinPrototype::Parameter{type, parameter.usage});
+      match_score += kScorePerMatchedParam;
+    } else {
+      overload_matched = false;
+    }
+  }
+
+  if (overload_matched) {
+    // Check all constrained open types matched
+    for (uint32_t ot = 0; ot < overload.num_open_types; ot++) {
+      auto& open_type = overload.open_types[ot];
+      if (open_type.matcher_index != kNoMatcher) {
+        auto* index = &open_type.matcher_index;
+        if (Match(closed, overload, index).Type(closed.Type(ot))) {
+          match_score += kScorePerMatchedOpenType;
+        } else {
+          overload_matched = false;
+        }
+      }
+    }
+  }
+
+  if (overload_matched) {
+    // Check all constrained open numbers matched
+    for (uint32_t on = 0; on < overload.num_open_numbers; on++) {
+      auto& open_number = overload.open_numbers[on];
+      if (open_number.matcher_index != kNoMatcher) {
+        auto* index = &open_number.matcher_index;
+        if (Match(closed, overload, index).Num(closed.Num(on)).IsValid()) {
+          match_score += kScorePerMatchedOpenNumber;
+        } else {
+          overload_matched = false;
+        }
+      }
+    }
+  }
+
+  if (!overload_matched) {
+    return nullptr;
+  }
+
+  // Build the return type
+  const sem::Type* return_type = nullptr;
+  if (auto* indices = overload.return_matcher_indices) {
+    Any any;
+    return_type = Match(closed, overload, indices).Type(&any);
+    if (!return_type) {
+      std::stringstream ss;
+      PrintOverload(ss, overload, builtin_type);
+      TINT_ICE(Resolver, builder.Diagnostics())
+          << "MatchState.Match() returned null for " << ss.str();
+      return nullptr;
+    }
+  } else {
+    return_type = builder.create<sem::Void>();
+  }
+
+  BuiltinPrototype builtin;
+  builtin.type = builtin_type;
+  builtin.return_type = return_type;
+  builtin.parameters = std::move(parameters);
+  builtin.supported_stages = overload.supported_stages;
+  builtin.is_deprecated = overload.is_deprecated;
+
+  // De-duplicate builtins that are identical.
+  return utils::GetOrCreate(builtins, builtin, [&] {
+    std::vector<sem::Parameter*> params;
+    params.reserve(builtin.parameters.size());
+    for (auto& p : builtin.parameters) {
+      params.emplace_back(builder.create<sem::Parameter>(
+          nullptr, static_cast<uint32_t>(params.size()), p.type,
+          ast::StorageClass::kNone, ast::Access::kUndefined, p.usage));
+    }
+    return builder.create<sem::Builtin>(
+        builtin.type, builtin.return_type, std::move(params),
+        builtin.supported_stages, builtin.is_deprecated);
+  });
+}
+
+MatchState Impl::Match(ClosedState& closed,
+                       const OverloadInfo& overload,
+                       MatcherIndex const* matcher_indices) const {
+  return MatchState(builder, closed, matchers, overload, matcher_indices);
+}
+
+void Impl::PrintOverload(std::ostream& ss,
+                         const OverloadInfo& overload,
+                         sem::BuiltinType builtin_type) const {
+  ClosedState closed(builder);
+
+  ss << builtin_type << "(";
+  for (uint32_t p = 0; p < overload.num_parameters; p++) {
+    auto& parameter = overload.parameters[p];
+    if (p > 0) {
+      ss << ", ";
+    }
+    if (parameter.usage != ParameterUsage::kNone) {
+      ss << sem::str(parameter.usage) << ": ";
+    }
+    auto* indices = parameter.matcher_indices;
+    ss << Match(closed, overload, indices).TypeName();
+  }
+  ss << ")";
+  if (overload.return_matcher_indices) {
+    ss << " -> ";
+    auto* indices = overload.return_matcher_indices;
+    ss << Match(closed, overload, indices).TypeName();
+  }
+
+  bool first = true;
+  auto separator = [&] {
+    ss << (first ? "  where: " : ", ");
+    first = false;
+  };
+  for (uint32_t i = 0; i < overload.num_open_types; i++) {
+    auto& open_type = overload.open_types[i];
+    if (open_type.matcher_index != kNoMatcher) {
+      separator();
+      ss << open_type.name;
+      auto* index = &open_type.matcher_index;
+      ss << " is " << Match(closed, overload, index).TypeName();
+    }
+  }
+  for (uint32_t i = 0; i < overload.num_open_numbers; i++) {
+    auto& open_number = overload.open_numbers[i];
+    if (open_number.matcher_index != kNoMatcher) {
+      separator();
+      ss << open_number.name;
+      auto* index = &open_number.matcher_index;
+      ss << " is " << Match(closed, overload, index).NumName();
+    }
+  }
+}
+
+const sem::Type* MatchState::Type(const sem::Type* ty) {
+  MatcherIndex matcher_index = *matcher_indices_++;
+  auto* matcher = matchers.type[matcher_index];
+  return matcher->Match(*this, ty);
+}
+
+Number MatchState::Num(Number number) {
+  MatcherIndex matcher_index = *matcher_indices_++;
+  auto* matcher = matchers.number[matcher_index];
+  return matcher->Match(*this, number);
+}
+
+std::string MatchState::TypeName() {
+  MatcherIndex matcher_index = *matcher_indices_++;
+  auto* matcher = matchers.type[matcher_index];
+  return matcher->String(*this);
+}
+
+std::string MatchState::NumName() {
+  MatcherIndex matcher_index = *matcher_indices_++;
+  auto* matcher = matchers.number[matcher_index];
+  return matcher->String(*this);
+}
+
+}  // namespace
+
+std::unique_ptr<BuiltinTable> BuiltinTable::Create(ProgramBuilder& builder) {
+  return std::make_unique<Impl>(builder);
+}
+
+BuiltinTable::~BuiltinTable() = default;
+
+/// TypeInfo for the Any type declared in the anonymous namespace above
+TINT_INSTANTIATE_TYPEINFO(Any);
+
+}  // namespace tint
diff --git a/src/tint/builtin_table.h b/src/tint/builtin_table.h
new file mode 100644
index 0000000..246eed2
--- /dev/null
+++ b/src/tint/builtin_table.h
@@ -0,0 +1,52 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_BUILTIN_TABLE_H_
+#define SRC_TINT_BUILTIN_TABLE_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "src/tint/sem/builtin.h"
+
+namespace tint {
+
+// Forward declarations
+class ProgramBuilder;
+
+/// BuiltinTable is a lookup table of all the WGSL builtin functions
+class BuiltinTable {
+ public:
+  /// @param builder the program builder
+  /// @return a pointer to a newly created BuiltinTable
+  static std::unique_ptr<BuiltinTable> Create(ProgramBuilder& builder);
+
+  /// Destructor
+  virtual ~BuiltinTable();
+
+  /// Lookup looks for the builtin overload with the given signature, raising
+  /// an error diagnostic if the builtin was not found.
+  /// @param type the builtin type
+  /// @param args the argument types passed to the builtin function
+  /// @param source the source of the builtin call
+  /// @return the semantic builtin if found, otherwise nullptr
+  virtual const sem::Builtin* Lookup(sem::BuiltinType type,
+                                     const std::vector<const sem::Type*>& args,
+                                     const Source& source) = 0;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_BUILTIN_TABLE_H_
diff --git a/src/tint/builtin_table.inl b/src/tint/builtin_table.inl
new file mode 100644
index 0000000..fa48359
--- /dev/null
+++ b/src/tint/builtin_table.inl
@@ -0,0 +1,9638 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+////////////////////////////////////////////////////////////////////////////////
+// File generated by tools/builtin-gen
+// using the template:
+//   src/tint/builtin_table.inl.tmpl
+// and the builtin defintion file:
+//   src/tint/builtins.def
+//
+// Do not modify this file directly
+////////////////////////////////////////////////////////////////////////////////
+
+// clang-format off
+
+/// TypeMatcher for 'type bool'
+/// @see src/tint/builtins.def:68:6
+class Bool : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Bool::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_bool(ty)) {
+    return nullptr;
+  }
+  return build_bool(state);
+}
+
+std::string Bool::String(MatchState&) const {
+  return "bool";
+}
+
+/// TypeMatcher for 'type f32'
+/// @see src/tint/builtins.def:69:6
+class F32 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* F32::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_f32(ty)) {
+    return nullptr;
+  }
+  return build_f32(state);
+}
+
+std::string F32::String(MatchState&) const {
+  return "f32";
+}
+
+/// TypeMatcher for 'type i32'
+/// @see src/tint/builtins.def:70:6
+class I32 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* I32::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_i32(ty)) {
+    return nullptr;
+  }
+  return build_i32(state);
+}
+
+std::string I32::String(MatchState&) const {
+  return "i32";
+}
+
+/// TypeMatcher for 'type u32'
+/// @see src/tint/builtins.def:71:6
+class U32 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* U32::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_u32(ty)) {
+    return nullptr;
+  }
+  return build_u32(state);
+}
+
+std::string U32::String(MatchState&) const {
+  return "u32";
+}
+
+/// TypeMatcher for 'type vec2'
+/// @see src/tint/builtins.def:72:6
+class Vec2 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Vec2::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_vec2(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_vec2(state, T);
+}
+
+std::string Vec2::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "vec2<" + T + ">";
+}
+
+/// TypeMatcher for 'type vec3'
+/// @see src/tint/builtins.def:73:6
+class Vec3 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Vec3::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_vec3(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_vec3(state, T);
+}
+
+std::string Vec3::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "vec3<" + T + ">";
+}
+
+/// TypeMatcher for 'type vec4'
+/// @see src/tint/builtins.def:74:6
+class Vec4 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Vec4::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_vec4(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_vec4(state, T);
+}
+
+std::string Vec4::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "vec4<" + T + ">";
+}
+
+/// TypeMatcher for 'type vec'
+/// @see src/tint/builtins.def:75:37
+class Vec : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Vec::Match(MatchState& state, const sem::Type* ty) const {
+  Number N = Number::invalid;
+  const sem::Type* T = nullptr;
+  if (!match_vec(ty, N, T)) {
+    return nullptr;
+  }
+  N = state.Num(N);
+  if (!N.IsValid()) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_vec(state, N, T);
+}
+
+std::string Vec::String(MatchState& state) const {
+  const std::string N = state.NumName();
+  const std::string T = state.TypeName();
+  std::stringstream ss;
+  ss << "vec" << N << "<" << T << ">";
+  return ss.str();
+}
+
+/// TypeMatcher for 'type mat'
+/// @see src/tint/builtins.def:76:37
+class Mat : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Mat::Match(MatchState& state, const sem::Type* ty) const {
+  Number N = Number::invalid;
+  Number M = Number::invalid;
+  const sem::Type* T = nullptr;
+  if (!match_mat(ty, N, M, T)) {
+    return nullptr;
+  }
+  N = state.Num(N);
+  if (!N.IsValid()) {
+    return nullptr;
+  }
+  M = state.Num(M);
+  if (!M.IsValid()) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_mat(state, N, M, T);
+}
+
+std::string Mat::String(MatchState& state) const {
+  const std::string N = state.NumName();
+  const std::string M = state.NumName();
+  const std::string T = state.TypeName();
+  std::stringstream ss;
+  ss << "mat" << N << "x" << M << "<" << T << ">";
+  return ss.str();
+}
+
+/// TypeMatcher for 'type ptr'
+/// @see src/tint/builtins.def:77:6
+class Ptr : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Ptr::Match(MatchState& state, const sem::Type* ty) const {
+  Number S = Number::invalid;
+  const sem::Type* T = nullptr;
+  Number A = Number::invalid;
+  if (!match_ptr(ty, S, T, A)) {
+    return nullptr;
+  }
+  S = state.Num(S);
+  if (!S.IsValid()) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  A = state.Num(A);
+  if (!A.IsValid()) {
+    return nullptr;
+  }
+  return build_ptr(state, S, T, A);
+}
+
+std::string Ptr::String(MatchState& state) const {
+  const std::string S = state.NumName();
+  const std::string T = state.TypeName();
+  const std::string A = state.NumName();
+  return "ptr<" + S + ", " + T + ", " + A + ">";
+}
+
+/// TypeMatcher for 'type atomic'
+/// @see src/tint/builtins.def:78:6
+class Atomic : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Atomic::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_atomic(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_atomic(state, T);
+}
+
+std::string Atomic::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "atomic<" + T + ">";
+}
+
+/// TypeMatcher for 'type array'
+/// @see src/tint/builtins.def:79:6
+class Array : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Array::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_array(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_array(state, T);
+}
+
+std::string Array::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "array<" + T + ">";
+}
+
+/// TypeMatcher for 'type sampler'
+/// @see src/tint/builtins.def:80:6
+class Sampler : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Sampler::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_sampler(ty)) {
+    return nullptr;
+  }
+  return build_sampler(state);
+}
+
+std::string Sampler::String(MatchState&) const {
+  return "sampler";
+}
+
+/// TypeMatcher for 'type sampler_comparison'
+/// @see src/tint/builtins.def:81:6
+class SamplerComparison : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* SamplerComparison::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_sampler_comparison(ty)) {
+    return nullptr;
+  }
+  return build_sampler_comparison(state);
+}
+
+std::string SamplerComparison::String(MatchState&) const {
+  return "sampler_comparison";
+}
+
+/// TypeMatcher for 'type texture_1d'
+/// @see src/tint/builtins.def:82:6
+class Texture1D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Texture1D::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_1d(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_1d(state, T);
+}
+
+std::string Texture1D::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_1d<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_2d'
+/// @see src/tint/builtins.def:83:6
+class Texture2D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Texture2D::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_2d(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_2d(state, T);
+}
+
+std::string Texture2D::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_2d<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_2d_array'
+/// @see src/tint/builtins.def:84:6
+class Texture2DArray : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Texture2DArray::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_2d_array(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_2d_array(state, T);
+}
+
+std::string Texture2DArray::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_2d_array<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_3d'
+/// @see src/tint/builtins.def:85:6
+class Texture3D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Texture3D::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_3d(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_3d(state, T);
+}
+
+std::string Texture3D::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_3d<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_cube'
+/// @see src/tint/builtins.def:86:6
+class TextureCube : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureCube::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_cube(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_cube(state, T);
+}
+
+std::string TextureCube::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_cube<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_cube_array'
+/// @see src/tint/builtins.def:87:6
+class TextureCubeArray : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureCubeArray::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_cube_array(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_cube_array(state, T);
+}
+
+std::string TextureCubeArray::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_cube_array<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_multisampled_2d'
+/// @see src/tint/builtins.def:88:6
+class TextureMultisampled2D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureMultisampled2D::Match(MatchState& state, const sem::Type* ty) const {
+  const sem::Type* T = nullptr;
+  if (!match_texture_multisampled_2d(ty, T)) {
+    return nullptr;
+  }
+  T = state.Type(T);
+  if (T == nullptr) {
+    return nullptr;
+  }
+  return build_texture_multisampled_2d(state, T);
+}
+
+std::string TextureMultisampled2D::String(MatchState& state) const {
+  const std::string T = state.TypeName();
+  return "texture_multisampled_2d<" + T + ">";
+}
+
+/// TypeMatcher for 'type texture_depth_2d'
+/// @see src/tint/builtins.def:89:6
+class TextureDepth2D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureDepth2D::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_depth_2d(ty)) {
+    return nullptr;
+  }
+  return build_texture_depth_2d(state);
+}
+
+std::string TextureDepth2D::String(MatchState&) const {
+  return "texture_depth_2d";
+}
+
+/// TypeMatcher for 'type texture_depth_2d_array'
+/// @see src/tint/builtins.def:90:6
+class TextureDepth2DArray : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureDepth2DArray::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_depth_2d_array(ty)) {
+    return nullptr;
+  }
+  return build_texture_depth_2d_array(state);
+}
+
+std::string TextureDepth2DArray::String(MatchState&) const {
+  return "texture_depth_2d_array";
+}
+
+/// TypeMatcher for 'type texture_depth_cube'
+/// @see src/tint/builtins.def:91:6
+class TextureDepthCube : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureDepthCube::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_depth_cube(ty)) {
+    return nullptr;
+  }
+  return build_texture_depth_cube(state);
+}
+
+std::string TextureDepthCube::String(MatchState&) const {
+  return "texture_depth_cube";
+}
+
+/// TypeMatcher for 'type texture_depth_cube_array'
+/// @see src/tint/builtins.def:92:6
+class TextureDepthCubeArray : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureDepthCubeArray::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_depth_cube_array(ty)) {
+    return nullptr;
+  }
+  return build_texture_depth_cube_array(state);
+}
+
+std::string TextureDepthCubeArray::String(MatchState&) const {
+  return "texture_depth_cube_array";
+}
+
+/// TypeMatcher for 'type texture_depth_multisampled_2d'
+/// @see src/tint/builtins.def:93:6
+class TextureDepthMultisampled2D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureDepthMultisampled2D::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_depth_multisampled_2d(ty)) {
+    return nullptr;
+  }
+  return build_texture_depth_multisampled_2d(state);
+}
+
+std::string TextureDepthMultisampled2D::String(MatchState&) const {
+  return "texture_depth_multisampled_2d";
+}
+
+/// TypeMatcher for 'type texture_storage_1d'
+/// @see src/tint/builtins.def:94:6
+class TextureStorage1D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureStorage1D::Match(MatchState& state, const sem::Type* ty) const {
+  Number F = Number::invalid;
+  Number A = Number::invalid;
+  if (!match_texture_storage_1d(ty, F, A)) {
+    return nullptr;
+  }
+  F = state.Num(F);
+  if (!F.IsValid()) {
+    return nullptr;
+  }
+  A = state.Num(A);
+  if (!A.IsValid()) {
+    return nullptr;
+  }
+  return build_texture_storage_1d(state, F, A);
+}
+
+std::string TextureStorage1D::String(MatchState& state) const {
+  const std::string F = state.NumName();
+  const std::string A = state.NumName();
+  return "texture_storage_1d<" + F + ", " + A + ">";
+}
+
+/// TypeMatcher for 'type texture_storage_2d'
+/// @see src/tint/builtins.def:95:6
+class TextureStorage2D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureStorage2D::Match(MatchState& state, const sem::Type* ty) const {
+  Number F = Number::invalid;
+  Number A = Number::invalid;
+  if (!match_texture_storage_2d(ty, F, A)) {
+    return nullptr;
+  }
+  F = state.Num(F);
+  if (!F.IsValid()) {
+    return nullptr;
+  }
+  A = state.Num(A);
+  if (!A.IsValid()) {
+    return nullptr;
+  }
+  return build_texture_storage_2d(state, F, A);
+}
+
+std::string TextureStorage2D::String(MatchState& state) const {
+  const std::string F = state.NumName();
+  const std::string A = state.NumName();
+  return "texture_storage_2d<" + F + ", " + A + ">";
+}
+
+/// TypeMatcher for 'type texture_storage_2d_array'
+/// @see src/tint/builtins.def:96:6
+class TextureStorage2DArray : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureStorage2DArray::Match(MatchState& state, const sem::Type* ty) const {
+  Number F = Number::invalid;
+  Number A = Number::invalid;
+  if (!match_texture_storage_2d_array(ty, F, A)) {
+    return nullptr;
+  }
+  F = state.Num(F);
+  if (!F.IsValid()) {
+    return nullptr;
+  }
+  A = state.Num(A);
+  if (!A.IsValid()) {
+    return nullptr;
+  }
+  return build_texture_storage_2d_array(state, F, A);
+}
+
+std::string TextureStorage2DArray::String(MatchState& state) const {
+  const std::string F = state.NumName();
+  const std::string A = state.NumName();
+  return "texture_storage_2d_array<" + F + ", " + A + ">";
+}
+
+/// TypeMatcher for 'type texture_storage_3d'
+/// @see src/tint/builtins.def:97:6
+class TextureStorage3D : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureStorage3D::Match(MatchState& state, const sem::Type* ty) const {
+  Number F = Number::invalid;
+  Number A = Number::invalid;
+  if (!match_texture_storage_3d(ty, F, A)) {
+    return nullptr;
+  }
+  F = state.Num(F);
+  if (!F.IsValid()) {
+    return nullptr;
+  }
+  A = state.Num(A);
+  if (!A.IsValid()) {
+    return nullptr;
+  }
+  return build_texture_storage_3d(state, F, A);
+}
+
+std::string TextureStorage3D::String(MatchState& state) const {
+  const std::string F = state.NumName();
+  const std::string A = state.NumName();
+  return "texture_storage_3d<" + F + ", " + A + ">";
+}
+
+/// TypeMatcher for 'type texture_external'
+/// @see src/tint/builtins.def:98:6
+class TextureExternal : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* TextureExternal::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_texture_external(ty)) {
+    return nullptr;
+  }
+  return build_texture_external(state);
+}
+
+std::string TextureExternal::String(MatchState&) const {
+  return "texture_external";
+}
+
+/// TypeMatcher for 'type __modf_result'
+/// @see src/tint/builtins.def:100:6
+class ModfResult : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* ModfResult::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_modf_result(ty)) {
+    return nullptr;
+  }
+  return build_modf_result(state);
+}
+
+std::string ModfResult::String(MatchState&) const {
+  return "__modf_result";
+}
+
+/// TypeMatcher for 'type __modf_result_vec'
+/// @see src/tint/builtins.def:101:42
+class ModfResultVec : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* ModfResultVec::Match(MatchState& state, const sem::Type* ty) const {
+  Number N = Number::invalid;
+  if (!match_modf_result_vec(ty, N)) {
+    return nullptr;
+  }
+  N = state.Num(N);
+  if (!N.IsValid()) {
+    return nullptr;
+  }
+  return build_modf_result_vec(state, N);
+}
+
+std::string ModfResultVec::String(MatchState& state) const {
+  const std::string N = state.NumName();
+  std::stringstream ss;
+  ss << "__modf_result_vec" << N;
+  return ss.str();
+}
+
+/// TypeMatcher for 'type __frexp_result'
+/// @see src/tint/builtins.def:102:6
+class FrexpResult : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* FrexpResult::Match(MatchState& state, const sem::Type* ty) const {
+  if (!match_frexp_result(ty)) {
+    return nullptr;
+  }
+  return build_frexp_result(state);
+}
+
+std::string FrexpResult::String(MatchState&) const {
+  return "__frexp_result";
+}
+
+/// TypeMatcher for 'type __frexp_result_vec'
+/// @see src/tint/builtins.def:103:43
+class FrexpResultVec : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* FrexpResultVec::Match(MatchState& state, const sem::Type* ty) const {
+  Number N = Number::invalid;
+  if (!match_frexp_result_vec(ty, N)) {
+    return nullptr;
+  }
+  N = state.Num(N);
+  if (!N.IsValid()) {
+    return nullptr;
+  }
+  return build_frexp_result_vec(state, N);
+}
+
+std::string FrexpResultVec::String(MatchState& state) const {
+  const std::string N = state.NumName();
+  std::stringstream ss;
+  ss << "__frexp_result_vec" << N;
+  return ss.str();
+}
+
+/// TypeMatcher for 'match fiu32'
+/// @see src/tint/builtins.def:111:7
+class Fiu32 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Fiu32::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_f32(ty)) {
+    return build_f32(state);
+  }
+  if (match_i32(ty)) {
+    return build_i32(state);
+  }
+  if (match_u32(ty)) {
+    return build_u32(state);
+  }
+  return nullptr;
+}
+
+std::string Fiu32::String(MatchState&) const {
+  return "f32, i32 or u32";
+}
+
+/// TypeMatcher for 'match iu32'
+/// @see src/tint/builtins.def:112:7
+class Iu32 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Iu32::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_i32(ty)) {
+    return build_i32(state);
+  }
+  if (match_u32(ty)) {
+    return build_u32(state);
+  }
+  return nullptr;
+}
+
+std::string Iu32::String(MatchState&) const {
+  return "i32 or u32";
+}
+
+/// TypeMatcher for 'match scalar'
+/// @see src/tint/builtins.def:113:7
+class Scalar : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* Scalar::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_f32(ty)) {
+    return build_f32(state);
+  }
+  if (match_i32(ty)) {
+    return build_i32(state);
+  }
+  if (match_u32(ty)) {
+    return build_u32(state);
+  }
+  if (match_bool(ty)) {
+    return build_bool(state);
+  }
+  return nullptr;
+}
+
+std::string Scalar::String(MatchState&) const {
+  return "f32, i32, u32 or bool";
+}
+
+/// EnumMatcher for 'match f32_texel_format'
+/// @see src/tint/builtins.def:124:7
+class F32TexelFormat : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number F32TexelFormat::Match(MatchState&, Number number) const {
+  switch (static_cast<TexelFormat>(number.Value())) {
+    case TexelFormat::kRgba8Unorm:
+    case TexelFormat::kRgba8Snorm:
+    case TexelFormat::kRgba16Float:
+    case TexelFormat::kR32Float:
+    case TexelFormat::kRg32Float:
+    case TexelFormat::kRgba32Float:
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+
+std::string F32TexelFormat::String(MatchState&) const {
+  return "rgba8unorm, rgba8snorm, rgba16float, r32float, rg32float or rgba32float";
+}
+
+/// EnumMatcher for 'match i32_texel_format'
+/// @see src/tint/builtins.def:126:7
+class I32TexelFormat : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number I32TexelFormat::Match(MatchState&, Number number) const {
+  switch (static_cast<TexelFormat>(number.Value())) {
+    case TexelFormat::kRgba8Sint:
+    case TexelFormat::kRgba16Sint:
+    case TexelFormat::kR32Sint:
+    case TexelFormat::kRg32Sint:
+    case TexelFormat::kRgba32Sint:
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+
+std::string I32TexelFormat::String(MatchState&) const {
+  return "rgba8sint, rgba16sint, r32sint, rg32sint or rgba32sint";
+}
+
+/// EnumMatcher for 'match u32_texel_format'
+/// @see src/tint/builtins.def:128:7
+class U32TexelFormat : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number U32TexelFormat::Match(MatchState&, Number number) const {
+  switch (static_cast<TexelFormat>(number.Value())) {
+    case TexelFormat::kRgba8Uint:
+    case TexelFormat::kRgba16Uint:
+    case TexelFormat::kR32Uint:
+    case TexelFormat::kRg32Uint:
+    case TexelFormat::kRgba32Uint:
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+
+std::string U32TexelFormat::String(MatchState&) const {
+  return "rgba8uint, rgba16uint, r32uint, rg32uint or rgba32uint";
+}
+
+/// EnumMatcher for 'match write_only'
+/// @see src/tint/builtins.def:131:7
+class WriteOnly : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number WriteOnly::Match(MatchState&, Number number) const {
+  if (number.IsAny() || number.Value() == static_cast<uint32_t>(Access::kWrite)) {
+    return Number(static_cast<uint32_t>(Access::kWrite));
+  }
+  return Number::invalid;
+}
+
+std::string WriteOnly::String(MatchState&) const {
+  return "write";
+}
+
+/// EnumMatcher for 'match function_private_workgroup'
+/// @see src/tint/builtins.def:133:7
+class FunctionPrivateWorkgroup : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number FunctionPrivateWorkgroup::Match(MatchState&, Number number) const {
+  switch (static_cast<StorageClass>(number.Value())) {
+    case StorageClass::kFunction:
+    case StorageClass::kPrivate:
+    case StorageClass::kWorkgroup:
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+
+std::string FunctionPrivateWorkgroup::String(MatchState&) const {
+  return "function, private or workgroup";
+}
+
+/// EnumMatcher for 'match workgroup_or_storage'
+/// @see src/tint/builtins.def:134:7
+class WorkgroupOrStorage : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number WorkgroupOrStorage::Match(MatchState&, Number number) const {
+  switch (static_cast<StorageClass>(number.Value())) {
+    case StorageClass::kWorkgroup:
+    case StorageClass::kStorage:
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+
+std::string WorkgroupOrStorage::String(MatchState&) const {
+  return "workgroup or storage";
+}
+
+/// EnumMatcher for 'match storage'
+class Storage : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number Storage::Match(MatchState&, Number number) const {
+  if (number.IsAny() || number.Value() == static_cast<uint32_t>(StorageClass::kStorage)) {
+    return Number(static_cast<uint32_t>(StorageClass::kStorage));
+  }
+  return Number::invalid;
+}
+
+std::string Storage::String(MatchState&) const {
+  return "storage";
+}
+
+/// EnumMatcher for 'match write'
+class Write : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number Write::Match(MatchState&, Number number) const {
+  if (number.IsAny() || number.Value() == static_cast<uint32_t>(Access::kWrite)) {
+    return Number(static_cast<uint32_t>(Access::kWrite));
+  }
+  return Number::invalid;
+}
+
+std::string Write::String(MatchState&) const {
+  return "write";
+}
+
+/// EnumMatcher for 'match read_write'
+class ReadWrite : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+Number ReadWrite::Match(MatchState&, Number number) const {
+  if (number.IsAny() || number.Value() == static_cast<uint32_t>(Access::kReadWrite)) {
+    return Number(static_cast<uint32_t>(Access::kReadWrite));
+  }
+  return Number::invalid;
+}
+
+std::string ReadWrite::String(MatchState&) const {
+  return "read_write";
+}
+
+/// Matchers holds type and number matchers
+class Matchers {
+ private:
+  OpenTypeMatcher open_type_0_{0};
+  OpenNumberMatcher open_number_0_{0};
+  OpenNumberMatcher open_number_1_{1};
+  Bool Bool_;
+  F32 F32_;
+  I32 I32_;
+  U32 U32_;
+  Vec2 Vec2_;
+  Vec3 Vec3_;
+  Vec4 Vec4_;
+  Vec Vec_;
+  Mat Mat_;
+  Ptr Ptr_;
+  Atomic Atomic_;
+  Array Array_;
+  Sampler Sampler_;
+  SamplerComparison SamplerComparison_;
+  Texture1D Texture1D_;
+  Texture2D Texture2D_;
+  Texture2DArray Texture2DArray_;
+  Texture3D Texture3D_;
+  TextureCube TextureCube_;
+  TextureCubeArray TextureCubeArray_;
+  TextureMultisampled2D TextureMultisampled2D_;
+  TextureDepth2D TextureDepth2D_;
+  TextureDepth2DArray TextureDepth2DArray_;
+  TextureDepthCube TextureDepthCube_;
+  TextureDepthCubeArray TextureDepthCubeArray_;
+  TextureDepthMultisampled2D TextureDepthMultisampled2D_;
+  TextureStorage1D TextureStorage1D_;
+  TextureStorage2D TextureStorage2D_;
+  TextureStorage2DArray TextureStorage2DArray_;
+  TextureStorage3D TextureStorage3D_;
+  TextureExternal TextureExternal_;
+  ModfResult ModfResult_;
+  ModfResultVec ModfResultVec_;
+  FrexpResult FrexpResult_;
+  FrexpResultVec FrexpResultVec_;
+  Fiu32 Fiu32_;
+  Iu32 Iu32_;
+  Scalar Scalar_;
+  F32TexelFormat F32TexelFormat_;
+  I32TexelFormat I32TexelFormat_;
+  U32TexelFormat U32TexelFormat_;
+  WriteOnly WriteOnly_;
+  FunctionPrivateWorkgroup FunctionPrivateWorkgroup_;
+  WorkgroupOrStorage WorkgroupOrStorage_;
+  Storage Storage_;
+  Write Write_;
+  ReadWrite ReadWrite_;
+
+ public:
+  /// Constructor
+  Matchers();
+  /// Destructor
+  ~Matchers();
+
+  /// The open-types, types, and type matchers
+  TypeMatcher const* const type[39] = {
+    /* [0] */ &open_type_0_,
+    /* [1] */ &Bool_,
+    /* [2] */ &F32_,
+    /* [3] */ &I32_,
+    /* [4] */ &U32_,
+    /* [5] */ &Vec2_,
+    /* [6] */ &Vec3_,
+    /* [7] */ &Vec4_,
+    /* [8] */ &Vec_,
+    /* [9] */ &Mat_,
+    /* [10] */ &Ptr_,
+    /* [11] */ &Atomic_,
+    /* [12] */ &Array_,
+    /* [13] */ &Sampler_,
+    /* [14] */ &SamplerComparison_,
+    /* [15] */ &Texture1D_,
+    /* [16] */ &Texture2D_,
+    /* [17] */ &Texture2DArray_,
+    /* [18] */ &Texture3D_,
+    /* [19] */ &TextureCube_,
+    /* [20] */ &TextureCubeArray_,
+    /* [21] */ &TextureMultisampled2D_,
+    /* [22] */ &TextureDepth2D_,
+    /* [23] */ &TextureDepth2DArray_,
+    /* [24] */ &TextureDepthCube_,
+    /* [25] */ &TextureDepthCubeArray_,
+    /* [26] */ &TextureDepthMultisampled2D_,
+    /* [27] */ &TextureStorage1D_,
+    /* [28] */ &TextureStorage2D_,
+    /* [29] */ &TextureStorage2DArray_,
+    /* [30] */ &TextureStorage3D_,
+    /* [31] */ &TextureExternal_,
+    /* [32] */ &ModfResult_,
+    /* [33] */ &ModfResultVec_,
+    /* [34] */ &FrexpResult_,
+    /* [35] */ &FrexpResultVec_,
+    /* [36] */ &Fiu32_,
+    /* [37] */ &Iu32_,
+    /* [38] */ &Scalar_,
+  };
+
+  /// The open-numbers, and number matchers
+  NumberMatcher const* const number[11] = {
+    /* [0] */ &open_number_0_,
+    /* [1] */ &open_number_1_,
+    /* [2] */ &F32TexelFormat_,
+    /* [3] */ &I32TexelFormat_,
+    /* [4] */ &U32TexelFormat_,
+    /* [5] */ &WriteOnly_,
+    /* [6] */ &FunctionPrivateWorkgroup_,
+    /* [7] */ &WorkgroupOrStorage_,
+    /* [8] */ &Storage_,
+    /* [9] */ &Write_,
+    /* [10] */ &ReadWrite_,
+  };
+};
+
+Matchers::Matchers() = default;
+Matchers::~Matchers() = default;
+
+constexpr MatcherIndex kMatcherIndices[] = {
+  /* [0] */ 10,
+  /* [1] */ 0,
+  /* [2] */ 11,
+  /* [3] */ 0,
+  /* [4] */ 10,
+  /* [5] */ 8,
+  /* [6] */ 12,
+  /* [7] */ 0,
+  /* [8] */ 0,
+  /* [9] */ 9,
+  /* [10] */ 1,
+  /* [11] */ 0,
+  /* [12] */ 2,
+  /* [13] */ 9,
+  /* [14] */ 0,
+  /* [15] */ 1,
+  /* [16] */ 2,
+  /* [17] */ 9,
+  /* [18] */ 0,
+  /* [19] */ 0,
+  /* [20] */ 2,
+  /* [21] */ 8,
+  /* [22] */ 0,
+  /* [23] */ 2,
+  /* [24] */ 8,
+  /* [25] */ 0,
+  /* [26] */ 1,
+  /* [27] */ 29,
+  /* [28] */ 0,
+  /* [29] */ 1,
+  /* [30] */ 30,
+  /* [31] */ 0,
+  /* [32] */ 1,
+  /* [33] */ 28,
+  /* [34] */ 0,
+  /* [35] */ 1,
+  /* [36] */ 27,
+  /* [37] */ 0,
+  /* [38] */ 1,
+  /* [39] */ 8,
+  /* [40] */ 0,
+  /* [41] */ 0,
+  /* [42] */ 30,
+  /* [43] */ 4,
+  /* [44] */ 9,
+  /* [45] */ 29,
+  /* [46] */ 4,
+  /* [47] */ 9,
+  /* [48] */ 28,
+  /* [49] */ 4,
+  /* [50] */ 9,
+  /* [51] */ 27,
+  /* [52] */ 4,
+  /* [53] */ 9,
+  /* [54] */ 30,
+  /* [55] */ 3,
+  /* [56] */ 9,
+  /* [57] */ 29,
+  /* [58] */ 3,
+  /* [59] */ 9,
+  /* [60] */ 28,
+  /* [61] */ 3,
+  /* [62] */ 9,
+  /* [63] */ 27,
+  /* [64] */ 3,
+  /* [65] */ 9,
+  /* [66] */ 30,
+  /* [67] */ 2,
+  /* [68] */ 9,
+  /* [69] */ 29,
+  /* [70] */ 2,
+  /* [71] */ 9,
+  /* [72] */ 28,
+  /* [73] */ 2,
+  /* [74] */ 9,
+  /* [75] */ 27,
+  /* [76] */ 2,
+  /* [77] */ 9,
+  /* [78] */ 8,
+  /* [79] */ 0,
+  /* [80] */ 3,
+  /* [81] */ 7,
+  /* [82] */ 2,
+  /* [83] */ 17,
+  /* [84] */ 2,
+  /* [85] */ 5,
+  /* [86] */ 3,
+  /* [87] */ 5,
+  /* [88] */ 2,
+  /* [89] */ 16,
+  /* [90] */ 2,
+  /* [91] */ 6,
+  /* [92] */ 2,
+  /* [93] */ 18,
+  /* [94] */ 2,
+  /* [95] */ 20,
+  /* [96] */ 2,
+  /* [97] */ 19,
+  /* [98] */ 2,
+  /* [99] */ 6,
+  /* [100] */ 3,
+  /* [101] */ 35,
+  /* [102] */ 0,
+  /* [103] */ 33,
+  /* [104] */ 0,
+  /* [105] */ 5,
+  /* [106] */ 0,
+  /* [107] */ 7,
+  /* [108] */ 3,
+  /* [109] */ 7,
+  /* [110] */ 4,
+  /* [111] */ 15,
+  /* [112] */ 0,
+  /* [113] */ 7,
+  /* [114] */ 0,
+  /* [115] */ 16,
+  /* [116] */ 0,
+  /* [117] */ 17,
+  /* [118] */ 0,
+  /* [119] */ 18,
+  /* [120] */ 0,
+  /* [121] */ 21,
+  /* [122] */ 0,
+  /* [123] */ 19,
+  /* [124] */ 0,
+  /* [125] */ 20,
+  /* [126] */ 0,
+  /* [127] */ 15,
+  /* [128] */ 2,
+  /* [129] */ 14,
+  /* [130] */ 24,
+  /* [131] */ 23,
+  /* [132] */ 25,
+  /* [133] */ 22,
+  /* [134] */ 26,
+  /* [135] */ 13,
+  /* [136] */ 31,
+  /* [137] */ 32,
+  /* [138] */ 34,
+};
+
+// Assert that the MatcherIndex is big enough to index all the matchers, plus
+// kNoMatcher.
+static_assert(static_cast<int>(sizeof(kMatcherIndices) / sizeof(kMatcherIndices[0])) <
+              static_cast<int>(std::numeric_limits<MatcherIndex>::max() - 1),
+              "MatcherIndex is not large enough to index kMatcherIndices");
+
+constexpr ParameterInfo kParameters[] = {
+  {
+    /* [0] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [1] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [2] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [3] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [4] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [5] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [6] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [7] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [8] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [9] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [10] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [11] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [12] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [13] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[95],
+  },
+  {
+    /* [14] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [15] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [16] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [17] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [18] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [19] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [20] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [21] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [22] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [23] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [24] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [25] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [26] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [27] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [28] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [29] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [30] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [31] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [32] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [33] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [34] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [35] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [36] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [37] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [38] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [39] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [40] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [41] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [42] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [43] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [44] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [45] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [46] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [47] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [48] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [49] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [50] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [51] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [52] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [53] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [54] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [55] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [56] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [57] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [58] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [59] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [60] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [61] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [62] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [63] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [64] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [65] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [66] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [67] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [68] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [69] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [70] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [71] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [72] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [73] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [74] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [75] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [76] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [77] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [78] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [79] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [80] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [81] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [82] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [83] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [84] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [85] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [86] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [87] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [88] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[95],
+  },
+  {
+    /* [89] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [90] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [91] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [92] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [93] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [94] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [95] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [96] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [97] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [98] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [99] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[125],
+  },
+  {
+    /* [100] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [101] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [102] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [103] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [104] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [105] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [106] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [107] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [108] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [109] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [110] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [111] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [112] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [113] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [114] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [115] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [116] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [117] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [118] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [119] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [120] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [121] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [122] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [123] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [124] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [125] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [126] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [127] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [128] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [129] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [130] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [131] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [132] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [133] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [134] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [135] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [136] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [137] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [138] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [139] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [140] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [141] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [142] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [143] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [144] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [145] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [146] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [147] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [148] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [149] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [150] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [151] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [152] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [153] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [154] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [155] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [156] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [157] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [158] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [159] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [160] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [161] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [162] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [163] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[95],
+  },
+  {
+    /* [164] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [165] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [166] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [167] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [168] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [169] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [170] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [171] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [172] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [173] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [174] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [175] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [176] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [177] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [178] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [179] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [180] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [181] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [182] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [183] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [184] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [185] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [186] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [187] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [188] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [189] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [190] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [191] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [192] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [193] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [194] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [195] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [196] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [197] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [198] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [199] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [200] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [201] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [202] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [203] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [204] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [205] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [206] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [207] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [208] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[97],
+  },
+  {
+    /* [209] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [210] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [211] */
+    /* usage */ ParameterUsage::kDdx,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [212] */
+    /* usage */ ParameterUsage::kDdy,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [213] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [214] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [215] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [216] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [217] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [218] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [219] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[123],
+  },
+  {
+    /* [220] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [221] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [222] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [223] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [224] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [225] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [226] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [227] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [228] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [229] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [230] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[97],
+  },
+  {
+    /* [231] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [232] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [233] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [234] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [235] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [236] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [237] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [238] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[83],
+  },
+  {
+    /* [239] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [240] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [241] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [242] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [243] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [244] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [245] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [246] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [247] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [248] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [249] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [250] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [251] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [252] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [253] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [254] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [255] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [256] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [257] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [258] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [259] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [260] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [261] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [262] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[69],
+  },
+  {
+    /* [263] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [264] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [265] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [266] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [267] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [268] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [269] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [270] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[95],
+  },
+  {
+    /* [271] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [272] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [273] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [274] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [275] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [276] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [277] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [278] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [279] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [280] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [281] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [282] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [283] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [284] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [285] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [286] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [287] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [288] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [289] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [290] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [291] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [292] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [293] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [294] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [295] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[129],
+  },
+  {
+    /* [296] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [297] */
+    /* usage */ ParameterUsage::kDepthRef,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [298] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [299] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [300] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [301] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [302] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [303] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [304] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [305] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [306] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [307] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [308] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [309] */
+    /* usage */ ParameterUsage::kOffset,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [310] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[97],
+  },
+  {
+    /* [311] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [312] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [313] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [314] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[45],
+  },
+  {
+    /* [315] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [316] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [317] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[109],
+  },
+  {
+    /* [318] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [319] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [320] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [321] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [322] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [323] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [324] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [325] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [326] */
+    /* usage */ ParameterUsage::kComponent,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [327] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [328] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [329] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [330] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [331] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [332] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [333] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [334] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [335] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [336] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [337] */
+    /* usage */ ParameterUsage::kBias,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [338] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [339] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [340] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [341] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [342] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [343] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [344] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [345] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [346] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[57],
+  },
+  {
+    /* [347] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [348] */
+    /* usage */ ParameterUsage::kArrayIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [349] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[107],
+  },
+  {
+    /* [350] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [351] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [352] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [353] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [354] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [355] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [356] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [357] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [358] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [359] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [360] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [361] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [362] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [363] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [364] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [365] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [366] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [367] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [368] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[97],
+  },
+  {
+    /* [369] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [370] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [371] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [372] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [373] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [374] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[93],
+  },
+  {
+    /* [375] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [376] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [377] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [378] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [379] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [380] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [381] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [382] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [383] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[136],
+  },
+  {
+    /* [384] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [385] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [386] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[89],
+  },
+  {
+    /* [387] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [388] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [389] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[127],
+  },
+  {
+    /* [390] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [391] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [392] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[75],
+  },
+  {
+    /* [393] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [394] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [395] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [396] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [397] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [398] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [399] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [400] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [401] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [402] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [403] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [404] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[72],
+  },
+  {
+    /* [405] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [406] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [407] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[66],
+  },
+  {
+    /* [408] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [409] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [410] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [411] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [412] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [413] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [414] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [415] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [416] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[63],
+  },
+  {
+    /* [417] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [418] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[107],
+  },
+  {
+    /* [419] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [420] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [421] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [422] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[60],
+  },
+  {
+    /* [423] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [424] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[107],
+  },
+  {
+    /* [425] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[54],
+  },
+  {
+    /* [426] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [427] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[107],
+  },
+  {
+    /* [428] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[51],
+  },
+  {
+    /* [429] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [430] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[109],
+  },
+  {
+    /* [431] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [432] */
+    /* usage */ ParameterUsage::kSampler,
+    /* matcher indices */ &kMatcherIndices[135],
+  },
+  {
+    /* [433] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [434] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[48],
+  },
+  {
+    /* [435] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [436] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[109],
+  },
+  {
+    /* [437] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[42],
+  },
+  {
+    /* [438] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [439] */
+    /* usage */ ParameterUsage::kValue,
+    /* matcher indices */ &kMatcherIndices[109],
+  },
+  {
+    /* [440] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[111],
+  },
+  {
+    /* [441] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [442] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [443] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [444] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [445] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [446] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [447] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [448] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [449] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[119],
+  },
+  {
+    /* [450] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[99],
+  },
+  {
+    /* [451] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [452] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[121],
+  },
+  {
+    /* [453] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [454] */
+    /* usage */ ParameterUsage::kSampleIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [455] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [456] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [457] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [458] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[134],
+  },
+  {
+    /* [459] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [460] */
+    /* usage */ ParameterUsage::kSampleIndex,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [461] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [462] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [463] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[10],
+  },
+  {
+    /* [464] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [465] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [466] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[24],
+  },
+  {
+    /* [467] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [468] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [469] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [470] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [471] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [472] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [473] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [474] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [475] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [476] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [477] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [478] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[10],
+  },
+  {
+    /* [479] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [480] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [481] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [482] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [483] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [484] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [485] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [486] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [487] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [488] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [489] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [490] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [491] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [492] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [493] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [494] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [495] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [496] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [497] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [498] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [499] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [500] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [501] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[136],
+  },
+  {
+    /* [502] */
+    /* usage */ ParameterUsage::kCoords,
+    /* matcher indices */ &kMatcherIndices[85],
+  },
+  {
+    /* [503] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [504] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [505] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [506] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [507] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [508] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [509] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [510] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[91],
+  },
+  {
+    /* [511] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [512] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [513] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [514] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [515] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [516] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [517] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [518] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[78],
+  },
+  {
+    /* [519] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [520] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [521] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [522] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [523] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [524] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [525] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [526] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [527] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [528] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [529] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[111],
+  },
+  {
+    /* [530] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [531] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [532] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [533] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [534] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [535] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [536] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [537] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [538] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [539] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [540] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [541] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[119],
+  },
+  {
+    /* [542] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [543] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [544] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [545] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[123],
+  },
+  {
+    /* [546] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [547] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [548] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [549] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[125],
+  },
+  {
+    /* [550] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [551] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [552] */
+    /* usage */ ParameterUsage::kLevel,
+    /* matcher indices */ &kMatcherIndices[55],
+  },
+  {
+    /* [553] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [554] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [555] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [556] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[121],
+  },
+  {
+    /* [557] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [558] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[125],
+  },
+  {
+    /* [559] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [560] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [561] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[134],
+  },
+  {
+    /* [562] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[36],
+  },
+  {
+    /* [563] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[33],
+  },
+  {
+    /* [564] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[27],
+  },
+  {
+    /* [565] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[30],
+  },
+  {
+    /* [566] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[136],
+  },
+  {
+    /* [567] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [568] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [569] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [570] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[123],
+  },
+  {
+    /* [571] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [572] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [573] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[119],
+  },
+  {
+    /* [574] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [575] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [576] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[81],
+  },
+  {
+    /* [577] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [578] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [579] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[87],
+  },
+  {
+    /* [580] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [581] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [582] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [583] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [584] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[111],
+  },
+  {
+    /* [585] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [586] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[125],
+  },
+  {
+    /* [587] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [588] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [589] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[27],
+  },
+  {
+    /* [590] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[111],
+  },
+  {
+    /* [591] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[115],
+  },
+  {
+    /* [592] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[117],
+  },
+  {
+    /* [593] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[119],
+  },
+  {
+    /* [594] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[123],
+  },
+  {
+    /* [595] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[125],
+  },
+  {
+    /* [596] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[133],
+  },
+  {
+    /* [597] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[131],
+  },
+  {
+    /* [598] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[130],
+  },
+  {
+    /* [599] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[132],
+  },
+  {
+    /* [600] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[121],
+  },
+  {
+    /* [601] */
+    /* usage */ ParameterUsage::kTexture,
+    /* matcher indices */ &kMatcherIndices[134],
+  },
+  {
+    /* [602] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [603] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [604] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [605] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [606] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [607] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [608] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [609] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [610] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [611] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [612] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[43],
+  },
+  {
+    /* [613] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [614] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [615] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [616] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [617] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [618] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [619] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [620] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [621] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [622] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [623] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [624] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [625] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [626] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [627] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [628] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [629] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [630] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [631] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [632] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [633] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [634] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [635] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [636] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [637] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [638] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [639] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [640] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [641] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [642] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [643] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [644] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [645] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [646] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [647] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [648] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [649] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [650] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[17],
+  },
+  {
+    /* [651] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [652] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [653] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[13],
+  },
+  {
+    /* [654] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [655] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [656] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [657] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [658] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [659] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[1],
+  },
+  {
+    /* [660] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [661] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [662] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [663] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [664] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [665] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [666] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [667] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [668] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [669] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [670] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [671] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [672] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[4],
+  },
+  {
+    /* [673] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[24],
+  },
+  {
+    /* [674] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[10],
+  },
+  {
+    /* [675] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[24],
+  },
+  {
+    /* [676] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[10],
+  },
+  {
+    /* [677] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [678] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [679] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[39],
+  },
+  {
+    /* [680] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [681] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[0],
+  },
+  {
+    /* [682] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [683] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [684] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [685] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [686] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+  {
+    /* [687] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [688] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[21],
+  },
+  {
+    /* [689] */
+    /* usage */ ParameterUsage::kNone,
+    /* matcher indices */ &kMatcherIndices[12],
+  },
+};
+
+constexpr OpenTypeInfo kOpenTypes[] = {
+  {
+    /* [0] */
+    /* name */ "T",
+    /* matcher index */ 37,
+  },
+  {
+    /* [1] */
+    /* name */ "T",
+    /* matcher index */ 36,
+  },
+  {
+    /* [2] */
+    /* name */ "T",
+    /* matcher index */ kNoMatcher,
+  },
+  {
+    /* [3] */
+    /* name */ "T",
+    /* matcher index */ 38,
+  },
+};
+
+constexpr OpenNumberInfo kOpenNumbers[] = {
+  {
+    /* [0] */
+    /* name */ "F",
+    /* matcher index */ kNoMatcher,
+  },
+  {
+    /* [1] */
+    /* name */ "A",
+    /* matcher index */ 5,
+  },
+  {
+    /* [2] */
+    /* name */ "M",
+    /* matcher index */ kNoMatcher,
+  },
+  {
+    /* [3] */
+    /* name */ "N",
+    /* matcher index */ kNoMatcher,
+  },
+  {
+    /* [4] */
+    /* name */ "A",
+    /* matcher index */ kNoMatcher,
+  },
+  {
+    /* [5] */
+    /* name */ "S",
+    /* matcher index */ 7,
+  },
+};
+
+constexpr OverloadInfo kOverloads[] = {
+  {
+    /* [0] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[584],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [1] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[529],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [2] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[582],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [3] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[533],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [4] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[581],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [5] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[535],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [6] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[573],
+    /* return matcher indices */ &kMatcherIndices[99],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [7] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[541],
+    /* return matcher indices */ &kMatcherIndices[99],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [8] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[570],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [9] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[545],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [10] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[558],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [11] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[549],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [12] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[556],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [13] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[554],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [14] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[479],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [15] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[555],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [16] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[551],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [17] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[557],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [18] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[547],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [19] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[559],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [20] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[523],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [21] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[561],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [22] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[0],
+    /* parameters */ &kParameters[562],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [23] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[0],
+    /* parameters */ &kParameters[563],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [24] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[0],
+    /* parameters */ &kParameters[564],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [25] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[0],
+    /* parameters */ &kParameters[565],
+    /* return matcher indices */ &kMatcherIndices[99],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [26] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[566],
+    /* return matcher indices */ &kMatcherIndices[85],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [27] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[222],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [28] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[193],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [29] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[213],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [30] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[25],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [31] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[226],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [32] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[173],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [33] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[230],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [34] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[163],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [35] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[234],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [36] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[83],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [37] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[148],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [38] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[55],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [39] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[246],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [40] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[133],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [41] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[383],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [42] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[389],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [43] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[386],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [44] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[250],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [45] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[238],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [46] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[138],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [47] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[374],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [48] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[266],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [49] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[368],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [50] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[270],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [51] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[362],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [52] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[298],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [53] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[302],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [54] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[183],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [55] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[350],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [56] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[338],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [57] */
+    /* num parameters */ 4,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[326],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [58] */
+    /* num parameters */ 5,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[178],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [59] */
+    /* num parameters */ 5,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[168],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [60] */
+    /* num parameters */ 6,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[49],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [61] */
+    /* num parameters */ 4,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[218],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [62] */
+    /* num parameters */ 5,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[98],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [63] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[431],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [64] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[306],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [65] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[290],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [66] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[113],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [67] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[419],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [68] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[286],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [69] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[392],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [70] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[404],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [71] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[262],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [72] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[407],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [73] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[416],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [74] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[422],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [75] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[346],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [76] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[425],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [77] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[428],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [78] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[434],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [79] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[314],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [80] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[437],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [81] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[590],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [82] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[591],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [83] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[592],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [84] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[593],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [85] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[594],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [86] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[595],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [87] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[596],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [88] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[597],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [89] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[598],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [90] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[599],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [91] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[440],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [92] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[446],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [93] */
+    /* num parameters */ 4,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[322],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [94] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[449],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [95] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[452],
+    /* return matcher indices */ &kMatcherIndices[113],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [96] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[455],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [97] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[330],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [98] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[458],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [99] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[501],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [100] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[334],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [101] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[198],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [102] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[188],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [103] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[37],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [104] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[318],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [105] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[108],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [106] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[310],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [107] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[88],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [108] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[158],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [109] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[43],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [110] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[31],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [111] */
+    /* num parameters */ 7,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[0],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [112] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[73],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [113] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[19],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [114] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[208],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [115] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[13],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [116] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[282],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [117] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[143],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [118] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[203],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [119] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[61],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [120] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[254],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [121] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[123],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [122] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[258],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [123] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[118],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [124] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[128],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [125] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[7],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [126] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[242],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [127] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[153],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [128] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[294],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [129] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[78],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [130] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[93],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [131] */
+    /* num parameters */ 6,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[67],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [132] */
+    /* num parameters */ 4,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[278],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [133] */
+    /* num parameters */ 5,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[103],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [134] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[585],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [135] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[586],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [136] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[587],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [137] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[588],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [138] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[0],
+    /* parameters */ &kParameters[589],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [139] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[395],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [140] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[398],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [141] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[401],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [142] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[3],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[476],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [143] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[3],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[461],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [144] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[3],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[464],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [145] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[649],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [146] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[648],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [147] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[652],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [148] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[651],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [149] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[655],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [150] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[654],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [151] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[511],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [152] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[513],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [153] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[657],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [154] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[656],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [155] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[646],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [156] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[645],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [157] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[644],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [158] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[643],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [159] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[642],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [160] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[641],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [161] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[640],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [162] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[639],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [163] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[638],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [164] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[637],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [165] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[636],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [166] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[635],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [167] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[634],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [168] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[633],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [169] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[632],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [170] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[631],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [171] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[377],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [172] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[371],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [173] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[659],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [174] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[658],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [175] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[630],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [176] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[629],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [177] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[628],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [178] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[627],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [179] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[626],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [180] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[625],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [181] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[359],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [182] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[356],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [183] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[624],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [184] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[623],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [185] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[622],
+    /* return matcher indices */ &kMatcherIndices[138],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [186] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[621],
+    /* return matcher indices */ &kMatcherIndices[101],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [187] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[620],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [188] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[619],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [189] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[618],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [190] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[617],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [191] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[616],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [192] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[615],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [193] */
+    /* num parameters */ 4,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[274],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [194] */
+    /* num parameters */ 4,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[342],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [195] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[614],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [196] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[613],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [197] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[515],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [198] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[517],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [199] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[610],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [200] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[609],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [201] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[608],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [202] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[607],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [203] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[606],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [204] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[605],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [205] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[531],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [206] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[519],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [207] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[521],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [208] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[527],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [209] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[661],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [210] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[660],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [211] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[583],
+    /* return matcher indices */ &kMatcherIndices[137],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [212] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[604],
+    /* return matcher indices */ &kMatcherIndices[103],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [213] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[663],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [214] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[662],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [215] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[413],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [216] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[410],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [217] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[665],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [218] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[664],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [219] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[380],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [220] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[473],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [221] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[600],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [222] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[601],
+    /* return matcher indices */ &kMatcherIndices[55],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [223] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[505],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [224] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[507],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [225] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[537],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [226] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[539],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [227] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[572],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [228] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[571],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [229] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[689],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [230] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[668],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [231] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[671],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [232] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[670],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [233] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[569],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [234] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[568],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [235] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[567],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [236] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[574],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [237] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[674],
+    /* return matcher indices */ &kMatcherIndices[10],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [238] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[673],
+    /* return matcher indices */ &kMatcherIndices[10],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [239] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[684],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [240] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[685],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [241] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[686],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [242] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[687],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [243] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[560],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [244] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[688],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [245] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[676],
+    /* return matcher indices */ &kMatcherIndices[10],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [246] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[675],
+    /* return matcher indices */ &kMatcherIndices[10],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [247] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[470],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ true,
+  },
+  {
+    /* [248] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[467],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ true,
+  },
+  {
+    /* [249] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[683],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [250] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[682],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [251] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[495],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [252] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[497],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [253] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[678],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [254] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[677],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [255] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[680],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [256] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[669],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [257] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[667],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [258] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[666],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [259] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[647],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [260] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[679],
+    /* return matcher indices */ &kMatcherIndices[39],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [261] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 2,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[2],
+    /* parameters */ &kParameters[653],
+    /* return matcher indices */ &kMatcherIndices[9],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [262] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[612],
+    /* return matcher indices */ &kMatcherIndices[87],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [263] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[611],
+    /* return matcher indices */ &kMatcherIndices[87],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [264] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[603],
+    /* return matcher indices */ &kMatcherIndices[87],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [265] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[602],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [266] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[553],
+    /* return matcher indices */ &kMatcherIndices[81],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [267] */
+    /* num parameters */ 0,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[690],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [268] */
+    /* num parameters */ 0,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[690],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [269] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[577],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [270] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[443],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [271] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[543],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [272] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[575],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [273] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[576],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [274] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[578],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [275] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[579],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [276] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[580],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [277] */
+    /* num parameters */ 3,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[365],
+    /* return matcher indices */ &kMatcherIndices[21],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [278] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[1],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[503],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [279] */
+    /* num parameters */ 1,
+    /* num open types */ 0,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[3],
+    /* parameters */ &kParameters[650],
+    /* return matcher indices */ &kMatcherIndices[12],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [280] */
+    /* num parameters */ 2,
+    /* num open types */ 0,
+    /* num open numbers */ 0,
+    /* open types */ &kOpenTypes[4],
+    /* open numbers */ &kOpenNumbers[6],
+    /* parameters */ &kParameters[509],
+    /* return matcher indices */ &kMatcherIndices[91],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [281] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[2],
+    /* open numbers */ &kOpenNumbers[4],
+    /* parameters */ &kParameters[672],
+    /* return matcher indices */ &kMatcherIndices[43],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kVertex, PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [282] */
+    /* num parameters */ 1,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[681],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [283] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[493],
+    /* return matcher indices */ nullptr,
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [284] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[491],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [285] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[489],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [286] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[487],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [287] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[485],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [288] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[483],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [289] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[481],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [290] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[525],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [291] */
+    /* num parameters */ 2,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[499],
+    /* return matcher indices */ &kMatcherIndices[1],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+  {
+    /* [292] */
+    /* num parameters */ 3,
+    /* num open types */ 1,
+    /* num open numbers */ 1,
+    /* open types */ &kOpenTypes[0],
+    /* open numbers */ &kOpenNumbers[5],
+    /* parameters */ &kParameters[353],
+    /* return matcher indices */ &kMatcherIndices[105],
+    /* supported_stages */ PipelineStageSet(PipelineStage::kFragment, PipelineStage::kCompute),
+    /* is_deprecated */ false,
+  },
+};
+
+constexpr BuiltinInfo kBuiltins[] = {
+  {
+    /* [0] */
+    /* fn abs<T : fiu32>(T) -> T */
+    /* fn abs<N : num, T : fiu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[259],
+  },
+  {
+    /* [1] */
+    /* fn acos(f32) -> f32 */
+    /* fn acos<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[253],
+  },
+  {
+    /* [2] */
+    /* fn all(bool) -> bool */
+    /* fn all<N : num>(vec<N, bool>) -> bool */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[245],
+  },
+  {
+    /* [3] */
+    /* fn any(bool) -> bool */
+    /* fn any<N : num>(vec<N, bool>) -> bool */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[237],
+  },
+  {
+    /* [4] */
+    /* fn arrayLength<T, A : access>(ptr<storage, array<T>, A>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[281],
+  },
+  {
+    /* [5] */
+    /* fn asin(f32) -> f32 */
+    /* fn asin<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[231],
+  },
+  {
+    /* [6] */
+    /* fn atan(f32) -> f32 */
+    /* fn atan<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[229],
+  },
+  {
+    /* [7] */
+    /* fn atan2(f32, f32) -> f32 */
+    /* fn atan2<N : num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[223],
+  },
+  {
+    /* [8] */
+    /* fn ceil(f32) -> f32 */
+    /* fn ceil<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[217],
+  },
+  {
+    /* [9] */
+    /* fn clamp<T : fiu32>(T, T, T) -> T */
+    /* fn clamp<N : num, T : fiu32>(vec<N, T>, vec<N, T>, vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[215],
+  },
+  {
+    /* [10] */
+    /* fn cos(f32) -> f32 */
+    /* fn cos<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[213],
+  },
+  {
+    /* [11] */
+    /* fn cosh(f32) -> f32 */
+    /* fn cosh<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[209],
+  },
+  {
+    /* [12] */
+    /* fn countLeadingZeros<T : iu32>(T) -> T */
+    /* fn countLeadingZeros<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[173],
+  },
+  {
+    /* [13] */
+    /* fn countOneBits<T : iu32>(T) -> T */
+    /* fn countOneBits<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[153],
+  },
+  {
+    /* [14] */
+    /* fn countTrailingZeros<T : iu32>(T) -> T */
+    /* fn countTrailingZeros<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[149],
+  },
+  {
+    /* [15] */
+    /* fn cross(vec3<f32>, vec3<f32>) -> vec3<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[280],
+  },
+  {
+    /* [16] */
+    /* fn degrees(f32) -> f32 */
+    /* fn degrees<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[147],
+  },
+  {
+    /* [17] */
+    /* fn determinant<N : num>(mat<N, N, f32>) -> f32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[279],
+  },
+  {
+    /* [18] */
+    /* fn distance(f32, f32) -> f32 */
+    /* fn distance<N : num>(vec<N, f32>, vec<N, f32>) -> f32 */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[151],
+  },
+  {
+    /* [19] */
+    /* fn dot<N : num, T : fiu32>(vec<N, T>, vec<N, T>) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[278],
+  },
+  {
+    /* [20] */
+    /* fn dpdx(f32) -> f32 */
+    /* fn dpdx<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[155],
+  },
+  {
+    /* [21] */
+    /* fn dpdxCoarse(f32) -> f32 */
+    /* fn dpdxCoarse<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[157],
+  },
+  {
+    /* [22] */
+    /* fn dpdxFine(f32) -> f32 */
+    /* fn dpdxFine<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[159],
+  },
+  {
+    /* [23] */
+    /* fn dpdy(f32) -> f32 */
+    /* fn dpdy<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[161],
+  },
+  {
+    /* [24] */
+    /* fn dpdyCoarse(f32) -> f32 */
+    /* fn dpdyCoarse<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[163],
+  },
+  {
+    /* [25] */
+    /* fn dpdyFine(f32) -> f32 */
+    /* fn dpdyFine<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[165],
+  },
+  {
+    /* [26] */
+    /* fn exp(f32) -> f32 */
+    /* fn exp<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[167],
+  },
+  {
+    /* [27] */
+    /* fn exp2(f32) -> f32 */
+    /* fn exp2<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[169],
+  },
+  {
+    /* [28] */
+    /* fn extractBits<T : iu32>(T, u32, u32) -> T */
+    /* fn extractBits<N : num, T : iu32>(vec<N, T>, u32, u32) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[171],
+  },
+  {
+    /* [29] */
+    /* fn faceForward<N : num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[277],
+  },
+  {
+    /* [30] */
+    /* fn firstLeadingBit<T : iu32>(T) -> T */
+    /* fn firstLeadingBit<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[175],
+  },
+  {
+    /* [31] */
+    /* fn firstTrailingBit<T : iu32>(T) -> T */
+    /* fn firstTrailingBit<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[177],
+  },
+  {
+    /* [32] */
+    /* fn floor(f32) -> f32 */
+    /* fn floor<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[179],
+  },
+  {
+    /* [33] */
+    /* fn fma(f32, f32, f32) -> f32 */
+    /* fn fma<N : num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[181],
+  },
+  {
+    /* [34] */
+    /* fn fract(f32) -> f32 */
+    /* fn fract<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[183],
+  },
+  {
+    /* [35] */
+    /* fn frexp(f32) -> __frexp_result */
+    /* fn frexp<N : num>(vec<N, f32>) -> __frexp_result_vec<N> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[185],
+  },
+  {
+    /* [36] */
+    /* fn fwidth(f32) -> f32 */
+    /* fn fwidth<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[187],
+  },
+  {
+    /* [37] */
+    /* fn fwidthCoarse(f32) -> f32 */
+    /* fn fwidthCoarse<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[189],
+  },
+  {
+    /* [38] */
+    /* fn fwidthFine(f32) -> f32 */
+    /* fn fwidthFine<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[191],
+  },
+  {
+    /* [39] */
+    /* fn insertBits<T : iu32>(T, T, u32, u32) -> T */
+    /* fn insertBits<N : num, T : iu32>(vec<N, T>, vec<N, T>, u32, u32) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[193],
+  },
+  {
+    /* [40] */
+    /* fn inverseSqrt(f32) -> f32 */
+    /* fn inverseSqrt<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[195],
+  },
+  {
+    /* [41] */
+    /* fn ldexp(f32, i32) -> f32 */
+    /* fn ldexp<N : num>(vec<N, f32>, vec<N, i32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[197],
+  },
+  {
+    /* [42] */
+    /* fn length(f32) -> f32 */
+    /* fn length<N : num>(vec<N, f32>) -> f32 */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[199],
+  },
+  {
+    /* [43] */
+    /* fn log(f32) -> f32 */
+    /* fn log<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[201],
+  },
+  {
+    /* [44] */
+    /* fn log2(f32) -> f32 */
+    /* fn log2<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[203],
+  },
+  {
+    /* [45] */
+    /* fn max<T : fiu32>(T, T) -> T */
+    /* fn max<N : num, T : fiu32>(vec<N, T>, vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[205],
+  },
+  {
+    /* [46] */
+    /* fn min<T : fiu32>(T, T) -> T */
+    /* fn min<N : num, T : fiu32>(vec<N, T>, vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[207],
+  },
+  {
+    /* [47] */
+    /* fn mix(f32, f32, f32) -> f32 */
+    /* fn mix<N : num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* fn mix<N : num>(vec<N, f32>, vec<N, f32>, f32) -> vec<N, f32> */
+    /* num overloads */ 3,
+    /* overloads */ &kOverloads[139],
+  },
+  {
+    /* [48] */
+    /* fn modf(f32) -> __modf_result */
+    /* fn modf<N : num>(vec<N, f32>) -> __modf_result_vec<N> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[211],
+  },
+  {
+    /* [49] */
+    /* fn normalize<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[276],
+  },
+  {
+    /* [50] */
+    /* fn pack2x16float(vec2<f32>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[275],
+  },
+  {
+    /* [51] */
+    /* fn pack2x16snorm(vec2<f32>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[274],
+  },
+  {
+    /* [52] */
+    /* fn pack2x16unorm(vec2<f32>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[269],
+  },
+  {
+    /* [53] */
+    /* fn pack4x8snorm(vec4<f32>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[273],
+  },
+  {
+    /* [54] */
+    /* fn pack4x8unorm(vec4<f32>) -> u32 */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[272],
+  },
+  {
+    /* [55] */
+    /* fn pow(f32, f32) -> f32 */
+    /* fn pow<N : num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[225],
+  },
+  {
+    /* [56] */
+    /* fn radians(f32) -> f32 */
+    /* fn radians<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[227],
+  },
+  {
+    /* [57] */
+    /* fn reflect<N : num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[271],
+  },
+  {
+    /* [58] */
+    /* fn refract<N : num>(vec<N, f32>, vec<N, f32>, f32) -> vec<N, f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[270],
+  },
+  {
+    /* [59] */
+    /* fn reverseBits<T : iu32>(T) -> T */
+    /* fn reverseBits<N : num, T : iu32>(vec<N, T>) -> vec<N, T> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[233],
+  },
+  {
+    /* [60] */
+    /* fn round(f32) -> f32 */
+    /* fn round<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[235],
+  },
+  {
+    /* [61] */
+    /* fn select<T : scalar>(T, T, bool) -> T */
+    /* fn select<T : scalar, N : num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T> */
+    /* fn select<N : num, T : scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T> */
+    /* num overloads */ 3,
+    /* overloads */ &kOverloads[142],
+  },
+  {
+    /* [62] */
+    /* fn sign(f32) -> f32 */
+    /* fn sign<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[239],
+  },
+  {
+    /* [63] */
+    /* fn sin(f32) -> f32 */
+    /* fn sin<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[241],
+  },
+  {
+    /* [64] */
+    /* fn sinh(f32) -> f32 */
+    /* fn sinh<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[243],
+  },
+  {
+    /* [65] */
+    /* fn smoothstep(f32, f32, f32) -> f32 */
+    /* fn smoothstep<N : num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[219],
+  },
+  {
+    /* [66] */
+    /* fn smoothStep(f32, f32, f32) -> f32 */
+    /* fn smoothStep<N : num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[247],
+  },
+  {
+    /* [67] */
+    /* fn sqrt(f32) -> f32 */
+    /* fn sqrt<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[249],
+  },
+  {
+    /* [68] */
+    /* fn step(f32, f32) -> f32 */
+    /* fn step<N : num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[251],
+  },
+  {
+    /* [69] */
+    /* fn storageBarrier() */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[268],
+  },
+  {
+    /* [70] */
+    /* fn tan(f32) -> f32 */
+    /* fn tan<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[255],
+  },
+  {
+    /* [71] */
+    /* fn tanh(f32) -> f32 */
+    /* fn tanh<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[257],
+  },
+  {
+    /* [72] */
+    /* fn transpose<M : num, N : num>(mat<M, N, f32>) -> mat<N, M, f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[261],
+  },
+  {
+    /* [73] */
+    /* fn trunc(f32) -> f32 */
+    /* fn trunc<N : num>(vec<N, f32>) -> vec<N, f32> */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[145],
+  },
+  {
+    /* [74] */
+    /* fn unpack2x16float(u32) -> vec2<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[262],
+  },
+  {
+    /* [75] */
+    /* fn unpack2x16snorm(u32) -> vec2<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[263],
+  },
+  {
+    /* [76] */
+    /* fn unpack2x16unorm(u32) -> vec2<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[264],
+  },
+  {
+    /* [77] */
+    /* fn unpack4x8snorm(u32) -> vec4<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[265],
+  },
+  {
+    /* [78] */
+    /* fn unpack4x8unorm(u32) -> vec4<f32> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[266],
+  },
+  {
+    /* [79] */
+    /* fn workgroupBarrier() */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[267],
+  },
+  {
+    /* [80] */
+    /* fn textureDimensions<T : fiu32>(texture: texture_1d<T>) -> i32 */
+    /* fn textureDimensions<T : fiu32>(texture: texture_1d<T>, level: i32) -> i32 */
+    /* fn textureDimensions<T : fiu32>(texture: texture_2d<T>) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_2d<T>, level: i32) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_2d_array<T>) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_2d_array<T>, level: i32) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_3d<T>) -> vec3<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_3d<T>, level: i32) -> vec3<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_cube<T>) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_cube<T>, level: i32) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_cube_array<T>) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_cube_array<T>, level: i32) -> vec2<i32> */
+    /* fn textureDimensions<T : fiu32>(texture: texture_multisampled_2d<T>) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_2d) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_2d, level: i32) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_2d_array) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_2d_array, level: i32) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_cube) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_cube, level: i32) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_cube_array) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_cube_array, level: i32) -> vec2<i32> */
+    /* fn textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<i32> */
+    /* fn textureDimensions<F : texel_format, A : write_only>(texture: texture_storage_1d<F, A>) -> i32 */
+    /* fn textureDimensions<F : texel_format, A : write_only>(texture: texture_storage_2d<F, A>) -> vec2<i32> */
+    /* fn textureDimensions<F : texel_format, A : write_only>(texture: texture_storage_2d_array<F, A>) -> vec2<i32> */
+    /* fn textureDimensions<F : texel_format, A : write_only>(texture: texture_storage_3d<F, A>) -> vec3<i32> */
+    /* fn textureDimensions(texture: texture_external) -> vec2<i32> */
+    /* num overloads */ 27,
+    /* overloads */ &kOverloads[0],
+  },
+  {
+    /* [81] */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>) -> vec4<T> */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<T> */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_2d_array<T>, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<T> */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_2d_array<T>, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<T> */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_cube<T>, sampler: sampler, coords: vec3<f32>) -> vec4<T> */
+    /* fn textureGather<T : fiu32>(component: i32, texture: texture_cube_array<T>, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<T> */
+    /* fn textureGather(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
+    /* fn textureGather(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureGather(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<f32> */
+    /* fn textureGather(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureGather(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>) -> vec4<f32> */
+    /* fn textureGather(texture: texture_depth_cube_array, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<f32> */
+    /* num overloads */ 12,
+    /* overloads */ &kOverloads[57],
+  },
+  {
+    /* [82] */
+    /* fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> vec4<f32> */
+    /* fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureGatherCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> vec4<f32> */
+    /* fn textureGatherCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureGatherCompare(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> vec4<f32> */
+    /* fn textureGatherCompare(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> vec4<f32> */
+    /* num overloads */ 6,
+    /* overloads */ &kOverloads[116],
+  },
+  {
+    /* [83] */
+    /* fn textureNumLayers<T : fiu32>(texture: texture_2d_array<T>) -> i32 */
+    /* fn textureNumLayers<T : fiu32>(texture: texture_cube_array<T>) -> i32 */
+    /* fn textureNumLayers(texture: texture_depth_2d_array) -> i32 */
+    /* fn textureNumLayers(texture: texture_depth_cube_array) -> i32 */
+    /* fn textureNumLayers<F : texel_format, A : write_only>(texture: texture_storage_2d_array<F, A>) -> i32 */
+    /* num overloads */ 5,
+    /* overloads */ &kOverloads[134],
+  },
+  {
+    /* [84] */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_1d<T>) -> i32 */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_2d<T>) -> i32 */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_2d_array<T>) -> i32 */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_3d<T>) -> i32 */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_cube<T>) -> i32 */
+    /* fn textureNumLevels<T : fiu32>(texture: texture_cube_array<T>) -> i32 */
+    /* fn textureNumLevels(texture: texture_depth_2d) -> i32 */
+    /* fn textureNumLevels(texture: texture_depth_2d_array) -> i32 */
+    /* fn textureNumLevels(texture: texture_depth_cube) -> i32 */
+    /* fn textureNumLevels(texture: texture_depth_cube_array) -> i32 */
+    /* num overloads */ 10,
+    /* overloads */ &kOverloads[81],
+  },
+  {
+    /* [85] */
+    /* fn textureNumSamples<T : fiu32>(texture: texture_multisampled_2d<T>) -> i32 */
+    /* fn textureNumSamples(texture: texture_depth_multisampled_2d) -> i32 */
+    /* num overloads */ 2,
+    /* overloads */ &kOverloads[221],
+  },
+  {
+    /* [86] */
+    /* fn textureSample(texture: texture_1d<f32>, sampler: sampler, coords: f32) -> vec4<f32> */
+    /* fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<f32> */
+    /* fn textureSample(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, offset: vec3<i32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>) -> vec4<f32> */
+    /* fn textureSample(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<f32> */
+    /* fn textureSample(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>) -> f32 */
+    /* fn textureSample(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> f32 */
+    /* fn textureSample(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32) -> f32 */
+    /* fn textureSample(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> f32 */
+    /* fn textureSample(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>) -> f32 */
+    /* fn textureSample(texture: texture_depth_cube_array, sampler: sampler, coords: vec3<f32>, array_index: i32) -> f32 */
+    /* num overloads */ 15,
+    /* overloads */ &kOverloads[42],
+  },
+  {
+    /* [87] */
+    /* fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, bias: f32) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, bias: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, bias: f32, offset: vec3<i32>) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32> */
+    /* fn textureSampleBias(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, bias: f32) -> vec4<f32> */
+    /* num overloads */ 8,
+    /* overloads */ &kOverloads[100],
+  },
+  {
+    /* [88] */
+    /* fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleCompare(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompare(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> f32 */
+    /* num overloads */ 6,
+    /* overloads */ &kOverloads[128],
+  },
+  {
+    /* [89] */
+    /* fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleCompareLevel(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompareLevel(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleCompareLevel(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32 */
+    /* fn textureSampleCompareLevel(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> f32 */
+    /* num overloads */ 6,
+    /* overloads */ &kOverloads[122],
+  },
+  {
+    /* [90] */
+    /* fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>, offset: vec3<i32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32> */
+    /* fn textureSampleGrad(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32> */
+    /* num overloads */ 8,
+    /* overloads */ &kOverloads[108],
+  },
+  {
+    /* [91] */
+    /* fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, level: f32) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, level: f32, offset: vec2<i32>) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, level: f32) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, level: f32, offset: vec3<i32>) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, level: f32) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, level: f32) -> vec4<f32> */
+    /* fn textureSampleLevel(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, level: i32) -> f32 */
+    /* fn textureSampleLevel(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, level: i32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleLevel(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, level: i32) -> f32 */
+    /* fn textureSampleLevel(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, level: i32, offset: vec2<i32>) -> f32 */
+    /* fn textureSampleLevel(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>, level: i32) -> f32 */
+    /* fn textureSampleLevel(texture: texture_depth_cube_array, sampler: sampler, coords: vec3<f32>, array_index: i32, level: i32) -> f32 */
+    /* fn textureSampleLevel(texture: texture_external, sampler: sampler, coords: vec2<f32>) -> vec4<f32> */
+    /* num overloads */ 15,
+    /* overloads */ &kOverloads[27],
+  },
+  {
+    /* [92] */
+    /* fn textureStore(texture: texture_storage_1d<f32_texel_format, write>, coords: i32, value: vec4<f32>) */
+    /* fn textureStore(texture: texture_storage_2d<f32_texel_format, write>, coords: vec2<i32>, value: vec4<f32>) */
+    /* fn textureStore(texture: texture_storage_2d_array<f32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<f32>) */
+    /* fn textureStore(texture: texture_storage_3d<f32_texel_format, write>, coords: vec3<i32>, value: vec4<f32>) */
+    /* fn textureStore(texture: texture_storage_1d<i32_texel_format, write>, coords: i32, value: vec4<i32>) */
+    /* fn textureStore(texture: texture_storage_2d<i32_texel_format, write>, coords: vec2<i32>, value: vec4<i32>) */
+    /* fn textureStore(texture: texture_storage_2d_array<i32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<i32>) */
+    /* fn textureStore(texture: texture_storage_3d<i32_texel_format, write>, coords: vec3<i32>, value: vec4<i32>) */
+    /* fn textureStore(texture: texture_storage_1d<u32_texel_format, write>, coords: i32, value: vec4<u32>) */
+    /* fn textureStore(texture: texture_storage_2d<u32_texel_format, write>, coords: vec2<i32>, value: vec4<u32>) */
+    /* fn textureStore(texture: texture_storage_2d_array<u32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<u32>) */
+    /* fn textureStore(texture: texture_storage_3d<u32_texel_format, write>, coords: vec3<i32>, value: vec4<u32>) */
+    /* num overloads */ 12,
+    /* overloads */ &kOverloads[69],
+  },
+  {
+    /* [93] */
+    /* fn textureLoad<T : fiu32>(texture: texture_1d<T>, coords: i32, level: i32) -> vec4<T> */
+    /* fn textureLoad<T : fiu32>(texture: texture_2d<T>, coords: vec2<i32>, level: i32) -> vec4<T> */
+    /* fn textureLoad<T : fiu32>(texture: texture_2d_array<T>, coords: vec2<i32>, array_index: i32, level: i32) -> vec4<T> */
+    /* fn textureLoad<T : fiu32>(texture: texture_3d<T>, coords: vec3<i32>, level: i32) -> vec4<T> */
+    /* fn textureLoad<T : fiu32>(texture: texture_multisampled_2d<T>, coords: vec2<i32>, sample_index: i32) -> vec4<T> */
+    /* fn textureLoad(texture: texture_depth_2d, coords: vec2<i32>, level: i32) -> f32 */
+    /* fn textureLoad(texture: texture_depth_2d_array, coords: vec2<i32>, array_index: i32, level: i32) -> f32 */
+    /* fn textureLoad(texture: texture_depth_multisampled_2d, coords: vec2<i32>, sample_index: i32) -> f32 */
+    /* fn textureLoad(texture: texture_external, coords: vec2<i32>) -> vec4<f32> */
+    /* num overloads */ 9,
+    /* overloads */ &kOverloads[91],
+  },
+  {
+    /* [94] */
+    /* fn atomicLoad<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[282],
+  },
+  {
+    /* [95] */
+    /* fn atomicStore<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[283],
+  },
+  {
+    /* [96] */
+    /* fn atomicAdd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[284],
+  },
+  {
+    /* [97] */
+    /* fn atomicSub<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[285],
+  },
+  {
+    /* [98] */
+    /* fn atomicMax<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[286],
+  },
+  {
+    /* [99] */
+    /* fn atomicMin<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[287],
+  },
+  {
+    /* [100] */
+    /* fn atomicAnd<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[288],
+  },
+  {
+    /* [101] */
+    /* fn atomicOr<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[289],
+  },
+  {
+    /* [102] */
+    /* fn atomicXor<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[290],
+  },
+  {
+    /* [103] */
+    /* fn atomicExchange<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[291],
+  },
+  {
+    /* [104] */
+    /* fn atomicCompareExchangeWeak<T : iu32, S : workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T, T) -> vec2<T> */
+    /* num overloads */ 1,
+    /* overloads */ &kOverloads[292],
+  },
+};
+
+// clang-format on
diff --git a/src/tint/builtin_table.inl.tmpl b/src/tint/builtin_table.inl.tmpl
new file mode 100644
index 0000000..b7604b8
--- /dev/null
+++ b/src/tint/builtin_table.inl.tmpl
@@ -0,0 +1,401 @@
+{{- /*
+--------------------------------------------------------------------------------
+Template file for use with tools/builtin-gen to generate builtin_table.inl
+Used by BuiltinTable.cc for builtin overload resolution.
+
+See:
+* tools/cmd/builtin-gen/gen for structures used by this template
+* https://golang.org/pkg/text/template/ for documentation on the template syntax
+--------------------------------------------------------------------------------
+*/ -}}
+
+// clang-format off
+
+{{  with .Sem -}}
+{{    range .Types -}}
+{{      template "Type" . }}
+{{    end -}}
+{{    range .TypeMatchers -}}
+{{      template "TypeMatcher" . }}
+{{    end -}}
+{{    range .EnumMatchers -}}
+{{      template "EnumMatcher" . }}
+{{    end -}}
+{{- end -}}
+
+{{- with BuiltinTable -}}
+{{- template "Matchers" . }}
+
+constexpr MatcherIndex kMatcherIndices[] = {
+{{- range $i, $idx := .MatcherIndices }}
+  /* [{{$i}}] */ {{$idx}},
+{{- end }}
+};
+
+// Assert that the MatcherIndex is big enough to index all the matchers, plus
+// kNoMatcher.
+static_assert(static_cast<int>(sizeof(kMatcherIndices) / sizeof(kMatcherIndices[0])) <
+              static_cast<int>(std::numeric_limits<MatcherIndex>::max() - 1),
+              "MatcherIndex is not large enough to index kMatcherIndices");
+
+constexpr ParameterInfo kParameters[] = {
+{{- range $i, $p := .Parameters }}
+  {
+    /* [{{$i}}] */
+    /* usage */ ParameterUsage::
+{{-   if $p.Usage }}k{{PascalCase $p.Usage}}
+{{-   else        }}kNone
+{{-   end         }},
+    /* matcher indices */ &kMatcherIndices[{{$p.MatcherIndicesOffset}}],
+  },
+{{- end }}
+};
+
+constexpr OpenTypeInfo kOpenTypes[] = {
+{{- range $i, $o := .OpenTypes }}
+  {
+    /* [{{$i}}] */
+    /* name */ "{{$o.Name}}",
+    /* matcher index */
+{{-   if ge $o.MatcherIndex 0 }} {{$o.MatcherIndex}}
+{{-   else                    }} kNoMatcher
+{{-   end                     }},
+  },
+{{- end }}
+};
+
+constexpr OpenNumberInfo kOpenNumbers[] = {
+{{- range $i, $o := .OpenNumbers }}
+  {
+    /* [{{$i}}] */
+    /* name */ "{{$o.Name}}",
+    /* matcher index */
+{{-   if ge $o.MatcherIndex 0 }} {{$o.MatcherIndex}}
+{{-   else                    }} kNoMatcher
+{{-   end                     }},
+  },
+{{- end }}
+};
+
+constexpr OverloadInfo kOverloads[] = {
+{{- range $i, $o := .Overloads }}
+  {
+    /* [{{$i}}] */
+    /* num parameters */ {{$o.NumParameters}},
+    /* num open types */ {{$o.NumOpenTypes}},
+    /* num open numbers */ {{$o.NumOpenNumbers}},
+    /* open types */
+{{-   if $o.OpenTypesOffset }} &kOpenTypes[{{$o.OpenTypesOffset}}],
+{{-   else                  }} nullptr,
+{{-   end }}
+    /* open numbers */
+{{-   if $o.OpenNumbersOffset }} &kOpenNumbers[{{$o.OpenNumbersOffset}}]
+{{-   else                    }} nullptr
+{{-   end }},
+    /* parameters */ &kParameters[{{$o.ParametersOffset}}],
+    /* return matcher indices */
+{{-   if $o.ReturnMatcherIndicesOffset }} &kMatcherIndices[{{$o.ReturnMatcherIndicesOffset}}]
+{{-   else                             }} nullptr
+{{-   end }},
+    /* supported_stages */ PipelineStageSet(
+{{-   range $i, $u := $o.CanBeUsedInStage.List -}}
+{{-     if $i -}}, {{end}}PipelineStage::k{{Title $u}}
+{{-   end }}),
+    /* is_deprecated */ {{$o.IsDeprecated}},
+  },
+{{- end }}
+};
+
+constexpr BuiltinInfo kBuiltins[] = {
+{{- range $i, $f := .Functions }}
+  {
+    /* [{{$i}}] */
+{{-   range $f.OverloadDescriptions }}
+    /* {{.}} */
+{{-   end }}
+    /* num overloads */ {{$f.NumOverloads}},
+    /* overloads */ &kOverloads[{{$f.OverloadsOffset}}],
+  },
+{{- end }}
+};
+
+// clang-format on
+{{ end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                              define "Type"                               -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{- $class := PascalCase .Name -}}
+/// TypeMatcher for 'type {{.Name}}'
+{{- if .Decl.Source.S.Filepath  }}
+/// @see {{.Decl.Source}}
+{{- end  }}
+class {{$class}} : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* {{$class}}::Match(MatchState& state, const sem::Type* ty) const {
+{{- range .TemplateParams }}
+{{-   template "DeclareLocalTemplateParam" . }}
+{{- end  }}
+  if (!match_{{TrimLeft .Name "_"}}(ty{{range .TemplateParams}}, {{.GetName}}{{end}})) {
+    return nullptr;
+  }
+{{- range .TemplateParams }}
+  {{.Name}} = {{ template "MatchTemplateParam" .}}({{.Name}});
+  if ({{ template "IsTemplateParamInvalid" .}}) {
+    return nullptr;
+  }
+{{- end  }}
+  return build_{{TrimLeft .Name "_"}}(state{{range .TemplateParams}}, {{.GetName}}{{end}});
+}
+
+std::string {{$class}}::String(MatchState&{{if .TemplateParams}} state{{end}}) const {
+{{- range .TemplateParams }}
+{{-   template "DeclareLocalTemplateParamName" . }}
+{{- end  }}
+
+{{- if .DisplayName }}
+  std::stringstream ss;
+  ss{{range SplitDisplayName .DisplayName}} << {{.}}{{end}};
+  return ss.str();
+{{- else if .TemplateParams }}
+  return "{{.Name}}<"{{template "AppendTemplateParamNames" .TemplateParams}} + ">";
+{{- else }}
+  return "{{.Name}}";
+{{- end  }}
+}
+{{  end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                          define "TypeMatcher"                            -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{- $class := PascalCase .Name -}}
+/// TypeMatcher for 'match {{.Name}}'
+{{- if .Decl.Source.S.Filepath  }}
+/// @see {{.Decl.Source}}
+{{- end  }}
+class {{$class}} : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+const sem::Type* {{$class}}::Match(MatchState& state, const sem::Type* ty) const {
+{{- range .Types }}
+  if (match_{{.Name}}(ty)) {
+    return build_{{.Name}}(state);
+  }
+{{- end }}
+  return nullptr;
+}
+
+std::string {{$class}}::String(MatchState&) const {
+  return "
+{{- range .Types -}}
+{{-   if      IsFirstIn . $.Types }}{{.Name}}
+{{-   else if IsLastIn  . $.Types }} or {{.Name}}
+{{-   else                        }}, {{.Name}}
+{{-   end -}}
+{{- end -}}
+  ";
+}
+{{  end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                          define "EnumMatcher"                            -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{- $class := PascalCase .Name -}}
+{{- $enum := PascalCase .Enum.Name -}}
+/// EnumMatcher for 'match {{.Name}}'
+{{- if .Decl.Source.S.Filepath  }}
+/// @see {{.Decl.Source}}
+{{- end  }}
+class {{$class}} : public NumberMatcher {
+ public:
+  /// Checks whether the given number matches the enum matcher rules.
+  /// Match may close open types and numbers in state.
+  /// @param state the MatchState
+  /// @param number the enum value as a Number
+  /// @return true if the enum value matches the set
+  Number Match(MatchState& state, Number number) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState& state) const override;
+};
+
+{{ if eq 1 (len .Options) -}}
+{{-   $option := index .Options 0 }}
+{{-   $entry := printf "k%v" (PascalCase $option.Name) -}}
+Number {{$class}}::Match(MatchState&, Number number) const {
+  if (number.IsAny() || number.Value() == static_cast<uint32_t>({{$enum}}::{{$entry}})) {
+    return Number(static_cast<uint32_t>({{$enum}}::{{$entry}}));
+  }
+  return Number::invalid;
+}
+{{- else -}}
+Number {{$class}}::Match(MatchState&, Number number) const {
+  switch (static_cast<{{$enum}}>(number.Value())) {
+{{-   range .Options }}
+    case {{$enum}}::k{{PascalCase .Name}}:
+{{-   end }}
+      return number;
+    default:
+      return Number::invalid;
+  }
+}
+{{- end }}
+
+std::string {{$class}}::String(MatchState&) const {
+  return "
+{{- range .Options -}}
+{{-   if      IsFirstIn . $.Options }}{{.Name}}
+{{-   else if IsLastIn  . $.Options }} or {{.Name}}
+{{-   else                          }}, {{.Name}}
+{{-   end -}}
+{{- end -}}
+";
+}
+{{  end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                            define "Matchers"                             -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+/// Matchers holds type and number matchers
+class Matchers {
+ private:
+{{- $t_names := Map -}}
+{{- $n_names := Map -}}
+{{- range Iterate .Sem.MaxOpenTypes -}}
+{{-   $name := printf "open_type_%v" . -}}
+{{-   $t_names.Put . $name }}
+  OpenTypeMatcher {{$name}}_{ {{- . -}} };
+{{- end }}
+{{- range Iterate .Sem.MaxOpenNumbers -}}
+{{-   $name := printf "open_number_%v" . -}}
+{{-   $n_names.Put . $name }}
+  OpenNumberMatcher {{$name}}_{ {{- . -}} };
+{{- end }}
+{{- range .Sem.Types -}}
+{{-   $name := PascalCase .Name -}}
+{{-   $t_names.Put . $name }}
+  {{$name}} {{$name}}_;
+{{- end }}
+{{- range .Sem.TypeMatchers -}}
+{{-   $name := PascalCase .Name -}}
+{{-   $t_names.Put . $name }}
+  {{$name}} {{$name}}_;
+{{- end }}
+{{- range .Sem.EnumMatchers -}}
+{{-   $name := PascalCase .Name -}}
+{{-   $n_names.Put . $name }}
+  {{$name}} {{$name}}_;
+{{- end }}
+
+ public:
+  /// Constructor
+  Matchers();
+  /// Destructor
+  ~Matchers();
+
+  /// The open-types, types, and type matchers
+  TypeMatcher const* const type[{{len .TMatchers}}] = {
+{{- range $i, $m := .TMatchers }}
+    /* [{{$i}}] */
+{{-   if $m }} &{{$t_names.Get $m}}_,
+{{-   else  }} &{{$t_names.Get $i}}_,
+{{-   end   }}
+{{- end }}
+  };
+
+  /// The open-numbers, and number matchers
+  NumberMatcher const* const number[{{len .NMatchers}}] = {
+{{- range $i, $m := .NMatchers }}
+    /* [{{$i}}] */
+{{-   if $m }} &{{$n_names.Get $m}}_,
+{{-   else  }} &{{$n_names.Get $i}}_,
+{{-   end   }}
+{{- end }}
+  };
+};
+
+Matchers::Matchers() = default;
+Matchers::~Matchers() = default;
+{{- end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                     define "DeclareLocalTemplateParam"                   -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-   if      IsTemplateTypeParam . }}
+  const sem::Type* {{.Name}} = nullptr;
+{{-   else if IsTemplateNumberParam . }}
+  Number {{.Name}} = Number::invalid;
+{{-   else if IsTemplateEnumParam . }}
+  Number {{.Name}} = Number::invalid;
+{{-   end -}}
+{{- end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                   define "DeclareLocalTemplateParamName"                 -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-   if      IsTemplateTypeParam . }}
+  const std::string {{.Name}} = state.TypeName();
+{{-   else if IsTemplateNumberParam . }}
+  const std::string {{.Name}} = state.NumName();
+{{-   else if IsTemplateEnumParam . }}
+  const std::string {{.Name}} = state.NumName();
+{{-   end -}}
+{{- end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                       define "MatchTemplateParam"                        -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-   if      IsTemplateTypeParam . -}}
+  state.Type
+{{-   else if IsTemplateNumberParam . -}}
+  state.Num
+{{-   else if IsTemplateEnumParam . -}}
+  state.Num
+{{-   end -}}
+{{- end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                       define "IsTemplateParamInvalid"                    -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-   if      IsTemplateTypeParam . -}}
+  {{.Name}} == nullptr
+{{-   else if IsTemplateNumberParam . -}}
+  !{{.Name}}.IsValid()
+{{-   else if IsTemplateEnumParam . -}}
+  !{{.Name}}.IsValid()
+{{-   end -}}
+{{- end -}}
+
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-                      define "AppendTemplateParamNames"                   -}}
+{{- /* ------------------------------------------------------------------ */ -}}
+{{-   range $i, $ := . -}}
+{{-     if $i }} + ", " + {{.Name}}
+{{-     else }} + {{.Name}}
+{{-     end -}}
+{{-   end -}}
+{{- end -}}
diff --git a/src/tint/builtin_table_test.cc b/src/tint/builtin_table_test.cc
new file mode 100644
index 0000000..b8454d1
--- /dev/null
+++ b/src/tint/builtin_table_test.cc
@@ -0,0 +1,601 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/builtin_table.h"
+
+#include "gmock/gmock.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/atomic_type.h"
+#include "src/tint/sem/depth_multisampled_texture_type.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/external_texture_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/reference_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/storage_texture_type.h"
+
+namespace tint {
+namespace {
+
+using ::testing::HasSubstr;
+
+using BuiltinType = sem::BuiltinType;
+using Parameter = sem::Parameter;
+using ParameterUsage = sem::ParameterUsage;
+
+class BuiltinTableTest : public testing::Test, public ProgramBuilder {
+ public:
+  std::unique_ptr<BuiltinTable> table = BuiltinTable::Create(*this);
+};
+
+TEST_F(BuiltinTableTest, MatchF32) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kCos, {f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kCos);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+}
+
+TEST_F(BuiltinTableTest, MismatchF32) {
+  auto* i32 = create<sem::I32>();
+  auto* result = table->Lookup(BuiltinType::kCos, {i32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchU32) {
+  auto* f32 = create<sem::F32>();
+  auto* u32 = create<sem::U32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* result = table->Lookup(BuiltinType::kUnpack2x16float, {u32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kUnpack2x16float);
+  EXPECT_EQ(result->ReturnType(), vec2_f32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
+}
+
+TEST_F(BuiltinTableTest, MismatchU32) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kUnpack2x16float, {f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchI32) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k1d, f32);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), vec4_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MismatchI32) {
+  auto* f32 = create<sem::F32>();
+  auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k1d, f32);
+  auto* result = table->Lookup(BuiltinType::kTextureLoad, {tex, f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchIU32AsI32) {
+  auto* i32 = create<sem::I32>();
+  auto* result = table->Lookup(BuiltinType::kCountOneBits, {i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kCountOneBits);
+  EXPECT_EQ(result->ReturnType(), i32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), i32);
+}
+
+TEST_F(BuiltinTableTest, MatchIU32AsU32) {
+  auto* u32 = create<sem::U32>();
+  auto* result = table->Lookup(BuiltinType::kCountOneBits, {u32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kCountOneBits);
+  EXPECT_EQ(result->ReturnType(), u32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
+}
+
+TEST_F(BuiltinTableTest, MismatchIU32) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kCountOneBits, {f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchFIU32AsI32) {
+  auto* i32 = create<sem::I32>();
+  auto* result = table->Lookup(BuiltinType::kClamp, {i32, i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kClamp);
+  EXPECT_EQ(result->ReturnType(), i32);
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), i32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), i32);
+}
+
+TEST_F(BuiltinTableTest, MatchFIU32AsU32) {
+  auto* u32 = create<sem::U32>();
+  auto* result = table->Lookup(BuiltinType::kClamp, {u32, u32, u32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kClamp);
+  EXPECT_EQ(result->ReturnType(), u32);
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), u32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), u32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), u32);
+}
+
+TEST_F(BuiltinTableTest, MatchFIU32AsF32) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kClamp, {f32, f32, f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kClamp);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), f32);
+}
+
+TEST_F(BuiltinTableTest, MismatchFIU32) {
+  auto* bool_ = create<sem::Bool>();
+  auto* result =
+      table->Lookup(BuiltinType::kClamp, {bool_, bool_, bool_}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchBool) {
+  auto* f32 = create<sem::F32>();
+  auto* bool_ = create<sem::Bool>();
+  auto* result =
+      table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kSelect);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 3u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), bool_);
+}
+
+TEST_F(BuiltinTableTest, MismatchBool) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kSelect, {f32, f32, f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchPointer) {
+  auto* i32 = create<sem::I32>();
+  auto* atomicI32 = create<sem::Atomic>(i32);
+  auto* ptr = create<sem::Pointer>(atomicI32, ast::StorageClass::kWorkgroup,
+                                   ast::Access::kReadWrite);
+  auto* result = table->Lookup(BuiltinType::kAtomicLoad, {ptr}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kAtomicLoad);
+  EXPECT_EQ(result->ReturnType(), i32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), ptr);
+}
+
+TEST_F(BuiltinTableTest, MismatchPointer) {
+  auto* i32 = create<sem::I32>();
+  auto* atomicI32 = create<sem::Atomic>(i32);
+  auto* result = table->Lookup(BuiltinType::kAtomicLoad, {atomicI32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchArray) {
+  auto* arr = create<sem::Array>(create<sem::U32>(), 0u, 4u, 4u, 4u, 4u);
+  auto* arr_ptr = create<sem::Pointer>(arr, ast::StorageClass::kStorage,
+                                       ast::Access::kReadWrite);
+  auto* result = table->Lookup(BuiltinType::kArrayLength, {arr_ptr}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kArrayLength);
+  EXPECT_TRUE(result->ReturnType()->Is<sem::U32>());
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  auto* param_type = result->Parameters()[0]->Type();
+  ASSERT_TRUE(param_type->Is<sem::Pointer>());
+  EXPECT_TRUE(param_type->As<sem::Pointer>()->StoreType()->Is<sem::Array>());
+}
+
+TEST_F(BuiltinTableTest, MismatchArray) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kArrayLength, {f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchSampler) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
+  auto* sampler = create<sem::Sampler>(ast::SamplerKind::kSampler);
+  auto* result = table->Lookup(BuiltinType::kTextureSample,
+                               {tex, sampler, vec2_f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureSample);
+  EXPECT_EQ(result->ReturnType(), vec4_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MismatchSampler) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
+  auto* result = table->Lookup(BuiltinType::kTextureSample,
+                               {tex, f32, vec2_f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchSampledTexture) {
+  auto* i32 = create<sem::I32>();
+  auto* f32 = create<sem::F32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* tex = create<sem::SampledTexture>(ast::TextureDimension::k2d, f32);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), vec4_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MatchMultisampledTexture) {
+  auto* i32 = create<sem::I32>();
+  auto* f32 = create<sem::F32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* tex = create<sem::MultisampledTexture>(ast::TextureDimension::k2d, f32);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), vec4_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MatchDepthTexture) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* tex = create<sem::DepthTexture>(ast::TextureDimension::k2d);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MatchDepthMultisampledTexture) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* tex = create<sem::DepthMultisampledTexture>(ast::TextureDimension::k2d);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32, i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MatchExternalTexture) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* tex = create<sem::ExternalTexture>();
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {tex, vec2_i32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureLoad);
+  EXPECT_EQ(result->ReturnType(), vec4_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MatchWOStorageTexture) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* vec4_f32 = create<sem::Vector>(f32, 4u);
+  auto* subtype =
+      sem::StorageTexture::SubtypeFor(ast::TexelFormat::kR32Float, Types());
+  auto* tex = create<sem::StorageTexture>(ast::TextureDimension::k2d,
+                                          ast::TexelFormat::kR32Float,
+                                          ast::Access::kWrite, subtype);
+
+  auto* result = table->Lookup(BuiltinType::kTextureStore,
+                               {tex, vec2_i32, vec4_f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kTextureStore);
+  EXPECT_TRUE(result->ReturnType()->Is<sem::Void>());
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MismatchTexture) {
+  auto* f32 = create<sem::F32>();
+  auto* i32 = create<sem::I32>();
+  auto* vec2_i32 = create<sem::Vector>(i32, 2u);
+  auto* result =
+      table->Lookup(BuiltinType::kTextureLoad, {f32, vec2_i32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, ImplicitLoadOnReference) {
+  auto* f32 = create<sem::F32>();
+  auto* result =
+      table->Lookup(BuiltinType::kCos,
+                    {create<sem::Reference>(f32, ast::StorageClass::kFunction,
+                                            ast::Access::kReadWrite)},
+                    Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kCos);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+}
+
+TEST_F(BuiltinTableTest, MatchOpenType) {
+  auto* f32 = create<sem::F32>();
+  auto* result = table->Lookup(BuiltinType::kClamp, {f32, f32, f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kClamp);
+  EXPECT_EQ(result->ReturnType(), f32);
+  EXPECT_EQ(result->Parameters()[0]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[1]->Type(), f32);
+  EXPECT_EQ(result->Parameters()[2]->Type(), f32);
+}
+
+TEST_F(BuiltinTableTest, MismatchOpenType) {
+  auto* f32 = create<sem::F32>();
+  auto* u32 = create<sem::U32>();
+  auto* result = table->Lookup(BuiltinType::kClamp, {f32, u32, f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchOpenSizeVector) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* result = table->Lookup(BuiltinType::kClamp,
+                               {vec2_f32, vec2_f32, vec2_f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kClamp);
+  EXPECT_EQ(result->ReturnType(), vec2_f32);
+  ASSERT_EQ(result->Parameters().size(), 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(BuiltinTableTest, MismatchOpenSizeVector) {
+  auto* f32 = create<sem::F32>();
+  auto* u32 = create<sem::U32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* result =
+      table->Lookup(BuiltinType::kClamp, {vec2_f32, u32, vec2_f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, MatchOpenSizeMatrix) {
+  auto* f32 = create<sem::F32>();
+  auto* vec3_f32 = create<sem::Vector>(f32, 3u);
+  auto* mat3_f32 = create<sem::Matrix>(vec3_f32, 3u);
+  auto* result = table->Lookup(BuiltinType::kDeterminant, {mat3_f32}, Source{});
+  ASSERT_NE(result, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+  EXPECT_EQ(result->Type(), BuiltinType::kDeterminant);
+  EXPECT_EQ(result->ReturnType(), f32);
+  ASSERT_EQ(result->Parameters().size(), 1u);
+  EXPECT_EQ(result->Parameters()[0]->Type(), mat3_f32);
+}
+
+TEST_F(BuiltinTableTest, MismatchOpenSizeMatrix) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(f32, 2u);
+  auto* mat3x2_f32 = create<sem::Matrix>(vec2_f32, 3u);
+  auto* result =
+      table->Lookup(BuiltinType::kDeterminant, {mat3x2_f32}, Source{});
+  ASSERT_EQ(result, nullptr);
+  ASSERT_THAT(Diagnostics().str(), HasSubstr("no matching call"));
+}
+
+TEST_F(BuiltinTableTest, OverloadOrderByNumberOfParameters) {
+  // None of the arguments match, so expect the overloads with 2 parameters to
+  // come first
+  auto* bool_ = create<sem::Bool>();
+  table->Lookup(BuiltinType::kTextureDimensions, {bool_, bool_}, Source{});
+  ASSERT_EQ(Diagnostics().str(),
+            R"(error: no matching call to textureDimensions(bool, bool)
+
+27 candidate functions:
+  textureDimensions(texture: texture_1d<T>, level: i32) -> i32  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d_array<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_3d<T>, level: i32) -> vec3<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube_array<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_depth_2d, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_2d_array, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube_array, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_1d<T>) -> i32  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d_array<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_3d<T>) -> vec3<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube_array<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_depth_2d) -> vec2<i32>
+  textureDimensions(texture: texture_depth_2d_array) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube_array) -> vec2<i32>
+  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<i32>
+  textureDimensions(texture: texture_storage_1d<F, A>) -> i32  where: A is write
+  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<i32>  where: A is write
+  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<i32>  where: A is write
+  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<i32>  where: A is write
+  textureDimensions(texture: texture_external) -> vec2<i32>
+)");
+}
+
+TEST_F(BuiltinTableTest, OverloadOrderByMatchingParameter) {
+  auto* tex = create<sem::DepthTexture>(ast::TextureDimension::k2d);
+  auto* bool_ = create<sem::Bool>();
+  table->Lookup(BuiltinType::kTextureDimensions, {tex, bool_}, Source{});
+  ASSERT_EQ(
+      Diagnostics().str(),
+      R"(error: no matching call to textureDimensions(texture_depth_2d, bool)
+
+27 candidate functions:
+  textureDimensions(texture: texture_depth_2d, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_2d) -> vec2<i32>
+  textureDimensions(texture: texture_1d<T>, level: i32) -> i32  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d_array<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_3d<T>, level: i32) -> vec3<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube_array<T>, level: i32) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_depth_2d_array, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube_array, level: i32) -> vec2<i32>
+  textureDimensions(texture: texture_1d<T>) -> i32  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_2d_array<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_3d<T>) -> vec3<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_cube_array<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_multisampled_2d<T>) -> vec2<i32>  where: T is f32, i32 or u32
+  textureDimensions(texture: texture_depth_2d_array) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube) -> vec2<i32>
+  textureDimensions(texture: texture_depth_cube_array) -> vec2<i32>
+  textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<i32>
+  textureDimensions(texture: texture_storage_1d<F, A>) -> i32  where: A is write
+  textureDimensions(texture: texture_storage_2d<F, A>) -> vec2<i32>  where: A is write
+  textureDimensions(texture: texture_storage_2d_array<F, A>) -> vec2<i32>  where: A is write
+  textureDimensions(texture: texture_storage_3d<F, A>) -> vec3<i32>  where: A is write
+  textureDimensions(texture: texture_external) -> vec2<i32>
+)");
+}
+
+TEST_F(BuiltinTableTest, SameOverloadReturnsSameBuiltinPointer) {
+  auto* f32 = create<sem::F32>();
+  auto* vec2_f32 = create<sem::Vector>(create<sem::F32>(), 2u);
+  auto* bool_ = create<sem::Bool>();
+  auto* a = table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+  ASSERT_NE(a, nullptr) << Diagnostics().str();
+
+  auto* b = table->Lookup(BuiltinType::kSelect, {f32, f32, bool_}, Source{});
+  ASSERT_NE(b, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+
+  auto* c = table->Lookup(BuiltinType::kSelect, {vec2_f32, vec2_f32, bool_},
+                          Source{});
+  ASSERT_NE(c, nullptr) << Diagnostics().str();
+  ASSERT_EQ(Diagnostics().str(), "");
+
+  EXPECT_EQ(a, b);
+  EXPECT_NE(a, c);
+  EXPECT_NE(b, c);
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/builtins.def b/src/tint/builtins.def
new file mode 100644
index 0000000..443634d
--- /dev/null
+++ b/src/tint/builtins.def
@@ -0,0 +1,560 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+////////////////////////////////////////////////////////////////////////////////
+// WGSL builtin definition file                                               //
+//                                                                            //
+// This file is used to generate parts of the Tint BuiltinTable, various      //
+// enum definition files, as well as test .wgsl files.                        //
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// Enumerators                                                                //
+////////////////////////////////////////////////////////////////////////////////
+
+// https://gpuweb.github.io/gpuweb/wgsl/#storage-class
+enum storage_class {
+  function
+  private
+  workgroup
+  uniform
+  storage
+  [[internal]] handle
+}
+
+// https://gpuweb.github.io/gpuweb/wgsl/#memory-access-mode
+enum access {
+  read
+  write
+  read_write
+}
+
+// https://gpuweb.github.io/gpuweb/wgsl/#texel-formats
+enum texel_format {
+  rgba8unorm
+  rgba8snorm
+  rgba8uint
+  rgba8sint
+  rgba16uint
+  rgba16sint
+  rgba16float
+  r32uint
+  r32sint
+  r32float
+  rg32uint
+  rg32sint
+  rg32float
+  rgba32uint
+  rgba32sint
+  rgba32float
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// WGSL primitive types                                                       //
+////////////////////////////////////////////////////////////////////////////////
+
+// https://gpuweb.github.io/gpuweb/wgsl/#plain-types-section
+type bool
+type f32
+type i32
+type u32
+type vec2<T>
+type vec3<T>
+type vec4<T>
+[[display("vec{N}<{T}>")]]     type vec<N: num, T>
+[[display("mat{N}x{M}<{T}>")]] type mat<N: num, M: num, T>
+type ptr<S: storage_class, T, A: access>
+type atomic<T>
+type array<T>
+type sampler
+type sampler_comparison
+type texture_1d<T>
+type texture_2d<T>
+type texture_2d_array<T>
+type texture_3d<T>
+type texture_cube<T>
+type texture_cube_array<T>
+type texture_multisampled_2d<T>
+type texture_depth_2d
+type texture_depth_2d_array
+type texture_depth_cube
+type texture_depth_cube_array
+type texture_depth_multisampled_2d
+type texture_storage_1d<F: texel_format, A: access>
+type texture_storage_2d<F: texel_format, A: access>
+type texture_storage_2d_array<F: texel_format, A: access>
+type texture_storage_3d<F: texel_format, A: access>
+type texture_external
+
+type __modf_result
+[[display("__modf_result_vec{N}")]] type __modf_result_vec<N: num>
+type __frexp_result
+[[display("__frexp_result_vec{N}")]] type __frexp_result_vec<N: num>
+
+////////////////////////////////////////////////////////////////////////////////
+// Type matchers                                                              //
+//                                                                            //
+// A type matcher that can match one or more types.                           //
+////////////////////////////////////////////////////////////////////////////////
+
+match fiu32: f32 | i32 | u32
+match iu32: i32 | u32
+match scalar: f32 | i32 | u32 | bool
+
+////////////////////////////////////////////////////////////////////////////////
+// Enum matchers                                                              //
+//                                                                            //
+// A number matcher that can match one or more enumerator values.             //
+// All enumerator values listed in the match declaration need to be from the  //
+// same enum.                                                                 //
+////////////////////////////////////////////////////////////////////////////////
+
+// https://gpuweb.github.io/gpuweb/wgsl/#texel-formats
+match f32_texel_format:
+  rgba8unorm | rgba8snorm | rgba16float | r32float | rg32float | rgba32float
+match i32_texel_format:
+  rgba8sint | rgba16sint | r32sint | rg32sint | rgba32sint
+match u32_texel_format:
+  rgba8uint | rgba16uint | r32uint | rg32uint | rgba32uint
+
+match write_only: write
+
+match function_private_workgroup: function | private | workgroup
+match workgroup_or_storage: workgroup | storage
+
+////////////////////////////////////////////////////////////////////////////////
+// Builtin Functions                                                          //
+//                                                                            //
+// The builtin function declarations below declare all the built-in           //
+// functions supported by the WGSL language. This builtin definition          //
+// language supports simple static-type function declarations, as well as     //
+// single overload declarations that can match a number of different          //
+// argument types via the use of 'open-types' and 'open-numbers'.             //
+//                                                                            //
+// * Basic example:                                                           //
+//                                                                            //
+//    fn isInf(f32) -> bool                                                   //
+//                                                                            //
+//   Declares an overload of the function 'isInf' that accepts a single       //
+//   parameter of type 'f32' and returns a 'bool'.                            //
+//                                                                            //
+// An 'open-type' can be thought as a template type that is determined by the //
+// arguments to the builtin.                                                  //
+//                                                                            //
+// * Open-type example without constraint:                                    //
+//                                                                            //
+//    fn arrayLength<T>(array<T>) -> u32                                      //
+//                                                                            //
+//    Declares an overload of the function 'arrayLength' that accepts a       //
+//    single argument of an array type with no constraints on the array       //
+//    element type. This overload will always return a value of the same type //
+//    as its single argument.                                                 //
+//                                                                            //
+// * Open-type example with constraint:                                       //
+//                                                                            //
+//    fn abs<T: fiu32>(T) -> T                                                //
+//                                                                            //
+//    Declares an overload of the function 'abs' that accepts a single        //
+//    argument of type 'f32', 'i32' or 'u32', which returns a value of the    //
+//    same argument type.                                                     //
+//                                                                            //
+// Similarly an 'open-number' can be thought as a template number or          //
+// enumerator that is determined by the arguments to the builtin.             //
+//                                                                            //
+// * Open-number example:                                                     //
+//                                                                            //
+//    fn dpdx<N: num>(vec<N, f32>) -> vec<N, f32>                             //
+//                                                                            //
+//    Declares an overload of the function 'dpdx' that accepts a single       //
+//    argument of a variable-sized vector of 'f32', which returns a value of  //
+//    the same argument type.                                                 //
+//                                                                            //
+//                                                                            //
+// Matching algorithm:                                                        //
+// -------------------                                                        //
+//                                                                            //
+// Prior to matching an overload, all open-types are undefined.               //
+//                                                                            //
+// Open-types become closed-types (pinned to a fixed type) on the first       //
+// attempt to match an argument to that open-type.                            //
+// Once open-types are closed, they remain that type for the rest of the      //
+// overload evaluation.                                                       //
+//                                                                            //
+// To better understand, let's consider the following hypothetical overload   //
+// declaration:                                                               //
+//                                                                            //
+//    fn foo<T: scalar>(T, T);                                                //
+//                                                                            //
+//    T           - is the open-type                                          //
+//    scalar      - is a matcher for the types 'f32', 'i32', 'u32' or 'bool'  //
+//                  (declared above)                                          //
+//    <T: scalar> - declares the open-type T, with the constraint that T must //
+//                  match one of 'f32', 'i32', 'u32' or 'bool'.               //
+//                                                                            //
+// The process for resolving this overload is as follows:                     //
+//                                                                            //
+//   (1) The overload resolver begins by attempting to match the argument     //
+//       types from left to right.                                            //
+//       The first parameter type is compared against the argument type.      //
+//       As the open-type T has not been closed yet, T is closed as the type  //
+//       of the first argument.                                               //
+//       There's no verification that the T type is a scalar at this stage.   //
+//   (2) The second parameter is then compared against the second argument.   //
+//       As the open-type T is now closed, the argument type is compared      //
+//       against the value of the closed-type of T. If the types match, then  //
+//       the overload is still a candidate for matching, otherwise the        //
+//       overload is no longer considered.                                    //
+//   (3) If all the parameters matched, constraints on the open-types need    //
+//       to be checked next. If the closed-type does not match the 'match'    //
+//       constraint, then the overload is no longer considered.               //
+//                                                                            //
+// The algorithm for matching open-numbers is almost identical to open-types, //
+// except of course, they match against integer numbers or enumerators        //
+// instead of types.                                                          //
+//                                                                            //
+//                                                                            //
+// * More examples:                                                           //
+//                                                                            //
+//   fn F()                                                                   //
+//     - Function called F.                                                   //
+//       No open types or numbers, no parameters, no return value             //
+//                                                                            //
+//   fn F() -> RETURN_TYPE                                                    //
+//     - Function with RETURN_TYPE as the return type value                   //
+//                                                                            //
+//   fn F(f32, i32)                                                           //
+//     - Two fixed-type, anonymous parameters                                 //
+//                                                                            //
+//   fn F(USAGE : f32)                                                        //
+//     - Single parameter with name USAGE.                                    //
+//       Note: Parameter names are used by Tint to infer parameter order for  //
+//       some builtin functions                                               //
+//                                                                            //
+//   fn F<T>(T)                                                               //
+//     - Single parameter of unconstrained open-type T (any type)             //
+//                                                                            //
+//   fn F<T: scalar>(T)                                                       //
+//     - Single parameter of constrained open-type T (must be a scalar)       //
+//                                                                            //
+//   fn F<T: fiu32>(T) -> T                                                   //
+//     - Single parameter of constrained open-type T (must be a one of fiu32) //
+//       Return type matches parameter type                                   //
+//                                                                            //
+//   fn F<T, N: num>(vec<N, T>)                                               //
+//     - Single parameter of vector type with open-number size N and element  //
+//       open-type T                                                          //
+//                                                                            //
+//   fn F<A: access>(texture_storage_1d<f32_texel_format, A>)                 //
+//     - Single parameter of texture_storage_1d type with open-number         //
+//       access-control C, and of a texel format that is listed in            //
+//       f32_texel_format                                                     //
+//                                                                            //
+////////////////////////////////////////////////////////////////////////////////
+
+// https://gpuweb.github.io/gpuweb/wgsl/#builtin-functions
+fn abs<T: fiu32>(T) -> T
+fn abs<N: num, T: fiu32>(vec<N, T>) -> vec<N, T>
+fn acos(f32) -> f32
+fn acos<N: num>(vec<N, f32>) -> vec<N, f32>
+fn all(bool) -> bool
+fn all<N: num>(vec<N, bool>) -> bool
+fn any(bool) -> bool
+fn any<N: num>(vec<N, bool>) -> bool
+fn arrayLength<T, A: access>(ptr<storage, array<T>, A>) -> u32
+fn asin(f32) -> f32
+fn asin<N: num>(vec<N, f32>) -> vec<N, f32>
+fn atan(f32) -> f32
+fn atan<N: num>(vec<N, f32>) -> vec<N, f32>
+fn atan2(f32, f32) -> f32
+fn atan2<N: num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn ceil(f32) -> f32
+fn ceil<N: num>(vec<N, f32>) -> vec<N, f32>
+fn clamp<T: fiu32>(T, T, T) -> T
+fn clamp<N: num, T: fiu32>(vec<N, T>, vec<N, T>, vec<N, T>) -> vec<N, T>
+fn cos(f32) -> f32
+fn cos<N: num>(vec<N, f32>) -> vec<N, f32>
+fn cosh(f32) -> f32
+fn cosh<N: num>(vec<N, f32>) -> vec<N, f32>
+fn countLeadingZeros<T: iu32>(T) -> T
+fn countLeadingZeros<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn countOneBits<T: iu32>(T) -> T
+fn countOneBits<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn countTrailingZeros<T: iu32>(T) -> T
+fn countTrailingZeros<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn cross(vec3<f32>, vec3<f32>) -> vec3<f32>
+fn degrees(f32) -> f32
+fn degrees<N: num>(vec<N, f32>) -> vec<N, f32>
+fn determinant<N: num>(mat<N, N, f32>) -> f32
+fn distance(f32, f32) -> f32
+fn distance<N: num>(vec<N, f32>, vec<N, f32>) -> f32
+fn dot<N: num, T: fiu32>(vec<N, T>, vec<N, T>) -> T
+[[stage("fragment")]] fn dpdx(f32) -> f32
+[[stage("fragment")]] fn dpdx<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn dpdxCoarse(f32) -> f32
+[[stage("fragment")]] fn dpdxCoarse<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn dpdxFine(f32) -> f32
+[[stage("fragment")]] fn dpdxFine<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn dpdy(f32) -> f32
+[[stage("fragment")]] fn dpdy<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn dpdyCoarse(f32) -> f32
+[[stage("fragment")]] fn dpdyCoarse<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn dpdyFine(f32) -> f32
+[[stage("fragment")]] fn dpdyFine<N: num>(vec<N, f32>) -> vec<N, f32>
+fn exp(f32) -> f32
+fn exp<N: num>(vec<N, f32>) -> vec<N, f32>
+fn exp2(f32) -> f32
+fn exp2<N: num>(vec<N, f32>) -> vec<N, f32>
+fn extractBits<T: iu32>(T, u32, u32) -> T
+fn extractBits<N: num, T: iu32>(vec<N, T>, u32, u32) -> vec<N, T>
+fn faceForward<N: num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn firstLeadingBit<T: iu32>(T) -> T
+fn firstLeadingBit<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn firstTrailingBit<T: iu32>(T) -> T
+fn firstTrailingBit<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn floor(f32) -> f32
+fn floor<N: num>(vec<N, f32>) -> vec<N, f32>
+fn fma(f32, f32, f32) -> f32
+fn fma<N: num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn fract(f32) -> f32
+fn fract<N: num>(vec<N, f32>) -> vec<N, f32>
+fn frexp(f32) -> __frexp_result
+fn frexp<N: num>(vec<N, f32>) -> __frexp_result_vec<N>
+[[stage("fragment")]] fn fwidth(f32) -> f32
+[[stage("fragment")]] fn fwidth<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn fwidthCoarse(f32) -> f32
+[[stage("fragment")]] fn fwidthCoarse<N: num>(vec<N, f32>) -> vec<N, f32>
+[[stage("fragment")]] fn fwidthFine(f32) -> f32
+[[stage("fragment")]] fn fwidthFine<N: num>(vec<N, f32>) -> vec<N, f32>
+fn insertBits<T: iu32>(T, T, u32, u32) -> T
+fn insertBits<N: num, T: iu32>(vec<N, T>, vec<N, T>, u32, u32) -> vec<N, T>
+fn inverseSqrt(f32) -> f32
+fn inverseSqrt<N: num>(vec<N, f32>) -> vec<N, f32>
+fn ldexp(f32, i32) -> f32
+fn ldexp<N: num>(vec<N, f32>, vec<N, i32>) -> vec<N, f32>
+fn length(f32) -> f32
+fn length<N: num>(vec<N, f32>) -> f32
+fn log(f32) -> f32
+fn log<N: num>(vec<N, f32>) -> vec<N, f32>
+fn log2(f32) -> f32
+fn log2<N: num>(vec<N, f32>) -> vec<N, f32>
+fn max<T: fiu32>(T, T) -> T
+fn max<N: num, T: fiu32>(vec<N, T>, vec<N, T>) -> vec<N, T>
+fn min<T: fiu32>(T, T) -> T
+fn min<N: num, T: fiu32>(vec<N, T>, vec<N, T>) -> vec<N, T>
+fn mix(f32, f32, f32) -> f32
+fn mix<N: num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn mix<N: num>(vec<N, f32>, vec<N, f32>, f32) -> vec<N, f32>
+fn modf(f32) -> __modf_result
+fn modf<N: num>(vec<N, f32>) -> __modf_result_vec<N>
+fn normalize<N: num>(vec<N, f32>) -> vec<N, f32>
+fn pack2x16float(vec2<f32>) -> u32
+fn pack2x16snorm(vec2<f32>) -> u32
+fn pack2x16unorm(vec2<f32>) -> u32
+fn pack4x8snorm(vec4<f32>) -> u32
+fn pack4x8unorm(vec4<f32>) -> u32
+fn pow(f32, f32) -> f32
+fn pow<N: num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn radians(f32) -> f32
+fn radians<N: num>(vec<N, f32>) -> vec<N, f32>
+fn reflect<N: num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn refract<N: num>(vec<N, f32>, vec<N, f32>, f32) -> vec<N, f32>
+fn reverseBits<T: iu32>(T) -> T
+fn reverseBits<N: num, T: iu32>(vec<N, T>) -> vec<N, T>
+fn round(f32) -> f32
+fn round<N: num>(vec<N, f32>) -> vec<N, f32>
+fn select<T: scalar>(T, T, bool) -> T
+fn select<T: scalar, N: num>(vec<N, T>, vec<N, T>, bool) -> vec<N, T>
+fn select<N: num, T: scalar>(vec<N, T>, vec<N, T>, vec<N, bool>) -> vec<N, T>
+fn sign(f32) -> f32
+fn sign<N: num>(vec<N, f32>) -> vec<N, f32>
+fn sin(f32) -> f32
+fn sin<N: num>(vec<N, f32>) -> vec<N, f32>
+fn sinh(f32) -> f32
+fn sinh<N: num>(vec<N, f32>) -> vec<N, f32>
+fn smoothstep(f32, f32, f32) -> f32
+fn smoothstep<N: num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+[[deprecated]] fn smoothStep(f32, f32, f32) -> f32
+[[deprecated]] fn smoothStep<N: num>(vec<N, f32>, vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+fn sqrt(f32) -> f32
+fn sqrt<N: num>(vec<N, f32>) -> vec<N, f32>
+fn step(f32, f32) -> f32
+fn step<N: num>(vec<N, f32>, vec<N, f32>) -> vec<N, f32>
+[[stage("compute")]] fn storageBarrier()
+fn tan(f32) -> f32
+fn tan<N: num>(vec<N, f32>) -> vec<N, f32>
+fn tanh(f32) -> f32
+fn tanh<N: num>(vec<N, f32>) -> vec<N, f32>
+fn transpose<M: num, N: num>(mat<M, N, f32>) -> mat<N, M, f32>
+fn trunc(f32) -> f32
+fn trunc<N: num>(vec<N, f32>) -> vec<N, f32>
+fn unpack2x16float(u32) -> vec2<f32>
+fn unpack2x16snorm(u32) -> vec2<f32>
+fn unpack2x16unorm(u32) -> vec2<f32>
+fn unpack4x8snorm(u32) -> vec4<f32>
+fn unpack4x8unorm(u32) -> vec4<f32>
+[[stage("compute")]] fn workgroupBarrier()
+
+fn textureDimensions<T: fiu32>(texture: texture_1d<T>) -> i32
+fn textureDimensions<T: fiu32>(texture: texture_1d<T>, level: i32) -> i32
+fn textureDimensions<T: fiu32>(texture: texture_2d<T>) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_2d<T>, level: i32) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_2d_array<T>) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_2d_array<T>, level: i32) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_3d<T>) -> vec3<i32>
+fn textureDimensions<T: fiu32>(texture: texture_3d<T>, level: i32) -> vec3<i32>
+fn textureDimensions<T: fiu32>(texture: texture_cube<T>) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_cube<T>, level: i32) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_cube_array<T>) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_cube_array<T>, level: i32) -> vec2<i32>
+fn textureDimensions<T: fiu32>(texture: texture_multisampled_2d<T>) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_2d) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_2d, level: i32) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_2d_array) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_2d_array, level: i32) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_cube) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_cube, level: i32) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_cube_array) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_cube_array, level: i32) -> vec2<i32>
+fn textureDimensions(texture: texture_depth_multisampled_2d) -> vec2<i32>
+fn textureDimensions<F: texel_format, A: write_only>(texture: texture_storage_1d<F, A>) -> i32
+fn textureDimensions<F: texel_format, A: write_only>(texture: texture_storage_2d<F, A>) -> vec2<i32>
+fn textureDimensions<F: texel_format, A: write_only>(texture: texture_storage_2d_array<F, A>) -> vec2<i32>
+fn textureDimensions<F: texel_format, A: write_only>(texture: texture_storage_3d<F, A>) -> vec3<i32>
+fn textureDimensions(texture: texture_external) -> vec2<i32>
+fn textureGather<T: fiu32>(component: i32, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>) -> vec4<T>
+fn textureGather<T: fiu32>(component: i32, texture: texture_2d<T>, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<T>
+fn textureGather<T: fiu32>(component: i32, texture: texture_2d_array<T>, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<T>
+fn textureGather<T: fiu32>(component: i32, texture: texture_2d_array<T>, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<T>
+fn textureGather<T: fiu32>(component: i32, texture: texture_cube<T>, sampler: sampler, coords: vec3<f32>) -> vec4<T>
+fn textureGather<T: fiu32>(component: i32, texture: texture_cube_array<T>, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<T>
+fn textureGather(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>) -> vec4<f32>
+fn textureGather(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+fn textureGather(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<f32>
+fn textureGather(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<f32>
+fn textureGather(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>) -> vec4<f32>
+fn textureGather(texture: texture_depth_cube_array, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> vec4<f32>
+fn textureGatherCompare(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> vec4<f32>
+fn textureNumLayers<T: fiu32>(texture: texture_2d_array<T>) -> i32
+fn textureNumLayers<T: fiu32>(texture: texture_cube_array<T>) -> i32
+fn textureNumLayers(texture: texture_depth_2d_array) -> i32
+fn textureNumLayers(texture: texture_depth_cube_array) -> i32
+fn textureNumLayers<F: texel_format, A: write_only>(texture: texture_storage_2d_array<F, A>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_1d<T>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_2d<T>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_2d_array<T>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_3d<T>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_cube<T>) -> i32
+fn textureNumLevels<T: fiu32>(texture: texture_cube_array<T>) -> i32
+fn textureNumLevels(texture: texture_depth_2d) -> i32
+fn textureNumLevels(texture: texture_depth_2d_array) -> i32
+fn textureNumLevels(texture: texture_depth_cube) -> i32
+fn textureNumLevels(texture: texture_depth_cube_array) -> i32
+fn textureNumSamples<T: fiu32>(texture: texture_multisampled_2d<T>) -> i32
+fn textureNumSamples(texture: texture_depth_multisampled_2d) -> i32
+[[stage("fragment")]] fn textureSample(texture: texture_1d<f32>, sampler: sampler, coords: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, offset: vec3<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32) -> vec4<f32>
+[[stage("fragment")]] fn textureSample(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>) -> f32
+[[stage("fragment")]] fn textureSample(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, offset: vec2<i32>) -> f32
+[[stage("fragment")]] fn textureSample(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32) -> f32
+[[stage("fragment")]] fn textureSample(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, offset: vec2<i32>) -> f32
+[[stage("fragment")]] fn textureSample(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>) -> f32
+[[stage("fragment")]] fn textureSample(texture: texture_depth_cube_array, sampler: sampler, coords: vec3<f32>, array_index: i32) -> f32
+[[stage("fragment")]] fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, bias: f32, offset: vec2<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, bias: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, bias: f32, offset: vec2<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, bias: f32, offset: vec3<i32>) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, bias: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleBias(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, bias: f32) -> vec4<f32>
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> f32
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> f32
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32
+[[stage("fragment")]] fn textureSampleCompare(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_2d, sampler: sampler_comparison, coords: vec2<f32>, depth_ref: f32, offset: vec2<i32>) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_2d_array, sampler: sampler_comparison, coords: vec2<f32>, array_index: i32, depth_ref: f32, offset: vec2<i32>) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_cube, sampler: sampler_comparison, coords: vec3<f32>, depth_ref: f32) -> f32
+fn textureSampleCompareLevel(texture: texture_depth_cube_array, sampler: sampler_comparison, coords: vec3<f32>, array_index: i32, depth_ref: f32) -> f32
+fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, ddx: vec2<f32>, ddy: vec2<f32>, offset: vec2<i32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>, offset: vec3<i32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+fn textureSampleGrad(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, ddx: vec3<f32>, ddy: vec3<f32>) -> vec4<f32>
+fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32) -> vec4<f32>
+fn textureSampleLevel(texture: texture_2d<f32>, sampler: sampler, coords: vec2<f32>, level: f32, offset: vec2<i32>) -> vec4<f32>
+fn textureSampleLevel(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, level: f32) -> vec4<f32>
+fn textureSampleLevel(texture: texture_2d_array<f32>, sampler: sampler, coords: vec2<f32>, array_index: i32, level: f32, offset: vec2<i32>) -> vec4<f32>
+fn textureSampleLevel(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, level: f32) -> vec4<f32>
+fn textureSampleLevel(texture: texture_3d<f32>, sampler: sampler, coords: vec3<f32>, level: f32, offset: vec3<i32>) -> vec4<f32>
+fn textureSampleLevel(texture: texture_cube<f32>, sampler: sampler, coords: vec3<f32>, level: f32) -> vec4<f32>
+fn textureSampleLevel(texture: texture_cube_array<f32>, sampler: sampler, coords: vec3<f32>, array_index: i32, level: f32) -> vec4<f32>
+fn textureSampleLevel(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, level: i32) -> f32
+fn textureSampleLevel(texture: texture_depth_2d, sampler: sampler, coords: vec2<f32>, level: i32, offset: vec2<i32>) -> f32
+fn textureSampleLevel(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, level: i32) -> f32
+fn textureSampleLevel(texture: texture_depth_2d_array, sampler: sampler, coords: vec2<f32>, array_index: i32, level: i32, offset: vec2<i32>) -> f32
+fn textureSampleLevel(texture: texture_depth_cube, sampler: sampler, coords: vec3<f32>, level: i32) -> f32
+fn textureSampleLevel(texture: texture_depth_cube_array,sampler: sampler, coords: vec3<f32>, array_index: i32, level: i32) -> f32
+fn textureSampleLevel(texture: texture_external, sampler: sampler, coords: vec2<f32>) -> vec4<f32>
+fn textureStore(texture: texture_storage_1d<f32_texel_format, write>, coords: i32, value: vec4<f32>)
+fn textureStore(texture: texture_storage_2d<f32_texel_format, write>, coords: vec2<i32>, value: vec4<f32>)
+fn textureStore(texture: texture_storage_2d_array<f32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<f32>)
+fn textureStore(texture: texture_storage_3d<f32_texel_format, write>, coords: vec3<i32>, value: vec4<f32>)
+fn textureStore(texture: texture_storage_1d<i32_texel_format, write>, coords: i32, value: vec4<i32>)
+fn textureStore(texture: texture_storage_2d<i32_texel_format, write>, coords: vec2<i32>, value: vec4<i32>)
+fn textureStore(texture: texture_storage_2d_array<i32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<i32>)
+fn textureStore(texture: texture_storage_3d<i32_texel_format, write>, coords: vec3<i32>, value: vec4<i32>)
+fn textureStore(texture: texture_storage_1d<u32_texel_format, write>, coords: i32, value: vec4<u32>)
+fn textureStore(texture: texture_storage_2d<u32_texel_format, write>, coords: vec2<i32>, value: vec4<u32>)
+fn textureStore(texture: texture_storage_2d_array<u32_texel_format, write>, coords: vec2<i32>, array_index: i32, value: vec4<u32>)
+fn textureStore(texture: texture_storage_3d<u32_texel_format, write>, coords: vec3<i32>, value: vec4<u32>)
+fn textureLoad<T: fiu32>(texture: texture_1d<T>, coords: i32, level: i32) -> vec4<T>
+fn textureLoad<T: fiu32>(texture: texture_2d<T>, coords: vec2<i32>, level: i32) -> vec4<T>
+fn textureLoad<T: fiu32>(texture: texture_2d_array<T>, coords: vec2<i32>, array_index: i32, level: i32) -> vec4<T>
+fn textureLoad<T: fiu32>(texture: texture_3d<T>, coords: vec3<i32>, level: i32) -> vec4<T>
+fn textureLoad<T: fiu32>(texture: texture_multisampled_2d<T>, coords: vec2<i32>, sample_index: i32) -> vec4<T>
+fn textureLoad(texture: texture_depth_2d, coords: vec2<i32>, level: i32) -> f32
+fn textureLoad(texture: texture_depth_2d_array, coords: vec2<i32>, array_index: i32, level: i32) -> f32
+fn textureLoad(texture: texture_depth_multisampled_2d, coords: vec2<i32>, sample_index: i32) -> f32
+fn textureLoad(texture: texture_external, coords: vec2<i32>) -> vec4<f32>
+
+[[stage("fragment", "compute")]] fn atomicLoad<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>) -> T
+[[stage("fragment", "compute")]] fn atomicStore<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T)
+[[stage("fragment", "compute")]] fn atomicAdd<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicSub<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicMax<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicMin<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicAnd<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicOr<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicXor<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicExchange<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T) -> T
+[[stage("fragment", "compute")]] fn atomicCompareExchangeWeak<T: iu32, S: workgroup_or_storage>(ptr<S, atomic<T>, read_write>, T, T) -> vec2<T>
diff --git a/src/tint/castable.cc b/src/tint/castable.cc
new file mode 100644
index 0000000..cff430e
--- /dev/null
+++ b/src/tint/castable.cc
@@ -0,0 +1,29 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/castable.h"
+
+namespace tint {
+
+/// The unique TypeInfo for the CastableBase type
+/// @return doxygen-thinks-this-static-field-is-a-function :(
+template <>
+const TypeInfo detail::TypeInfoOf<CastableBase>::info{
+    nullptr,
+    "CastableBase",
+    tint::TypeInfo::HashCodeOf<CastableBase>(),
+    tint::TypeInfo::FullHashCodeOf<CastableBase>(),
+};
+
+}  // namespace tint
diff --git a/src/tint/castable.h b/src/tint/castable.h
new file mode 100644
index 0000000..f5d2cb2
--- /dev/null
+++ b/src/tint/castable.h
@@ -0,0 +1,815 @@
+// Copyright 2020 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_CASTABLE_H_
+#define SRC_TINT_CASTABLE_H_
+
+#include <stdint.h>
+#include <functional>
+#include <tuple>
+#include <utility>
+
+#include "src/tint/traits.h"
+#include "src/tint/utils/crc32.h"
+
+#if defined(__clang__)
+/// Temporarily disable certain warnings when using Castable API
+#define TINT_CASTABLE_PUSH_DISABLE_WARNINGS()                               \
+  _Pragma("clang diagnostic push")                                     /**/ \
+      _Pragma("clang diagnostic ignored \"-Wundefined-var-template\"") /**/ \
+      static_assert(true, "require extra semicolon")
+
+/// Restore disabled warnings
+#define TINT_CASTABLE_POP_DISABLE_WARNINGS() \
+  _Pragma("clang diagnostic pop") /**/       \
+      static_assert(true, "require extra semicolon")
+#else
+#define TINT_CASTABLE_PUSH_DISABLE_WARNINGS() \
+  static_assert(true, "require extra semicolon")
+#define TINT_CASTABLE_POP_DISABLE_WARNINGS() \
+  static_assert(true, "require extra semicolon")
+#endif
+
+TINT_CASTABLE_PUSH_DISABLE_WARNINGS();
+
+namespace tint {
+
+// Forward declaration
+class CastableBase;
+
+/// Ignore is used as a special type used for skipping over types for trait
+/// helper functions.
+class Ignore {};
+
+namespace detail {
+template <typename T>
+struct TypeInfoOf;
+
+}  // namespace detail
+
+/// True if all template types that are not Ignore derive from CastableBase
+template <typename... TYPES>
+static constexpr bool IsCastable =
+    ((traits::IsTypeOrDerived<TYPES, CastableBase> ||
+      std::is_same_v<TYPES, Ignore>)&&...) &&
+    !(std::is_same_v<TYPES, Ignore> && ...);
+
+/// Helper macro to instantiate the TypeInfo<T> template for `CLASS`.
+#define TINT_INSTANTIATE_TYPEINFO(CLASS)                      \
+  TINT_CASTABLE_PUSH_DISABLE_WARNINGS();                      \
+  template <>                                                 \
+  const tint::TypeInfo tint::detail::TypeInfoOf<CLASS>::info{ \
+      &tint::detail::TypeInfoOf<CLASS::TrueBase>::info,       \
+      #CLASS,                                                 \
+      tint::TypeInfo::HashCodeOf<CLASS>(),                    \
+      tint::TypeInfo::FullHashCodeOf<CLASS>(),                \
+  };                                                          \
+  TINT_CASTABLE_POP_DISABLE_WARNINGS()
+
+/// Bit flags that can be passed to the template parameter `FLAGS` of Is() and
+/// As().
+enum CastFlags {
+  /// Disables the static_assert() inside Is(), that compile-time-verifies that
+  /// the cast is possible. This flag may be useful for highly-generic template
+  /// code that needs to compile for template permutations that generate
+  /// impossible casts.
+  kDontErrorOnImpossibleCast = 1,
+};
+
+/// TypeInfo holds type information for a Castable type.
+struct TypeInfo {
+  /// The type of a hash code
+  using HashCode = uint64_t;
+
+  /// The base class of this type
+  const TypeInfo* base;
+  /// The type name
+  const char* name;
+  /// The type hash code
+  const HashCode hashcode;
+  /// The type hash code bitwise-or'd with all ancestor's hashcodes.
+  const HashCode full_hashcode;
+
+  /// @param type the test type info
+  /// @returns true if the class with this TypeInfo is of, or derives from the
+  /// class with the given TypeInfo.
+  inline bool Is(const tint::TypeInfo* type) const {
+    // Optimization: Check whether the all the bits of the type's hashcode can
+    // be found in the full_hashcode. If a single bit is missing, then we
+    // can quickly tell that that this TypeInfo does not derive from `type`.
+    if ((full_hashcode & type->hashcode) != type->hashcode) {
+      return false;
+    }
+
+    // Walk the base types, starting with this TypeInfo, to see if any of the
+    // pointers match `type`.
+    for (auto* ti = this; ti != nullptr; ti = ti->base) {
+      if (ti == type) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /// @returns true if `type` derives from the class `TO`
+  /// @param type the object type to test from, which must be, or derive from
+  /// type `FROM`.
+  /// @see CastFlags
+  template <typename TO, typename FROM, int FLAGS = 0>
+  static inline bool Is(const tint::TypeInfo* type) {
+    constexpr const bool downcast = std::is_base_of<FROM, TO>::value;
+    constexpr const bool upcast = std::is_base_of<TO, FROM>::value;
+    constexpr const bool nocast = std::is_same<FROM, TO>::value;
+    constexpr const bool assert_is_castable =
+        (FLAGS & kDontErrorOnImpossibleCast) == 0;
+
+    static_assert(upcast || downcast || nocast || !assert_is_castable,
+                  "impossible cast");
+
+    if (upcast || nocast) {
+      return true;
+    }
+
+    return type->Is(&Of<std::remove_cv_t<TO>>());
+  }
+
+  /// @returns the static TypeInfo for the type T
+  template <typename T>
+  static const TypeInfo& Of() {
+    return detail::TypeInfoOf<std::remove_cv_t<T>>::info;
+  }
+
+  /// @returns a compile-time hashcode for the type `T`.
+  /// @note the returned hashcode will have at most 2 bits set, as the hashes
+  /// are expected to be used in bloom-filters which will quickly saturate when
+  /// multiple hashcodes are bitwise-or'd together.
+  template <typename T>
+  static constexpr HashCode HashCodeOf() {
+    static_assert(IsCastable<T>, "T is not Castable");
+    static_assert(
+        std::is_same_v<T, std::remove_cv_t<T>>,
+        "Strip const / volatile decorations before calling HashCodeOf");
+    /// Use the compiler's "pretty" function name, which includes the template
+    /// type, to obtain a unique hash value.
+#ifdef _MSC_VER
+    constexpr uint32_t crc = utils::CRC32(__FUNCSIG__);
+#else
+    constexpr uint32_t crc = utils::CRC32(__PRETTY_FUNCTION__);
+#endif
+    constexpr uint32_t bit_a = (crc & 63);
+    constexpr uint32_t bit_b = ((crc >> 6) & 63);
+    return (static_cast<HashCode>(1) << bit_a) |
+           (static_cast<HashCode>(1) << bit_b);
+  }
+
+  /// @returns the hashcode of the given type, bitwise-or'd with the hashcodes
+  /// of all base classes.
+  template <typename T>
+  static constexpr HashCode FullHashCodeOf() {
+    if constexpr (std::is_same_v<T, CastableBase>) {
+      return HashCodeOf<CastableBase>();
+    } else {
+      return HashCodeOf<T>() | FullHashCodeOf<typename T::TrueBase>();
+    }
+  }
+
+  /// @returns the bitwise-or'd hashcodes of all the types of the tuple `TUPLE`.
+  /// @see HashCodeOf
+  template <typename TUPLE>
+  static constexpr HashCode CombinedHashCodeOfTuple() {
+    constexpr auto kCount = std::tuple_size_v<TUPLE>;
+    if constexpr (kCount == 0) {
+      return 0;
+    } else if constexpr (kCount == 1) {
+      return HashCodeOf<std::remove_cv_t<std::tuple_element_t<0, TUPLE>>>();
+    } else {
+      constexpr auto kMid = kCount / 2;
+      return CombinedHashCodeOfTuple<traits::SliceTuple<0, kMid, TUPLE>>() |
+             CombinedHashCodeOfTuple<
+                 traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
+    }
+  }
+
+  /// @returns the bitwise-or'd hashcodes of all the template parameter types.
+  /// @see HashCodeOf
+  template <typename... TYPES>
+  static constexpr HashCode CombinedHashCodeOf() {
+    return CombinedHashCodeOfTuple<std::tuple<TYPES...>>();
+  }
+
+  /// @returns true if this TypeInfo is of, or derives from any of the types in
+  /// `TUPLE`.
+  template <typename TUPLE>
+  inline bool IsAnyOfTuple() const {
+    constexpr auto kCount = std::tuple_size_v<TUPLE>;
+    if constexpr (kCount == 0) {
+      return false;
+    } else if constexpr (kCount == 1) {
+      return Is(&Of<std::tuple_element_t<0, TUPLE>>());
+    } else if constexpr (kCount == 2) {
+      return Is(&Of<std::tuple_element_t<0, TUPLE>>()) ||
+             Is(&Of<std::tuple_element_t<1, TUPLE>>());
+    } else if constexpr (kCount == 3) {
+      return Is(&Of<std::tuple_element_t<0, TUPLE>>()) ||
+             Is(&Of<std::tuple_element_t<1, TUPLE>>()) ||
+             Is(&Of<std::tuple_element_t<2, TUPLE>>());
+    } else {
+      // Optimization: Compare the object's hashcode to the bitwise-or of all
+      // the tested type's hashcodes. If there's no intersection of bits in
+      // the two masks, then we can guarantee that the type is not in `TO`.
+      if (full_hashcode & TypeInfo::CombinedHashCodeOfTuple<TUPLE>()) {
+        // Possibly one of the types in `TUPLE`.
+        // Split the search in two, and scan each block.
+        static constexpr auto kMid = kCount / 2;
+        return IsAnyOfTuple<traits::SliceTuple<0, kMid, TUPLE>>() ||
+               IsAnyOfTuple<traits::SliceTuple<kMid, kCount - kMid, TUPLE>>();
+      }
+      return false;
+    }
+  }
+
+  /// @returns true if this TypeInfo is of, or derives from any of the types in
+  /// `TYPES`.
+  template <typename... TYPES>
+  inline bool IsAnyOf() const {
+    return IsAnyOfTuple<std::tuple<TYPES...>>();
+  }
+};
+
+namespace detail {
+
+/// TypeInfoOf contains a single TypeInfo field for the type T.
+/// TINT_INSTANTIATE_TYPEINFO() must be defined in a .cpp file for each type
+/// `T`.
+template <typename T>
+struct TypeInfoOf {
+  /// The unique TypeInfo for the type T.
+  static const TypeInfo info;
+};
+
+/// A placeholder structure used for template parameters that need a default
+/// type, but can always be automatically inferred.
+struct Infer;
+
+}  // namespace detail
+
+/// @returns true if `obj` is a valid pointer, and is of, or derives from the
+/// class `TO`
+/// @param obj the object to test from
+/// @see CastFlags
+template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+inline bool Is(FROM* obj) {
+  if (obj == nullptr) {
+    return false;
+  }
+  return TypeInfo::Is<TO, FROM, FLAGS>(&obj->TypeInfo());
+}
+
+/// @returns true if `obj` is a valid pointer, and is of, or derives from the
+/// type `TYPE`, and pred(const TYPE*) returns true
+/// @param obj the object to test from
+/// @param pred predicate function with signature `bool(const TYPE*)` called iff
+/// object is of, or derives from the class `TYPE`.
+/// @see CastFlags
+template <typename TYPE,
+          int FLAGS = 0,
+          typename OBJ = detail::Infer,
+          typename Pred = detail::Infer>
+inline bool Is(OBJ* obj, Pred&& pred) {
+  return Is<TYPE, FLAGS, OBJ>(obj) &&
+         pred(static_cast<std::add_const_t<TYPE>*>(obj));
+}
+
+/// @returns true if `obj` is a valid pointer, and is of, or derives from any of
+/// the types in `TYPES`.OBJ
+/// @param obj the object to query.
+template <typename... TYPES, typename OBJ>
+inline bool IsAnyOf(OBJ* obj) {
+  if (!obj) {
+    return false;
+  }
+  return obj->TypeInfo().template IsAnyOf<TYPES...>();
+}
+
+/// @returns obj dynamically cast to the type `TO` or `nullptr` if
+/// this object does not derive from `TO`.
+/// @param obj the object to cast from
+/// @see CastFlags
+template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+inline TO* As(FROM* obj) {
+  auto* as_castable = static_cast<CastableBase*>(obj);
+  return Is<TO, FLAGS>(obj) ? static_cast<TO*>(as_castable) : nullptr;
+}
+
+/// @returns obj dynamically cast to the type `TO` or `nullptr` if
+/// this object does not derive from `TO`.
+/// @param obj the object to cast from
+/// @see CastFlags
+template <typename TO, int FLAGS = 0, typename FROM = detail::Infer>
+inline const TO* As(const FROM* obj) {
+  auto* as_castable = static_cast<const CastableBase*>(obj);
+  return Is<TO, FLAGS>(obj) ? static_cast<const TO*>(as_castable) : nullptr;
+}
+
+/// CastableBase is the base class for all Castable objects.
+/// It is not encouraged to directly derive from CastableBase without using the
+/// Castable helper template.
+/// @see Castable
+class CastableBase {
+ public:
+  /// Copy constructor
+  CastableBase(const CastableBase&) = default;
+
+  /// Destructor
+  virtual ~CastableBase() = default;
+
+  /// Copy assignment
+  /// @param other the CastableBase to copy
+  /// @returns the new CastableBase
+  CastableBase& operator=(const CastableBase& other) = default;
+
+  /// @returns the TypeInfo of the object
+  virtual const tint::TypeInfo& TypeInfo() const = 0;
+
+  /// @returns true if this object is of, or derives from the class `TO`
+  template <typename TO>
+  inline bool Is() const {
+    return tint::Is<TO>(this);
+  }
+
+  /// @returns true if this object is of, or derives from the class `TO` and
+  /// pred(const TO*) returns true
+  /// @param pred predicate function with signature `bool(const TO*)` called iff
+  /// object is of, or derives from the class `TO`.
+  template <typename TO, int FLAGS = 0, typename Pred = detail::Infer>
+  inline bool Is(Pred&& pred) const {
+    return tint::Is<TO, FLAGS>(this, std::forward<Pred>(pred));
+  }
+
+  /// @returns true if this object is of, or derives from any of the `TO`
+  /// classes.
+  template <typename... TO>
+  inline bool IsAnyOf() const {
+    return tint::IsAnyOf<TO...>(this);
+  }
+
+  /// @returns this object dynamically cast to the type `TO` or `nullptr` if
+  /// this object does not derive from `TO`.
+  /// @see CastFlags
+  template <typename TO, int FLAGS = 0>
+  inline TO* As() {
+    return tint::As<TO, FLAGS>(this);
+  }
+
+  /// @returns this object dynamically cast to the type `TO` or `nullptr` if
+  /// this object does not derive from `TO`.
+  /// @see CastFlags
+  template <typename TO, int FLAGS = 0>
+  inline const TO* As() const {
+    return tint::As<const TO, FLAGS>(this);
+  }
+
+ protected:
+  CastableBase() = default;
+};
+
+/// Castable is a helper to derive `CLASS` from `BASE`, automatically
+/// implementing the Is() and As() methods, along with a #Base type alias.
+///
+/// Example usage:
+///
+/// ```
+/// class Animal : public Castable<Animal> {};
+///
+/// class Sheep : public Castable<Sheep, Animal> {};
+///
+/// Sheep* cast_to_sheep(Animal* animal) {
+///    // You can query whether a Castable is of the given type with Is<T>():
+///    printf("animal is a sheep? %s", animal->Is<Sheep>() ? "yes" : "no");
+///
+///    // You can always just try the cast with As<T>().
+///    // If the object is not of the correct type, As<T>() will return nullptr:
+///    return animal->As<Sheep>();
+/// }
+/// ```
+template <typename CLASS, typename BASE = CastableBase>
+class Castable : public BASE {
+ public:
+  // Inherit the `BASE` class constructors.
+  using BASE::BASE;
+
+  /// A type alias for `CLASS` to easily access the `BASE` class members.
+  /// Base actually aliases to the Castable instead of `BASE` so that you can
+  /// use Base in the `CLASS` constructor.
+  using Base = Castable;
+
+  /// A type alias for `BASE`.
+  using TrueBase = BASE;
+
+  /// @returns the TypeInfo of the object
+  const tint::TypeInfo& TypeInfo() const override {
+    return TypeInfo::Of<CLASS>();
+  }
+
+  /// @returns true if this object is of, or derives from the class `TO`
+  /// @see CastFlags
+  template <typename TO, int FLAGS = 0>
+  inline bool Is() const {
+    return tint::Is<TO, FLAGS>(static_cast<const CLASS*>(this));
+  }
+
+  /// @returns true if this object is of, or derives from the class `TO` and
+  /// pred(const TO*) returns true
+  /// @param pred predicate function with signature `bool(const TO*)` called iff
+  /// object is of, or derives from the class `TO`.
+  template <int FLAGS = 0, typename Pred = detail::Infer>
+  inline bool Is(Pred&& pred) const {
+    using TO =
+        typename std::remove_pointer<traits::ParameterType<Pred, 0>>::type;
+    return tint::Is<TO, FLAGS>(static_cast<const CLASS*>(this),
+                               std::forward<Pred>(pred));
+  }
+
+  /// @returns true if this object is of, or derives from any of the `TO`
+  /// classes.
+  template <typename... TO>
+  inline bool IsAnyOf() const {
+    return tint::IsAnyOf<TO...>(static_cast<const CLASS*>(this));
+  }
+
+  /// @returns this object dynamically cast to the type `TO` or `nullptr` if
+  /// this object does not derive from `TO`.
+  /// @see CastFlags
+  template <typename TO, int FLAGS = 0>
+  inline TO* As() {
+    return tint::As<TO, FLAGS>(this);
+  }
+
+  /// @returns this object dynamically cast to the type `TO` or `nullptr` if
+  /// this object does not derive from `TO`.
+  /// @see CastFlags
+  template <typename TO, int FLAGS = 0>
+  inline const TO* As() const {
+    return tint::As<const TO, FLAGS>(this);
+  }
+};
+
+namespace detail {
+/// <code>typename CastableCommonBaseImpl<TYPES>::type</code> resolves to the
+/// common base class for all of TYPES.
+template <typename... TYPES>
+struct CastableCommonBaseImpl {};
+
+/// Alias to typename CastableCommonBaseImpl<TYPES>::type
+template <typename... TYPES>
+using CastableCommonBase =
+    typename detail::CastableCommonBaseImpl<TYPES...>::type;
+
+/// CastableCommonBaseImpl template specialization for a single type
+template <typename T>
+struct CastableCommonBaseImpl<T> {
+  /// Common base class of a single type is itself
+  using type = T;
+};
+
+/// CastableCommonBaseImpl A <-> CastableBase specialization
+template <typename A>
+struct CastableCommonBaseImpl<A, CastableBase> {
+  /// Common base class for A and CastableBase is CastableBase
+  using type = CastableBase;
+};
+
+/// CastableCommonBaseImpl T <-> Ignore specialization
+template <typename T>
+struct CastableCommonBaseImpl<T, Ignore> {
+  /// Resolves to T as the other type is ignored
+  using type = T;
+};
+
+/// CastableCommonBaseImpl Ignore <-> T specialization
+template <typename T>
+struct CastableCommonBaseImpl<Ignore, T> {
+  /// Resolves to T as the other type is ignored
+  using type = T;
+};
+
+/// CastableCommonBaseImpl A <-> B specialization
+template <typename A, typename B>
+struct CastableCommonBaseImpl<A, B> {
+  /// The common base class for A, B and OTHERS
+  using type = std::conditional_t<traits::IsTypeOrDerived<A, B>,
+                                  B,  // A derives from B
+                                  CastableCommonBase<A, typename B::TrueBase>>;
+};
+
+/// CastableCommonBaseImpl 3+ types specialization
+template <typename A, typename B, typename... OTHERS>
+struct CastableCommonBaseImpl<A, B, OTHERS...> {
+  /// The common base class for A, B and OTHERS
+  using type = CastableCommonBase<CastableCommonBase<A, B>, OTHERS...>;
+};
+
+}  // namespace detail
+
+/// Resolves to the common most derived type that each of the types in `TYPES`
+/// derives from.
+template <typename... TYPES>
+using CastableCommonBase = detail::CastableCommonBase<TYPES...>;
+
+/// Default can be used as the default case for a Switch(), when all previous
+/// cases failed to match.
+///
+/// Example:
+/// ```
+/// Switch(object,
+///     [&](TypeA*) { /* ... */ },
+///     [&](TypeB*) { /* ... */ },
+///     [&](Default) { /* If not TypeA or TypeB */ });
+/// ```
+struct Default {};
+
+namespace detail {
+
+/// Evaluates to the Switch case type being matched by the switch case function
+/// `FN`.
+/// @note does not handle the Default case
+/// @see Switch().
+template <typename FN>
+using SwitchCaseType = std::remove_pointer_t<
+    traits::ParameterType<std::remove_reference_t<FN>, 0>>;
+
+/// Evaluates to true if the function `FN` has the signature of a Default case
+/// in a Switch().
+/// @see Switch().
+template <typename FN>
+inline constexpr bool IsDefaultCase =
+    std::is_same_v<traits::ParameterType<std::remove_reference_t<FN>, 0>,
+                   Default>;
+
+/// Searches the list of Switch cases for a Default case, returning the index of
+/// the Default case. If the a Default case is not found in the tuple, then -1
+/// is returned.
+template <typename TUPLE, std::size_t START_IDX = 0>
+constexpr int IndexOfDefaultCase() {
+  if constexpr (START_IDX < std::tuple_size_v<TUPLE>) {
+    return IsDefaultCase<std::tuple_element_t<START_IDX, TUPLE>>
+               ? static_cast<int>(START_IDX)
+               : IndexOfDefaultCase<TUPLE, START_IDX + 1>();
+  } else {
+    return -1;
+  }
+}
+
+/// The implementation of Switch() for non-Default cases.
+/// Switch splits the cases into two a low and high block of cases, and quickly
+/// rules out blocks that cannot match by comparing the TypeInfo::HashCode of
+/// the object and the cases in the block. If a block of cases may match the
+/// given object's type, then that block is split into two, and the process
+/// recurses. When NonDefaultCases() is called with a single case, then As<>
+/// will be used to dynamically cast to the case type and if the cast succeeds,
+/// then the case handler is called.
+/// @returns true if a case handler was found, otherwise false.
+template <typename T, typename RETURN_TYPE, typename... CASES>
+inline bool NonDefaultCases(T* object,
+                            const TypeInfo* type,
+                            RETURN_TYPE* result,
+                            std::tuple<CASES...>&& cases) {
+  using Cases = std::tuple<CASES...>;
+
+  (void)result;  // Not always used, avoid warning.
+
+  static constexpr bool kHasReturnType = !std::is_same_v<RETURN_TYPE, void>;
+  static constexpr size_t kNumCases = sizeof...(CASES);
+
+  if constexpr (kNumCases == 0) {
+    // No cases. Nothing to do.
+    return false;
+  } else if constexpr (kNumCases == 1) {  // NOLINT: cpplint doesn't understand
+                                          // `else if constexpr`
+    // Single case.
+    using CaseFunc = std::tuple_element_t<0, Cases>;
+    static_assert(!IsDefaultCase<CaseFunc>,
+                  "NonDefaultCases called with a Default case");
+    // Attempt to dynamically cast the object to the handler type. If that
+    // succeeds, call the case handler with the cast object.
+    using CaseType = SwitchCaseType<CaseFunc>;
+    if (type->Is(&TypeInfo::Of<CaseType>())) {
+      auto* ptr = static_cast<CaseType*>(object);
+      if constexpr (kHasReturnType) {
+        *result = static_cast<RETURN_TYPE>(std::get<0>(cases)(ptr));
+      } else {
+        std::get<0>(cases)(ptr);
+      }
+      return true;
+    }
+    return false;
+  } else {
+    // Multiple cases.
+    // Check the hashcode bits to see if there's any possibility of a case
+    // matching in these cases. If there isn't, we can skip all these cases.
+    if (type->full_hashcode &
+        TypeInfo::CombinedHashCodeOf<SwitchCaseType<CASES>...>()) {
+      // There's a possibility. We need to scan further.
+      // Split the cases into two, and recurse.
+      constexpr size_t kMid = kNumCases / 2;
+      return NonDefaultCases(object, type, result,
+                             traits::Slice<0, kMid>(cases)) ||
+             NonDefaultCases(object, type, result,
+                             traits::Slice<kMid, kNumCases - kMid>(cases));
+    } else {
+      return false;
+    }
+  }
+}
+
+/// The implementation of Switch() for all cases.
+/// @see NonDefaultCases
+template <typename T, typename RETURN_TYPE, typename... CASES>
+inline void SwitchCases(T* object,
+                        RETURN_TYPE* result,
+                        std::tuple<CASES...>&& cases) {
+  using Cases = std::tuple<CASES...>;
+  static constexpr int kDefaultIndex = detail::IndexOfDefaultCase<Cases>();
+  static_assert(
+      kDefaultIndex == -1 || kDefaultIndex == std::tuple_size_v<Cases> - 1,
+      "Default case must be last in Switch()");
+  static constexpr bool kHasDefaultCase = kDefaultIndex >= 0;
+  static constexpr bool kHasReturnType = !std::is_same_v<RETURN_TYPE, void>;
+
+  if (object) {
+    auto* type = &object->TypeInfo();
+    if constexpr (kHasDefaultCase) {
+      // Evaluate non-default cases.
+      if (!detail::NonDefaultCases<T>(object, type, result,
+                                      traits::Slice<0, kDefaultIndex>(cases))) {
+        // Nothing matched. Evaluate default case.
+        if constexpr (kHasReturnType) {
+          *result =
+              static_cast<RETURN_TYPE>(std::get<kDefaultIndex>(cases)({}));
+        } else {
+          std::get<kDefaultIndex>(cases)({});
+        }
+      }
+    } else {
+      detail::NonDefaultCases<T>(object, type, result, std::move(cases));
+    }
+  } else {
+    // Object is nullptr, so no cases can match
+    if constexpr (kHasDefaultCase) {
+      // Evaluate default case.
+      if constexpr (kHasReturnType) {
+        *result = static_cast<RETURN_TYPE>(std::get<kDefaultIndex>(cases)({}));
+      } else {
+        std::get<kDefaultIndex>(cases)({});
+      }
+    }
+  }
+}
+
+/// Resolves to T if T is not nullptr_t, otherwise resolves to Ignore.
+template <typename T>
+using NullptrToIgnore =
+    std::conditional_t<std::is_same_v<T, std::nullptr_t>, Ignore, T>;
+
+/// Resolves to `const TYPE` if any of `CASE_RETURN_TYPES` are const or
+/// pointer-to-const, otherwise resolves to TYPE.
+template <typename TYPE, typename... CASE_RETURN_TYPES>
+using PropagateReturnConst = std::conditional_t<
+    // Are any of the pointer-stripped types const?
+    (std::is_const_v<std::remove_pointer_t<CASE_RETURN_TYPES>> || ...),
+    const TYPE,  // Yes: Apply const to TYPE
+    TYPE>;       // No:  Passthrough
+
+/// SwitchReturnTypeImpl is the implementation of SwitchReturnType
+template <bool IS_CASTABLE,
+          typename REQUESTED_TYPE,
+          typename... CASE_RETURN_TYPES>
+struct SwitchReturnTypeImpl;
+
+/// SwitchReturnTypeImpl specialization for non-castable case types and an
+/// explicitly specified return type.
+template <typename REQUESTED_TYPE, typename... CASE_RETURN_TYPES>
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ false,
+                            REQUESTED_TYPE,
+                            CASE_RETURN_TYPES...> {
+  /// Resolves to `REQUESTED_TYPE`
+  using type = REQUESTED_TYPE;
+};
+
+/// SwitchReturnTypeImpl specialization for non-castable case types and an
+/// inferred return type.
+template <typename... CASE_RETURN_TYPES>
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ false,
+                            Infer,
+                            CASE_RETURN_TYPES...> {
+  /// Resolves to the common type for all the cases return types.
+  using type = std::common_type_t<CASE_RETURN_TYPES...>;
+};
+
+/// SwitchReturnTypeImpl specialization for castable case types and an
+/// explicitly specified return type.
+template <typename REQUESTED_TYPE, typename... CASE_RETURN_TYPES>
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ true,
+                            REQUESTED_TYPE,
+                            CASE_RETURN_TYPES...> {
+ public:
+  /// Resolves to `const REQUESTED_TYPE*` or `REQUESTED_TYPE*`
+  using type = PropagateReturnConst<std::remove_pointer_t<REQUESTED_TYPE>,
+                                    CASE_RETURN_TYPES...>*;
+};
+
+/// SwitchReturnTypeImpl specialization for castable case types and an infered
+/// return type.
+template <typename... CASE_RETURN_TYPES>
+struct SwitchReturnTypeImpl</*IS_CASTABLE*/ true, Infer, CASE_RETURN_TYPES...> {
+ private:
+  using InferredType = CastableCommonBase<
+      detail::NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>;
+
+ public:
+  /// `const T*` or `T*`, where T is the common base type for all the castable
+  /// case types.
+  using type = PropagateReturnConst<InferredType, CASE_RETURN_TYPES...>*;
+};
+
+/// Resolves to the return type for a Switch() with the requested return type
+/// `REQUESTED_TYPE` and case statement return types. If `REQUESTED_TYPE` is
+/// Infer then the return type will be inferred from the case return types.
+template <typename REQUESTED_TYPE, typename... CASE_RETURN_TYPES>
+using SwitchReturnType = typename SwitchReturnTypeImpl<
+    IsCastable<NullptrToIgnore<std::remove_pointer_t<CASE_RETURN_TYPES>>...>,
+    REQUESTED_TYPE,
+    CASE_RETURN_TYPES...>::type;
+
+}  // namespace detail
+
+/// Switch is used to dispatch one of the provided callback case handler
+/// functions based on the type of `object` and the parameter type of the case
+/// handlers. Switch will sequentially check the type of `object` against each
+/// of the switch case handler functions, and will invoke the first case handler
+/// function which has a parameter type that matches the object type. When a
+/// case handler is matched, it will be called with the single argument of
+/// `object` cast to the case handler's parameter type. Switch will invoke at
+/// most one case handler. Each of the case functions must have the signature
+/// `R(T*)` or `R(const T*)`, where `T` is the type matched by that case and `R`
+/// is the return type, consistent across all case handlers.
+///
+/// An optional default case function with the signature `R(Default)` can be
+/// used as the last case. This default case will be called if all previous
+/// cases failed to match.
+///
+/// If `object` is nullptr and a default case is provided, then the default case
+/// will be called. If `object` is nullptr and no default case is provided, then
+/// no cases will be called.
+///
+/// Example:
+/// ```
+/// Switch(object,
+///     [&](TypeA*) { /* ... */ },
+///     [&](TypeB*) { /* ... */ });
+///
+/// Switch(object,
+///     [&](TypeA*) { /* ... */ },
+///     [&](TypeB*) { /* ... */ },
+///     [&](Default) { /* Called if object is not TypeA or TypeB */ });
+/// ```
+///
+/// @param object the object who's type is used to
+/// @param cases the switch cases
+/// @return the value returned by the called case. If no cases matched, then the
+/// zero value for the consistent case type.
+template <typename RETURN_TYPE = detail::Infer,
+          typename T = CastableBase,
+          typename... CASES>
+inline auto Switch(T* object, CASES&&... cases) {
+  using ReturnType =
+      detail::SwitchReturnType<RETURN_TYPE, traits::ReturnType<CASES>...>;
+  static constexpr bool kHasReturnType = !std::is_same_v<ReturnType, void>;
+
+  if constexpr (kHasReturnType) {
+    ReturnType res = {};
+    detail::SwitchCases(object, &res,
+                        std::forward_as_tuple(std::forward<CASES>(cases)...));
+    return res;
+  } else {
+    detail::SwitchCases<T, void>(
+        object, nullptr, std::forward_as_tuple(std::forward<CASES>(cases)...));
+  }
+}
+
+}  // namespace tint
+
+TINT_CASTABLE_POP_DISABLE_WARNINGS();
+
+#endif  // SRC_TINT_CASTABLE_H_
diff --git a/src/tint/castable_bench.cc b/src/tint/castable_bench.cc
new file mode 100644
index 0000000..839a932
--- /dev/null
+++ b/src/tint/castable_bench.cc
@@ -0,0 +1,270 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "bench/benchmark.h"
+
+namespace tint {
+namespace {
+
+struct Base : public tint::Castable<Base> {};
+struct A : public tint::Castable<A, Base> {};
+struct AA : public tint::Castable<AA, A> {};
+struct AAA : public tint::Castable<AAA, AA> {};
+struct AAB : public tint::Castable<AAB, AA> {};
+struct AAC : public tint::Castable<AAC, AA> {};
+struct AB : public tint::Castable<AB, A> {};
+struct ABA : public tint::Castable<ABA, AB> {};
+struct ABB : public tint::Castable<ABB, AB> {};
+struct ABC : public tint::Castable<ABC, AB> {};
+struct AC : public tint::Castable<AC, A> {};
+struct ACA : public tint::Castable<ACA, AC> {};
+struct ACB : public tint::Castable<ACB, AC> {};
+struct ACC : public tint::Castable<ACC, AC> {};
+struct B : public tint::Castable<B, Base> {};
+struct BA : public tint::Castable<BA, B> {};
+struct BAA : public tint::Castable<BAA, BA> {};
+struct BAB : public tint::Castable<BAB, BA> {};
+struct BAC : public tint::Castable<BAC, BA> {};
+struct BB : public tint::Castable<BB, B> {};
+struct BBA : public tint::Castable<BBA, BB> {};
+struct BBB : public tint::Castable<BBB, BB> {};
+struct BBC : public tint::Castable<BBC, BB> {};
+struct BC : public tint::Castable<BC, B> {};
+struct BCA : public tint::Castable<BCA, BC> {};
+struct BCB : public tint::Castable<BCB, BC> {};
+struct BCC : public tint::Castable<BCC, BC> {};
+struct C : public tint::Castable<C, Base> {};
+struct CA : public tint::Castable<CA, C> {};
+struct CAA : public tint::Castable<CAA, CA> {};
+struct CAB : public tint::Castable<CAB, CA> {};
+struct CAC : public tint::Castable<CAC, CA> {};
+struct CB : public tint::Castable<CB, C> {};
+struct CBA : public tint::Castable<CBA, CB> {};
+struct CBB : public tint::Castable<CBB, CB> {};
+struct CBC : public tint::Castable<CBC, CB> {};
+struct CC : public tint::Castable<CC, C> {};
+struct CCA : public tint::Castable<CCA, CC> {};
+struct CCB : public tint::Castable<CCB, CC> {};
+struct CCC : public tint::Castable<CCC, CC> {};
+
+using AllTypes = std::tuple<Base,
+                            A,
+                            AA,
+                            AAA,
+                            AAB,
+                            AAC,
+                            AB,
+                            ABA,
+                            ABB,
+                            ABC,
+                            AC,
+                            ACA,
+                            ACB,
+                            ACC,
+                            B,
+                            BA,
+                            BAA,
+                            BAB,
+                            BAC,
+                            BB,
+                            BBA,
+                            BBB,
+                            BBC,
+                            BC,
+                            BCA,
+                            BCB,
+                            BCC,
+                            C,
+                            CA,
+                            CAA,
+                            CAB,
+                            CAC,
+                            CB,
+                            CBA,
+                            CBB,
+                            CBC,
+                            CC,
+                            CCA,
+                            CCB,
+                            CCC>;
+
+std::vector<std::unique_ptr<Base>> MakeObjects() {
+  std::vector<std::unique_ptr<Base>> out;
+  out.emplace_back(std::make_unique<Base>());
+  out.emplace_back(std::make_unique<A>());
+  out.emplace_back(std::make_unique<AA>());
+  out.emplace_back(std::make_unique<AAA>());
+  out.emplace_back(std::make_unique<AAB>());
+  out.emplace_back(std::make_unique<AAC>());
+  out.emplace_back(std::make_unique<AB>());
+  out.emplace_back(std::make_unique<ABA>());
+  out.emplace_back(std::make_unique<ABB>());
+  out.emplace_back(std::make_unique<ABC>());
+  out.emplace_back(std::make_unique<AC>());
+  out.emplace_back(std::make_unique<ACA>());
+  out.emplace_back(std::make_unique<ACB>());
+  out.emplace_back(std::make_unique<ACC>());
+  out.emplace_back(std::make_unique<B>());
+  out.emplace_back(std::make_unique<BA>());
+  out.emplace_back(std::make_unique<BAA>());
+  out.emplace_back(std::make_unique<BAB>());
+  out.emplace_back(std::make_unique<BAC>());
+  out.emplace_back(std::make_unique<BB>());
+  out.emplace_back(std::make_unique<BBA>());
+  out.emplace_back(std::make_unique<BBB>());
+  out.emplace_back(std::make_unique<BBC>());
+  out.emplace_back(std::make_unique<BC>());
+  out.emplace_back(std::make_unique<BCA>());
+  out.emplace_back(std::make_unique<BCB>());
+  out.emplace_back(std::make_unique<BCC>());
+  out.emplace_back(std::make_unique<C>());
+  out.emplace_back(std::make_unique<CA>());
+  out.emplace_back(std::make_unique<CAA>());
+  out.emplace_back(std::make_unique<CAB>());
+  out.emplace_back(std::make_unique<CAC>());
+  out.emplace_back(std::make_unique<CB>());
+  out.emplace_back(std::make_unique<CBA>());
+  out.emplace_back(std::make_unique<CBB>());
+  out.emplace_back(std::make_unique<CBC>());
+  out.emplace_back(std::make_unique<CC>());
+  out.emplace_back(std::make_unique<CCA>());
+  out.emplace_back(std::make_unique<CCB>());
+  out.emplace_back(std::make_unique<CCC>());
+  return out;
+}
+
+void CastableLargeSwitch(::benchmark::State& state) {
+  auto objects = MakeObjects();
+  size_t i = 0;
+  for (auto _ : state) {
+    auto* object = objects[i % objects.size()].get();
+    Switch(
+        object,  //
+        [&](const AAA*) { ::benchmark::DoNotOptimize(i += 40); },
+        [&](const AAB*) { ::benchmark::DoNotOptimize(i += 50); },
+        [&](const AAC*) { ::benchmark::DoNotOptimize(i += 60); },
+        [&](const ABA*) { ::benchmark::DoNotOptimize(i += 80); },
+        [&](const ABB*) { ::benchmark::DoNotOptimize(i += 90); },
+        [&](const ABC*) { ::benchmark::DoNotOptimize(i += 100); },
+        [&](const ACA*) { ::benchmark::DoNotOptimize(i += 120); },
+        [&](const ACB*) { ::benchmark::DoNotOptimize(i += 130); },
+        [&](const ACC*) { ::benchmark::DoNotOptimize(i += 140); },
+        [&](const BAA*) { ::benchmark::DoNotOptimize(i += 170); },
+        [&](const BAB*) { ::benchmark::DoNotOptimize(i += 180); },
+        [&](const BAC*) { ::benchmark::DoNotOptimize(i += 190); },
+        [&](const BBA*) { ::benchmark::DoNotOptimize(i += 210); },
+        [&](const BBB*) { ::benchmark::DoNotOptimize(i += 220); },
+        [&](const BBC*) { ::benchmark::DoNotOptimize(i += 230); },
+        [&](const BCA*) { ::benchmark::DoNotOptimize(i += 250); },
+        [&](const BCB*) { ::benchmark::DoNotOptimize(i += 260); },
+        [&](const BCC*) { ::benchmark::DoNotOptimize(i += 270); },
+        [&](const CA*) { ::benchmark::DoNotOptimize(i += 290); },
+        [&](const CAA*) { ::benchmark::DoNotOptimize(i += 300); },
+        [&](const CAB*) { ::benchmark::DoNotOptimize(i += 310); },
+        [&](const CAC*) { ::benchmark::DoNotOptimize(i += 320); },
+        [&](const CBA*) { ::benchmark::DoNotOptimize(i += 340); },
+        [&](const CBB*) { ::benchmark::DoNotOptimize(i += 350); },
+        [&](const CBC*) { ::benchmark::DoNotOptimize(i += 360); },
+        [&](const CCA*) { ::benchmark::DoNotOptimize(i += 380); },
+        [&](const CCB*) { ::benchmark::DoNotOptimize(i += 390); },
+        [&](const CCC*) { ::benchmark::DoNotOptimize(i += 400); },
+        [&](Default) { ::benchmark::DoNotOptimize(i += 123); });
+    i = (i * 31) ^ (i << 5);
+  }
+}
+
+BENCHMARK(CastableLargeSwitch);
+
+void CastableMediumSwitch(::benchmark::State& state) {
+  auto objects = MakeObjects();
+  size_t i = 0;
+  for (auto _ : state) {
+    auto* object = objects[i % objects.size()].get();
+    Switch(
+        object,  //
+        [&](const ACB*) { ::benchmark::DoNotOptimize(i += 130); },
+        [&](const BAA*) { ::benchmark::DoNotOptimize(i += 170); },
+        [&](const BAB*) { ::benchmark::DoNotOptimize(i += 180); },
+        [&](const BBA*) { ::benchmark::DoNotOptimize(i += 210); },
+        [&](const BBB*) { ::benchmark::DoNotOptimize(i += 220); },
+        [&](const CAA*) { ::benchmark::DoNotOptimize(i += 300); },
+        [&](const CCA*) { ::benchmark::DoNotOptimize(i += 380); },
+        [&](const CCB*) { ::benchmark::DoNotOptimize(i += 390); },
+        [&](const CCC*) { ::benchmark::DoNotOptimize(i += 400); },
+        [&](Default) { ::benchmark::DoNotOptimize(i += 123); });
+    i = (i * 31) ^ (i << 5);
+  }
+}
+
+BENCHMARK(CastableMediumSwitch);
+
+void CastableSmallSwitch(::benchmark::State& state) {
+  auto objects = MakeObjects();
+  size_t i = 0;
+  for (auto _ : state) {
+    auto* object = objects[i % objects.size()].get();
+    Switch(
+        object,  //
+        [&](const AAB*) { ::benchmark::DoNotOptimize(i += 30); },
+        [&](const CAC*) { ::benchmark::DoNotOptimize(i += 290); },
+        [&](const CAA*) { ::benchmark::DoNotOptimize(i += 300); });
+    i = (i * 31) ^ (i << 5);
+  }
+}
+
+BENCHMARK(CastableSmallSwitch);
+
+}  // namespace
+}  // namespace tint
+
+TINT_INSTANTIATE_TYPEINFO(tint::Base);
+TINT_INSTANTIATE_TYPEINFO(tint::A);
+TINT_INSTANTIATE_TYPEINFO(tint::AA);
+TINT_INSTANTIATE_TYPEINFO(tint::AAA);
+TINT_INSTANTIATE_TYPEINFO(tint::AAB);
+TINT_INSTANTIATE_TYPEINFO(tint::AAC);
+TINT_INSTANTIATE_TYPEINFO(tint::AB);
+TINT_INSTANTIATE_TYPEINFO(tint::ABA);
+TINT_INSTANTIATE_TYPEINFO(tint::ABB);
+TINT_INSTANTIATE_TYPEINFO(tint::ABC);
+TINT_INSTANTIATE_TYPEINFO(tint::AC);
+TINT_INSTANTIATE_TYPEINFO(tint::ACA);
+TINT_INSTANTIATE_TYPEINFO(tint::ACB);
+TINT_INSTANTIATE_TYPEINFO(tint::ACC);
+TINT_INSTANTIATE_TYPEINFO(tint::B);
+TINT_INSTANTIATE_TYPEINFO(tint::BA);
+TINT_INSTANTIATE_TYPEINFO(tint::BAA);
+TINT_INSTANTIATE_TYPEINFO(tint::BAB);
+TINT_INSTANTIATE_TYPEINFO(tint::BAC);
+TINT_INSTANTIATE_TYPEINFO(tint::BB);
+TINT_INSTANTIATE_TYPEINFO(tint::BBA);
+TINT_INSTANTIATE_TYPEINFO(tint::BBB);
+TINT_INSTANTIATE_TYPEINFO(tint::BBC);
+TINT_INSTANTIATE_TYPEINFO(tint::BC);
+TINT_INSTANTIATE_TYPEINFO(tint::BCA);
+TINT_INSTANTIATE_TYPEINFO(tint::BCB);
+TINT_INSTANTIATE_TYPEINFO(tint::BCC);
+TINT_INSTANTIATE_TYPEINFO(tint::C);
+TINT_INSTANTIATE_TYPEINFO(tint::CA);
+TINT_INSTANTIATE_TYPEINFO(tint::CAA);
+TINT_INSTANTIATE_TYPEINFO(tint::CAB);
+TINT_INSTANTIATE_TYPEINFO(tint::CAC);
+TINT_INSTANTIATE_TYPEINFO(tint::CB);
+TINT_INSTANTIATE_TYPEINFO(tint::CBA);
+TINT_INSTANTIATE_TYPEINFO(tint::CBB);
+TINT_INSTANTIATE_TYPEINFO(tint::CBC);
+TINT_INSTANTIATE_TYPEINFO(tint::CC);
+TINT_INSTANTIATE_TYPEINFO(tint::CCA);
+TINT_INSTANTIATE_TYPEINFO(tint::CCB);
+TINT_INSTANTIATE_TYPEINFO(tint::CCC);
diff --git a/src/tint/castable_test.cc b/src/tint/castable_test.cc
new file mode 100644
index 0000000..7ed66cb
--- /dev/null
+++ b/src/tint/castable_test.cc
@@ -0,0 +1,787 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/castable.h"
+
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+
+struct Animal : public tint::Castable<Animal> {};
+struct Amphibian : public tint::Castable<Amphibian, Animal> {};
+struct Mammal : public tint::Castable<Mammal, Animal> {};
+struct Reptile : public tint::Castable<Reptile, Animal> {};
+struct Frog : public tint::Castable<Frog, Amphibian> {};
+struct Bear : public tint::Castable<Bear, Mammal> {};
+struct Lizard : public tint::Castable<Lizard, Reptile> {};
+struct Gecko : public tint::Castable<Gecko, Lizard> {};
+struct Iguana : public tint::Castable<Iguana, Lizard> {};
+
+namespace {
+
+TEST(CastableBase, Is) {
+  std::unique_ptr<CastableBase> frog = std::make_unique<Frog>();
+  std::unique_ptr<CastableBase> bear = std::make_unique<Bear>();
+  std::unique_ptr<CastableBase> gecko = std::make_unique<Gecko>();
+
+  ASSERT_TRUE(frog->Is<Animal>());
+  ASSERT_TRUE(bear->Is<Animal>());
+  ASSERT_TRUE(gecko->Is<Animal>());
+
+  ASSERT_TRUE(frog->Is<Amphibian>());
+  ASSERT_FALSE(bear->Is<Amphibian>());
+  ASSERT_FALSE(gecko->Is<Amphibian>());
+
+  ASSERT_FALSE(frog->Is<Mammal>());
+  ASSERT_TRUE(bear->Is<Mammal>());
+  ASSERT_FALSE(gecko->Is<Mammal>());
+
+  ASSERT_FALSE(frog->Is<Reptile>());
+  ASSERT_FALSE(bear->Is<Reptile>());
+  ASSERT_TRUE(gecko->Is<Reptile>());
+}
+
+TEST(CastableBase, Is_kDontErrorOnImpossibleCast) {
+  // Unlike TEST(CastableBase, Is), we're dynamically querying [A -> B] without
+  // going via CastableBase.
+  auto frog = std::make_unique<Frog>();
+  auto bear = std::make_unique<Bear>();
+  auto gecko = std::make_unique<Gecko>();
+
+  ASSERT_TRUE((frog->Is<Animal, kDontErrorOnImpossibleCast>()));
+  ASSERT_TRUE((bear->Is<Animal, kDontErrorOnImpossibleCast>()));
+  ASSERT_TRUE((gecko->Is<Animal, kDontErrorOnImpossibleCast>()));
+
+  ASSERT_TRUE((frog->Is<Amphibian, kDontErrorOnImpossibleCast>()));
+  ASSERT_FALSE((bear->Is<Amphibian, kDontErrorOnImpossibleCast>()));
+  ASSERT_FALSE((gecko->Is<Amphibian, kDontErrorOnImpossibleCast>()));
+
+  ASSERT_FALSE((frog->Is<Mammal, kDontErrorOnImpossibleCast>()));
+  ASSERT_TRUE((bear->Is<Mammal, kDontErrorOnImpossibleCast>()));
+  ASSERT_FALSE((gecko->Is<Mammal, kDontErrorOnImpossibleCast>()));
+
+  ASSERT_FALSE((frog->Is<Reptile, kDontErrorOnImpossibleCast>()));
+  ASSERT_FALSE((bear->Is<Reptile, kDontErrorOnImpossibleCast>()));
+  ASSERT_TRUE((gecko->Is<Reptile, kDontErrorOnImpossibleCast>()));
+}
+
+TEST(CastableBase, IsWithPredicate) {
+  std::unique_ptr<CastableBase> frog = std::make_unique<Frog>();
+
+  frog->Is<Animal>([&frog](const Animal* a) {
+    EXPECT_EQ(a, frog.get());
+    return true;
+  });
+
+  ASSERT_TRUE((frog->Is<Animal>([](const Animal*) { return true; })));
+  ASSERT_FALSE((frog->Is<Animal>([](const Animal*) { return false; })));
+
+  // Predicate not called if cast is invalid
+  auto expect_not_called = [] { FAIL() << "Should not be called"; };
+  ASSERT_FALSE((frog->Is<Bear>([&](const Animal*) {
+    expect_not_called();
+    return true;
+  })));
+}
+
+TEST(CastableBase, IsAnyOf) {
+  std::unique_ptr<CastableBase> frog = std::make_unique<Frog>();
+  std::unique_ptr<CastableBase> bear = std::make_unique<Bear>();
+  std::unique_ptr<CastableBase> gecko = std::make_unique<Gecko>();
+
+  ASSERT_TRUE((frog->IsAnyOf<Animal, Mammal, Amphibian, Reptile>()));
+  ASSERT_TRUE((frog->IsAnyOf<Mammal, Amphibian>()));
+  ASSERT_TRUE((frog->IsAnyOf<Amphibian, Reptile>()));
+  ASSERT_FALSE((frog->IsAnyOf<Mammal, Reptile>()));
+
+  ASSERT_TRUE((bear->IsAnyOf<Animal, Mammal, Amphibian, Reptile>()));
+  ASSERT_TRUE((bear->IsAnyOf<Mammal, Amphibian>()));
+  ASSERT_TRUE((bear->IsAnyOf<Mammal, Reptile>()));
+  ASSERT_FALSE((bear->IsAnyOf<Amphibian, Reptile>()));
+
+  ASSERT_TRUE((gecko->IsAnyOf<Animal, Mammal, Amphibian, Reptile>()));
+  ASSERT_TRUE((gecko->IsAnyOf<Mammal, Reptile>()));
+  ASSERT_TRUE((gecko->IsAnyOf<Amphibian, Reptile>()));
+  ASSERT_FALSE((gecko->IsAnyOf<Mammal, Amphibian>()));
+}
+
+TEST(CastableBase, As) {
+  std::unique_ptr<CastableBase> frog = std::make_unique<Frog>();
+  std::unique_ptr<CastableBase> bear = std::make_unique<Bear>();
+  std::unique_ptr<CastableBase> gecko = std::make_unique<Gecko>();
+
+  ASSERT_EQ(frog->As<Animal>(), static_cast<Animal*>(frog.get()));
+  ASSERT_EQ(bear->As<Animal>(), static_cast<Animal*>(bear.get()));
+  ASSERT_EQ(gecko->As<Animal>(), static_cast<Animal*>(gecko.get()));
+
+  ASSERT_EQ(frog->As<Amphibian>(), static_cast<Amphibian*>(frog.get()));
+  ASSERT_EQ(bear->As<Amphibian>(), nullptr);
+  ASSERT_EQ(gecko->As<Amphibian>(), nullptr);
+
+  ASSERT_EQ(frog->As<Mammal>(), nullptr);
+  ASSERT_EQ(bear->As<Mammal>(), static_cast<Mammal*>(bear.get()));
+  ASSERT_EQ(gecko->As<Mammal>(), nullptr);
+
+  ASSERT_EQ(frog->As<Reptile>(), nullptr);
+  ASSERT_EQ(bear->As<Reptile>(), nullptr);
+  ASSERT_EQ(gecko->As<Reptile>(), static_cast<Reptile*>(gecko.get()));
+}
+
+TEST(CastableBase, As_kDontErrorOnImpossibleCast) {
+  // Unlike TEST(CastableBase, As), we're dynamically casting [A -> B] without
+  // going via CastableBase.
+  auto frog = std::make_unique<Frog>();
+  auto bear = std::make_unique<Bear>();
+  auto gecko = std::make_unique<Gecko>();
+
+  ASSERT_EQ((frog->As<Animal, kDontErrorOnImpossibleCast>()),
+            static_cast<Animal*>(frog.get()));
+  ASSERT_EQ((bear->As<Animal, kDontErrorOnImpossibleCast>()),
+            static_cast<Animal*>(bear.get()));
+  ASSERT_EQ((gecko->As<Animal, kDontErrorOnImpossibleCast>()),
+            static_cast<Animal*>(gecko.get()));
+
+  ASSERT_EQ((frog->As<Amphibian, kDontErrorOnImpossibleCast>()),
+            static_cast<Amphibian*>(frog.get()));
+  ASSERT_EQ((bear->As<Amphibian, kDontErrorOnImpossibleCast>()), nullptr);
+  ASSERT_EQ((gecko->As<Amphibian, kDontErrorOnImpossibleCast>()), nullptr);
+
+  ASSERT_EQ((frog->As<Mammal, kDontErrorOnImpossibleCast>()), nullptr);
+  ASSERT_EQ((bear->As<Mammal, kDontErrorOnImpossibleCast>()),
+            static_cast<Mammal*>(bear.get()));
+  ASSERT_EQ((gecko->As<Mammal, kDontErrorOnImpossibleCast>()), nullptr);
+
+  ASSERT_EQ((frog->As<Reptile, kDontErrorOnImpossibleCast>()), nullptr);
+  ASSERT_EQ((bear->As<Reptile, kDontErrorOnImpossibleCast>()), nullptr);
+  ASSERT_EQ((gecko->As<Reptile, kDontErrorOnImpossibleCast>()),
+            static_cast<Reptile*>(gecko.get()));
+}
+
+TEST(Castable, Is) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+
+  ASSERT_TRUE(frog->Is<Animal>());
+  ASSERT_TRUE(bear->Is<Animal>());
+  ASSERT_TRUE(gecko->Is<Animal>());
+
+  ASSERT_TRUE(frog->Is<Amphibian>());
+  ASSERT_FALSE(bear->Is<Amphibian>());
+  ASSERT_FALSE(gecko->Is<Amphibian>());
+
+  ASSERT_FALSE(frog->Is<Mammal>());
+  ASSERT_TRUE(bear->Is<Mammal>());
+  ASSERT_FALSE(gecko->Is<Mammal>());
+
+  ASSERT_FALSE(frog->Is<Reptile>());
+  ASSERT_FALSE(bear->Is<Reptile>());
+  ASSERT_TRUE(gecko->Is<Reptile>());
+}
+
+TEST(Castable, IsWithPredicate) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+
+  frog->Is([&frog](const Animal* a) {
+    EXPECT_EQ(a, frog.get());
+    return true;
+  });
+
+  ASSERT_TRUE((frog->Is([](const Animal*) { return true; })));
+  ASSERT_FALSE((frog->Is([](const Animal*) { return false; })));
+
+  // Predicate not called if cast is invalid
+  auto expect_not_called = [] { FAIL() << "Should not be called"; };
+  ASSERT_FALSE((frog->Is([&](const Bear*) {
+    expect_not_called();
+    return true;
+  })));
+}
+
+TEST(Castable, As) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+
+  ASSERT_EQ(frog->As<Animal>(), static_cast<Animal*>(frog.get()));
+  ASSERT_EQ(bear->As<Animal>(), static_cast<Animal*>(bear.get()));
+  ASSERT_EQ(gecko->As<Animal>(), static_cast<Animal*>(gecko.get()));
+
+  ASSERT_EQ(frog->As<Amphibian>(), static_cast<Amphibian*>(frog.get()));
+  ASSERT_EQ(bear->As<Amphibian>(), nullptr);
+  ASSERT_EQ(gecko->As<Amphibian>(), nullptr);
+
+  ASSERT_EQ(frog->As<Mammal>(), nullptr);
+  ASSERT_EQ(bear->As<Mammal>(), static_cast<Mammal*>(bear.get()));
+  ASSERT_EQ(gecko->As<Mammal>(), nullptr);
+
+  ASSERT_EQ(frog->As<Reptile>(), nullptr);
+  ASSERT_EQ(bear->As<Reptile>(), nullptr);
+  ASSERT_EQ(gecko->As<Reptile>(), static_cast<Reptile*>(gecko.get()));
+}
+
+TEST(Castable, SwitchNoDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    bool frog_matched_amphibian = false;
+    Switch(
+        frog.get(),  //
+        [&](Reptile*) { FAIL() << "frog is not reptile"; },
+        [&](Mammal*) { FAIL() << "frog is not mammal"; },
+        [&](Amphibian* amphibian) {
+          EXPECT_EQ(amphibian, frog.get());
+          frog_matched_amphibian = true;
+        });
+    EXPECT_TRUE(frog_matched_amphibian);
+  }
+  {
+    bool bear_matched_mammal = false;
+    Switch(
+        bear.get(),  //
+        [&](Reptile*) { FAIL() << "bear is not reptile"; },
+        [&](Amphibian*) { FAIL() << "bear is not amphibian"; },
+        [&](Mammal* mammal) {
+          EXPECT_EQ(mammal, bear.get());
+          bear_matched_mammal = true;
+        });
+    EXPECT_TRUE(bear_matched_mammal);
+  }
+  {
+    bool gecko_matched_reptile = false;
+    Switch(
+        gecko.get(),  //
+        [&](Mammal*) { FAIL() << "gecko is not mammal"; },
+        [&](Amphibian*) { FAIL() << "gecko is not amphibian"; },
+        [&](Reptile* reptile) {
+          EXPECT_EQ(reptile, gecko.get());
+          gecko_matched_reptile = true;
+        });
+    EXPECT_TRUE(gecko_matched_reptile);
+  }
+}
+
+TEST(Castable, SwitchWithUnusedDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    bool frog_matched_amphibian = false;
+    Switch(
+        frog.get(),  //
+        [&](Reptile*) { FAIL() << "frog is not reptile"; },
+        [&](Mammal*) { FAIL() << "frog is not mammal"; },
+        [&](Amphibian* amphibian) {
+          EXPECT_EQ(amphibian, frog.get());
+          frog_matched_amphibian = true;
+        },
+        [&](Default) { FAIL() << "default should not have been selected"; });
+    EXPECT_TRUE(frog_matched_amphibian);
+  }
+  {
+    bool bear_matched_mammal = false;
+    Switch(
+        bear.get(),  //
+        [&](Reptile*) { FAIL() << "bear is not reptile"; },
+        [&](Amphibian*) { FAIL() << "bear is not amphibian"; },
+        [&](Mammal* mammal) {
+          EXPECT_EQ(mammal, bear.get());
+          bear_matched_mammal = true;
+        },
+        [&](Default) { FAIL() << "default should not have been selected"; });
+    EXPECT_TRUE(bear_matched_mammal);
+  }
+  {
+    bool gecko_matched_reptile = false;
+    Switch(
+        gecko.get(),  //
+        [&](Mammal*) { FAIL() << "gecko is not mammal"; },
+        [&](Amphibian*) { FAIL() << "gecko is not amphibian"; },
+        [&](Reptile* reptile) {
+          EXPECT_EQ(reptile, gecko.get());
+          gecko_matched_reptile = true;
+        },
+        [&](Default) { FAIL() << "default should not have been selected"; });
+    EXPECT_TRUE(gecko_matched_reptile);
+  }
+}
+
+TEST(Castable, SwitchDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    bool frog_matched_default = false;
+    Switch(
+        frog.get(),  //
+        [&](Reptile*) { FAIL() << "frog is not reptile"; },
+        [&](Mammal*) { FAIL() << "frog is not mammal"; },
+        [&](Default) { frog_matched_default = true; });
+    EXPECT_TRUE(frog_matched_default);
+  }
+  {
+    bool bear_matched_default = false;
+    Switch(
+        bear.get(),  //
+        [&](Reptile*) { FAIL() << "bear is not reptile"; },
+        [&](Amphibian*) { FAIL() << "bear is not amphibian"; },
+        [&](Default) { bear_matched_default = true; });
+    EXPECT_TRUE(bear_matched_default);
+  }
+  {
+    bool gecko_matched_default = false;
+    Switch(
+        gecko.get(),  //
+        [&](Mammal*) { FAIL() << "gecko is not mammal"; },
+        [&](Amphibian*) { FAIL() << "gecko is not amphibian"; },
+        [&](Default) { gecko_matched_default = true; });
+    EXPECT_TRUE(gecko_matched_default);
+  }
+}
+
+TEST(Castable, SwitchMatchFirst) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  {
+    bool frog_matched_animal = false;
+    Switch(
+        frog.get(),
+        [&](Animal* animal) {
+          EXPECT_EQ(animal, frog.get());
+          frog_matched_animal = true;
+        },
+        [&](Amphibian*) { FAIL() << "animal should have been matched first"; });
+    EXPECT_TRUE(frog_matched_animal);
+  }
+  {
+    bool frog_matched_amphibian = false;
+    Switch(
+        frog.get(),
+        [&](Amphibian* amphibain) {
+          EXPECT_EQ(amphibain, frog.get());
+          frog_matched_amphibian = true;
+        },
+        [&](Animal*) { FAIL() << "amphibian should have been matched first"; });
+    EXPECT_TRUE(frog_matched_amphibian);
+  }
+}
+
+TEST(Castable, SwitchReturnValueWithDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    const char* result = Switch(
+        frog.get(),                              //
+        [](Mammal*) { return "mammal"; },        //
+        [](Amphibian*) { return "amphibian"; },  //
+        [](Default) { return "unknown"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(std::string(result), "amphibian");
+  }
+  {
+    const char* result = Switch(
+        bear.get(),                              //
+        [](Mammal*) { return "mammal"; },        //
+        [](Amphibian*) { return "amphibian"; },  //
+        [](Default) { return "unknown"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(std::string(result), "mammal");
+  }
+  {
+    const char* result = Switch(
+        gecko.get(),                             //
+        [](Mammal*) { return "mammal"; },        //
+        [](Amphibian*) { return "amphibian"; },  //
+        [](Default) { return "unknown"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(std::string(result), "unknown");
+  }
+}
+
+TEST(Castable, SwitchReturnValueWithoutDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    const char* result = Switch(
+        frog.get(),                        //
+        [](Mammal*) { return "mammal"; },  //
+        [](Amphibian*) { return "amphibian"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(std::string(result), "amphibian");
+  }
+  {
+    const char* result = Switch(
+        bear.get(),                        //
+        [](Mammal*) { return "mammal"; },  //
+        [](Amphibian*) { return "amphibian"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(std::string(result), "mammal");
+  }
+  {
+    auto* result = Switch(
+        gecko.get(),                       //
+        [](Mammal*) { return "mammal"; },  //
+        [](Amphibian*) { return "amphibian"; });
+    static_assert(std::is_same_v<decltype(result), const char*>);
+    EXPECT_EQ(result, nullptr);
+  }
+}
+
+TEST(Castable, SwitchInferPODReturnTypeWithDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto result = Switch(
+        frog.get(),                       //
+        [](Mammal*) { return 1; },        //
+        [](Amphibian*) { return 2.0f; },  //
+        [](Default) { return 3.0; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 2.0);
+  }
+  {
+    auto result = Switch(
+        bear.get(),                       //
+        [](Mammal*) { return 1.0; },      //
+        [](Amphibian*) { return 2.0f; },  //
+        [](Default) { return 3; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 1.0);
+  }
+  {
+    auto result = Switch(
+        gecko.get(),                   //
+        [](Mammal*) { return 1.0f; },  //
+        [](Amphibian*) { return 2; },  //
+        [](Default) { return 3.0; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 3.0);
+  }
+}
+
+TEST(Castable, SwitchInferPODReturnTypeWithoutDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto result = Switch(
+        frog.get(),                 //
+        [](Mammal*) { return 1; },  //
+        [](Amphibian*) { return 2.0f; });
+    static_assert(std::is_same_v<decltype(result), float>);
+    EXPECT_EQ(result, 2.0f);
+  }
+  {
+    auto result = Switch(
+        bear.get(),                    //
+        [](Mammal*) { return 1.0f; },  //
+        [](Amphibian*) { return 2; });
+    static_assert(std::is_same_v<decltype(result), float>);
+    EXPECT_EQ(result, 1.0f);
+  }
+  {
+    auto result = Switch(
+        gecko.get(),                  //
+        [](Mammal*) { return 1.0; },  //
+        [](Amphibian*) { return 2.0f; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 0.0);
+  }
+}
+
+TEST(Castable, SwitchInferCastableReturnTypeWithDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto* result = Switch(
+        frog.get(),                          //
+        [](Mammal* p) { return p; },         //
+        [](Amphibian*) { return nullptr; },  //
+        [](Default) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), Mammal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+  {
+    auto* result = Switch(
+        bear.get(),                   //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian* p) { return const_cast<const Amphibian*>(p); },
+        [](Default) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), const Animal*>);
+    EXPECT_EQ(result, bear.get());
+  }
+  {
+    auto* result = Switch(
+        gecko.get(),                     //
+        [](Mammal* p) { return p; },     //
+        [](Amphibian* p) { return p; },  //
+        [](Default) -> CastableBase* { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), CastableBase*>);
+    EXPECT_EQ(result, nullptr);
+  }
+}
+
+TEST(Castable, SwitchInferCastableReturnTypeWithoutDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto* result = Switch(
+        frog.get(),                   //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian*) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), Mammal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+  {
+    auto* result = Switch(
+        bear.get(),                                                     //
+        [](Mammal* p) { return p; },                                    //
+        [](Amphibian* p) { return const_cast<const Amphibian*>(p); });  //
+    static_assert(std::is_same_v<decltype(result), const Animal*>);
+    EXPECT_EQ(result, bear.get());
+  }
+  {
+    auto* result = Switch(
+        gecko.get(),                  //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian* p) { return p; });
+    static_assert(std::is_same_v<decltype(result), Animal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+}
+
+TEST(Castable, SwitchExplicitPODReturnTypeWithDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto result = Switch<double>(
+        frog.get(),                       //
+        [](Mammal*) { return 1; },        //
+        [](Amphibian*) { return 2.0f; },  //
+        [](Default) { return 3.0; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 2.0f);
+  }
+  {
+    auto result = Switch<double>(
+        bear.get(),                    //
+        [](Mammal*) { return 1; },     //
+        [](Amphibian*) { return 2; },  //
+        [](Default) { return 3; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 1.0f);
+  }
+  {
+    auto result = Switch<double>(
+        gecko.get(),                      //
+        [](Mammal*) { return 1.0f; },     //
+        [](Amphibian*) { return 2.0f; },  //
+        [](Default) { return 3.0f; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 3.0f);
+  }
+}
+
+TEST(Castable, SwitchExplicitPODReturnTypeWithoutDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto result = Switch<double>(
+        frog.get(),                 //
+        [](Mammal*) { return 1; },  //
+        [](Amphibian*) { return 2.0f; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 2.0f);
+  }
+  {
+    auto result = Switch<double>(
+        bear.get(),                    //
+        [](Mammal*) { return 1.0f; },  //
+        [](Amphibian*) { return 2; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 1.0f);
+  }
+  {
+    auto result = Switch<double>(
+        gecko.get(),                  //
+        [](Mammal*) { return 1.0; },  //
+        [](Amphibian*) { return 2.0f; });
+    static_assert(std::is_same_v<decltype(result), double>);
+    EXPECT_EQ(result, 0.0);
+  }
+}
+
+TEST(Castable, SwitchExplicitCastableReturnTypeWithDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto* result = Switch<Animal>(
+        frog.get(),                          //
+        [](Mammal* p) { return p; },         //
+        [](Amphibian*) { return nullptr; },  //
+        [](Default) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), Animal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+  {
+    auto* result = Switch<CastableBase>(
+        bear.get(),                   //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian* p) { return const_cast<const Amphibian*>(p); },
+        [](Default) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), const CastableBase*>);
+    EXPECT_EQ(result, bear.get());
+  }
+  {
+    auto* result = Switch<const Animal>(
+        gecko.get(),                     //
+        [](Mammal* p) { return p; },     //
+        [](Amphibian* p) { return p; },  //
+        [](Default) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), const Animal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+}
+
+TEST(Castable, SwitchExplicitCastableReturnTypeWithoutDefault) {
+  std::unique_ptr<Animal> frog = std::make_unique<Frog>();
+  std::unique_ptr<Animal> bear = std::make_unique<Bear>();
+  std::unique_ptr<Animal> gecko = std::make_unique<Gecko>();
+  {
+    auto* result = Switch<Animal>(
+        frog.get(),                   //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian*) { return nullptr; });
+    static_assert(std::is_same_v<decltype(result), Animal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+  {
+    auto* result = Switch<CastableBase>(
+        bear.get(),                                                     //
+        [](Mammal* p) { return p; },                                    //
+        [](Amphibian* p) { return const_cast<const Amphibian*>(p); });  //
+    static_assert(std::is_same_v<decltype(result), const CastableBase*>);
+    EXPECT_EQ(result, bear.get());
+  }
+  {
+    auto* result = Switch<const Animal*>(
+        gecko.get(),                  //
+        [](Mammal* p) { return p; },  //
+        [](Amphibian* p) { return p; });
+    static_assert(std::is_same_v<decltype(result), const Animal*>);
+    EXPECT_EQ(result, nullptr);
+  }
+}
+
+TEST(Castable, SwitchNull) {
+  Animal* null = nullptr;
+  Switch(
+      null,  //
+      [&](Amphibian*) { FAIL() << "should not be called"; },
+      [&](Animal*) { FAIL() << "should not be called"; });
+}
+
+TEST(Castable, SwitchNullNoDefault) {
+  Animal* null = nullptr;
+  bool default_called = false;
+  Switch(
+      null,  //
+      [&](Amphibian*) { FAIL() << "should not be called"; },
+      [&](Animal*) { FAIL() << "should not be called"; },
+      [&](Default) { default_called = true; });
+  EXPECT_TRUE(default_called);
+}
+
+// IsCastable static tests
+static_assert(IsCastable<CastableBase>);
+static_assert(IsCastable<Animal>);
+static_assert(IsCastable<Ignore, Frog, Bear>);
+static_assert(IsCastable<Mammal, Ignore, Amphibian, Gecko>);
+static_assert(!IsCastable<Mammal, int, Amphibian, Ignore, Gecko>);
+static_assert(!IsCastable<bool>);
+static_assert(!IsCastable<int, float>);
+static_assert(!IsCastable<Ignore>);
+
+// CastableCommonBase static tests
+static_assert(std::is_same_v<Animal, CastableCommonBase<Animal>>);
+static_assert(std::is_same_v<Amphibian, CastableCommonBase<Amphibian>>);
+static_assert(std::is_same_v<Mammal, CastableCommonBase<Mammal>>);
+static_assert(std::is_same_v<Reptile, CastableCommonBase<Reptile>>);
+static_assert(std::is_same_v<Frog, CastableCommonBase<Frog>>);
+static_assert(std::is_same_v<Bear, CastableCommonBase<Bear>>);
+static_assert(std::is_same_v<Lizard, CastableCommonBase<Lizard>>);
+static_assert(std::is_same_v<Gecko, CastableCommonBase<Gecko>>);
+static_assert(std::is_same_v<Iguana, CastableCommonBase<Iguana>>);
+
+static_assert(std::is_same_v<Animal, CastableCommonBase<Animal, Animal>>);
+static_assert(
+    std::is_same_v<Amphibian, CastableCommonBase<Amphibian, Amphibian>>);
+static_assert(std::is_same_v<Mammal, CastableCommonBase<Mammal, Mammal>>);
+static_assert(std::is_same_v<Reptile, CastableCommonBase<Reptile, Reptile>>);
+static_assert(std::is_same_v<Frog, CastableCommonBase<Frog, Frog>>);
+static_assert(std::is_same_v<Bear, CastableCommonBase<Bear, Bear>>);
+static_assert(std::is_same_v<Lizard, CastableCommonBase<Lizard, Lizard>>);
+static_assert(std::is_same_v<Gecko, CastableCommonBase<Gecko, Gecko>>);
+static_assert(std::is_same_v<Iguana, CastableCommonBase<Iguana, Iguana>>);
+
+static_assert(
+    std::is_same_v<CastableBase, CastableCommonBase<CastableBase, Animal>>);
+static_assert(
+    std::is_same_v<CastableBase, CastableCommonBase<Animal, CastableBase>>);
+static_assert(std::is_same_v<Amphibian, CastableCommonBase<Amphibian, Frog>>);
+static_assert(std::is_same_v<Amphibian, CastableCommonBase<Frog, Amphibian>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Reptile, Frog>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Frog, Reptile>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Bear, Frog>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Frog, Bear>>);
+static_assert(std::is_same_v<Lizard, CastableCommonBase<Gecko, Iguana>>);
+
+static_assert(std::is_same_v<Animal, CastableCommonBase<Bear, Frog, Iguana>>);
+static_assert(
+    std::is_same_v<Lizard, CastableCommonBase<Lizard, Gecko, Iguana>>);
+static_assert(
+    std::is_same_v<Lizard, CastableCommonBase<Gecko, Iguana, Lizard>>);
+static_assert(
+    std::is_same_v<Lizard, CastableCommonBase<Gecko, Lizard, Iguana>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Frog, Gecko, Iguana>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Gecko, Iguana, Frog>>);
+static_assert(std::is_same_v<Animal, CastableCommonBase<Gecko, Frog, Iguana>>);
+
+static_assert(
+    std::is_same_v<CastableBase,
+                   CastableCommonBase<Bear, Frog, Iguana, CastableBase>>);
+
+}  // namespace
+
+TINT_INSTANTIATE_TYPEINFO(Animal);
+TINT_INSTANTIATE_TYPEINFO(Amphibian);
+TINT_INSTANTIATE_TYPEINFO(Mammal);
+TINT_INSTANTIATE_TYPEINFO(Reptile);
+TINT_INSTANTIATE_TYPEINFO(Frog);
+TINT_INSTANTIATE_TYPEINFO(Bear);
+TINT_INSTANTIATE_TYPEINFO(Lizard);
+TINT_INSTANTIATE_TYPEINFO(Gecko);
+
+}  // namespace tint
diff --git a/src/tint/clone_context.cc b/src/tint/clone_context.cc
new file mode 100644
index 0000000..afdf488
--- /dev/null
+++ b/src/tint/clone_context.cc
@@ -0,0 +1,116 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/clone_context.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+#include "src/tint/utils/map.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::Cloneable);
+
+namespace tint {
+
+CloneContext::ListTransforms::ListTransforms() = default;
+CloneContext::ListTransforms::~ListTransforms() = default;
+
+CloneContext::CloneContext(ProgramBuilder* to,
+                           Program const* from,
+                           bool auto_clone_symbols)
+    : dst(to), src(from) {
+  if (auto_clone_symbols) {
+    // Almost all transforms will want to clone all symbols before doing any
+    // work, to avoid any newly created symbols clashing with existing symbols
+    // in the source program and causing them to be renamed.
+    from->Symbols().Foreach([&](Symbol s, const std::string&) { Clone(s); });
+  }
+}
+
+CloneContext::CloneContext(ProgramBuilder* builder)
+    : CloneContext(builder, nullptr, false) {}
+
+CloneContext::~CloneContext() = default;
+
+Symbol CloneContext::Clone(Symbol s) {
+  if (!src) {
+    return s;  // In-place clone
+  }
+  return utils::GetOrCreate(cloned_symbols_, s, [&]() -> Symbol {
+    if (symbol_transform_) {
+      return symbol_transform_(s);
+    }
+    return dst->Symbols().New(src->Symbols().NameFor(s));
+  });
+}
+
+void CloneContext::Clone() {
+  dst->AST().Copy(this, &src->AST());
+}
+
+ast::FunctionList CloneContext::Clone(const ast::FunctionList& v) {
+  ast::FunctionList out;
+  out.reserve(v.size());
+  for (const ast::Function* el : v) {
+    out.Add(Clone(el));
+  }
+  return out;
+}
+
+const tint::Cloneable* CloneContext::CloneCloneable(const Cloneable* object) {
+  // If the input is nullptr, there's nothing to clone - just return nullptr.
+  if (object == nullptr) {
+    return nullptr;
+  }
+
+  // Was Replace() called for this object?
+  auto it = replacements_.find(object);
+  if (it != replacements_.end()) {
+    return it->second();
+  }
+
+  // Attempt to clone using the registered replacer functions.
+  auto& typeinfo = object->TypeInfo();
+  for (auto& transform : transforms_) {
+    if (typeinfo.Is(transform.typeinfo)) {
+      if (auto* transformed = transform.function(object)) {
+        return transformed;
+      }
+      break;
+    }
+  }
+
+  // No transform for this type, or the transform returned nullptr.
+  // Clone with T::Clone().
+  return object->Clone(this);
+}
+
+void CloneContext::CheckedCastFailure(const Cloneable* got,
+                                      const TypeInfo& expected) {
+  TINT_ICE(Clone, Diagnostics())
+      << "Cloned object was not of the expected type\n"
+      << "got:      " << got->TypeInfo().name << "\n"
+      << "expected: " << expected.name;
+}
+
+diag::List& CloneContext::Diagnostics() const {
+  return dst->Diagnostics();
+}
+
+CloneContext::CloneableTransform::CloneableTransform() = default;
+CloneContext::CloneableTransform::CloneableTransform(
+    const CloneableTransform&) = default;
+CloneContext::CloneableTransform::~CloneableTransform() = default;
+
+}  // namespace tint
diff --git a/src/tint/clone_context.h b/src/tint/clone_context.h
new file mode 100644
index 0000000..8c02c83
--- /dev/null
+++ b/src/tint/clone_context.h
@@ -0,0 +1,584 @@
+// Copyright 2020 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_CLONE_CONTEXT_H_
+#define SRC_TINT_CLONE_CONTEXT_H_
+
+#include <algorithm>
+#include <functional>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "src/tint/castable.h"
+#include "src/tint/debug.h"
+#include "src/tint/program_id.h"
+#include "src/tint/symbol.h"
+#include "src/tint/traits.h"
+
+namespace tint {
+
+// Forward declarations
+class CloneContext;
+class Program;
+class ProgramBuilder;
+namespace ast {
+class FunctionList;
+class Node;
+}  // namespace ast
+
+ProgramID ProgramIDOf(const Program*);
+ProgramID ProgramIDOf(const ProgramBuilder*);
+
+/// Cloneable is the base class for all objects that can be cloned
+class Cloneable : public Castable<Cloneable> {
+ public:
+  /// Performs a deep clone of this object using the CloneContext `ctx`.
+  /// @param ctx the clone context
+  /// @return the newly cloned object
+  virtual const Cloneable* Clone(CloneContext* ctx) const = 0;
+};
+
+/// @returns an invalid ProgramID
+inline ProgramID ProgramIDOf(const Cloneable*) {
+  return ProgramID();
+}
+
+/// CloneContext holds the state used while cloning AST nodes.
+class CloneContext {
+  /// ParamTypeIsPtrOf<F, T> is true iff the first parameter of
+  /// F is a pointer of (or derives from) type T.
+  template <typename F, typename T>
+  static constexpr bool ParamTypeIsPtrOf = traits::IsTypeOrDerived<
+      typename std::remove_pointer<traits::ParameterType<F, 0>>::type,
+      T>;
+
+ public:
+  /// SymbolTransform is a function that takes a symbol and returns a new
+  /// symbol.
+  using SymbolTransform = std::function<Symbol(Symbol)>;
+
+  /// Constructor for cloning objects from `from` into `to`.
+  /// @param to the target ProgramBuilder to clone into
+  /// @param from the source Program to clone from
+  /// @param auto_clone_symbols clone all symbols in `from` before returning
+  CloneContext(ProgramBuilder* to,
+               Program const* from,
+               bool auto_clone_symbols = true);
+
+  /// Constructor for cloning objects from and to the ProgramBuilder `builder`.
+  /// @param builder the ProgramBuilder
+  explicit CloneContext(ProgramBuilder* builder);
+
+  /// Destructor
+  ~CloneContext();
+
+  /// Clones the Node or sem::Type `a` into the ProgramBuilder #dst if `a` is
+  /// not null. If `a` is null, then Clone() returns null.
+  ///
+  /// Clone() may use a function registered with ReplaceAll() to create a
+  /// transformed version of the object. See ReplaceAll() for more information.
+  ///
+  /// If the CloneContext is cloning from a Program to a ProgramBuilder, then
+  /// the Node or sem::Type `a` must be owned by the Program #src.
+  ///
+  /// @param object the type deriving from Cloneable to clone
+  /// @return the cloned node
+  template <typename T>
+  const T* Clone(const T* object) {
+    if (src) {
+      TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, object);
+    }
+    if (auto* cloned = CloneCloneable(object)) {
+      auto* out = CheckedCast<T>(cloned);
+      TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, out);
+      return out;
+    }
+    return nullptr;
+  }
+
+  /// Clones the Node or sem::Type `a` into the ProgramBuilder #dst if `a` is
+  /// not null. If `a` is null, then Clone() returns null.
+  ///
+  /// Unlike Clone(), this method does not invoke or use any transformations
+  /// registered by ReplaceAll().
+  ///
+  /// If the CloneContext is cloning from a Program to a ProgramBuilder, then
+  /// the Node or sem::Type `a` must be owned by the Program #src.
+  ///
+  /// @param a the type deriving from Cloneable to clone
+  /// @return the cloned node
+  template <typename T>
+  const T* CloneWithoutTransform(const T* a) {
+    // If the input is nullptr, there's nothing to clone - just return nullptr.
+    if (a == nullptr) {
+      return nullptr;
+    }
+    if (src) {
+      TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, a);
+    }
+    auto* c = a->Clone(this);
+    return CheckedCast<T>(c);
+  }
+
+  /// Clones the Source `s` into #dst
+  /// TODO(bclayton) - Currently this 'clone' is a shallow copy. If/when
+  /// `Source.File`s are owned by the Program this should make a copy of the
+  /// file.
+  /// @param s the `Source` to clone
+  /// @return the cloned source
+  Source Clone(const Source& s) const { return s; }
+
+  /// Clones the Symbol `s` into #dst
+  ///
+  /// The Symbol `s` must be owned by the Program #src.
+  ///
+  /// @param s the Symbol to clone
+  /// @return the cloned source
+  Symbol Clone(Symbol s);
+
+  /// Clones each of the elements of the vector `v` into the ProgramBuilder
+  /// #dst.
+  ///
+  /// All the elements of the vector `v` must be owned by the Program #src.
+  ///
+  /// @param v the vector to clone
+  /// @return the cloned vector
+  template <typename T>
+  std::vector<T> Clone(const std::vector<T>& v) {
+    std::vector<T> out;
+    out.reserve(v.size());
+    for (auto& el : v) {
+      out.emplace_back(Clone(el));
+    }
+    return out;
+  }
+
+  /// Clones each of the elements of the vector `v` using the ProgramBuilder
+  /// #dst, inserting any additional elements into the list that were registered
+  /// with calls to InsertBefore().
+  ///
+  /// All the elements of the vector `v` must be owned by the Program #src.
+  ///
+  /// @param v the vector to clone
+  /// @return the cloned vector
+  template <typename T>
+  std::vector<T*> Clone(const std::vector<T*>& v) {
+    std::vector<T*> out;
+    Clone(out, v);
+    return out;
+  }
+
+  /// Clones each of the elements of the vector `from` into the vector `to`,
+  /// inserting any additional elements into the list that were registered with
+  /// calls to InsertBefore().
+  ///
+  /// All the elements of the vector `from` must be owned by the Program #src.
+  ///
+  /// @param from the vector to clone
+  /// @param to the cloned result
+  template <typename T>
+  void Clone(std::vector<T*>& to, const std::vector<T*>& from) {
+    to.reserve(from.size());
+
+    auto list_transform_it = list_transforms_.find(&from);
+    if (list_transform_it != list_transforms_.end()) {
+      const auto& transforms = list_transform_it->second;
+      for (auto* o : transforms.insert_front_) {
+        to.emplace_back(CheckedCast<T>(o));
+      }
+      for (auto& el : from) {
+        auto insert_before_it = transforms.insert_before_.find(el);
+        if (insert_before_it != transforms.insert_before_.end()) {
+          for (auto insert : insert_before_it->second) {
+            to.emplace_back(CheckedCast<T>(insert));
+          }
+        }
+        if (transforms.remove_.count(el) == 0) {
+          to.emplace_back(Clone(el));
+        }
+        auto insert_after_it = transforms.insert_after_.find(el);
+        if (insert_after_it != transforms.insert_after_.end()) {
+          for (auto insert : insert_after_it->second) {
+            to.emplace_back(CheckedCast<T>(insert));
+          }
+        }
+      }
+      for (auto* o : transforms.insert_back_) {
+        to.emplace_back(CheckedCast<T>(o));
+      }
+    } else {
+      for (auto& el : from) {
+        to.emplace_back(Clone(el));
+
+        // Clone(el) may have inserted after
+        list_transform_it = list_transforms_.find(&from);
+        if (list_transform_it != list_transforms_.end()) {
+          const auto& transforms = list_transform_it->second;
+
+          auto insert_after_it = transforms.insert_after_.find(el);
+          if (insert_after_it != transforms.insert_after_.end()) {
+            for (auto insert : insert_after_it->second) {
+              to.emplace_back(CheckedCast<T>(insert));
+            }
+          }
+        }
+      }
+
+      // Clone(el)s may have inserted back
+      list_transform_it = list_transforms_.find(&from);
+      if (list_transform_it != list_transforms_.end()) {
+        const auto& transforms = list_transform_it->second;
+
+        for (auto* o : transforms.insert_back_) {
+          to.emplace_back(CheckedCast<T>(o));
+        }
+      }
+    }
+  }
+
+  /// Clones each of the elements of the vector `v` into the ProgramBuilder
+  /// #dst.
+  ///
+  /// All the elements of the vector `v` must be owned by the Program #src.
+  ///
+  /// @param v the vector to clone
+  /// @return the cloned vector
+  ast::FunctionList Clone(const ast::FunctionList& v);
+
+  /// ReplaceAll() registers `replacer` to be called whenever the Clone() method
+  /// is called with a Cloneable type that matches (or derives from) the type of
+  /// the single parameter of `replacer`.
+  /// The returned Cloneable of `replacer` will be used as the replacement for
+  /// all references to the object that's being cloned. This returned Cloneable
+  /// must be owned by the Program #dst.
+  ///
+  /// `replacer` must be function-like with the signature: `T* (T*)`
+  ///  where `T` is a type deriving from Cloneable.
+  ///
+  /// If `replacer` returns a nullptr then Clone() will call `T::Clone()` to
+  /// clone the object.
+  ///
+  /// Example:
+  ///
+  /// ```
+  ///   // Replace all ast::UintLiteralExpressions with the number 42
+  ///   CloneCtx ctx(&out, in);
+  ///   ctx.ReplaceAll([&] (ast::UintLiteralExpression* l) {
+  ///       return ctx->dst->create<ast::UintLiteralExpression>(
+  ///           ctx->Clone(l->source),
+  ///           ctx->Clone(l->type),
+  ///           42);
+  ///     });
+  ///   ctx.Clone();
+  /// ```
+  ///
+  /// @warning a single handler can only be registered for any given type.
+  /// Attempting to register two handlers for the same type will result in an
+  /// ICE.
+  /// @warning The replacement object must be of the correct type for all
+  /// references of the original object. A type mismatch will result in an
+  /// assertion in debug builds, and undefined behavior in release builds.
+  /// @param replacer a function or function-like object with the signature
+  ///        `T* (T*)`, where `T` derives from Cloneable
+  /// @returns this CloneContext so calls can be chained
+  template <typename F>
+  traits::EnableIf<ParamTypeIsPtrOf<F, Cloneable>, CloneContext>& ReplaceAll(
+      F&& replacer) {
+    using TPtr = traits::ParameterType<F, 0>;
+    using T = typename std::remove_pointer<TPtr>::type;
+    for (auto& transform : transforms_) {
+      if (transform.typeinfo->Is(&TypeInfo::Of<T>()) ||
+          TypeInfo::Of<T>().Is(transform.typeinfo)) {
+        TINT_ICE(Clone, Diagnostics())
+            << "ReplaceAll() called with a handler for type "
+            << TypeInfo::Of<T>().name
+            << " that is already handled by a handler for type "
+            << transform.typeinfo->name;
+        return *this;
+      }
+    }
+    CloneableTransform transform;
+    transform.typeinfo = &TypeInfo::Of<T>();
+    transform.function = [=](const Cloneable* in) {
+      return replacer(in->As<T>());
+    };
+    transforms_.emplace_back(std::move(transform));
+    return *this;
+  }
+
+  /// ReplaceAll() registers `replacer` to be called whenever the Clone() method
+  /// is called with a Symbol.
+  /// The returned symbol of `replacer` will be used as the replacement for
+  /// all references to the symbol that's being cloned. This returned Symbol
+  /// must be owned by the Program #dst.
+  /// @param replacer a function the signature `Symbol(Symbol)`.
+  /// @warning a SymbolTransform can only be registered once. Attempting to
+  /// register a SymbolTransform more than once will result in an ICE.
+  /// @returns this CloneContext so calls can be chained
+  CloneContext& ReplaceAll(const SymbolTransform& replacer) {
+    if (symbol_transform_) {
+      TINT_ICE(Clone, Diagnostics())
+          << "ReplaceAll(const SymbolTransform&) called "
+             "multiple times on the same CloneContext";
+      return *this;
+    }
+    symbol_transform_ = replacer;
+    return *this;
+  }
+
+  /// Replace replaces all occurrences of `what` in #src with the pointer `with`
+  /// in #dst when calling Clone().
+  /// [DEPRECATED]: This function cannot handle nested replacements. Use the
+  /// overload of Replace() that take a function for the `WITH` argument.
+  /// @param what a pointer to the object in #src that will be replaced with
+  /// `with`
+  /// @param with a pointer to the replacement object owned by #dst that will be
+  /// used as a replacement for `what`
+  /// @warning The replacement object must be of the correct type for all
+  /// references of the original object. A type mismatch will result in an
+  /// assertion in debug builds, and undefined behavior in release builds.
+  /// @returns this CloneContext so calls can be chained
+  template <typename WHAT,
+            typename WITH,
+            typename = traits::EnableIfIsType<WITH, Cloneable>>
+  CloneContext& Replace(const WHAT* what, const WITH* with) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, with);
+    replacements_[what] = [with]() -> const Cloneable* { return with; };
+    return *this;
+  }
+
+  /// Replace replaces all occurrences of `what` in #src with the result of the
+  /// function `with` in #dst when calling Clone(). `with` will be called each
+  /// time `what` is cloned by this context. If `what` is not cloned, then
+  /// `with` may never be called.
+  /// @param what a pointer to the object in #src that will be replaced with
+  /// `with`
+  /// @param with a function that takes no arguments and returns a pointer to
+  /// the replacement object owned by #dst. The returned pointer will be used as
+  /// a replacement for `what`.
+  /// @warning The replacement object must be of the correct type for all
+  /// references of the original object. A type mismatch will result in an
+  /// assertion in debug builds, and undefined behavior in release builds.
+  /// @returns this CloneContext so calls can be chained
+  template <typename WHAT, typename WITH, typename = std::result_of_t<WITH()>>
+  CloneContext& Replace(const WHAT* what, WITH&& with) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, what);
+    replacements_[what] = with;
+    return *this;
+  }
+
+  /// Removes `object` from the cloned copy of `vector`.
+  /// @param vector the vector in #src
+  /// @param object a pointer to the object in #src that will be omitted from
+  /// the cloned vector.
+  /// @returns this CloneContext so calls can be chained
+  template <typename T, typename OBJECT>
+  CloneContext& Remove(const std::vector<T>& vector, OBJECT* object) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, object);
+    if (std::find(vector.begin(), vector.end(), object) == vector.end()) {
+      TINT_ICE(Clone, Diagnostics())
+          << "CloneContext::Remove() vector does not contain object";
+      return *this;
+    }
+
+    list_transforms_[&vector].remove_.emplace(object);
+    return *this;
+  }
+
+  /// Inserts `object` before any other objects of `vector`, when it is cloned.
+  /// @param vector the vector in #src
+  /// @param object a pointer to the object in #dst that will be inserted at the
+  /// front of the vector
+  /// @returns this CloneContext so calls can be chained
+  template <typename T, typename OBJECT>
+  CloneContext& InsertFront(const std::vector<T>& vector, OBJECT* object) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
+    auto& transforms = list_transforms_[&vector];
+    auto& list = transforms.insert_front_;
+    list.emplace_back(object);
+    return *this;
+  }
+
+  /// Inserts `object` after any other objects of `vector`, when it is cloned.
+  /// @param vector the vector in #src
+  /// @param object a pointer to the object in #dst that will be inserted at the
+  /// end of the vector
+  /// @returns this CloneContext so calls can be chained
+  template <typename T, typename OBJECT>
+  CloneContext& InsertBack(const std::vector<T>& vector, OBJECT* object) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
+    auto& transforms = list_transforms_[&vector];
+    auto& list = transforms.insert_back_;
+    list.emplace_back(object);
+    return *this;
+  }
+
+  /// Inserts `object` before `before` whenever `vector` is cloned.
+  /// @param vector the vector in #src
+  /// @param before a pointer to the object in #src
+  /// @param object a pointer to the object in #dst that will be inserted before
+  /// any occurrence of the clone of `before`
+  /// @returns this CloneContext so calls can be chained
+  template <typename T, typename BEFORE, typename OBJECT>
+  CloneContext& InsertBefore(const std::vector<T>& vector,
+                             const BEFORE* before,
+                             const OBJECT* object) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, before);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
+    if (std::find(vector.begin(), vector.end(), before) == vector.end()) {
+      TINT_ICE(Clone, Diagnostics())
+          << "CloneContext::InsertBefore() vector does not contain before";
+      return *this;
+    }
+
+    auto& transforms = list_transforms_[&vector];
+    auto& list = transforms.insert_before_[before];
+    list.emplace_back(object);
+    return *this;
+  }
+
+  /// Inserts `object` after `after` whenever `vector` is cloned.
+  /// @param vector the vector in #src
+  /// @param after a pointer to the object in #src
+  /// @param object a pointer to the object in #dst that will be inserted after
+  /// any occurrence of the clone of `after`
+  /// @returns this CloneContext so calls can be chained
+  template <typename T, typename AFTER, typename OBJECT>
+  CloneContext& InsertAfter(const std::vector<T>& vector,
+                            const AFTER* after,
+                            const OBJECT* object) {
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, after);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, object);
+    if (std::find(vector.begin(), vector.end(), after) == vector.end()) {
+      TINT_ICE(Clone, Diagnostics())
+          << "CloneContext::InsertAfter() vector does not contain after";
+      return *this;
+    }
+
+    auto& transforms = list_transforms_[&vector];
+    auto& list = transforms.insert_after_[after];
+    list.emplace_back(object);
+    return *this;
+  }
+
+  /// Clone performs the clone of the Program's AST nodes, types and symbols
+  /// from #src to #dst. Semantic nodes are not cloned, as these will be rebuilt
+  /// when the ProgramBuilder #dst builds its Program.
+  void Clone();
+
+  /// The target ProgramBuilder to clone into.
+  ProgramBuilder* const dst;
+
+  /// The source Program to clone from.
+  Program const* const src;
+
+ private:
+  struct CloneableTransform {
+    /// Constructor
+    CloneableTransform();
+    /// Copy constructor
+    /// @param other the CloneableTransform to copy
+    CloneableTransform(const CloneableTransform& other);
+    /// Destructor
+    ~CloneableTransform();
+
+    // TypeInfo of the Cloneable that the transform operates on
+    const TypeInfo* typeinfo;
+    std::function<const Cloneable*(const Cloneable*)> function;
+  };
+
+  CloneContext(const CloneContext&) = delete;
+  CloneContext& operator=(const CloneContext&) = delete;
+
+  /// Cast `obj` from type `FROM` to type `TO`, returning the cast object.
+  /// Reports an internal compiler error if the cast failed.
+  template <typename TO, typename FROM>
+  const TO* CheckedCast(const FROM* obj) {
+    if (obj == nullptr) {
+      return nullptr;
+    }
+    if (const TO* cast = obj->template As<TO>()) {
+      return cast;
+    }
+    CheckedCastFailure(obj, TypeInfo::Of<TO>());
+    return nullptr;
+  }
+
+  /// Clones a Cloneable object, using any replacements or transforms that have
+  /// been configured.
+  const Cloneable* CloneCloneable(const Cloneable* object);
+
+  /// Adds an error diagnostic to Diagnostics() that the cloned object was not
+  /// of the expected type.
+  void CheckedCastFailure(const Cloneable* got, const TypeInfo& expected);
+
+  /// @returns the diagnostic list of #dst
+  diag::List& Diagnostics() const;
+
+  /// A vector of const Cloneable*
+  using CloneableList = std::vector<const Cloneable*>;
+
+  /// Transformations to be applied to a list (vector)
+  struct ListTransforms {
+    /// Constructor
+    ListTransforms();
+    /// Destructor
+    ~ListTransforms();
+
+    /// A map of object in #src to omit when cloned into #dst.
+    std::unordered_set<const Cloneable*> remove_;
+
+    /// A list of objects in #dst to insert before any others when the vector is
+    /// cloned.
+    CloneableList insert_front_;
+
+    /// A list of objects in #dst to insert befor after any others when the
+    /// vector is cloned.
+    CloneableList insert_back_;
+
+    /// A map of object in #src to the list of cloned objects in #dst.
+    /// Clone(const std::vector<T*>& v) will use this to insert the map-value
+    /// list into the target vector before cloning and inserting the map-key.
+    std::unordered_map<const Cloneable*, CloneableList> insert_before_;
+
+    /// A map of object in #src to the list of cloned objects in #dst.
+    /// Clone(const std::vector<T*>& v) will use this to insert the map-value
+    /// list into the target vector after cloning and inserting the map-key.
+    std::unordered_map<const Cloneable*, CloneableList> insert_after_;
+  };
+
+  /// A map of object in #src to functions that create their replacement in
+  /// #dst
+  std::unordered_map<const Cloneable*, std::function<const Cloneable*()>>
+      replacements_;
+
+  /// A map of symbol in #src to their cloned equivalent in #dst
+  std::unordered_map<Symbol, Symbol> cloned_symbols_;
+
+  /// Cloneable transform functions registered with ReplaceAll()
+  std::vector<CloneableTransform> transforms_;
+
+  /// Map of std::vector pointer to transforms for that list
+  std::unordered_map<const void*, ListTransforms> list_transforms_;
+
+  /// Symbol transform registered with ReplaceAll()
+  SymbolTransform symbol_transform_;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_CLONE_CONTEXT_H_
diff --git a/src/tint/clone_context_test.cc b/src/tint/clone_context_test.cc
new file mode 100644
index 0000000..3a5a8c9
--- /dev/null
+++ b/src/tint/clone_context_test.cc
@@ -0,0 +1,950 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <unordered_set>
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace {
+
+struct Allocator {
+  template <typename T, typename... ARGS>
+  T* Create(ARGS&&... args) {
+    return alloc.Create<T>(this, std::forward<ARGS>(args)...);
+  }
+
+ private:
+  utils::BlockAllocator<Cloneable> alloc;
+};
+
+struct Node : public Castable<Node, Cloneable> {
+  Node(Allocator* alloc,
+       Symbol n,
+       const Node* node_a = nullptr,
+       const Node* node_b = nullptr,
+       const Node* node_c = nullptr)
+      : allocator(alloc), name(n), a(node_a), b(node_b), c(node_c) {}
+  Allocator* const allocator;
+  Symbol name;
+  const Node* a = nullptr;
+  const Node* b = nullptr;
+  const Node* c = nullptr;
+  std::vector<const Node*> vec;
+
+  Node* Clone(CloneContext* ctx) const override {
+    auto* out = allocator->Create<Node>(ctx->Clone(name));
+    out->a = ctx->Clone(a);
+    out->b = ctx->Clone(b);
+    out->c = ctx->Clone(c);
+    out->vec = ctx->Clone(vec);
+    return out;
+  }
+};
+
+struct Replaceable : public Castable<Replaceable, Node> {
+  Replaceable(Allocator* alloc,
+              Symbol n,
+              const Node* node_a = nullptr,
+              const Node* node_b = nullptr,
+              const Node* node_c = nullptr)
+      : Base(alloc, n, node_a, node_b, node_c) {}
+};
+
+struct Replacement : public Castable<Replacement, Replaceable> {
+  Replacement(Allocator* alloc, Symbol n) : Base(alloc, n) {}
+};
+
+struct NotANode : public Castable<NotANode, Cloneable> {
+  explicit NotANode(Allocator* alloc) : allocator(alloc) {}
+
+  Allocator* const allocator;
+  NotANode* Clone(CloneContext*) const override {
+    return allocator->Create<NotANode>();
+  }
+};
+
+struct ProgramNode : public Castable<ProgramNode, Cloneable> {
+  ProgramNode(Allocator* alloc, ProgramID id, ProgramID cloned_id)
+      : allocator(alloc), program_id(id), cloned_program_id(cloned_id) {}
+
+  Allocator* const allocator;
+  const ProgramID program_id;
+  const ProgramID cloned_program_id;
+
+  ProgramNode* Clone(CloneContext*) const override {
+    return allocator->Create<ProgramNode>(cloned_program_id, cloned_program_id);
+  }
+};
+
+ProgramID ProgramIDOf(const ProgramNode* node) {
+  return node->program_id;
+}
+
+using CloneContextNodeTest = ::testing::Test;
+
+TEST_F(CloneContextNodeTest, Clone) {
+  Allocator alloc;
+
+  ProgramBuilder builder;
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+    auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
+  Program original(std::move(builder));
+
+  //                          root
+  //        ╭──────────────────┼──────────────────╮
+  //       (a)                (b)                (c)
+  //        N  <──────┐        N  <───────────────┘
+  //   ╭────┼────╮    │   ╭────┼────╮
+  //  (a)  (b)  (c)   │  (a)  (b)  (c)
+  //        N         └───┘    N
+  //
+  // N: Node
+
+  ProgramBuilder cloned;
+  auto* cloned_root = CloneContext(&cloned, &original).Clone(original_root);
+
+  EXPECT_NE(cloned_root->a, nullptr);
+  EXPECT_EQ(cloned_root->a->a, nullptr);
+  EXPECT_NE(cloned_root->a->b, nullptr);
+  EXPECT_EQ(cloned_root->a->c, nullptr);
+  EXPECT_NE(cloned_root->b, nullptr);
+  EXPECT_NE(cloned_root->b->a, nullptr);
+  EXPECT_NE(cloned_root->b->b, nullptr);
+  EXPECT_EQ(cloned_root->b->c, nullptr);
+  EXPECT_NE(cloned_root->c, nullptr);
+
+  EXPECT_NE(cloned_root->a, original_root->a);
+  EXPECT_NE(cloned_root->a->b, original_root->a->b);
+  EXPECT_NE(cloned_root->b, original_root->b);
+  EXPECT_NE(cloned_root->b->a, original_root->b->a);
+  EXPECT_NE(cloned_root->b->b, original_root->b->b);
+  EXPECT_NE(cloned_root->c, original_root->c);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("a->b"));
+  EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("b->b"));
+
+  EXPECT_NE(cloned_root->b->a, cloned_root->a);  // De-aliased
+  EXPECT_NE(cloned_root->c, cloned_root->b);     // De-aliased
+
+  EXPECT_EQ(cloned_root->b->a->name, cloned_root->a->name);
+  EXPECT_EQ(cloned_root->c->name, cloned_root->b->name);
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Cloneable) {
+  Allocator alloc;
+
+  ProgramBuilder builder;
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Replaceable>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b =
+        alloc.Create<Replaceable>(builder.Symbols().New("b"), b_a, nullptr);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
+  Program original(std::move(builder));
+
+  //                          root
+  //        ╭──────────────────┼──────────────────╮
+  //       (a)                (b)                (c)
+  //        N  <──────┐        R  <───────────────┘
+  //   ╭────┼────╮    │   ╭────┼────╮
+  //  (a)  (b)  (c)   │  (a)  (b)  (c)
+  //        R         └───┘
+  //
+  // N: Node
+  // R: Replaceable
+
+  ProgramBuilder cloned;
+
+  CloneContext ctx(&cloned, &original);
+  ctx.ReplaceAll([&](const Replaceable* in) {
+    auto out_name = cloned.Symbols().Register(
+        "replacement:" + original.Symbols().NameFor(in->name));
+    auto b_name = cloned.Symbols().Register(
+        "replacement-child:" + original.Symbols().NameFor(in->name));
+    auto* out = alloc.Create<Replacement>(out_name);
+    out->b = alloc.Create<Node>(b_name);
+    out->c = ctx.Clone(in->a);
+    return out;
+  });
+  auto* cloned_root = ctx.Clone(original_root);
+
+  //                         root
+  //        ╭─────────────────┼──────────────────╮
+  //       (a)               (b)                (c)
+  //        N  <──────┐       R  <───────────────┘
+  //   ╭────┼────╮    │  ╭────┼────╮
+  //  (a)  (b)  (c)   │ (a)  (b)  (c)
+  //        R         │       N    |
+  //   ╭────┼────╮    └────────────┘
+  //  (a)  (b)  (c)
+  //        N
+  //
+  // N: Node
+  // R: Replacement
+
+  EXPECT_NE(cloned_root->a, nullptr);
+  EXPECT_EQ(cloned_root->a->a, nullptr);
+  EXPECT_NE(cloned_root->a->b, nullptr);     // Replaced
+  EXPECT_EQ(cloned_root->a->b->a, nullptr);  // From replacement
+  EXPECT_NE(cloned_root->a->b->b, nullptr);  // From replacement
+  EXPECT_EQ(cloned_root->a->b->c, nullptr);  // From replacement
+  EXPECT_EQ(cloned_root->a->c, nullptr);
+  EXPECT_NE(cloned_root->b, nullptr);
+  EXPECT_EQ(cloned_root->b->a, nullptr);  // From replacement
+  EXPECT_NE(cloned_root->b->b, nullptr);  // From replacement
+  EXPECT_NE(cloned_root->b->c, nullptr);  // From replacement
+  EXPECT_NE(cloned_root->c, nullptr);
+
+  EXPECT_NE(cloned_root->a, original_root->a);
+  EXPECT_NE(cloned_root->a->b, original_root->a->b);
+  EXPECT_NE(cloned_root->b, original_root->b);
+  EXPECT_NE(cloned_root->b->a, original_root->b->a);
+  EXPECT_NE(cloned_root->c, original_root->c);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("replacement:a->b"));
+  EXPECT_EQ(cloned_root->a->b->b->name,
+            cloned.Symbols().Get("replacement-child:a->b"));
+  EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement:b"));
+  EXPECT_EQ(cloned_root->b->b->name,
+            cloned.Symbols().Get("replacement-child:b"));
+
+  EXPECT_NE(cloned_root->b->c, cloned_root->a);  // De-aliased
+  EXPECT_NE(cloned_root->c, cloned_root->b);     // De-aliased
+
+  EXPECT_EQ(cloned_root->b->c->name, cloned_root->a->name);
+  EXPECT_EQ(cloned_root->c->name, cloned_root->b->name);
+
+  EXPECT_FALSE(Is<Replacement>(cloned_root->a));
+  EXPECT_TRUE(Is<Replacement>(cloned_root->a->b));
+  EXPECT_FALSE(Is<Replacement>(cloned_root->a->b->b));
+  EXPECT_TRUE(Is<Replacement>(cloned_root->b));
+  EXPECT_FALSE(Is<Replacement>(cloned_root->b->b));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceAll_Symbols) {
+  Allocator alloc;
+
+  ProgramBuilder builder;
+  Node* original_root;
+  {
+    auto* a_b = alloc.Create<Node>(builder.Symbols().New("a->b"));
+    auto* a = alloc.Create<Node>(builder.Symbols().New("a"), nullptr, a_b);
+    auto* b_a = a;  // Aliased
+    auto* b_b = alloc.Create<Node>(builder.Symbols().New("b->b"));
+    auto* b = alloc.Create<Node>(builder.Symbols().New("b"), b_a, b_b);
+    auto* c = b;  // Aliased
+    original_root = alloc.Create<Node>(builder.Symbols().New("root"), a, b, c);
+  }
+  Program original(std::move(builder));
+
+  //                          root
+  //        ╭──────────────────┼──────────────────╮
+  //       (a)                (b)                (c)
+  //        N  <──────┐        N  <───────────────┘
+  //   ╭────┼────╮    │   ╭────┼────╮
+  //  (a)  (b)  (c)   │  (a)  (b)  (c)
+  //        N         └───┘    N
+  //
+  // N: Node
+
+  ProgramBuilder cloned;
+  auto* cloned_root = CloneContext(&cloned, &original, false)
+                          .ReplaceAll([&](Symbol sym) {
+                            auto in = original.Symbols().NameFor(sym);
+                            auto out = "transformed<" + in + ">";
+                            return cloned.Symbols().New(out);
+                          })
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("transformed<root>"));
+  EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("transformed<a>"));
+  EXPECT_EQ(cloned_root->a->b->name, cloned.Symbols().Get("transformed<a->b>"));
+  EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("transformed<b>"));
+  EXPECT_EQ(cloned_root->b->b->name, cloned.Symbols().Get("transformed<b->b>"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithoutTransform) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_node = a.Create<Node>(builder.Symbols().New("root"));
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original);
+  ctx.ReplaceAll([&](const Node*) {
+    return a.Create<Replacement>(builder.Symbols().New("<unexpected-node>"));
+  });
+
+  auto* cloned_node = ctx.CloneWithoutTransform(original_node);
+  EXPECT_NE(cloned_node, original_node);
+  EXPECT_EQ(cloned_node->name, cloned.Symbols().Get("root"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplacePointer) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+  original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+  original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+  original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+  Program original(std::move(builder));
+
+  //                          root
+  //        ╭──────────────────┼──────────────────╮
+  //       (a)                (b)                (c)
+  //                        Replaced
+
+  ProgramBuilder cloned;
+  auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .Replace(original_root->b, replacement)
+                          .Clone(original_root);
+
+  EXPECT_NE(cloned_root->a, replacement);
+  EXPECT_EQ(cloned_root->b, replacement);
+  EXPECT_NE(cloned_root->c, replacement);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement"));
+  EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceFunction) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().New("root"));
+  original_root->a = a.Create<Node>(builder.Symbols().New("a"));
+  original_root->b = a.Create<Node>(builder.Symbols().New("b"));
+  original_root->c = a.Create<Node>(builder.Symbols().New("c"));
+  Program original(std::move(builder));
+
+  //                          root
+  //        ╭──────────────────┼──────────────────╮
+  //       (a)                (b)                (c)
+  //                        Replaced
+
+  ProgramBuilder cloned;
+  auto* replacement = a.Create<Node>(cloned.Symbols().New("replacement"));
+
+  auto* cloned_root =
+      CloneContext(&cloned, &original)
+          .Replace(original_root->b, [=] { return replacement; })
+          .Clone(original_root);
+
+  EXPECT_NE(cloned_root->a, replacement);
+  EXPECT_EQ(cloned_root->b, replacement);
+  EXPECT_NE(cloned_root->c, replacement);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->a->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->b->name, cloned.Symbols().Get("replacement"));
+  EXPECT_EQ(cloned_root->c->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithRemove) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .Remove(original_root->vec, original_root->vec[1])
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 2u);
+
+  EXPECT_NE(cloned_root->vec[0], cloned_root->a);
+  EXPECT_NE(cloned_root->vec[1], cloned_root->c);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertFront) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertFront(original_root->vec, insertion)
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_NE(cloned_root->vec[0], cloned_root->a);
+  EXPECT_NE(cloned_root->vec[1], cloned_root->b);
+  EXPECT_NE(cloned_root->vec[2], cloned_root->c);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertFront_Empty) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {};
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertFront(original_root->vec, insertion)
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 1u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertBack) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertBack(original_root->vec, insertion)
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertBack_Empty) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {};
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertBack(original_root->vec, insertion)
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 1u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertFrontAndBack_Empty) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {};
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion_front =
+      a.Create<Node>(cloned.Symbols().New("insertion_front"));
+  auto* insertion_back = a.Create<Node>(cloned.Symbols().New("insertion_back"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertBack(original_root->vec, insertion_back)
+                          .InsertFront(original_root->vec, insertion_front)
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 2u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("insertion_front"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion_back"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertBefore) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root =
+      CloneContext(&cloned, &original)
+          .InsertBefore(original_root->vec, original_root->vec[1], insertion)
+          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("insertion"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertAfter) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+
+  auto* cloned_root =
+      CloneContext(&cloned, &original)
+          .InsertAfter(original_root->vec, original_root->vec[1], insertion)
+          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertAfterInVectorNodeClone) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Replaceable>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original);
+  ctx.ReplaceAll([&](const Replaceable* r) {
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    ctx.InsertAfter(original_root->vec, r, insertion);
+    return nullptr;
+  });
+
+  auto* cloned_root = ctx.Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertBackInVectorNodeClone) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Replaceable>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original);
+  ctx.ReplaceAll([&](const Replaceable* /*r*/) {
+    auto* insertion = a.Create<Node>(cloned.Symbols().New("insertion"));
+    ctx.InsertBack(original_root->vec, insertion);
+    return nullptr;
+  });
+
+  auto* cloned_root = ctx.Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name, cloned.Symbols().Get("b"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("c"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("insertion"));
+}
+
+TEST_F(CloneContextNodeTest, CloneWithInsertBeforeAndAfterRemoved) {
+  Allocator a;
+
+  ProgramBuilder builder;
+  auto* original_root = a.Create<Node>(builder.Symbols().Register("root"));
+  original_root->vec = {
+      a.Create<Node>(builder.Symbols().Register("a")),
+      a.Create<Node>(builder.Symbols().Register("b")),
+      a.Create<Node>(builder.Symbols().Register("c")),
+  };
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  auto* insertion_before =
+      a.Create<Node>(cloned.Symbols().New("insertion_before"));
+  auto* insertion_after =
+      a.Create<Node>(cloned.Symbols().New("insertion_after"));
+
+  auto* cloned_root = CloneContext(&cloned, &original)
+                          .InsertBefore(original_root->vec,
+                                        original_root->vec[1], insertion_before)
+                          .InsertAfter(original_root->vec,
+                                       original_root->vec[1], insertion_after)
+                          .Remove(original_root->vec, original_root->vec[1])
+                          .Clone(original_root);
+
+  EXPECT_EQ(cloned_root->vec.size(), 4u);
+
+  EXPECT_EQ(cloned_root->name, cloned.Symbols().Get("root"));
+  EXPECT_EQ(cloned_root->vec[0]->name, cloned.Symbols().Get("a"));
+  EXPECT_EQ(cloned_root->vec[1]->name,
+            cloned.Symbols().Get("insertion_before"));
+  EXPECT_EQ(cloned_root->vec[2]->name, cloned.Symbols().Get("insertion_after"));
+  EXPECT_EQ(cloned_root->vec[3]->name, cloned.Symbols().Get("c"));
+}
+
+TEST_F(CloneContextNodeTest, CloneIntoSameBuilder) {
+  ProgramBuilder builder;
+  CloneContext ctx(&builder);
+  Allocator allocator;
+  auto* original = allocator.Create<Node>(builder.Symbols().New());
+  auto* cloned_a = ctx.Clone(original);
+  auto* cloned_b = ctx.Clone(original);
+  EXPECT_NE(original, cloned_a);
+  EXPECT_NE(original, cloned_b);
+
+  EXPECT_NE(cloned_a, cloned_b);
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceAll_SameTypeTwice) {
+  std::string node_name = TypeInfo::Of<Node>().name;
+
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder cloned;
+        Program original;
+        CloneContext ctx(&cloned, &original);
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+      },
+      "internal compiler error: ReplaceAll() called with a handler for type " +
+          node_name + " that is already handled by a handler for type " +
+          node_name);
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceAll_BaseThenDerived) {
+  std::string node_name = TypeInfo::Of<Node>().name;
+  std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder cloned;
+        Program original;
+        CloneContext ctx(&cloned, &original);
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+        ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
+      },
+      "internal compiler error: ReplaceAll() called with a handler for type " +
+          replaceable_name + " that is already handled by a handler for type " +
+          node_name);
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceAll_DerivedThenBase) {
+  std::string node_name = TypeInfo::Of<Node>().name;
+  std::string replaceable_name = TypeInfo::Of<Replaceable>().name;
+
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder cloned;
+        Program original;
+        CloneContext ctx(&cloned, &original);
+        ctx.ReplaceAll([](const Replaceable*) { return nullptr; });
+        ctx.ReplaceAll([](const Node*) { return nullptr; });
+      },
+      "internal compiler error: ReplaceAll() called with a handler for type " +
+          node_name + " that is already handled by a handler for type " +
+          replaceable_name);
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplacePointer_WithNotANode) {
+  EXPECT_FATAL_FAILURE(
+      {
+        Allocator allocator;
+        ProgramBuilder builder;
+        auto* original_root =
+            allocator.Create<Node>(builder.Symbols().New("root"));
+        original_root->a = allocator.Create<Node>(builder.Symbols().New("a"));
+        original_root->b = allocator.Create<Node>(builder.Symbols().New("b"));
+        original_root->c = allocator.Create<Node>(builder.Symbols().New("c"));
+        Program original(std::move(builder));
+
+        //                          root
+        //        ╭──────────────────┼──────────────────╮
+        //       (a)                (b)                (c)
+        //                        Replaced
+
+        ProgramBuilder cloned;
+        auto* replacement = allocator.Create<NotANode>();
+
+        CloneContext ctx(&cloned, &original);
+        ctx.Replace(original_root->b, replacement);
+
+        ctx.Clone(original_root);
+      },
+      "internal compiler error");
+}
+
+TEST_F(CloneContextNodeTest, CloneWithReplaceFunction_WithNotANode) {
+  EXPECT_FATAL_FAILURE(
+      {
+        Allocator allocator;
+        ProgramBuilder builder;
+        auto* original_root =
+            allocator.Create<Node>(builder.Symbols().New("root"));
+        original_root->a = allocator.Create<Node>(builder.Symbols().New("a"));
+        original_root->b = allocator.Create<Node>(builder.Symbols().New("b"));
+        original_root->c = allocator.Create<Node>(builder.Symbols().New("c"));
+        Program original(std::move(builder));
+
+        //                          root
+        //        ╭──────────────────┼──────────────────╮
+        //       (a)                (b)                (c)
+        //                        Replaced
+
+        ProgramBuilder cloned;
+        auto* replacement = allocator.Create<NotANode>();
+
+        CloneContext ctx(&cloned, &original);
+        ctx.Replace(original_root->b, [=] { return replacement; });
+
+        ctx.Clone(original_root);
+      },
+      "internal compiler error");
+}
+
+using CloneContextTest = ::testing::Test;
+
+TEST_F(CloneContextTest, CloneWithReplaceAll_SymbolsTwice) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder cloned;
+        Program original;
+        CloneContext ctx(&cloned, &original);
+        ctx.ReplaceAll([](const Symbol s) { return s; });
+        ctx.ReplaceAll([](const Symbol s) { return s; });
+      },
+      "internal compiler error: ReplaceAll(const SymbolTransform&) called "
+      "multiple times on the same CloneContext");
+}
+
+TEST_F(CloneContextTest, CloneNewUnnamedSymbols) {
+  ProgramBuilder builder;
+  Symbol old_a = builder.Symbols().New();
+  Symbol old_b = builder.Symbols().New();
+  Symbol old_c = builder.Symbols().New();
+  EXPECT_EQ(builder.Symbols().NameFor(old_a), "tint_symbol");
+  EXPECT_EQ(builder.Symbols().NameFor(old_b), "tint_symbol_1");
+  EXPECT_EQ(builder.Symbols().NameFor(old_c), "tint_symbol_2");
+
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original, false);
+  Symbol new_x = cloned.Symbols().New();
+  Symbol new_a = ctx.Clone(old_a);
+  Symbol new_y = cloned.Symbols().New();
+  Symbol new_b = ctx.Clone(old_b);
+  Symbol new_z = cloned.Symbols().New();
+  Symbol new_c = ctx.Clone(old_c);
+
+  EXPECT_EQ(cloned.Symbols().NameFor(new_x), "tint_symbol");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_a), "tint_symbol_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_y), "tint_symbol_2");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_b), "tint_symbol_1_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_z), "tint_symbol_3");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_c), "tint_symbol_2_1");
+}
+
+TEST_F(CloneContextTest, CloneNewSymbols) {
+  ProgramBuilder builder;
+  Symbol old_a = builder.Symbols().New("a");
+  Symbol old_b = builder.Symbols().New("b");
+  Symbol old_c = builder.Symbols().New("c");
+  EXPECT_EQ(builder.Symbols().NameFor(old_a), "a");
+  EXPECT_EQ(builder.Symbols().NameFor(old_b), "b");
+  EXPECT_EQ(builder.Symbols().NameFor(old_c), "c");
+
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original, false);
+  Symbol new_x = cloned.Symbols().New("a");
+  Symbol new_a = ctx.Clone(old_a);
+  Symbol new_y = cloned.Symbols().New("b");
+  Symbol new_b = ctx.Clone(old_b);
+  Symbol new_z = cloned.Symbols().New("c");
+  Symbol new_c = ctx.Clone(old_c);
+
+  EXPECT_EQ(cloned.Symbols().NameFor(new_x), "a");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_a), "a_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_y), "b");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_b), "b_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_z), "c");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_c), "c_1");
+}
+
+TEST_F(CloneContextTest, CloneNewSymbols_AfterCloneSymbols) {
+  ProgramBuilder builder;
+  Symbol old_a = builder.Symbols().New("a");
+  Symbol old_b = builder.Symbols().New("b");
+  Symbol old_c = builder.Symbols().New("c");
+  EXPECT_EQ(builder.Symbols().NameFor(old_a), "a");
+  EXPECT_EQ(builder.Symbols().NameFor(old_b), "b");
+  EXPECT_EQ(builder.Symbols().NameFor(old_c), "c");
+
+  Program original(std::move(builder));
+
+  ProgramBuilder cloned;
+  CloneContext ctx(&cloned, &original);
+  Symbol new_x = cloned.Symbols().New("a");
+  Symbol new_a = ctx.Clone(old_a);
+  Symbol new_y = cloned.Symbols().New("b");
+  Symbol new_b = ctx.Clone(old_b);
+  Symbol new_z = cloned.Symbols().New("c");
+  Symbol new_c = ctx.Clone(old_c);
+
+  EXPECT_EQ(cloned.Symbols().NameFor(new_x), "a_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_a), "a");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_y), "b_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_b), "b");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_z), "c_1");
+  EXPECT_EQ(cloned.Symbols().NameFor(new_c), "c");
+}
+
+TEST_F(CloneContextTest, ProgramIDs) {
+  ProgramBuilder dst;
+  Program src(ProgramBuilder{});
+  CloneContext ctx(&dst, &src);
+  Allocator allocator;
+  auto* cloned = ctx.Clone(allocator.Create<ProgramNode>(src.ID(), dst.ID()));
+  EXPECT_EQ(cloned->program_id, dst.ID());
+}
+
+TEST_F(CloneContextTest, ProgramIDs_Clone_ObjectNotOwnedBySrc) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder dst;
+        Program src(ProgramBuilder{});
+        CloneContext ctx(&dst, &src);
+        Allocator allocator;
+        ctx.Clone(allocator.Create<ProgramNode>(ProgramID::New(), dst.ID()));
+      },
+      R"(internal compiler error: TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, src, object))");
+}
+
+TEST_F(CloneContextTest, ProgramIDs_Clone_ObjectNotOwnedByDst) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder dst;
+        Program src(ProgramBuilder{});
+        CloneContext ctx(&dst, &src);
+        Allocator allocator;
+        ctx.Clone(allocator.Create<ProgramNode>(src.ID(), ProgramID::New()));
+      },
+      R"(internal compiler error: TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(Clone, dst, out))");
+}
+
+}  // namespace
+
+TINT_INSTANTIATE_TYPEINFO(Node);
+TINT_INSTANTIATE_TYPEINFO(Replaceable);
+TINT_INSTANTIATE_TYPEINFO(Replacement);
+TINT_INSTANTIATE_TYPEINFO(NotANode);
+TINT_INSTANTIATE_TYPEINFO(ProgramNode);
+
+}  // namespace tint
diff --git a/src/tint/cmd/BUILD.gn b/src/tint/cmd/BUILD.gn
new file mode 100644
index 0000000..c2f4322
--- /dev/null
+++ b/src/tint/cmd/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2021 The Tint Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+import("../../../tint_overrides_with_defaults.gni")
+
+executable("tint") {
+  sources = [ "main.cc" ]
+  deps = [
+    "${tint_root_dir}/src/tint:libtint",
+    "${tint_root_dir}/src/tint:tint_val",
+    "${tint_spirv_tools_dir}/:spvtools",
+    "${tint_spirv_tools_dir}/:spvtools_opt",
+    "${tint_spirv_tools_dir}/:spvtools_val",
+  ]
+
+  if (tint_build_glsl_writer) {
+    deps += [
+      "${tint_root_dir}/third_party/vulkan-deps/glslang/src:glslang_default_resource_limits_sources",
+      "${tint_root_dir}/third_party/vulkan-deps/glslang/src:glslang_lib_sources",
+    ]
+  }
+
+  configs += [
+    "${tint_root_dir}/src/tint:tint_common_config",
+    "${tint_root_dir}/src/tint:tint_config",
+  ]
+
+  if (build_with_chromium) {
+    configs -= [ "//build/config/compiler:chromium_code" ]
+    configs += [ "//build/config/compiler:no_chromium_code" ]
+  }
+}
diff --git a/src/tint/cmd/CMakeLists.txt b/src/tint/cmd/CMakeLists.txt
new file mode 100644
index 0000000..5c6c6c9
--- /dev/null
+++ b/src/tint/cmd/CMakeLists.txt
@@ -0,0 +1,39 @@
+# Copyright 2020 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.
+
+set(TINT_SRCS
+  main.cc
+)
+
+## Tint executable
+add_executable(tint ${TINT_SRCS})
+tint_default_compile_options(tint)
+target_link_libraries(tint libtint tint_val)
+
+if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
+  target_link_libraries(tint SPIRV-Tools)
+endif()
+
+if(${TINT_BUILD_GLSL_WRITER})
+  target_link_libraries(tint glslang)
+  target_link_libraries(tint glslang-default-resource-limits)
+  if(NOT MSVC)
+    target_compile_options(tint PRIVATE
+      -Wno-reserved-id-macro
+      -Wno-shadow-field-in-constructor
+      -Wno-shadow
+      -Wno-weak-vtables
+    )
+  endif()
+endif()
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
new file mode 100644
index 0000000..8a0d293
--- /dev/null
+++ b/src/tint/cmd/main.cc
@@ -0,0 +1,1220 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#if TINT_BUILD_GLSL_WRITER
+#include "StandAlone/ResourceLimits.h"
+#include "glslang/Public/ShaderLang.h"
+#endif
+
+#if TINT_BUILD_SPV_READER
+#include "spirv-tools/libspirv.hpp"
+#endif  // TINT_BUILD_SPV_READER
+
+#include "src/tint/utils/io/command.h"
+#include "src/tint/utils/string.h"
+#include "src/tint/val/val.h"
+#include "tint/tint.h"
+
+namespace {
+
+[[noreturn]] void TintInternalCompilerErrorReporter(
+    const tint::diag::List& diagnostics) {
+  auto printer = tint::diag::Printer::create(stderr, true);
+  tint::diag::Formatter{}.format(diagnostics, printer.get());
+  tint::diag::Style bold_red{tint::diag::Color::kRed, true};
+  constexpr const char* please_file_bug = R"(
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+)";
+  printer->write(please_file_bug, bold_red);
+  exit(1);
+}
+
+enum class Format {
+  kNone = -1,
+  kSpirv,
+  kSpvAsm,
+  kWgsl,
+  kMsl,
+  kHlsl,
+  kGlsl,
+};
+
+struct Options {
+  bool show_help = false;
+
+  std::string input_filename;
+  std::string output_file = "-";  // Default to stdout
+
+  bool parse_only = false;
+  bool disable_workgroup_init = false;
+  bool validate = false;
+  bool demangle = false;
+  bool dump_inspector_bindings = false;
+
+  Format format = Format::kNone;
+
+  bool emit_single_entry_point = false;
+  std::string ep_name;
+
+  std::vector<std::string> transforms;
+
+  bool use_fxc = false;
+  std::string dxc_path;
+  std::string xcrun_path;
+};
+
+const char kUsage[] = R"(Usage: tint [options] <input-file>
+
+ options:
+  --format <spirv|spvasm|wgsl|msl|hlsl>  -- Output format.
+                               If not provided, will be inferred from output
+                               filename extension:
+                                   .spvasm -> spvasm
+                                   .spv    -> spirv
+                                   .wgsl   -> wgsl
+                                   .metal  -> msl
+                                   .hlsl   -> hlsl
+                               If none matches, then default to SPIR-V assembly.
+  -ep <name>                -- Output single entry point
+  --output-file <name>      -- Output file name.  Use "-" for standard output
+  -o <name>                 -- Output file name.  Use "-" for standard output
+  --transform <name list>   -- Runs transforms, name list is comma separated
+                               Available transforms:
+${transforms}
+  --parse-only              -- Stop after parsing the input
+  --disable-workgroup-init  -- Disable workgroup memory zero initialization.
+  --demangle                -- Preserve original source names. Demangle them.
+                               Affects AST dumping, and text-based output languages.
+  --dump-inspector-bindings -- Dump reflection data about bindins to stdout.
+  -h                        -- This help text
+  --validate                -- Validates the generated shader
+  --fxc                     -- Ask to validate HLSL output using FXC instead of DXC.
+                               When specified, automatically enables --validate
+  --dxc                     -- Path to DXC executable, used to validate HLSL output.
+                               When specified, automatically enables --validate
+  --xcrun                   -- Path to xcrun executable, used to validate MSL output.
+                               When specified, automatically enables --validate)";
+
+Format parse_format(const std::string& fmt) {
+  (void)fmt;
+
+#if TINT_BUILD_SPV_WRITER
+  if (fmt == "spirv")
+    return Format::kSpirv;
+  if (fmt == "spvasm")
+    return Format::kSpvAsm;
+#endif  // TINT_BUILD_SPV_WRITER
+
+#if TINT_BUILD_WGSL_WRITER
+  if (fmt == "wgsl")
+    return Format::kWgsl;
+#endif  // TINT_BUILD_WGSL_WRITER
+
+#if TINT_BUILD_MSL_WRITER
+  if (fmt == "msl")
+    return Format::kMsl;
+#endif  // TINT_BUILD_MSL_WRITER
+
+#if TINT_BUILD_HLSL_WRITER
+  if (fmt == "hlsl")
+    return Format::kHlsl;
+#endif  // TINT_BUILD_HLSL_WRITER
+
+#if TINT_BUILD_GLSL_WRITER
+  if (fmt == "glsl")
+    return Format::kGlsl;
+#endif  // TINT_BUILD_GLSL_WRITER
+
+  return Format::kNone;
+}
+
+#if TINT_BUILD_SPV_WRITER || TINT_BUILD_WGSL_WRITER || \
+    TINT_BUILD_MSL_WRITER || TINT_BUILD_HLSL_WRITER
+/// @param input input string
+/// @param suffix potential suffix string
+/// @returns true if input ends with the given suffix.
+bool ends_with(const std::string& input, const std::string& suffix) {
+  const auto input_len = input.size();
+  const auto suffix_len = suffix.size();
+  // Avoid integer overflow.
+  return (input_len >= suffix_len) &&
+         (input_len - suffix_len == input.rfind(suffix));
+}
+#endif
+
+/// @param filename the filename to inspect
+/// @returns the inferred format for the filename suffix
+Format infer_format(const std::string& filename) {
+  (void)filename;
+
+#if TINT_BUILD_SPV_WRITER
+  if (ends_with(filename, ".spv")) {
+    return Format::kSpirv;
+  }
+  if (ends_with(filename, ".spvasm")) {
+    return Format::kSpvAsm;
+  }
+#endif  // TINT_BUILD_SPV_WRITER
+
+#if TINT_BUILD_WGSL_WRITER
+  if (ends_with(filename, ".wgsl")) {
+    return Format::kWgsl;
+  }
+#endif  // TINT_BUILD_WGSL_WRITER
+
+#if TINT_BUILD_MSL_WRITER
+  if (ends_with(filename, ".metal")) {
+    return Format::kMsl;
+  }
+#endif  // TINT_BUILD_MSL_WRITER
+
+#if TINT_BUILD_HLSL_WRITER
+  if (ends_with(filename, ".hlsl")) {
+    return Format::kHlsl;
+  }
+#endif  // TINT_BUILD_HLSL_WRITER
+
+  return Format::kNone;
+}
+
+std::vector<std::string> split_transform_names(std::string list) {
+  std::vector<std::string> res;
+
+  std::stringstream str(list);
+  while (str.good()) {
+    std::string substr;
+    getline(str, substr, ',');
+    res.push_back(substr);
+  }
+  return res;
+}
+
+std::string TextureDimensionToString(
+    tint::inspector::ResourceBinding::TextureDimension dim) {
+  switch (dim) {
+    case tint::inspector::ResourceBinding::TextureDimension::kNone:
+      return "None";
+    case tint::inspector::ResourceBinding::TextureDimension::k1d:
+      return "1d";
+    case tint::inspector::ResourceBinding::TextureDimension::k2d:
+      return "2d";
+    case tint::inspector::ResourceBinding::TextureDimension::k2dArray:
+      return "2dArray";
+    case tint::inspector::ResourceBinding::TextureDimension::k3d:
+      return "3d";
+    case tint::inspector::ResourceBinding::TextureDimension::kCube:
+      return "Cube";
+    case tint::inspector::ResourceBinding::TextureDimension::kCubeArray:
+      return "CubeArray";
+  }
+
+  return "Unknown";
+}
+
+std::string SampledKindToString(
+    tint::inspector::ResourceBinding::SampledKind kind) {
+  switch (kind) {
+    case tint::inspector::ResourceBinding::SampledKind::kFloat:
+      return "Float";
+    case tint::inspector::ResourceBinding::SampledKind::kUInt:
+      return "UInt";
+    case tint::inspector::ResourceBinding::SampledKind::kSInt:
+      return "SInt";
+    case tint::inspector::ResourceBinding::SampledKind::kUnknown:
+      break;
+  }
+
+  return "Unknown";
+}
+
+std::string TexelFormatToString(
+    tint::inspector::ResourceBinding::TexelFormat format) {
+  switch (format) {
+    case tint::inspector::ResourceBinding::TexelFormat::kR32Uint:
+      return "R32Uint";
+    case tint::inspector::ResourceBinding::TexelFormat::kR32Sint:
+      return "R32Sint";
+    case tint::inspector::ResourceBinding::TexelFormat::kR32Float:
+      return "R32Float";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba8Unorm:
+      return "Rgba8Unorm";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba8Snorm:
+      return "Rgba8Snorm";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba8Uint:
+      return "Rgba8Uint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba8Sint:
+      return "Rgba8Sint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRg32Uint:
+      return "Rg32Uint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRg32Sint:
+      return "Rg32Sint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRg32Float:
+      return "Rg32Float";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba16Uint:
+      return "Rgba16Uint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba16Sint:
+      return "Rgba16Sint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba16Float:
+      return "Rgba16Float";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba32Uint:
+      return "Rgba32Uint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba32Sint:
+      return "Rgba32Sint";
+    case tint::inspector::ResourceBinding::TexelFormat::kRgba32Float:
+      return "Rgba32Float";
+    case tint::inspector::ResourceBinding::TexelFormat::kNone:
+      return "None";
+  }
+  return "Unknown";
+}
+
+std::string ResourceTypeToString(
+    tint::inspector::ResourceBinding::ResourceType type) {
+  switch (type) {
+    case tint::inspector::ResourceBinding::ResourceType::kUniformBuffer:
+      return "UniformBuffer";
+    case tint::inspector::ResourceBinding::ResourceType::kStorageBuffer:
+      return "StorageBuffer";
+    case tint::inspector::ResourceBinding::ResourceType::kReadOnlyStorageBuffer:
+      return "ReadOnlyStorageBuffer";
+    case tint::inspector::ResourceBinding::ResourceType::kSampler:
+      return "Sampler";
+    case tint::inspector::ResourceBinding::ResourceType::kComparisonSampler:
+      return "ComparisonSampler";
+    case tint::inspector::ResourceBinding::ResourceType::kSampledTexture:
+      return "SampledTexture";
+    case tint::inspector::ResourceBinding::ResourceType::kMultisampledTexture:
+      return "MultisampledTexture";
+    case tint::inspector::ResourceBinding::ResourceType::
+        kWriteOnlyStorageTexture:
+      return "WriteOnlyStorageTexture";
+    case tint::inspector::ResourceBinding::ResourceType::kDepthTexture:
+      return "DepthTexture";
+    case tint::inspector::ResourceBinding::ResourceType::
+        kDepthMultisampledTexture:
+      return "DepthMultisampledTexture";
+    case tint::inspector::ResourceBinding::ResourceType::kExternalTexture:
+      return "ExternalTexture";
+  }
+
+  return "Unknown";
+}
+
+bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
+  for (size_t i = 1; i < args.size(); ++i) {
+    const std::string& arg = args[i];
+    if (arg == "--format") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for --format argument." << std::endl;
+        return false;
+      }
+      opts->format = parse_format(args[i]);
+
+      if (opts->format == Format::kNone) {
+        std::cerr << "Unknown output format: " << args[i] << std::endl;
+        return false;
+      }
+    } else if (arg == "-ep") {
+      if (i + 1 >= args.size()) {
+        std::cerr << "Missing value for -ep" << std::endl;
+        return false;
+      }
+      i++;
+      opts->ep_name = args[i];
+      opts->emit_single_entry_point = true;
+
+    } else if (arg == "-o" || arg == "--output-name") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for " << arg << std::endl;
+        return false;
+      }
+      opts->output_file = args[i];
+
+    } else if (arg == "-h" || arg == "--help") {
+      opts->show_help = true;
+    } else if (arg == "--transform") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for " << arg << std::endl;
+        return false;
+      }
+      opts->transforms = split_transform_names(args[i]);
+    } else if (arg == "--parse-only") {
+      opts->parse_only = true;
+    } else if (arg == "--disable-workgroup-init") {
+      opts->disable_workgroup_init = true;
+    } else if (arg == "--demangle") {
+      opts->demangle = true;
+    } else if (arg == "--dump-inspector-bindings") {
+      opts->dump_inspector_bindings = true;
+    } else if (arg == "--validate") {
+      opts->validate = true;
+    } else if (arg == "--fxc") {
+      opts->validate = true;
+      opts->use_fxc = true;
+    } else if (arg == "--dxc") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for " << arg << std::endl;
+        return false;
+      }
+      opts->dxc_path = args[i];
+      opts->validate = true;
+    } else if (arg == "--xcrun") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for " << arg << std::endl;
+        return false;
+      }
+      opts->xcrun_path = args[i];
+      opts->validate = true;
+    } else if (!arg.empty()) {
+      if (arg[0] == '-') {
+        std::cerr << "Unrecognized option: " << arg << std::endl;
+        return false;
+      }
+      if (!opts->input_filename.empty()) {
+        std::cerr << "More than one input file specified: '"
+                  << opts->input_filename << "' and '" << arg << "'"
+                  << std::endl;
+        return false;
+      }
+      opts->input_filename = arg;
+    }
+  }
+  return true;
+}
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
+  if (!buffer) {
+    std::cerr << "The buffer pointer was null" << std::endl;
+    return false;
+  }
+
+  FILE* file = nullptr;
+#if defined(_MSC_VER)
+  fopen_s(&file, input_file.c_str(), "rb");
+#else
+  file = fopen(input_file.c_str(), "rb");
+#endif
+  if (!file) {
+    std::cerr << "Failed to open " << input_file << std::endl;
+    return false;
+  }
+
+  fseek(file, 0, SEEK_END);
+  const auto file_size = static_cast<size_t>(ftell(file));
+  if (0 != (file_size % sizeof(T))) {
+    std::cerr << "File " << input_file
+              << " does not contain an integral number of objects: "
+              << file_size << " bytes in the file, require " << sizeof(T)
+              << " bytes per object" << std::endl;
+    fclose(file);
+    return false;
+  }
+  fseek(file, 0, SEEK_SET);
+
+  buffer->clear();
+  buffer->resize(file_size / sizeof(T));
+
+  size_t bytes_read = fread(buffer->data(), 1, file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    std::cerr << "Failed to read " << input_file << std::endl;
+    return false;
+  }
+
+  return true;
+}
+
+/// Writes the given `buffer` into the file named as `output_file` using the
+/// given `mode`.  If `output_file` is empty or "-", writes to standard
+/// output. If any error occurs, returns false and outputs error message to
+/// standard error. The ContainerT type must have data() and size() methods,
+/// like `std::string` and `std::vector` do.
+/// @returns true on success
+template <typename ContainerT>
+bool WriteFile(const std::string& output_file,
+               const std::string mode,
+               const ContainerT& buffer) {
+  const bool use_stdout = output_file.empty() || output_file == "-";
+  FILE* file = stdout;
+
+  if (!use_stdout) {
+#if defined(_MSC_VER)
+    fopen_s(&file, output_file.c_str(), mode.c_str());
+#else
+    file = fopen(output_file.c_str(), mode.c_str());
+#endif
+    if (!file) {
+      std::cerr << "Could not open file " << output_file << " for writing"
+                << std::endl;
+      return false;
+    }
+  }
+
+  size_t written =
+      fwrite(buffer.data(), sizeof(typename ContainerT::value_type),
+             buffer.size(), file);
+  if (buffer.size() != written) {
+    if (use_stdout) {
+      std::cerr << "Could not write all output to standard output" << std::endl;
+    } else {
+      std::cerr << "Could not write to file " << output_file << std::endl;
+      fclose(file);
+    }
+    return false;
+  }
+  if (!use_stdout) {
+    fclose(file);
+  }
+
+  return true;
+}
+
+#if TINT_BUILD_SPV_WRITER
+std::string Disassemble(const std::vector<uint32_t>& data) {
+  std::string spv_errors;
+  spv_target_env target_env = SPV_ENV_UNIVERSAL_1_0;
+
+  auto msg_consumer = [&spv_errors](spv_message_level_t level, const char*,
+                                    const spv_position_t& position,
+                                    const char* message) {
+    switch (level) {
+      case SPV_MSG_FATAL:
+      case SPV_MSG_INTERNAL_ERROR:
+      case SPV_MSG_ERROR:
+        spv_errors += "error: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_WARNING:
+        spv_errors += "warning: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_INFO:
+        spv_errors += "info: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_DEBUG:
+        break;
+    }
+  };
+
+  spvtools::SpirvTools tools(target_env);
+  tools.SetMessageConsumer(msg_consumer);
+
+  std::string result;
+  if (!tools.Disassemble(data, &result,
+                         SPV_BINARY_TO_TEXT_OPTION_INDENT |
+                             SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) {
+    std::cerr << spv_errors << std::endl;
+  }
+  return result;
+}
+#endif  // TINT_BUILD_SPV_WRITER
+
+/// PrintWGSL writes the WGSL of the program to the provided ostream, if the
+/// WGSL writer is enabled, otherwise it does nothing.
+/// @param out the output stream to write the WGSL to
+/// @param program the program
+void PrintWGSL(std::ostream& out, const tint::Program& program) {
+#if TINT_BUILD_WGSL_WRITER
+  tint::writer::wgsl::Options options;
+  auto result = tint::writer::wgsl::Generate(&program, options);
+  out << std::endl << result.wgsl << std::endl;
+#else
+  (void)out;
+  (void)program;
+#endif
+}
+
+/// Generate SPIR-V code for a program.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns true on success
+bool GenerateSpirv(const tint::Program* program, const Options& options) {
+#if TINT_BUILD_SPV_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::spirv::Options gen_options;
+  gen_options.disable_workgroup_init = options.disable_workgroup_init;
+  gen_options.generate_external_texture_bindings = true;
+  auto result = tint::writer::spirv::Generate(program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (options.format == Format::kSpvAsm) {
+    if (!WriteFile(options.output_file, "w", Disassemble(result.spirv))) {
+      return false;
+    }
+  } else {
+    if (!WriteFile(options.output_file, "wb", result.spirv)) {
+      return false;
+    }
+  }
+
+  if (options.validate) {
+    // Use Vulkan 1.1, since this is what Tint, internally, uses.
+    spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
+    tools.SetMessageConsumer([](spv_message_level_t, const char*,
+                                const spv_position_t& pos, const char* msg) {
+      std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg
+                << std::endl;
+    });
+    if (!tools.Validate(result.spirv.data(), result.spirv.size(),
+                        spvtools::ValidatorOptions())) {
+      return false;
+    }
+  }
+
+  return true;
+#else
+  (void)program;
+  (void)options;
+  std::cerr << "SPIR-V writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_SPV_WRITER
+}
+
+/// Generate WGSL code for a program.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns true on success
+bool GenerateWgsl(const tint::Program* program, const Options& options) {
+#if TINT_BUILD_WGSL_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::wgsl::Options gen_options;
+  auto result = tint::writer::wgsl::Generate(program, gen_options);
+  if (!result.success) {
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (!WriteFile(options.output_file, "w", result.wgsl)) {
+    return false;
+  }
+
+  if (options.validate) {
+    // Attempt to re-parse the output program with Tint's WGSL reader.
+    auto source = std::make_unique<tint::Source::File>(options.input_filename,
+                                                       result.wgsl);
+    auto reparsed_program = tint::reader::wgsl::Parse(source.get());
+    if (!reparsed_program.IsValid()) {
+      auto diag_printer = tint::diag::Printer::create(stderr, true);
+      tint::diag::Formatter diag_formatter;
+      diag_formatter.format(reparsed_program.Diagnostics(), diag_printer.get());
+      return false;
+    }
+  }
+
+  return true;
+#else
+  (void)program;
+  (void)options;
+  std::cerr << "WGSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_WGSL_WRITER
+}
+
+/// Generate MSL code for a program.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns true on success
+bool GenerateMsl(const tint::Program* program, const Options& options) {
+#if TINT_BUILD_MSL_WRITER
+  const tint::Program* input_program = program;
+
+  // Remap resource numbers to a flat namespace.
+  // TODO(crbug.com/tint/1101): Make this more robust for multiple entry points.
+  using BindingPoint = tint::transform::BindingPoint;
+  tint::transform::BindingRemapper::BindingPoints binding_points;
+  uint32_t next_buffer_idx = 0;
+  uint32_t next_sampler_idx = 0;
+  uint32_t next_texture_idx = 0;
+
+  tint::inspector::Inspector inspector(program);
+  auto entry_points = inspector.GetEntryPoints();
+  for (auto& entry_point : entry_points) {
+    auto bindings = inspector.GetResourceBindings(entry_point.name);
+    for (auto& binding : bindings) {
+      BindingPoint src = {binding.bind_group, binding.binding};
+      if (binding_points.count(src)) {
+        continue;
+      }
+      switch (binding.resource_type) {
+        case tint::inspector::ResourceBinding::ResourceType::kUniformBuffer:
+        case tint::inspector::ResourceBinding::ResourceType::kStorageBuffer:
+        case tint::inspector::ResourceBinding::ResourceType::
+            kReadOnlyStorageBuffer:
+          binding_points.emplace(src, BindingPoint{0, next_buffer_idx++});
+          break;
+        case tint::inspector::ResourceBinding::ResourceType::kSampler:
+        case tint::inspector::ResourceBinding::ResourceType::kComparisonSampler:
+          binding_points.emplace(src, BindingPoint{0, next_sampler_idx++});
+          break;
+        case tint::inspector::ResourceBinding::ResourceType::kSampledTexture:
+        case tint::inspector::ResourceBinding::ResourceType::
+            kMultisampledTexture:
+        case tint::inspector::ResourceBinding::ResourceType::
+            kWriteOnlyStorageTexture:
+        case tint::inspector::ResourceBinding::ResourceType::kDepthTexture:
+        case tint::inspector::ResourceBinding::ResourceType::
+            kDepthMultisampledTexture:
+        case tint::inspector::ResourceBinding::ResourceType::kExternalTexture:
+          binding_points.emplace(src, BindingPoint{0, next_texture_idx++});
+          break;
+      }
+    }
+  }
+
+  // Run the binding remapper transform.
+  tint::transform::Output transform_output;
+  if (!binding_points.empty()) {
+    tint::transform::Manager manager;
+    tint::transform::DataMap inputs;
+    inputs.Add<tint::transform::BindingRemapper::Remappings>(
+        std::move(binding_points),
+        tint::transform::BindingRemapper::AccessControls{},
+        /* mayCollide */ true);
+    manager.Add<tint::transform::BindingRemapper>();
+    transform_output = manager.Run(program, inputs);
+    input_program = &transform_output.program;
+  }
+
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::msl::Options gen_options;
+  gen_options.disable_workgroup_init = options.disable_workgroup_init;
+  gen_options.generate_external_texture_bindings = true;
+  auto result = tint::writer::msl::Generate(input_program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (!WriteFile(options.output_file, "w", result.msl)) {
+    return false;
+  }
+
+  if (options.validate) {
+    tint::val::Result res;
+#ifdef TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
+    res = tint::val::MslUsingMetalAPI(result.msl);
+#else
+#ifdef _WIN32
+    const char* default_xcrun_exe = "metal.exe";
+#else
+    const char* default_xcrun_exe = "xcrun";
+#endif
+    auto xcrun = tint::utils::Command::LookPath(
+        options.xcrun_path.empty() ? default_xcrun_exe : options.xcrun_path);
+    if (xcrun.Found()) {
+      res = tint::val::Msl(xcrun.Path(), result.msl);
+    } else {
+      res.output = "xcrun executable not found. Cannot validate.";
+      res.failed = true;
+    }
+#endif  // TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
+    if (res.failed) {
+      std::cerr << res.output << std::endl;
+      return false;
+    }
+  }
+
+  return true;
+#else
+  (void)program;
+  (void)options;
+  std::cerr << "MSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_MSL_WRITER
+}
+
+/// Generate HLSL code for a program.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns true on success
+bool GenerateHlsl(const tint::Program* program, const Options& options) {
+#if TINT_BUILD_HLSL_WRITER
+  // TODO(jrprice): Provide a way for the user to set non-default options.
+  tint::writer::hlsl::Options gen_options;
+  gen_options.disable_workgroup_init = options.disable_workgroup_init;
+  gen_options.generate_external_texture_bindings = true;
+  auto result = tint::writer::hlsl::Generate(program, gen_options);
+  if (!result.success) {
+    PrintWGSL(std::cerr, *program);
+    std::cerr << "Failed to generate: " << result.error << std::endl;
+    return false;
+  }
+
+  if (!WriteFile(options.output_file, "w", result.hlsl)) {
+    return false;
+  }
+
+  if (options.validate) {
+    tint::val::Result res;
+    if (options.use_fxc) {
+#ifdef _WIN32
+      res = tint::val::HlslUsingFXC(result.hlsl, result.entry_points);
+#else
+      res.failed = true;
+      res.output = "FXC can only be used on Windows. Sorry :X";
+#endif  // _WIN32
+    } else {
+      auto dxc = tint::utils::Command::LookPath(
+          options.dxc_path.empty() ? "dxc" : options.dxc_path);
+      if (dxc.Found()) {
+        res = tint::val::HlslUsingDXC(dxc.Path(), result.hlsl,
+                                      result.entry_points);
+      } else {
+        res.failed = true;
+        res.output = "DXC executable not found. Cannot validate";
+      }
+    }
+    if (res.failed) {
+      std::cerr << res.output << std::endl;
+      return false;
+    }
+  }
+
+  return true;
+#else
+  (void)program;
+  (void)options;
+  std::cerr << "HLSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_HLSL_WRITER
+}
+
+#if TINT_BUILD_GLSL_WRITER
+EShLanguage pipeline_stage_to_esh_language(tint::ast::PipelineStage stage) {
+  switch (stage) {
+    case tint::ast::PipelineStage::kFragment:
+      return EShLangFragment;
+    case tint::ast::PipelineStage::kVertex:
+      return EShLangVertex;
+    case tint::ast::PipelineStage::kCompute:
+      return EShLangCompute;
+    default:
+      TINT_ASSERT(AST, false);
+      return EShLangVertex;
+  }
+}
+#endif
+
+/// Generate GLSL code for a program.
+/// @param program the program to generate
+/// @param options the options that Tint was invoked with
+/// @returns true on success
+bool GenerateGlsl(const tint::Program* program, const Options& options) {
+#if TINT_BUILD_GLSL_WRITER
+  if (options.validate) {
+    glslang::InitializeProcess();
+  }
+
+  auto generate = [&](const tint::Program* prg,
+                      const std::string entry_point_name) -> bool {
+    tint::writer::glsl::Options gen_options;
+    gen_options.generate_external_texture_bindings = true;
+    auto result =
+        tint::writer::glsl::Generate(prg, gen_options, entry_point_name);
+    if (!result.success) {
+      PrintWGSL(std::cerr, *prg);
+      std::cerr << "Failed to generate: " << result.error << std::endl;
+      return false;
+    }
+
+    if (!WriteFile(options.output_file, "w", result.glsl)) {
+      return false;
+    }
+
+    if (options.validate) {
+      for (auto entry_pt : result.entry_points) {
+        EShLanguage lang = pipeline_stage_to_esh_language(entry_pt.second);
+        glslang::TShader shader(lang);
+        const char* strings[1] = {result.glsl.c_str()};
+        int lengths[1] = {static_cast<int>(result.glsl.length())};
+        shader.setStringsWithLengths(strings, lengths, 1);
+        shader.setEntryPoint("main");
+        bool glslang_result =
+            shader.parse(&glslang::DefaultTBuiltInResource, 310, EEsProfile,
+                         false, false, EShMsgDefault);
+        if (!glslang_result) {
+          std::cerr << "Error parsing GLSL shader:\n"
+                    << shader.getInfoLog() << "\n"
+                    << shader.getInfoDebugLog() << "\n";
+          return false;
+        }
+      }
+    }
+    return true;
+  };
+
+  tint::inspector::Inspector inspector(program);
+
+  if (inspector.GetEntryPoints().empty()) {
+    // Pass empty string here so that the GLSL generator will generate
+    // code for all functions, reachable or not.
+    return generate(program, "");
+  }
+
+  bool success = true;
+  for (auto& entry_point : inspector.GetEntryPoints()) {
+    success &= generate(program, entry_point.name);
+  }
+  return success;
+#else
+  (void)program;
+  (void)options;
+  std::cerr << "GLSL writer not enabled in tint build" << std::endl;
+  return false;
+#endif  // TINT_BUILD_GLSL_WRITER
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+  std::vector<std::string> args(argv, argv + argc);
+  Options options;
+
+  tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+
+#if TINT_BUILD_WGSL_WRITER
+  tint::Program::printer = [](const tint::Program* program) {
+    auto result = tint::writer::wgsl::Generate(program, {});
+    if (!result.error.empty()) {
+      return "error: " + result.error;
+    }
+    return result.wgsl;
+  };
+#endif  // TINT_BUILD_WGSL_WRITER
+
+  if (!ParseArgs(args, &options)) {
+    std::cerr << "Failed to parse arguments." << std::endl;
+    return 1;
+  }
+
+  struct TransformFactory {
+    const char* name;
+    std::function<void(tint::transform::Manager& manager,
+                       tint::transform::DataMap& inputs)>
+        make;
+  };
+  std::vector<TransformFactory> transforms = {
+      {"first_index_offset",
+       [](tint::transform::Manager& m, tint::transform::DataMap& i) {
+         i.Add<tint::transform::FirstIndexOffset::BindingPoint>(0, 0);
+         m.Add<tint::transform::FirstIndexOffset>();
+       }},
+      {"fold_trivial_single_use_lets",
+       [](tint::transform::Manager& m, tint::transform::DataMap&) {
+         m.Add<tint::transform::FoldTrivialSingleUseLets>();
+       }},
+      {"renamer",
+       [](tint::transform::Manager& m, tint::transform::DataMap&) {
+         m.Add<tint::transform::Renamer>();
+       }},
+      {"robustness",
+       [](tint::transform::Manager& m, tint::transform::DataMap&) {
+         m.Add<tint::transform::Robustness>();
+       }},
+  };
+  auto transform_names = [&] {
+    std::stringstream names;
+    for (auto& t : transforms) {
+      names << "   " << t.name << std::endl;
+    }
+    return names.str();
+  };
+
+  if (options.show_help) {
+    std::string usage =
+        tint::utils::ReplaceAll(kUsage, "${transforms}", transform_names());
+    std::cout << usage << std::endl;
+    return 0;
+  }
+
+  // Implement output format defaults.
+  if (options.format == Format::kNone) {
+    // Try inferring from filename.
+    options.format = infer_format(options.output_file);
+  }
+  if (options.format == Format::kNone) {
+    // Ultimately, default to SPIR-V assembly. That's nice for interactive use.
+    options.format = Format::kSpvAsm;
+  }
+
+  auto diag_printer = tint::diag::Printer::create(stderr, true);
+  tint::diag::Formatter diag_formatter;
+
+  std::unique_ptr<tint::Program> program;
+  std::unique_ptr<tint::Source::File> source_file;
+
+  enum class InputFormat {
+    kUnknown,
+    kWgsl,
+    kSpirvBin,
+    kSpirvAsm,
+  };
+  auto input_format = InputFormat::kUnknown;
+
+  if (options.input_filename.size() > 5 &&
+      options.input_filename.substr(options.input_filename.size() - 5) ==
+          ".wgsl") {
+    input_format = InputFormat::kWgsl;
+  } else if (options.input_filename.size() > 4 &&
+             options.input_filename.substr(options.input_filename.size() - 4) ==
+                 ".spv") {
+    input_format = InputFormat::kSpirvBin;
+  } else if (options.input_filename.size() > 7 &&
+             options.input_filename.substr(options.input_filename.size() - 7) ==
+                 ".spvasm") {
+    input_format = InputFormat::kSpirvAsm;
+  }
+
+  switch (input_format) {
+    case InputFormat::kUnknown: {
+      std::cerr << "Unknown input format" << std::endl;
+      return 1;
+    }
+    case InputFormat::kWgsl: {
+#if TINT_BUILD_WGSL_READER
+      std::vector<uint8_t> data;
+      if (!ReadFile<uint8_t>(options.input_filename, &data)) {
+        return 1;
+      }
+      source_file = std::make_unique<tint::Source::File>(
+          options.input_filename, std::string(data.begin(), data.end()));
+      program = std::make_unique<tint::Program>(
+          tint::reader::wgsl::Parse(source_file.get()));
+      break;
+#else
+      std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
+      return 1;
+#endif  // TINT_BUILD_WGSL_READER
+    }
+    case InputFormat::kSpirvBin: {
+#if TINT_BUILD_SPV_READER
+      std::vector<uint32_t> data;
+      if (!ReadFile<uint32_t>(options.input_filename, &data)) {
+        return 1;
+      }
+      program =
+          std::make_unique<tint::Program>(tint::reader::spirv::Parse(data));
+      break;
+#else
+      std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+      return 1;
+#endif  // TINT_BUILD_SPV_READER
+    }
+    case InputFormat::kSpirvAsm: {
+#if TINT_BUILD_SPV_READER
+      std::vector<char> text;
+      if (!ReadFile<char>(options.input_filename, &text)) {
+        return 1;
+      }
+      // Use Vulkan 1.1, since this is what Tint, internally, is expecting.
+      spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
+      tools.SetMessageConsumer([](spv_message_level_t, const char*,
+                                  const spv_position_t& pos, const char* msg) {
+        std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg
+                  << std::endl;
+      });
+      std::vector<uint32_t> data;
+      if (!tools.Assemble(text.data(), text.size(), &data,
+                          SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
+        return 1;
+      }
+      program =
+          std::make_unique<tint::Program>(tint::reader::spirv::Parse(data));
+      break;
+#else
+      std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+      return 1;
+#endif  // TINT_BUILD_SPV_READER
+    }
+  }
+
+  if (!program) {
+    std::cerr << "Failed to parse input file: " << options.input_filename
+              << std::endl;
+    return 1;
+  }
+  if (program->Diagnostics().count() > 0) {
+    if (!program->IsValid() && input_format != InputFormat::kWgsl) {
+      // Invalid program from a non-wgsl source. Print the WGSL, to help
+      // understand the diagnostics.
+      PrintWGSL(std::cout, *program);
+    }
+    diag_formatter.format(program->Diagnostics(), diag_printer.get());
+  }
+
+  if (!program->IsValid()) {
+    return 1;
+  }
+  if (options.parse_only) {
+    return 1;
+  }
+
+  tint::transform::Manager transform_manager;
+  tint::transform::DataMap transform_inputs;
+  for (const auto& name : options.transforms) {
+    // TODO(dsinclair): The vertex pulling transform requires setup code to
+    // be run that needs user input. Should we find a way to support that here
+    // maybe through a provided file?
+
+    bool found = false;
+    for (auto& t : transforms) {
+      if (t.name == name) {
+        t.make(transform_manager, transform_inputs);
+        found = true;
+        break;
+      }
+    }
+    if (!found) {
+      std::cerr << "Unknown transform: " << name << std::endl;
+      std::cerr << "Available transforms: " << std::endl << transform_names();
+      return 1;
+    }
+  }
+
+  if (options.emit_single_entry_point) {
+    transform_manager.append(
+        std::make_unique<tint::transform::SingleEntryPoint>());
+    transform_inputs.Add<tint::transform::SingleEntryPoint::Config>(
+        options.ep_name);
+  }
+
+  switch (options.format) {
+    case Format::kMsl: {
+#if TINT_BUILD_MSL_WRITER
+      transform_inputs.Add<tint::transform::Renamer::Config>(
+          tint::transform::Renamer::Target::kMslKeywords,
+          /* preserve_unicode */ false);
+      transform_manager.Add<tint::transform::Renamer>();
+#endif  // TINT_BUILD_MSL_WRITER
+      break;
+    }
+#if TINT_BUILD_GLSL_WRITER
+    case Format::kGlsl: {
+      break;
+    }
+#endif  // TINT_BUILD_GLSL_WRITER
+    case Format::kHlsl: {
+#if TINT_BUILD_HLSL_WRITER
+      transform_inputs.Add<tint::transform::Renamer::Config>(
+          tint::transform::Renamer::Target::kHlslKeywords,
+          /* preserve_unicode */ false);
+      transform_manager.Add<tint::transform::Renamer>();
+#endif  // TINT_BUILD_HLSL_WRITER
+      break;
+    }
+    default:
+      break;
+  }
+
+  auto out = transform_manager.Run(program.get(), std::move(transform_inputs));
+  if (!out.program.IsValid()) {
+    PrintWGSL(std::cerr, out.program);
+    diag_formatter.format(out.program.Diagnostics(), diag_printer.get());
+    return 1;
+  }
+
+  *program = std::move(out.program);
+
+  if (options.dump_inspector_bindings) {
+    std::cout << std::string(80, '-') << std::endl;
+    tint::inspector::Inspector inspector(program.get());
+    auto entry_points = inspector.GetEntryPoints();
+    if (!inspector.error().empty()) {
+      std::cerr << "Failed to get entry points from Inspector: "
+                << inspector.error() << std::endl;
+      return 1;
+    }
+
+    for (auto& entry_point : entry_points) {
+      auto bindings = inspector.GetResourceBindings(entry_point.name);
+      if (!inspector.error().empty()) {
+        std::cerr << "Failed to get bindings from Inspector: "
+                  << inspector.error() << std::endl;
+        return 1;
+      }
+      std::cout << "Entry Point = " << entry_point.name << std::endl;
+      for (auto& binding : bindings) {
+        std::cout << "\t[" << binding.bind_group << "][" << binding.binding
+                  << "]:" << std::endl;
+        std::cout << "\t\t resource_type = "
+                  << ResourceTypeToString(binding.resource_type) << std::endl;
+        std::cout << "\t\t dim = " << TextureDimensionToString(binding.dim)
+                  << std::endl;
+        std::cout << "\t\t sampled_kind = "
+                  << SampledKindToString(binding.sampled_kind) << std::endl;
+        std::cout << "\t\t image_format = "
+                  << TexelFormatToString(binding.image_format) << std::endl;
+      }
+    }
+    std::cout << std::string(80, '-') << std::endl;
+  }
+
+  bool success = false;
+  switch (options.format) {
+    case Format::kSpirv:
+    case Format::kSpvAsm:
+      success = GenerateSpirv(program.get(), options);
+      break;
+    case Format::kWgsl:
+      success = GenerateWgsl(program.get(), options);
+      break;
+    case Format::kMsl:
+      success = GenerateMsl(program.get(), options);
+      break;
+    case Format::kHlsl:
+      success = GenerateHlsl(program.get(), options);
+      break;
+    case Format::kGlsl:
+      success = GenerateGlsl(program.get(), options);
+      break;
+    default:
+      std::cerr << "Unknown output format specified" << std::endl;
+      return 1;
+  }
+  if (!success) {
+    return 1;
+  }
+
+  return 0;
+}
diff --git a/src/tint/debug.cc b/src/tint/debug.cc
new file mode 100644
index 0000000..c51cf3f
--- /dev/null
+++ b/src/tint/debug.cc
@@ -0,0 +1,50 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/debug.h"
+
+#include <memory>
+
+#include "src/tint/utils/debugger.h"
+
+namespace tint {
+namespace {
+
+InternalCompilerErrorReporter* ice_reporter = nullptr;
+
+}  // namespace
+
+void SetInternalCompilerErrorReporter(InternalCompilerErrorReporter* reporter) {
+  ice_reporter = reporter;
+}
+
+InternalCompilerError::InternalCompilerError(const char* file,
+                                             size_t line,
+                                             diag::System system,
+                                             diag::List& diagnostics)
+    : file_(file), line_(line), system_(system), diagnostics_(diagnostics) {}
+
+InternalCompilerError::~InternalCompilerError() {
+  auto file = std::make_shared<Source::File>(file_, "");
+  Source source{Source::Range{{line_}}, file.get()};
+  diagnostics_.add_ice(system_, msg_.str(), source, std::move(file));
+
+  if (ice_reporter) {
+    ice_reporter(diagnostics_);
+  }
+
+  debugger::Break();
+}
+
+}  // namespace tint
diff --git a/src/tint/debug.h b/src/tint/debug.h
new file mode 100644
index 0000000..90e6b66
--- /dev/null
+++ b/src/tint/debug.h
@@ -0,0 +1,123 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_DEBUG_H_
+#define SRC_TINT_DEBUG_H_
+
+#include <utility>
+
+#include "src/tint/diagnostic/diagnostic.h"
+#include "src/tint/diagnostic/formatter.h"
+#include "src/tint/diagnostic/printer.h"
+
+namespace tint {
+
+/// Function type used for registering an internal compiler error reporter
+using InternalCompilerErrorReporter = void(const diag::List&);
+
+/// Sets the global error reporter to be called in case of internal compiler
+/// errors.
+/// @param reporter the error reporter
+void SetInternalCompilerErrorReporter(InternalCompilerErrorReporter* reporter);
+
+/// InternalCompilerError is a helper for reporting internal compiler errors.
+/// Construct the InternalCompilerError with the source location of the ICE
+/// fault and append any error details with the `<<` operator.
+/// When the InternalCompilerError is destructed, the concatenated error message
+/// is appended to the diagnostics list with the severity of
+/// tint::diag::Severity::InternalCompilerError, and if a
+/// InternalCompilerErrorReporter is set, then it is called with the diagnostic
+/// list.
+class InternalCompilerError {
+ public:
+  /// Constructor
+  /// @param file the file containing the ICE
+  /// @param line the line containing the ICE
+  /// @param system the Tint system that has raised the ICE
+  /// @param diagnostics the list of diagnostics to append the ICE message to
+  InternalCompilerError(const char* file,
+                        size_t line,
+                        diag::System system,
+                        diag::List& diagnostics);
+
+  /// Destructor.
+  /// Adds the internal compiler error message to the diagnostics list, and then
+  /// calls the InternalCompilerErrorReporter if one is set.
+  ~InternalCompilerError();
+
+  /// Appends `arg` to the ICE message.
+  /// @param arg the argument to append to the ICE message
+  /// @returns this object so calls can be chained
+  template <typename T>
+  InternalCompilerError& operator<<(T&& arg) {
+    msg_ << std::forward<T>(arg);
+    return *this;
+  }
+
+ private:
+  char const* const file_;
+  const size_t line_;
+  diag::System system_;
+  diag::List& diagnostics_;
+  std::stringstream msg_;
+};
+
+}  // namespace tint
+
+/// TINT_ICE() is a macro for appending an internal compiler error message
+/// to the diagnostics list `diagnostics`, and calling the
+/// InternalCompilerErrorReporter with the full diagnostic list if a reporter is
+/// set.
+/// The ICE message contains the callsite's file and line.
+/// Use the `<<` operator to append an error message to the ICE.
+#define TINT_ICE(system, diagnostics)             \
+  tint::InternalCompilerError(__FILE__, __LINE__, \
+                              ::tint::diag::System::system, diagnostics)
+
+/// TINT_UNREACHABLE() is a macro for appending a "TINT_UNREACHABLE"
+/// internal compiler error message to the diagnostics list `diagnostics`, and
+/// calling the InternalCompilerErrorReporter with the full diagnostic list if a
+/// reporter is set.
+/// The ICE message contains the callsite's file and line.
+/// Use the `<<` operator to append an error message to the ICE.
+#define TINT_UNREACHABLE(system, diagnostics) \
+  TINT_ICE(system, diagnostics) << "TINT_UNREACHABLE "
+
+/// TINT_UNIMPLEMENTED() is a macro for appending a "TINT_UNIMPLEMENTED"
+/// internal compiler error message to the diagnostics list `diagnostics`, and
+/// calling the InternalCompilerErrorReporter with the full diagnostic list if a
+/// reporter is set.
+/// The ICE message contains the callsite's file and line.
+/// Use the `<<` operator to append an error message to the ICE.
+#define TINT_UNIMPLEMENTED(system, diagnostics) \
+  TINT_ICE(system, diagnostics) << "TINT_UNIMPLEMENTED "
+
+/// TINT_ASSERT() is a macro for checking the expression is true, triggering a
+/// TINT_ICE if it is not.
+/// The ICE message contains the callsite's file and line.
+/// @warning: Unlike TINT_ICE() and TINT_UNREACHABLE(), TINT_ASSERT() does not
+/// append a message to an existing tint::diag::List. As such, TINT_ASSERT()
+/// may silently fail in builds where SetInternalCompilerErrorReporter() is not
+/// called. Only use in places where there's no sensible place to put proper
+/// error handling.
+#define TINT_ASSERT(system, condition)                   \
+  do {                                                   \
+    if (!(condition)) {                                  \
+      tint::diag::List diagnostics;                      \
+      TINT_ICE(system, diagnostics)                      \
+          << "TINT_ASSERT(" #system ", " #condition ")"; \
+    }                                                    \
+  } while (false)
+
+#endif  // SRC_TINT_DEBUG_H_
diff --git a/src/tint/debug_test.cc b/src/tint/debug_test.cc
new file mode 100644
index 0000000..257b312
--- /dev/null
+++ b/src/tint/debug_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/debug.h"
+
+#include "gtest/gtest-spi.h"
+
+namespace tint {
+namespace {
+
+TEST(DebugTest, Unreachable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        diag::List diagnostics;
+        TINT_UNREACHABLE(Test, diagnostics);
+      },
+      "internal compiler error");
+}
+
+TEST(DebugTest, AssertTrue) {
+  TINT_ASSERT(Test, true);
+}
+
+TEST(DebugTest, AssertFalse) {
+  EXPECT_FATAL_FAILURE({ TINT_ASSERT(Test, false); },
+                       "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/demangler.cc b/src/tint/demangler.cc
new file mode 100644
index 0000000..cf5e4d6
--- /dev/null
+++ b/src/tint/demangler.cc
@@ -0,0 +1,62 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/demangler.h"
+
+#include "src/tint/program.h"
+
+namespace tint {
+namespace {
+
+constexpr char kSymbol[] = "$";
+constexpr size_t kSymbolLen = sizeof(kSymbol) - 1;
+
+}  // namespace
+
+Demangler::Demangler() = default;
+
+Demangler::~Demangler() = default;
+
+std::string Demangler::Demangle(const SymbolTable& symbols,
+                                const std::string& str) const {
+  std::stringstream out;
+
+  size_t pos = 0;
+  for (;;) {
+    auto idx = str.find(kSymbol, pos);
+    if (idx == std::string::npos) {
+      out << str.substr(pos);
+      break;
+    }
+
+    out << str.substr(pos, idx - pos);
+
+    auto start_idx = idx + kSymbolLen;
+    auto end_idx = start_idx;
+    while (str[end_idx] >= '0' && str[end_idx] <= '9') {
+      end_idx++;
+    }
+    auto len = end_idx - start_idx;
+
+    auto id = str.substr(start_idx, len);
+    Symbol sym(std::stoi(id), symbols.ProgramID());
+    out << symbols.NameFor(sym);
+
+    pos = end_idx;
+  }
+
+  return out.str();
+}
+
+}  // namespace tint
diff --git a/src/tint/demangler.h b/src/tint/demangler.h
new file mode 100644
index 0000000..8c0c964
--- /dev/null
+++ b/src/tint/demangler.h
@@ -0,0 +1,42 @@
+// Copyright 2020 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_DEMANGLER_H_
+#define SRC_TINT_DEMANGLER_H_
+
+#include <string>
+
+namespace tint {
+
+class SymbolTable;
+
+/// Helper to demangle strings and replace symbols with original names
+class Demangler {
+ public:
+  /// Constructor
+  Demangler();
+  /// Destructor
+  ~Demangler();
+
+  /// Transforms given string and replaces any symbols with original names
+  /// @param symbols the symbol table
+  /// @param str the string to replace
+  /// @returns the string with any symbol replacements performed.
+  std::string Demangle(const SymbolTable& symbols,
+                       const std::string& str) const;
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_DEMANGLER_H_
diff --git a/src/tint/demangler_test.cc b/src/tint/demangler_test.cc
new file mode 100644
index 0000000..f2c7658
--- /dev/null
+++ b/src/tint/demangler_test.cc
@@ -0,0 +1,51 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/demangler.h"
+#include "src/tint/symbol_table.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace {
+
+using DemanglerTest = testing::Test;
+
+TEST_F(DemanglerTest, NoSymbols) {
+  SymbolTable t{ProgramID::New()};
+  t.Register("sym1");
+
+  Demangler d;
+  EXPECT_EQ("test str", d.Demangle(t, "test str"));
+}
+
+TEST_F(DemanglerTest, Symbol) {
+  SymbolTable t{ProgramID::New()};
+  t.Register("sym1");
+
+  Demangler d;
+  EXPECT_EQ("test sym1 str", d.Demangle(t, "test $1 str"));
+}
+
+TEST_F(DemanglerTest, MultipleSymbols) {
+  SymbolTable t{ProgramID::New()};
+  t.Register("sym1");
+  t.Register("sym2");
+
+  Demangler d;
+  EXPECT_EQ("test sym1 sym2 sym1 str", d.Demangle(t, "test $1 $2 $1 str"));
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/diagnostic/diagnostic.cc b/src/tint/diagnostic/diagnostic.cc
new file mode 100644
index 0000000..f7b36a4
--- /dev/null
+++ b/src/tint/diagnostic/diagnostic.cc
@@ -0,0 +1,48 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/diagnostic.h"
+
+#include <unordered_map>
+
+#include "src/tint/diagnostic/formatter.h"
+
+namespace tint {
+namespace diag {
+
+Diagnostic::Diagnostic() = default;
+Diagnostic::Diagnostic(const Diagnostic&) = default;
+Diagnostic::~Diagnostic() = default;
+Diagnostic& Diagnostic::operator=(const Diagnostic&) = default;
+
+List::List() = default;
+List::List(std::initializer_list<Diagnostic> list) : entries_(list) {}
+List::List(const List& rhs) = default;
+
+List::List(List&& rhs) = default;
+
+List::~List() = default;
+
+List& List::operator=(const List& rhs) = default;
+
+List& List::operator=(List&& rhs) = default;
+
+std::string List::str() const {
+  diag::Formatter::Style style;
+  style.print_newline_at_end = false;
+  return Formatter{style}.format(*this);
+}
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/diagnostic.h b/src/tint/diagnostic/diagnostic.h
new file mode 100644
index 0000000..28b0c66
--- /dev/null
+++ b/src/tint/diagnostic/diagnostic.h
@@ -0,0 +1,252 @@
+// Copyright 2020 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_DIAGNOSTIC_DIAGNOSTIC_H_
+#define SRC_TINT_DIAGNOSTIC_DIAGNOSTIC_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/source.h"
+
+namespace tint {
+namespace diag {
+
+/// Severity is an enumerator of diagnostic severities.
+enum class Severity { Note, Warning, Error, InternalCompilerError, Fatal };
+
+/// @return true iff `a` is more than, or of equal severity to `b`
+inline bool operator>=(Severity a, Severity b) {
+  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.
+enum class System {
+  AST,
+  Clone,
+  Inspector,
+  Program,
+  ProgramBuilder,
+  Reader,
+  Resolver,
+  Semantic,
+  Symbol,
+  Test,
+  Transform,
+  Utils,
+  Writer,
+};
+
+/// Diagnostic holds all the information for a single compiler diagnostic
+/// message.
+class Diagnostic {
+ public:
+  /// Constructor
+  Diagnostic();
+  /// Copy constructor
+  Diagnostic(const Diagnostic&);
+  /// Destructor
+  ~Diagnostic();
+
+  /// Copy assignment operator
+  /// @return this diagnostic
+  Diagnostic& operator=(const Diagnostic&);
+
+  /// severity is the severity of the diagnostic message.
+  Severity severity = Severity::Error;
+  /// source is the location of the diagnostic.
+  Source source;
+  /// message is the text associated with the diagnostic.
+  std::string message;
+  /// system is the Tint system that raised the diagnostic.
+  System system;
+  /// code is the error code, for example a validation error might have the code
+  /// `"v-0001"`.
+  const char* code = nullptr;
+  /// A shared pointer to a Source::File. Only used if the diagnostic Source
+  /// points to a file that was created specifically for this diagnostic
+  /// (usually an ICE).
+  std::shared_ptr<Source::File> owned_file = nullptr;
+};
+
+/// List is a container of Diagnostic messages.
+class List {
+ public:
+  /// iterator is the type used for range based iteration.
+  using iterator = std::vector<Diagnostic>::const_iterator;
+
+  /// Constructs the list with no elements.
+  List();
+
+  /// Copy constructor. Copies the diagnostics from `list` into this list.
+  /// @param list the list of diagnostics to copy into this list.
+  List(std::initializer_list<Diagnostic> list);
+
+  /// Copy constructor. Copies the diagnostics from `list` into this list.
+  /// @param list the list of diagnostics to copy into this list.
+  List(const List& list);
+
+  /// Move constructor. Moves the diagnostics from `list` into this list.
+  /// @param list the list of diagnostics to move into this list.
+  List(List&& list);
+
+  /// Destructor
+  ~List();
+
+  /// Assignment operator. Copies the diagnostics from `list` into this list.
+  /// @param list the list to copy into this list.
+  /// @return this list.
+  List& operator=(const List& list);
+
+  /// Assignment move operator. Moves the diagnostics from `list` into this
+  /// list.
+  /// @param list the list to move into this list.
+  /// @return this list.
+  List& operator=(List&& list);
+
+  /// adds a diagnostic to the end of this list.
+  /// @param diag the diagnostic to append to this list.
+  void add(Diagnostic&& diag) {
+    if (diag.severity >= Severity::Error) {
+      error_count_++;
+    }
+    entries_.emplace_back(std::move(diag));
+  }
+
+  /// adds a list of diagnostics to the end of this list.
+  /// @param list the diagnostic to append to this list.
+  void add(const List& list) {
+    for (auto diag : list) {
+      add(std::move(diag));
+    }
+  }
+
+  /// adds the note message with the given Source to the end of this list.
+  /// @param system the system raising the note message
+  /// @param note_msg the note message
+  /// @param source the source of the note diagnostic
+  void add_note(System system,
+                const std::string& note_msg,
+                const Source& source) {
+    diag::Diagnostic note{};
+    note.severity = diag::Severity::Note;
+    note.system = system;
+    note.source = source;
+    note.message = note_msg;
+    add(std::move(note));
+  }
+
+  /// adds the warning message with the given Source to the end of this list.
+  /// @param system the system raising the warning message
+  /// @param warning_msg the warning message
+  /// @param source the source of the warning diagnostic
+  void add_warning(System system,
+                   const std::string& warning_msg,
+                   const Source& source) {
+    diag::Diagnostic warning{};
+    warning.severity = diag::Severity::Warning;
+    warning.system = system;
+    warning.source = source;
+    warning.message = warning_msg;
+    add(std::move(warning));
+  }
+
+  /// adds the error message without a source to the end of this list.
+  /// @param system the system raising the error message
+  /// @param err_msg the error message
+  void add_error(System system, std::string err_msg) {
+    diag::Diagnostic error{};
+    error.severity = diag::Severity::Error;
+    error.system = system;
+    error.message = std::move(err_msg);
+    add(std::move(error));
+  }
+
+  /// adds the error message with the given Source to the end of this list.
+  /// @param system the system raising the error message
+  /// @param err_msg the error message
+  /// @param source the source of the error diagnostic
+  void add_error(System system, std::string err_msg, const Source& source) {
+    diag::Diagnostic error{};
+    error.severity = diag::Severity::Error;
+    error.system = system;
+    error.source = source;
+    error.message = std::move(err_msg);
+    add(std::move(error));
+  }
+
+  /// adds the error message with the given code and Source to the end of this
+  /// list.
+  /// @param system the system raising the error message
+  /// @param code the error code
+  /// @param err_msg the error message
+  /// @param source the source of the error diagnostic
+  void add_error(System system,
+                 const char* code,
+                 std::string err_msg,
+                 const Source& source) {
+    diag::Diagnostic error{};
+    error.code = code;
+    error.severity = diag::Severity::Error;
+    error.system = system;
+    error.source = source;
+    error.message = std::move(err_msg);
+    add(std::move(error));
+  }
+
+  /// adds an internal compiler error message to the end of this list.
+  /// @param system the system raising the error message
+  /// @param err_msg the error message
+  /// @param source the source of the internal compiler error
+  /// @param file the Source::File owned by this diagnostic
+  void add_ice(System system,
+               const std::string& err_msg,
+               const Source& source,
+               std::shared_ptr<Source::File> file) {
+    diag::Diagnostic ice{};
+    ice.severity = diag::Severity::InternalCompilerError;
+    ice.system = system;
+    ice.source = source;
+    ice.message = err_msg;
+    ice.owned_file = std::move(file);
+    add(std::move(ice));
+  }
+
+  /// @returns true iff the diagnostic list contains errors diagnostics (or of
+  /// higher severity).
+  bool contains_errors() const { return error_count_ > 0; }
+  /// @returns the number of error diagnostics (or of higher severity).
+  size_t error_count() const { return error_count_; }
+  /// @returns the number of entries in the list.
+  size_t count() const { return entries_.size(); }
+  /// @returns the first diagnostic in the list.
+  iterator begin() const { return entries_.begin(); }
+  /// @returns the last diagnostic in the list.
+  iterator end() const { return entries_.end(); }
+
+  /// @returns a formatted string of all the diagnostics in this list.
+  std::string str() const;
+
+ private:
+  std::vector<Diagnostic> entries_;
+  size_t error_count_ = 0;
+};
+
+}  // namespace diag
+}  // namespace tint
+
+#endif  // SRC_TINT_DIAGNOSTIC_DIAGNOSTIC_H_
diff --git a/src/tint/diagnostic/diagnostic_test.cc b/src/tint/diagnostic/diagnostic_test.cc
new file mode 100644
index 0000000..940971b
--- /dev/null
+++ b/src/tint/diagnostic/diagnostic_test.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/formatter.h"
+
+#include "gtest/gtest.h"
+#include "src/tint/diagnostic/diagnostic.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+TEST(DiagListTest, OwnedFilesShared) {
+  auto file = std::make_shared<Source::File>("path", "content");
+
+  diag::List list_a, list_b;
+  {
+    diag::Diagnostic diag{};
+    diag.source = Source{Source::Range{{0, 0}}, file.get()};
+    list_a.add(std::move(diag));
+  }
+
+  list_b = list_a;
+
+  ASSERT_EQ(list_b.count(), list_a.count());
+  EXPECT_EQ(list_b.begin()->source.file, file.get());
+}
+
+}  // namespace
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/formatter.cc b/src/tint/diagnostic/formatter.cc
new file mode 100644
index 0000000..1eabe91
--- /dev/null
+++ b/src/tint/diagnostic/formatter.cc
@@ -0,0 +1,271 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/formatter.h"
+
+#include <algorithm>
+#include <iterator>
+#include <vector>
+
+#include "src/tint/diagnostic/diagnostic.h"
+#include "src/tint/diagnostic/printer.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+const char* to_str(Severity severity) {
+  switch (severity) {
+    case Severity::Note:
+      return "note";
+    case Severity::Warning:
+      return "warning";
+    case Severity::Error:
+      return "error";
+    case Severity::InternalCompilerError:
+      return "internal compiler error";
+    case Severity::Fatal:
+      return "fatal";
+  }
+  return "";
+}
+
+std::string to_str(const Source::Location& location) {
+  std::stringstream ss;
+  if (location.line > 0) {
+    ss << location.line;
+    if (location.column > 0) {
+      ss << ":" << location.column;
+    }
+  }
+  return ss.str();
+}
+
+}  // namespace
+
+/// State holds the internal formatter state for a format() call.
+struct Formatter::State {
+  /// Constructs a State associated with the given printer.
+  /// @param p the printer to write formatted messages to.
+  explicit State(Printer* p) : printer(p) {}
+  ~State() { flush(); }
+
+  /// set_style() sets the current style to new_style, flushing any pending
+  /// messages to the printer if the style changed.
+  /// @param new_style the new style to apply for future written messages.
+  void set_style(const diag::Style& new_style) {
+    if (style.color != new_style.color || style.bold != new_style.bold) {
+      flush();
+      style = new_style;
+    }
+  }
+
+  /// flush writes any pending messages to the printer, clearing the buffer.
+  void flush() {
+    auto str = stream.str();
+    if (str.length() > 0) {
+      printer->write(str, style);
+      std::stringstream reset;
+      stream.swap(reset);
+    }
+  }
+
+  /// operator<< queues msg to be written to the printer.
+  /// @param msg the value or string to write to the printer
+  /// @returns this State so that calls can be chained
+  template <typename T>
+  State& operator<<(const T& msg) {
+    stream << msg;
+    return *this;
+  }
+
+  /// newline queues a newline to be written to the printer.
+  void newline() { stream << std::endl; }
+
+  /// repeat queues the character c to be written to the printer n times.
+  /// @param c the character to print `n` times
+  /// @param n the number of times to print character `c`
+  void repeat(char c, size_t n) {
+    std::fill_n(std::ostream_iterator<char>(stream), n, c);
+  }
+
+ private:
+  Printer* printer;
+  diag::Style style;
+  std::stringstream stream;
+};
+
+Formatter::Formatter() {}
+Formatter::Formatter(const Style& style) : style_(style) {}
+
+void Formatter::format(const List& list, Printer* printer) const {
+  State state{printer};
+
+  bool first = true;
+  for (auto diag : list) {
+    state.set_style({});
+    if (!first) {
+      state.newline();
+    }
+    format(diag, state);
+    first = false;
+  }
+
+  if (style_.print_newline_at_end) {
+    state.newline();
+  }
+}
+
+void Formatter::format(const Diagnostic& diag, State& state) const {
+  auto const& src = diag.source;
+  auto const& rng = src.range;
+  bool has_code = diag.code != nullptr && diag.code[0] != '\0';
+
+  state.set_style({Color::kDefault, true});
+
+  struct TextAndColor {
+    std::string text;
+    Color color;
+    bool bold = false;
+  };
+  std::vector<TextAndColor> prefix;
+  prefix.reserve(6);
+
+  if (style_.print_file && src.file != nullptr) {
+    if (rng.begin.line > 0) {
+      prefix.emplace_back(TextAndColor{src.file->path + ":" + to_str(rng.begin),
+                                       Color::kDefault});
+    } else {
+      prefix.emplace_back(TextAndColor{src.file->path, Color::kDefault});
+    }
+  } else if (rng.begin.line > 0) {
+    prefix.emplace_back(TextAndColor{to_str(rng.begin), Color::kDefault});
+  }
+
+  Color severity_color = Color::kDefault;
+  switch (diag.severity) {
+    case Severity::Note:
+      break;
+    case Severity::Warning:
+      severity_color = Color::kYellow;
+      break;
+    case Severity::Error:
+      severity_color = Color::kRed;
+      break;
+    case Severity::Fatal:
+    case Severity::InternalCompilerError:
+      severity_color = Color::kMagenta;
+      break;
+  }
+  if (style_.print_severity) {
+    prefix.emplace_back(
+        TextAndColor{to_str(diag.severity), severity_color, true});
+  }
+  if (has_code) {
+    prefix.emplace_back(TextAndColor{diag.code, severity_color});
+  }
+
+  for (size_t i = 0; i < prefix.size(); i++) {
+    if (i > 0) {
+      state << " ";
+    }
+    state.set_style({prefix[i].color, prefix[i].bold});
+    state << prefix[i].text;
+  }
+
+  state.set_style({Color::kDefault, true});
+  if (!prefix.empty()) {
+    state << ": ";
+  }
+  state << diag.message;
+
+  if (style_.print_line && src.file && rng.begin.line > 0) {
+    state.newline();
+    state.set_style({Color::kDefault, false});
+
+    for (size_t line_num = rng.begin.line;
+         (line_num <= rng.end.line) &&
+         (line_num <= src.file->content.lines.size());
+         line_num++) {
+      auto& line = src.file->content.lines[line_num - 1];
+      auto line_len = line.size();
+
+      bool is_ascii = true;
+      for (auto c : line) {
+        if (c == '\t') {
+          state.repeat(' ', style_.tab_width);
+        } else {
+          state << c;
+        }
+        if (c & 0x80) {
+          is_ascii = false;
+        }
+      }
+
+      state.newline();
+
+      // If the line contains non-ascii characters, then we cannot assume that
+      // a single utf8 code unit represents a single glyph, so don't attempt to
+      // draw squiggles.
+      if (!is_ascii) {
+        continue;
+      }
+
+      state.set_style({Color::kCyan, false});
+
+      // Count the number of glyphs in the line span.
+      // start and end use 1-based indexing.
+      auto num_glyphs = [&](size_t start, size_t end) {
+        size_t count = 0;
+        start = (start > 0) ? (start - 1) : 0;
+        end = (end > 0) ? (end - 1) : 0;
+        for (size_t i = start; (i < end) && (i < line_len); i++) {
+          count += (line[i] == '\t') ? style_.tab_width : 1;
+        }
+        return count;
+      };
+
+      if (line_num == rng.begin.line && line_num == rng.end.line) {
+        // Single line
+        state.repeat(' ', num_glyphs(1, rng.begin.column));
+        state.repeat('^', std::max<size_t>(
+                              num_glyphs(rng.begin.column, rng.end.column), 1));
+      } else if (line_num == rng.begin.line) {
+        // Start of multi-line
+        state.repeat(' ', num_glyphs(1, rng.begin.column));
+        state.repeat('^', num_glyphs(rng.begin.column, line_len + 1));
+      } else if (line_num == rng.end.line) {
+        // End of multi-line
+        state.repeat('^', num_glyphs(1, rng.end.column));
+      } else {
+        // Middle of multi-line
+        state.repeat('^', num_glyphs(1, line_len + 1));
+      }
+      state.newline();
+    }
+
+    state.set_style({});
+  }
+}
+
+std::string Formatter::format(const List& list) const {
+  StringPrinter printer;
+  format(list, &printer);
+  return printer.str();
+}
+
+Formatter::~Formatter() = default;
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/formatter.h b/src/tint/diagnostic/formatter.h
new file mode 100644
index 0000000..209bacb
--- /dev/null
+++ b/src/tint/diagnostic/formatter.h
@@ -0,0 +1,72 @@
+// Copyright 2020 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_DIAGNOSTIC_FORMATTER_H_
+#define SRC_TINT_DIAGNOSTIC_FORMATTER_H_
+
+#include <string>
+
+namespace tint {
+namespace diag {
+
+class Diagnostic;
+class List;
+class Printer;
+
+/// Formatter are used to print a list of diagnostics messages.
+class Formatter {
+ public:
+  /// Style controls the formatter's output style.
+  struct Style {
+    /// include the file path for each diagnostic
+    bool print_file = true;
+    /// include the severity for each diagnostic
+    bool print_severity = true;
+    /// include the source line(s) for the diagnostic
+    bool print_line = true;
+    /// print a newline at the end of a diagnostic list
+    bool print_newline_at_end = true;
+    /// width of a tab character
+    size_t tab_width = 2u;
+  };
+
+  /// Constructor for the formatter using a default style.
+  Formatter();
+
+  /// Constructor for the formatter using the custom style.
+  /// @param style the style used for the formatter.
+  explicit Formatter(const Style& style);
+
+  ~Formatter();
+
+  /// @param list the list of diagnostic messages to format
+  /// @param printer the printer used to display the formatted diagnostics
+  void format(const List& list, Printer* printer) const;
+
+  /// @return the list of diagnostics `list` formatted to a string.
+  /// @param list the list of diagnostic messages to format
+  std::string format(const List& list) const;
+
+ private:
+  struct State;
+
+  void format(const Diagnostic& diag, State& state) const;
+
+  const Style style_;
+};
+
+}  // namespace diag
+}  // namespace tint
+
+#endif  // SRC_TINT_DIAGNOSTIC_FORMATTER_H_
diff --git a/src/tint/diagnostic/formatter_test.cc b/src/tint/diagnostic/formatter_test.cc
new file mode 100644
index 0000000..cee4140
--- /dev/null
+++ b/src/tint/diagnostic/formatter_test.cc
@@ -0,0 +1,310 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/formatter.h"
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "src/tint/diagnostic/diagnostic.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+Diagnostic Diag(Severity severity,
+                Source source,
+                std::string message,
+                System system,
+                const char* code = nullptr) {
+  Diagnostic d;
+  d.severity = severity;
+  d.source = source;
+  d.message = std::move(message);
+  d.system = system;
+  d.code = code;
+  return d;
+}
+
+constexpr const char* ascii_content =  // Note: words are tab-delimited
+    R"(the	cat	says	meow
+the	dog	says	woof
+the	snake	says	quack
+the	snail	says	???
+)";
+
+constexpr const char* utf8_content =  // Note: words are tab-delimited
+    "the	\xf0\x9f\x90\xb1	says	meow\n"   // NOLINT: tabs
+    "the	\xf0\x9f\x90\x95	says	woof\n"   // NOLINT: tabs
+    "the	\xf0\x9f\x90\x8d	says	quack\n"  // NOLINT: tabs
+    "the	\xf0\x9f\x90\x8c	says	???\n";   // NOLINT: tabs
+
+class DiagFormatterTest : public testing::Test {
+ public:
+  Source::File ascii_file{"file.name", ascii_content};
+  Source::File utf8_file{"file.name", utf8_content};
+  Diagnostic ascii_diag_note =
+      Diag(Severity::Note,
+           Source{Source::Range{Source::Location{1, 14}}, &ascii_file},
+           "purr",
+           System::Test);
+  Diagnostic ascii_diag_warn =
+      Diag(Severity::Warning,
+           Source{Source::Range{{2, 14}, {2, 18}}, &ascii_file},
+           "grrr",
+           System::Test);
+  Diagnostic ascii_diag_err =
+      Diag(Severity::Error,
+           Source{Source::Range{{3, 16}, {3, 21}}, &ascii_file},
+           "hiss",
+           System::Test,
+           "abc123");
+  Diagnostic ascii_diag_ice =
+      Diag(Severity::InternalCompilerError,
+           Source{Source::Range{{4, 16}, {4, 19}}, &ascii_file},
+           "unreachable",
+           System::Test);
+  Diagnostic ascii_diag_fatal =
+      Diag(Severity::Fatal,
+           Source{Source::Range{{4, 16}, {4, 19}}, &ascii_file},
+           "nothing",
+           System::Test);
+
+  Diagnostic utf8_diag_note =
+      Diag(Severity::Note,
+           Source{Source::Range{Source::Location{1, 15}}, &utf8_file},
+           "purr",
+           System::Test);
+  Diagnostic utf8_diag_warn =
+      Diag(Severity::Warning,
+           Source{Source::Range{{2, 15}, {2, 19}}, &utf8_file},
+           "grrr",
+           System::Test);
+  Diagnostic utf8_diag_err =
+      Diag(Severity::Error,
+           Source{Source::Range{{3, 15}, {3, 20}}, &utf8_file},
+           "hiss",
+           System::Test,
+           "abc123");
+  Diagnostic utf8_diag_ice =
+      Diag(Severity::InternalCompilerError,
+           Source{Source::Range{{4, 15}, {4, 18}}, &utf8_file},
+           "unreachable",
+           System::Test);
+  Diagnostic utf8_diag_fatal =
+      Diag(Severity::Fatal,
+           Source{Source::Range{{4, 15}, {4, 18}}, &utf8_file},
+           "nothing",
+           System::Test);
+};
+
+TEST_F(DiagFormatterTest, Simple) {
+  Formatter fmt{{false, false, false, false}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(1:14: purr
+2:14: grrr
+3:16 abc123: hiss)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, SimpleNewlineAtEnd) {
+  Formatter fmt{{false, false, false, true}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(1:14: purr
+2:14: grrr
+3:16 abc123: hiss
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, SimpleNoSource) {
+  Formatter fmt{{false, false, false, false}};
+  auto diag = Diag(Severity::Note, Source{}, "no source!", System::Test);
+  auto got = fmt.format(List{diag});
+  auto* expect = "no source!";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithFile) {
+  Formatter fmt{{true, false, false, false}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(file.name:1:14: purr
+file.name:2:14: grrr
+file.name:3:16 abc123: hiss)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithSeverity) {
+  Formatter fmt{{false, true, false, false}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(1:14 note: purr
+2:14 warning: grrr
+3:16 error abc123: hiss)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, WithLine) {
+  Formatter fmt{{false, false, true, false}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(1:14: purr
+the  cat  says  meow
+                ^
+
+2:14: grrr
+the  dog  says  woof
+                ^^^^
+
+3:16 abc123: hiss
+the  snake  says  quack
+                  ^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, UnicodeWithLine) {
+  Formatter fmt{{false, false, true, false}};
+  auto got = fmt.format(List{utf8_diag_note, utf8_diag_warn, utf8_diag_err});
+  auto* expect =
+      "1:15: purr\n"
+      "the  \xf0\x9f\x90\xb1  says  meow\n"
+      "\n"
+      "2:15: grrr\n"
+      "the  \xf0\x9f\x90\x95  says  woof\n"
+      "\n"
+      "3:15 abc123: hiss\n"
+      "the  \xf0\x9f\x90\x8d  says  quack\n";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithFileSeverityLine) {
+  Formatter fmt{{true, true, true, false}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(file.name:1:14 note: purr
+the  cat  says  meow
+                ^
+
+file.name:2:14 warning: grrr
+the  dog  says  woof
+                ^^^^
+
+file.name:3:16 error abc123: hiss
+the  snake  says  quack
+                  ^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithMultiLine) {
+  auto multiline = Diag(Severity::Warning,
+                        Source{Source::Range{{2, 9}, {4, 15}}, &ascii_file},
+                        "multiline", System::Test);
+  Formatter fmt{{false, false, true, false}};
+  auto got = fmt.format(List{multiline});
+  auto* expect = R"(2:9: multiline
+the  dog  says  woof
+          ^^^^^^^^^^
+the  snake  says  quack
+^^^^^^^^^^^^^^^^^^^^^^^
+the  snail  says  ???
+^^^^^^^^^^^^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, UnicodeWithMultiLine) {
+  auto multiline = Diag(Severity::Warning,
+                        Source{Source::Range{{2, 9}, {4, 15}}, &utf8_file},
+                        "multiline", System::Test);
+  Formatter fmt{{false, false, true, false}};
+  auto got = fmt.format(List{multiline});
+  auto* expect =
+      "2:9: multiline\n"
+      "the  \xf0\x9f\x90\x95  says  woof\n"
+      "the  \xf0\x9f\x90\x8d  says  quack\n"
+      "the  \xf0\x9f\x90\x8c  says  ???\n";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithFileSeverityLineTab4) {
+  Formatter fmt{{true, true, true, false, 4u}};
+  auto got = fmt.format(List{ascii_diag_note, ascii_diag_warn, ascii_diag_err});
+  auto* expect = R"(file.name:1:14 note: purr
+the    cat    says    meow
+                      ^
+
+file.name:2:14 warning: grrr
+the    dog    says    woof
+                      ^^^^
+
+file.name:3:16 error abc123: hiss
+the    snake    says    quack
+                        ^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithMultiLineTab4) {
+  auto multiline = Diag(Severity::Warning,
+                        Source{Source::Range{{2, 9}, {4, 15}}, &ascii_file},
+                        "multiline", System::Test);
+  Formatter fmt{{false, false, true, false, 4u}};
+  auto got = fmt.format(List{multiline});
+  auto* expect = R"(2:9: multiline
+the    dog    says    woof
+              ^^^^^^^^^^^^
+the    snake    says    quack
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+the    snail    says    ???
+^^^^^^^^^^^^^^^^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, ICE) {
+  Formatter fmt{{}};
+  auto got = fmt.format(List{ascii_diag_ice});
+  auto* expect = R"(file.name:4:16 internal compiler error: unreachable
+the  snail  says  ???
+                  ^^^
+
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, Fatal) {
+  Formatter fmt{{}};
+  auto got = fmt.format(List{ascii_diag_fatal});
+  auto* expect = R"(file.name:4:16 fatal: nothing
+the  snail  says  ???
+                  ^^^
+
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, RangeOOB) {
+  Formatter fmt{{true, true, true, true}};
+  diag::List list;
+  list.add_error(System::Test, "oob",
+                 Source{{{10, 20}, {30, 20}}, &ascii_file});
+  auto got = fmt.format(list);
+  auto* expect = R"(file.name:10:20 error: oob
+
+)";
+  ASSERT_EQ(expect, got);
+}
+
+}  // namespace
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/printer.cc b/src/tint/diagnostic/printer.cc
new file mode 100644
index 0000000..54fd5f7
--- /dev/null
+++ b/src/tint/diagnostic/printer.cc
@@ -0,0 +1,34 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/printer.h"
+
+namespace tint {
+namespace diag {
+
+Printer::~Printer() = default;
+
+StringPrinter::StringPrinter() = default;
+StringPrinter::~StringPrinter() = default;
+
+std::string StringPrinter::str() const {
+  return stream.str();
+}
+
+void StringPrinter::write(const std::string& str, const Style&) {
+  stream << str;
+}
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/printer.h b/src/tint/diagnostic/printer.h
new file mode 100644
index 0000000..570223b
--- /dev/null
+++ b/src/tint/diagnostic/printer.h
@@ -0,0 +1,83 @@
+// Copyright 2020 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_DIAGNOSTIC_PRINTER_H_
+#define SRC_TINT_DIAGNOSTIC_PRINTER_H_
+
+#include <memory>
+#include <sstream>
+#include <string>
+
+namespace tint {
+namespace diag {
+
+class List;
+
+/// Color is an enumerator of colors used by Style.
+enum class Color {
+  kDefault,
+  kBlack,
+  kRed,
+  kGreen,
+  kYellow,
+  kBlue,
+  kMagenta,
+  kCyan,
+  kWhite,
+};
+
+/// Style describes how a diagnostic message should be printed.
+struct Style {
+  /// The foreground text color
+  Color color = Color::kDefault;
+  /// If true the text will be displayed with a strong weight
+  bool bold = false;
+};
+
+/// Printers are used to print formatted diagnostic messages to a terminal.
+class Printer {
+ public:
+  /// @returns a diagnostic Printer
+  /// @param out the file to print to.
+  /// @param use_colors if true, the printer will use colors if `out` is a
+  /// terminal and supports them.
+  static std::unique_ptr<Printer> create(FILE* out, bool use_colors);
+
+  virtual ~Printer();
+
+  /// writes the string str to the printer with the given style.
+  /// @param str the string to write to the printer
+  /// @param style the style used to print `str`
+  virtual void write(const std::string& str, const Style& style) = 0;
+};
+
+/// StringPrinter is an implementation of Printer that writes to a std::string.
+class StringPrinter : public Printer {
+ public:
+  StringPrinter();
+  ~StringPrinter() override;
+
+  /// @returns the printed string.
+  std::string str() const;
+
+  void write(const std::string& str, const Style&) override;
+
+ private:
+  std::stringstream stream;
+};
+
+}  // namespace diag
+}  // namespace tint
+
+#endif  // SRC_TINT_DIAGNOSTIC_PRINTER_H_
diff --git a/src/tint/diagnostic/printer_linux.cc b/src/tint/diagnostic/printer_linux.cc
new file mode 100644
index 0000000..fc40cf3
--- /dev/null
+++ b/src/tint/diagnostic/printer_linux.cc
@@ -0,0 +1,100 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <unistd.h>
+
+#include <cstring>
+
+#include "src/tint/diagnostic/printer.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+bool supports_colors(FILE* f) {
+  if (!isatty(fileno(f))) {
+    return false;
+  }
+
+  const char* cterm = getenv("TERM");
+  if (cterm == nullptr) {
+    return false;
+  }
+
+  std::string term = getenv("TERM");
+  if (term != "cygwin" && term != "linux" && term != "rxvt-unicode-256color" &&
+      term != "rxvt-unicode" && term != "screen-256color" && term != "screen" &&
+      term != "tmux-256color" && term != "tmux" && term != "xterm-256color" &&
+      term != "xterm-color" && term != "xterm") {
+    return false;
+  }
+
+  return true;
+}
+
+class PrinterLinux : public Printer {
+ public:
+  PrinterLinux(FILE* f, bool colors)
+      : file(f), use_colors(colors && supports_colors(f)) {}
+
+  void write(const std::string& str, const Style& style) override {
+    write_color(style.color, style.bold);
+    fwrite(str.data(), 1, str.size(), file);
+    write_color(Color::kDefault, false);
+  }
+
+ private:
+  constexpr const char* color_code(Color color, bool bold) {
+    switch (color) {
+      case Color::kDefault:
+        return bold ? "\u001b[1m" : "\u001b[0m";
+      case Color::kBlack:
+        return bold ? "\u001b[30;1m" : "\u001b[30m";
+      case Color::kRed:
+        return bold ? "\u001b[31;1m" : "\u001b[31m";
+      case Color::kGreen:
+        return bold ? "\u001b[32;1m" : "\u001b[32m";
+      case Color::kYellow:
+        return bold ? "\u001b[33;1m" : "\u001b[33m";
+      case Color::kBlue:
+        return bold ? "\u001b[34;1m" : "\u001b[34m";
+      case Color::kMagenta:
+        return bold ? "\u001b[35;1m" : "\u001b[35m";
+      case Color::kCyan:
+        return bold ? "\u001b[36;1m" : "\u001b[36m";
+      case Color::kWhite:
+        return bold ? "\u001b[37;1m" : "\u001b[37m";
+    }
+    return "";  // unreachable
+  }
+
+  void write_color(Color color, bool bold) {
+    if (use_colors) {
+      auto* code = color_code(color, bold);
+      fwrite(code, 1, strlen(code), file);
+    }
+  }
+
+  FILE* const file;
+  const bool use_colors;
+};
+
+}  // namespace
+
+std::unique_ptr<Printer> Printer::create(FILE* out, bool use_colors) {
+  return std::make_unique<PrinterLinux>(out, use_colors);
+}
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/printer_other.cc b/src/tint/diagnostic/printer_other.cc
new file mode 100644
index 0000000..21498be
--- /dev/null
+++ b/src/tint/diagnostic/printer_other.cc
@@ -0,0 +1,42 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstring>
+
+#include "src/tint/diagnostic/printer.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+class PrinterOther : public Printer {
+ public:
+  explicit PrinterOther(FILE* f) : file(f) {}
+
+  void write(const std::string& str, const Style&) override {
+    fwrite(str.data(), 1, str.size(), file);
+  }
+
+ private:
+  FILE* file;
+};
+
+}  // namespace
+
+std::unique_ptr<Printer> Printer::create(FILE* out, bool) {
+  return std::make_unique<PrinterOther>(out);
+}
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/printer_test.cc b/src/tint/diagnostic/printer_test.cc
new file mode 100644
index 0000000..f0cf374
--- /dev/null
+++ b/src/tint/diagnostic/printer_test.cc
@@ -0,0 +1,98 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/diagnostic/printer.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace diag {
+namespace {
+
+// Actually verifying that the expected colors are printed is exceptionally
+// difficult as:
+// a) The color emission varies by OS.
+// b) The logic checks to see if the printer is writing to a terminal, making
+//    mocking hard.
+// c) Actually probing what gets written to a FILE* is notoriously tricky.
+//
+// The least we can do is to exersice the code - which is what we do here.
+// The test will print each of the colors, and can be examined with human
+// eyeballs.
+// This can be enabled or disabled with ENABLE_PRINTER_TESTS
+#define ENABLE_PRINTER_TESTS 0
+#if ENABLE_PRINTER_TESTS
+
+using PrinterTest = testing::Test;
+
+TEST_F(PrinterTest, WithColors) {
+  auto printer = Printer::create(stdout, true);
+  printer->write("Default", Style{Color::kDefault, false});
+  printer->write("Black", Style{Color::kBlack, false});
+  printer->write("Red", Style{Color::kRed, false});
+  printer->write("Green", Style{Color::kGreen, false});
+  printer->write("Yellow", Style{Color::kYellow, false});
+  printer->write("Blue", Style{Color::kBlue, false});
+  printer->write("Magenta", Style{Color::kMagenta, false});
+  printer->write("Cyan", Style{Color::kCyan, false});
+  printer->write("White", Style{Color::kWhite, false});
+  printf("\n");
+}
+
+TEST_F(PrinterTest, BoldWithColors) {
+  auto printer = Printer::create(stdout, true);
+  printer->write("Default", Style{Color::kDefault, true});
+  printer->write("Black", Style{Color::kBlack, true});
+  printer->write("Red", Style{Color::kRed, true});
+  printer->write("Green", Style{Color::kGreen, true});
+  printer->write("Yellow", Style{Color::kYellow, true});
+  printer->write("Blue", Style{Color::kBlue, true});
+  printer->write("Magenta", Style{Color::kMagenta, true});
+  printer->write("Cyan", Style{Color::kCyan, true});
+  printer->write("White", Style{Color::kWhite, true});
+  printf("\n");
+}
+
+TEST_F(PrinterTest, WithoutColors) {
+  auto printer = Printer::create(stdout, false);
+  printer->write("Default", Style{Color::kDefault, false});
+  printer->write("Black", Style{Color::kBlack, false});
+  printer->write("Red", Style{Color::kRed, false});
+  printer->write("Green", Style{Color::kGreen, false});
+  printer->write("Yellow", Style{Color::kYellow, false});
+  printer->write("Blue", Style{Color::kBlue, false});
+  printer->write("Magenta", Style{Color::kMagenta, false});
+  printer->write("Cyan", Style{Color::kCyan, false});
+  printer->write("White", Style{Color::kWhite, false});
+  printf("\n");
+}
+
+TEST_F(PrinterTest, BoldWithoutColors) {
+  auto printer = Printer::create(stdout, false);
+  printer->write("Default", Style{Color::kDefault, true});
+  printer->write("Black", Style{Color::kBlack, true});
+  printer->write("Red", Style{Color::kRed, true});
+  printer->write("Green", Style{Color::kGreen, true});
+  printer->write("Yellow", Style{Color::kYellow, true});
+  printer->write("Blue", Style{Color::kBlue, true});
+  printer->write("Magenta", Style{Color::kMagenta, true});
+  printer->write("Cyan", Style{Color::kCyan, true});
+  printer->write("White", Style{Color::kWhite, true});
+  printf("\n");
+}
+
+#endif  // ENABLE_PRINTER_TESTS
+}  // namespace
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/diagnostic/printer_windows.cc b/src/tint/diagnostic/printer_windows.cc
new file mode 100644
index 0000000..9dcb43c
--- /dev/null
+++ b/src/tint/diagnostic/printer_windows.cc
@@ -0,0 +1,113 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstring>
+
+#include "src/tint/diagnostic/printer.h"
+
+#define WIN32_LEAN_AND_MEAN 1
+#include <Windows.h>
+
+namespace tint {
+namespace diag {
+namespace {
+
+struct ConsoleInfo {
+  HANDLE handle = INVALID_HANDLE_VALUE;
+  WORD default_attributes = 0;
+  operator bool() const { return handle != INVALID_HANDLE_VALUE; }
+};
+
+ConsoleInfo console_info(FILE* file) {
+  if (file == nullptr) {
+    return {};
+  }
+
+  ConsoleInfo console{};
+  if (file == stdout) {
+    console.handle = GetStdHandle(STD_OUTPUT_HANDLE);
+  } else if (file == stderr) {
+    console.handle = GetStdHandle(STD_ERROR_HANDLE);
+  } else {
+    return {};
+  }
+
+  CONSOLE_SCREEN_BUFFER_INFO info{};
+  if (GetConsoleScreenBufferInfo(console.handle, &info) == 0) {
+    return {};
+  }
+
+  console.default_attributes = info.wAttributes;
+  return console;
+}
+
+class PrinterWindows : public Printer {
+ public:
+  PrinterWindows(FILE* f, bool use_colors)
+      : file(f), console(console_info(use_colors ? f : nullptr)) {}
+
+  void write(const std::string& str, const Style& style) override {
+    write_color(style.color, style.bold);
+    fwrite(str.data(), 1, str.size(), file);
+    write_color(Color::kDefault, false);
+  }
+
+ private:
+  WORD attributes(Color color, bool bold) {
+    switch (color) {
+      case Color::kDefault:
+        return console.default_attributes;
+      case Color::kBlack:
+        return 0;
+      case Color::kRed:
+        return FOREGROUND_RED | (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kGreen:
+        return FOREGROUND_GREEN | (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kYellow:
+        return FOREGROUND_RED | FOREGROUND_GREEN |
+               (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kBlue:
+        return FOREGROUND_BLUE | (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kMagenta:
+        return FOREGROUND_RED | FOREGROUND_BLUE |
+               (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kCyan:
+        return FOREGROUND_GREEN | FOREGROUND_BLUE |
+               (bold ? FOREGROUND_INTENSITY : 0);
+      case Color::kWhite:
+        return FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE |
+               (bold ? FOREGROUND_INTENSITY : 0);
+    }
+    return 0;  // unreachable
+  }
+
+  void write_color(Color color, bool bold) {
+    if (console) {
+      SetConsoleTextAttribute(console.handle, attributes(color, bold));
+      fflush(file);
+    }
+  }
+
+  FILE* const file;
+  const ConsoleInfo console;
+};
+
+}  // namespace
+
+std::unique_ptr<Printer> Printer::create(FILE* out, bool use_colors) {
+  return std::make_unique<PrinterWindows>(out, use_colors);
+}
+
+}  // namespace diag
+}  // namespace tint
diff --git a/src/tint/fuzzers/BUILD.gn b/src/tint/fuzzers/BUILD.gn
index f2291d7..3253e94 100644
--- a/src/tint/fuzzers/BUILD.gn
+++ b/src/tint/fuzzers/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Dawn Authors
+# Copyright 2022 The Dawn & Tint Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,11 +12,323 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import("../../../scripts/dawn_overrides_with_defaults.gni")
+import("//build_overrides/build.gni")
+import("../../../tint_overrides_with_defaults.gni")
 
-# Target aliases to ease merging Tint->Dawn
+# Fuzzers - Libfuzzer based fuzzing targets for Chromium
+# To run the fuzzers outside of Chromium, use the CMake based builds.
 
-group("fuzzers") {
-  deps = [ "${dawn_tint_dir}/src/tint/fuzzers:fuzzers" ]
-  testonly = true
+if (build_with_chromium) {
+  import("//testing/libfuzzer/fuzzer_test.gni")
+
+  fuzzer_corpus_wgsl_dir = "${target_gen_dir}/fuzzer_corpus_wgsl"
+  action("tint_generate_wgsl_corpus") {
+    script = "generate_wgsl_corpus.py"
+    sources = [ "generate_wgsl_corpus.py" ]
+    args = [
+      rebase_path("${tint_root_dir}/test", root_build_dir),
+      rebase_path(fuzzer_corpus_wgsl_dir, root_build_dir),
+    ]
+    outputs = [ fuzzer_corpus_wgsl_dir ]
+  }
+
+  tint_fuzzer_common_libfuzzer_options = [
+    "only_ascii=1",
+    "max_len=10000",
+  ]
+
+  tint_ast_fuzzer_common_libfuzzer_options =
+      tint_fuzzer_common_libfuzzer_options + [
+        "cross_over=0",
+        "mutate_depth=1",
+        "tint_enable_all_mutations=false",
+        "tint_mutation_batch_size=5",
+      ]
+
+  tint_regex_fuzzer_common_libfuzzer_options =
+      tint_fuzzer_common_libfuzzer_options + [
+        "cross_over=0",
+        "mutate_depth=1",
+      ]
+
+  # fuzzer_test doesn't have configs members, so need to define them in an empty
+  # source_set.
+
+  source_set("tint_fuzzer_common_src") {
+    public_configs = [
+      "${tint_root_dir}/src/tint:tint_config",
+      "${tint_root_dir}/src/tint:tint_common_config",
+    ]
+
+    public_deps = [
+      "${tint_root_dir}/src/tint:libtint",
+      "${tint_spirv_tools_dir}/:spvtools_val",
+    ]
+
+    sources = [
+      "data_builder.h",
+      "mersenne_twister_engine.cc",
+      "mersenne_twister_engine.h",
+      "random_generator.cc",
+      "random_generator.h",
+      "random_generator_engine.cc",
+      "random_generator_engine.h",
+      "shuffle_transform.cc",
+      "shuffle_transform.h",
+      "tint_common_fuzzer.cc",
+      "tint_common_fuzzer.h",
+      "tint_reader_writer_fuzzer.h",
+      "transform_builder.h",
+    ]
+  }
+
+  source_set("tint_fuzzer_common_with_init_src") {
+    public_deps = [ ":tint_fuzzer_common_src" ]
+
+    sources = [
+      "cli.cc",
+      "cli.h",
+      "fuzzer_init.cc",
+      "fuzzer_init.h",
+    ]
+  }
+
+  if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+    fuzzer_test("tint_ast_clone_fuzzer") {
+      sources = [ "tint_ast_clone_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_ast_wgsl_writer_fuzzer") {
+      sources = [ "tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc" ]
+      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_regex_wgsl_writer_fuzzer") {
+      sources = [ "tint_regex_fuzzer/tint_regex_wgsl_writer_fuzzer.cc" ]
+      deps = [ "tint_regex_fuzzer:tint_regex_fuzzer" ]
+      libfuzzer_options = tint_regex_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_wgsl_reader_wgsl_writer_fuzzer") {
+      sources = [ "tint_wgsl_reader_wgsl_writer_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+  }
+
+  if (tint_build_wgsl_reader && tint_build_spv_writer) {
+    fuzzer_test("tint_all_transforms_fuzzer") {
+      sources = [ "tint_all_transforms_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_ast_spv_writer_fuzzer") {
+      sources = [ "tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc" ]
+      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_binding_remapper_fuzzer") {
+      sources = [ "tint_binding_remapper_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_first_index_offset_fuzzer") {
+      sources = [ "tint_first_index_offset_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_regex_spv_writer_fuzzer") {
+      sources = [ "tint_regex_fuzzer/tint_regex_spv_writer_fuzzer.cc" ]
+      deps = [ "tint_regex_fuzzer:tint_regex_fuzzer" ]
+      libfuzzer_options = tint_regex_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_renamer_fuzzer") {
+      sources = [ "tint_renamer_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_robustness_fuzzer") {
+      sources = [ "tint_robustness_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_single_entry_point_fuzzer") {
+      sources = [ "tint_single_entry_point_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_vertex_pulling_fuzzer") {
+      sources = [ "tint_vertex_pulling_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_wgsl_reader_spv_writer_fuzzer") {
+      sources = [ "tint_wgsl_reader_spv_writer_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+  }
+
+  if (tint_build_wgsl_reader && tint_build_hlsl_writer) {
+    fuzzer_test("tint_ast_hlsl_writer_fuzzer") {
+      sources = [ "tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc" ]
+      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_regex_hlsl_writer_fuzzer") {
+      sources = [ "tint_regex_fuzzer/tint_regex_hlsl_writer_fuzzer.cc" ]
+      deps = [ "tint_regex_fuzzer:tint_regex_fuzzer" ]
+      libfuzzer_options = tint_regex_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_wgsl_reader_hlsl_writer_fuzzer") {
+      sources = [ "tint_wgsl_reader_hlsl_writer_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+  }
+
+  if (tint_build_wgsl_reader && tint_build_msl_writer) {
+    fuzzer_test("tint_ast_msl_writer_fuzzer") {
+      sources = [ "tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc" ]
+      deps = [ "tint_ast_fuzzer:tint_ast_fuzzer" ]
+      libfuzzer_options = tint_ast_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_regex_msl_writer_fuzzer") {
+      sources = [ "tint_regex_fuzzer/tint_regex_msl_writer_fuzzer.cc" ]
+      deps = [ "tint_regex_fuzzer:tint_regex_fuzzer" ]
+      libfuzzer_options = tint_regex_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+
+    fuzzer_test("tint_wgsl_reader_msl_writer_fuzzer") {
+      sources = [ "tint_wgsl_reader_msl_writer_fuzzer.cc" ]
+      deps = [ ":tint_fuzzer_common_with_init_src" ]
+      dict = "dictionary.txt"
+      libfuzzer_options = tint_fuzzer_common_libfuzzer_options
+      seed_corpus = fuzzer_corpus_wgsl_dir
+      seed_corpus_deps = [ ":tint_generate_wgsl_corpus" ]
+    }
+  }
+
+  if (tint_build_wgsl_reader && tint_build_hlsl_writer &&
+      tint_build_msl_writer && tint_build_spv_writer &&
+      tint_build_wgsl_writer) {
+    executable("tint_black_box_fuzz_target") {
+      sources = [ "tint_black_box_fuzz_target.cc" ]
+      deps = [ ":tint_fuzzer_common_src" ]
+    }
+  }
+
+  group("fuzzers") {
+    testonly = true
+    deps = []
+
+    if (tint_build_wgsl_reader && tint_build_wgsl_writer) {
+      deps += [
+        ":tint_ast_clone_fuzzer",
+        ":tint_ast_wgsl_writer_fuzzer",
+        ":tint_regex_wgsl_writer_fuzzer",
+        ":tint_wgsl_reader_wgsl_writer_fuzzer",
+      ]
+    }
+    if (tint_build_wgsl_reader && tint_build_spv_writer) {
+      deps += [
+        ":tint_all_transforms_fuzzer",
+        ":tint_ast_spv_writer_fuzzer",
+        ":tint_binding_remapper_fuzzer",
+        ":tint_first_index_offset_fuzzer",
+        ":tint_regex_spv_writer_fuzzer",
+        ":tint_renamer_fuzzer",
+        ":tint_robustness_fuzzer",
+        ":tint_single_entry_point_fuzzer",
+        ":tint_vertex_pulling_fuzzer",
+        ":tint_wgsl_reader_spv_writer_fuzzer",
+      ]
+    }
+    if (tint_build_wgsl_reader && tint_build_hlsl_writer) {
+      deps += [
+        ":tint_ast_hlsl_writer_fuzzer",
+        ":tint_regex_hlsl_writer_fuzzer",
+        ":tint_wgsl_reader_hlsl_writer_fuzzer",
+      ]
+    }
+    if (tint_build_wgsl_reader && tint_build_msl_writer) {
+      deps += [
+        ":tint_ast_msl_writer_fuzzer",
+        ":tint_regex_msl_writer_fuzzer",
+        ":tint_wgsl_reader_msl_writer_fuzzer",
+      ]
+    }
+    if (tint_build_wgsl_reader && tint_build_hlsl_writer &&
+        tint_build_msl_writer && tint_build_spv_writer &&
+        tint_build_wgsl_writer) {
+      deps += [ ":tint_black_box_fuzz_target" ]
+    }
+  }
+} else {
+  group("fuzzers") {
+  }
 }
diff --git a/src/tint/fuzzers/CMakeLists.txt b/src/tint/fuzzers/CMakeLists.txt
new file mode 100644
index 0000000..55c9963
--- /dev/null
+++ b/src/tint/fuzzers/CMakeLists.txt
@@ -0,0 +1,114 @@
+# Copyright 2020 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.
+
+function(add_tint_fuzzer NAME)
+  add_executable(${NAME}
+    ${NAME}.cc
+    cli.cc
+    cli.h
+    data_builder.h
+    fuzzer_init.cc
+    fuzzer_init.h
+    mersenne_twister_engine.cc
+    mersenne_twister_engine.h
+    random_generator.cc
+    random_generator.h
+    random_generator_engine.cc
+    random_generator_engine.h
+    shuffle_transform.cc
+    shuffle_transform.h
+    tint_common_fuzzer.cc
+    tint_common_fuzzer.h
+    tint_reader_writer_fuzzer.h
+    transform_builder.h
+    )
+  target_link_libraries(${NAME} libtint-fuzz)
+  tint_default_compile_options(${NAME})
+  target_compile_options(${NAME} PRIVATE -Wno-missing-prototypes)
+endfunction()
+
+if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
+  add_tint_fuzzer(tint_wgsl_reader_wgsl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_SPV_WRITER})
+  add_tint_fuzzer(tint_all_transforms_fuzzer)
+  add_tint_fuzzer(tint_binding_remapper_fuzzer)
+  add_tint_fuzzer(tint_first_index_offset_fuzzer)
+  add_tint_fuzzer(tint_renamer_fuzzer)
+  add_tint_fuzzer(tint_robustness_fuzzer)
+  add_tint_fuzzer(tint_single_entry_point_fuzzer)
+  add_tint_fuzzer(tint_vertex_pulling_fuzzer)
+  add_tint_fuzzer(tint_wgsl_reader_spv_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_HLSL_WRITER})
+  add_tint_fuzzer(tint_wgsl_reader_hlsl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_MSL_WRITER})
+  add_tint_fuzzer(tint_wgsl_reader_msl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_SPV_READER} AND ${TINT_BUILD_WGSL_WRITER})
+  add_tint_fuzzer(tint_spv_reader_wgsl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_SPV_READER} AND ${TINT_BUILD_SPV_WRITER})
+  add_tint_fuzzer(tint_spv_reader_spv_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_SPV_READER} AND ${TINT_BUILD_HLSL_WRITER})
+  add_tint_fuzzer(tint_spv_reader_hlsl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_SPV_READER} AND ${TINT_BUILD_MSL_WRITER})
+  add_tint_fuzzer(tint_spv_reader_msl_writer_fuzzer)
+endif()
+
+if (${TINT_BUILD_WGSL_READER} AND ${TINT_BUILD_WGSL_WRITER})
+  add_tint_fuzzer(tint_ast_clone_fuzzer)
+endif()
+
+if (${TINT_BUILD_SPIRV_TOOLS_FUZZER})
+  add_subdirectory(tint_spirv_tools_fuzzer)
+endif()
+
+if (${TINT_BUILD_AST_FUZZER})
+  add_subdirectory(tint_ast_fuzzer)
+endif()
+
+if (${TINT_BUILD_REGEX_FUZZER})
+  add_subdirectory(tint_regex_fuzzer)
+endif()
+
+if (${TINT_BUILD_WGSL_READER}
+    AND ${TINT_BUILD_HLSL_WRITER}
+    AND ${TINT_BUILD_MSL_WRITER}
+    AND ${TINT_BUILD_SPV_WRITER}
+    AND ${TINT_BUILD_WGSL_WRITER})
+  add_executable(tint_black_box_fuzz_target
+    mersenne_twister_engine.cc
+    mersenne_twister_engine.h
+    random_generator.cc
+    random_generator.h
+    random_generator_engine.cc
+    random_generator_engine.h
+    tint_black_box_fuzz_target.cc
+    tint_common_fuzzer.cc
+    tint_common_fuzzer.h
+    )
+  target_link_libraries(tint_black_box_fuzz_target libtint)
+  tint_default_compile_options(tint_black_box_fuzz_target)
+endif()
diff --git a/src/tint/fuzzers/cli.cc b/src/tint/fuzzers/cli.cc
new file mode 100644
index 0000000..2a0b814
--- /dev/null
+++ b/src/tint/fuzzers/cli.cc
@@ -0,0 +1,116 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/cli.h"
+
+#include <cstring>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+
+namespace tint {
+namespace fuzzers {
+namespace {
+
+const char* const kHelpMessage = R"(
+This is a fuzzer for the Tint compiler that works by mutating the AST.
+
+Below is a list of all supported parameters for this fuzzer. You may want to
+run it with -help=1 to check out libfuzzer parameters.
+
+  -tint_dump_input=
+                       If `true`, the fuzzer will dump input data to a file with
+                       name tint_input_<hash>.spv/wgsl, where the hash is the hash
+                       of the input data.
+
+  -tint_help
+                       Show this message. Note that there is also a -help=1
+                       parameter that will display libfuzzer's help message.
+
+  -tint_enforce_validity=
+                       If `true`, the fuzzer will enforce that Tint does not
+                       generate invalid shaders. Currently `false` by default
+                       since options provided by the fuzzer are not guaranteed
+                       to be correct.
+                       See https://bugs.chromium.org/p/tint/issues/detail?id=1356
+)";
+
+[[noreturn]] void InvalidParam(const std::string& param) {
+  std::cout << "Invalid value for " << param << std::endl;
+  std::cout << kHelpMessage << std::endl;
+  exit(1);
+}
+
+bool ParseBool(const std::string& value, bool* out) {
+  if (value.compare("true") == 0) {
+    *out = true;
+  } else if (value.compare("false") == 0) {
+    *out = false;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+CliParams ParseCliParams(int* argc, char** argv) {
+  CliParams cli_params;
+  auto help = false;
+
+  for (int i = *argc - 1; i > 0; --i) {
+    std::string param(argv[i]);
+    auto recognized_parameter = true;
+
+    if (std::string::npos != param.find("-tint_dump_input=")) {
+      if (!ParseBool(param.substr(std::string("-tint_dump_input=").length()),
+                     &cli_params.dump_input)) {
+        InvalidParam(param);
+      }
+    } else if (std::string::npos != param.find("-tint_help")) {
+      help = true;
+    } else if (std::string::npos != param.find("-tint_enforce_validity=")) {
+      if (!ParseBool(
+              param.substr(std::string("-tint_enforce_validity=").length()),
+              &cli_params.enforce_validity)) {
+        InvalidParam(param);
+      }
+    } else {
+      recognized_parameter = false;
+    }
+
+    if (recognized_parameter) {
+      // Remove the recognized parameter from the list of all parameters by
+      // swapping it with the last one. This will suppress warnings in the
+      // libFuzzer about unrecognized parameters. By default, libFuzzer thinks
+      // that all user-defined parameters start with two dashes. However, we are
+      // forced to use a single one to make the fuzzer compatible with the
+      // ClusterFuzz.
+      std::swap(argv[i], argv[*argc - 1]);
+      *argc -= 1;
+    }
+  }
+
+  if (help) {
+    std::cout << kHelpMessage << std::endl;
+    exit(0);
+  }
+
+  return cli_params;
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/cli.h b/src/tint/fuzzers/cli.h
new file mode 100644
index 0000000..02ca3db
--- /dev/null
+++ b/src/tint/fuzzers/cli.h
@@ -0,0 +1,46 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_CLI_H_
+#define SRC_TINT_FUZZERS_CLI_H_
+
+#include <cstdint>
+
+namespace tint {
+namespace fuzzers {
+
+/// CLI parameters accepted by the fuzzer. Type -tint_help in the CLI to see the
+/// help message
+struct CliParams {
+  /// Log contents of input shader
+  bool dump_input = false;
+  /// Throw error if shader becomes invalid during run
+  bool enforce_validity = false;
+};
+
+/// @brief Parses CLI parameters.
+///
+/// This function will exit the process with non-zero return code if some
+/// parameters are invalid. This function will remove recognized parameters from
+/// `argv` and adjust `argc` accordingly.
+///
+/// @param argc - the total number of parameters.
+/// @param argv - array of all CLI parameters.
+/// @return parsed parameters.
+CliParams ParseCliParams(int* argc, char** argv);
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_CLI_H_
diff --git a/src/tint/fuzzers/data_builder.h b/src/tint/fuzzers/data_builder.h
new file mode 100644
index 0000000..e0c104a
--- /dev/null
+++ b/src/tint/fuzzers/data_builder.h
@@ -0,0 +1,246 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_DATA_BUILDER_H_
+#define SRC_TINT_FUZZERS_DATA_BUILDER_H_
+
+#include <cassert>
+#include <functional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/writer/hlsl/generator.h"
+#include "src/tint/writer/msl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Builder for generic pseudo-random data
+class DataBuilder {
+ public:
+  /// @brief Initializes the internal engine using a seed value
+  /// @param seed - seed value passed to engine
+  explicit DataBuilder(uint64_t seed) : generator_(seed) {}
+
+  /// @brief Initializes the internal engine using seed data
+  /// @param data - data fuzzer to calculate seed from
+  /// @param size - size of data buffer
+  explicit DataBuilder(const uint8_t* data, size_t size)
+      : generator_(RandomGenerator::CalculateSeed(data, size)) {
+    assert(data != nullptr && "|data| must be !nullptr");
+  }
+
+  /// Destructor
+  ~DataBuilder() = default;
+
+  /// Move Constructor
+  DataBuilder(DataBuilder&&) = default;
+
+  /// Generate pseudo-random data of a specific type
+  /// @tparam T - type of data to produce
+  /// @returns pseudo-random data of type T
+  template <typename T>
+  T build() {
+    return BuildImpl<T>::impl(this);
+  }
+
+  /// Generate pseudo-random data of a specific type in a vector
+  /// @tparam T - data type held vector
+  /// @returns pseudo-random data of type std::vector<T>
+  template <typename T>
+  std::vector<T> vector() {
+    auto count = build<uint8_t>();
+    std::vector<T> out(count);
+    for (uint8_t i = 0; i < count; i++) {
+      out[i] = build<T>();
+    }
+    return out;
+  }
+
+  /// Generate complex pseudo-random data of a specific type in a vector
+  /// @tparam T - data type held vector
+  /// @tparam Callback - callback that takes in a DataBuilder* and returns a T
+  /// @param generate - callback for generating each instance of T
+  /// @returns pseudo-random data of type std::vector<T>
+  template <typename T, typename Callback>
+  std::vector<T> vector(Callback generate) {
+    auto count = build<uint8_t>();
+    std::vector<T> out(count);
+    for (size_t i = 0; i < count; i++) {
+      out[i] = generate(this);
+    }
+    return out;
+  }
+
+  /// Generate an pseudo-random entry to a enum class.
+  /// Assumes enum is tightly packed starting at 0.
+  /// @tparam T - type of enum class
+  /// @param count - number of entries in enum class
+  /// @returns a random enum class entry
+  template <typename T>
+  T enum_class(uint32_t count) {
+    return static_cast<T>(generator_.Get4Bytes() % count);
+  }
+
+ private:
+  RandomGenerator generator_;
+
+  // Disallow copy & assign
+  DataBuilder(const DataBuilder&) = delete;
+  DataBuilder& operator=(const DataBuilder&) = delete;
+
+  /// Get N bytes of pseudo-random data
+  /// @param out - pointer to location to save data
+  /// @param n - number of bytes to get
+  void build(void* out, size_t n) {
+    assert(out != nullptr && "|out| cannot be nullptr");
+    assert(n > 0 && "|n| must be > 0");
+
+    generator_.GetNBytes(reinterpret_cast<uint8_t*>(out), n);
+  }
+
+  /// Generate pseudo-random data of a specific type into an output var
+  /// @tparam T - type of data to produce
+  /// @param out - output var to generate into
+  template <typename T>
+  void build(T& out) {
+    out = build<T>();
+  }
+
+  /// Implementation of ::build<T>()
+  /// @tparam T - type of data to produce
+  template <typename T>
+  struct BuildImpl {
+    /// Generate a pseudo-random variable of type T
+    /// @param b - data builder to use
+    /// @returns a variable of type T filled with pseudo-random data
+    static T impl(DataBuilder* b) {
+      T out{};
+      b->build(&out, sizeof(T));
+      return out;
+    }
+  };
+
+  /// Specialization for std::string
+  template <>
+  struct BuildImpl<std::string> {
+    /// Generate a pseudo-random string
+    /// @param b - data builder to use
+    /// @returns a string filled with pseudo-random data
+    static std::string impl(DataBuilder* b) {
+      auto count = b->build<uint8_t>();
+      if (count == 0) {
+        return "";
+      }
+      std::vector<uint8_t> source(count);
+      b->build(source.data(), count);
+      return {source.begin(), source.end()};
+    }
+  };
+
+  /// Specialization for bool
+  template <>
+  struct BuildImpl<bool> {
+    /// Generate a pseudo-random bool
+    /// @param b - data builder to use
+    /// @returns a boolean with even odds of being true or false
+    static bool impl(DataBuilder* b) { return b->generator_.GetBool(); }
+  };
+
+  /// Specialization for writer::msl::Options
+  template <>
+  struct BuildImpl<writer::msl::Options> {
+    /// Generate a pseudo-random writer::msl::Options struct
+    /// @param b - data builder to use
+    /// @returns writer::msl::Options filled with pseudo-random data
+    static writer::msl::Options impl(DataBuilder* b) {
+      writer::msl::Options out{};
+      b->build(out.buffer_size_ubo_index);
+      b->build(out.fixed_sample_mask);
+      b->build(out.emit_vertex_point_size);
+      b->build(out.disable_workgroup_init);
+      b->build(out.generate_external_texture_bindings);
+      b->build(out.array_length_from_uniform);
+      return out;
+    }
+  };
+
+  /// Specialization for writer::hlsl::Options
+  template <>
+  struct BuildImpl<writer::hlsl::Options> {
+    /// Generate a pseudo-random writer::hlsl::Options struct
+    /// @param b - data builder to use
+    /// @returns writer::hlsl::Options filled with pseudo-random data
+    static writer::hlsl::Options impl(DataBuilder* b) {
+      writer::hlsl::Options out{};
+      b->build(out.root_constant_binding_point);
+      b->build(out.disable_workgroup_init);
+      b->build(out.array_length_from_uniform);
+      return out;
+    }
+  };
+
+  /// Specialization for writer::spirv::Options
+  template <>
+  struct BuildImpl<writer::spirv::Options> {
+    /// Generate a pseudo-random writer::spirv::Options struct
+    /// @param b - data builder to use
+    /// @returns writer::spirv::Options filled with pseudo-random data
+    static writer::spirv::Options impl(DataBuilder* b) {
+      writer::spirv::Options out{};
+      b->build(out.emit_vertex_point_size);
+      b->build(out.disable_workgroup_init);
+      return out;
+    }
+  };
+
+  /// Specialization for writer::ArrayLengthFromUniformOptions
+  template <>
+  struct BuildImpl<writer::ArrayLengthFromUniformOptions> {
+    /// Generate a pseudo-random writer::ArrayLengthFromUniformOptions struct
+    /// @param b - data builder to use
+    /// @returns writer::ArrayLengthFromUniformOptions filled with pseudo-random
+    /// data
+    static writer::ArrayLengthFromUniformOptions impl(DataBuilder* b) {
+      writer::ArrayLengthFromUniformOptions out{};
+      b->build(out.ubo_binding);
+      b->build(out.bindpoint_to_size_index);
+      return out;
+    }
+  };
+
+  /// Specialization for std::unordered_map<K, V>
+  template <typename K, typename V>
+  struct BuildImpl<std::unordered_map<K, V>> {
+    /// Generate a pseudo-random std::unordered_map<K, V>
+    /// @param b - data builder to use
+    /// @returns std::unordered_map<K, V> filled with
+    /// pseudo-random data
+    static std::unordered_map<K, V> impl(DataBuilder* b) {
+      std::unordered_map<K, V> out;
+      uint8_t count = b->build<uint8_t>();
+      for (uint8_t i = 0; i < count; ++i) {
+        out.emplace(b->build<K>(), b->build<V>());
+      }
+      return out;
+    }
+  };
+};
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_DATA_BUILDER_H_
diff --git a/src/tint/fuzzers/dictionary.txt b/src/tint/fuzzers/dictionary.txt
new file mode 100644
index 0000000..8f15ff6
--- /dev/null
+++ b/src/tint/fuzzers/dictionary.txt
@@ -0,0 +1,112 @@
+"&"
+"&&"
+"->"
+"[["
+"]]"
+"/"
+"!"
+"["
+"]"
+"{"
+"}"
+":"
+","
+"="
+"=="
+">"
+">="
+"<"
+"<="
+"%"
+"-"
+"::"
+"!="
+"."
+"+"
+"|"
+"||"
+"("
+")"
+";"
+"*"
+"^"
+"array"
+"binding"
+"bitcast"
+"bool"
+"block"
+"break"
+"builtin"
+"case"
+"compute"
+"const"
+"continue"
+"continuing"
+"discard"
+"default"
+"else"
+"elseif"
+"f32"
+"fallthrough"
+"false"
+"fn"
+"fragment"
+"function"
+"i32"
+"if"
+"image"
+"import"
+"in"
+"location"
+"loop"
+"mat2x2"
+"mat2x3"
+"mat2x4"
+"mat3x2"
+"mat3x3"
+"mat3x4"
+"mat4x2"
+"mat4x3"
+"mat4x4"
+"offset"
+"out"
+"private"
+"ptr"
+"return"
+"sampler"
+"sampler_comparison"
+"set"
+"storage"
+"stage"
+"stride"
+"struct"
+"switch"
+"texture_depth_2d"
+"texture_depth_2d_array"
+"texture_depth_cube"
+"texture_depth_cube_array"
+"texture_depth_multisampled_2d"
+"texture_multisampled_2d"
+"texture_storage_1d"
+"texture_storage_2d_array"
+"texture_storage_2d"
+"texture_storage_2d_array"
+"texture_storage_3d"
+"texture_1d"
+"texture_2d"
+"texture_2d_array"
+"texture_3d"
+"texture_cube"
+"texture_cube_array"
+"true"
+"type"
+"u32"
+"uniform"
+"var"
+"vec2"
+"vec3"
+"vec4"
+"vertex"
+"void"
+"workgroup"
+"workgroup_size"
diff --git a/src/tint/fuzzers/fuzzer_init.cc b/src/tint/fuzzers/fuzzer_init.cc
new file mode 100644
index 0000000..ac9a4cf
--- /dev/null
+++ b/src/tint/fuzzers/fuzzer_init.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/cli.h"
+
+namespace tint {
+namespace fuzzers {
+
+namespace {
+CliParams cli_params;
+}
+
+const CliParams& GetCliParams() {
+  return cli_params;
+}
+
+extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
+  cli_params = ParseCliParams(argc, *argv);
+  return 0;
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/fuzzer_init.h b/src/tint/fuzzers/fuzzer_init.h
new file mode 100644
index 0000000..b81a850
--- /dev/null
+++ b/src/tint/fuzzers/fuzzer_init.h
@@ -0,0 +1,29 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_FUZZER_INIT_H_
+#define SRC_TINT_FUZZERS_FUZZER_INIT_H_
+
+#include "src/tint/fuzzers/cli.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Returns the common CliParams parsed and populated by LLVMFuzzerInitialize()
+const CliParams& GetCliParams();
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_FUZZER_INIT_H_
diff --git a/src/tint/fuzzers/generate_spirv_corpus.py b/src/tint/fuzzers/generate_spirv_corpus.py
new file mode 100644
index 0000000..c6089015
--- /dev/null
+++ b/src/tint/fuzzers/generate_spirv_corpus.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Collect all .spvasm files under a given directory, assemble them using
+# spirv-as, and emit the assembled binaries to a given corpus directory,
+# flattening their file names by replacing path separators with underscores.
+# If the output directory already exists, it will be deleted and re-created.
+# Files ending with ".expected.spvasm" are skipped.
+#
+# The intended use of this script is to generate a corpus of SPIR-V
+# binaries for fuzzing.
+#
+# Usage:
+#    generate_spirv_corpus.py <input_dir> <corpus_dir> <path to spirv-as>
+
+import os
+import pathlib
+import shutil
+import subprocess
+import sys
+
+
+def list_spvasm_files(root_search_dir):
+    for root, folders, files in os.walk(root_search_dir):
+        for filename in folders + files:
+            if pathlib.Path(filename).suffix == ".spvasm":
+                yield os.path.join(root, filename)
+
+
+def main():
+    if len(sys.argv) != 4:
+        print("Usage: " + sys.argv[0] +
+              " <input dir> <output dir> <spirv-as path>")
+        return 1
+    input_dir: str = os.path.abspath(sys.argv[1].rstrip(os.sep))
+    corpus_dir: str = os.path.abspath(sys.argv[2])
+    spirv_as_path: str = os.path.abspath(sys.argv[3])
+    if os.path.exists(corpus_dir):
+        shutil.rmtree(corpus_dir)
+    os.makedirs(corpus_dir)
+
+    # It might be that some of the attempts to convert SPIR-V assembly shaders
+    # into SPIR-V binaries go wrong. It is sensible to tolerate a small number
+    # of such errors, to avoid fuzzer preparation failing due to bugs in
+    # spirv-as. But it is important to know when a large number of failures
+    # occur, in case something is more deeply wrong.
+    num_errors = 0
+    max_tolerated_errors = 10
+    logged_errors = ""
+
+    for in_file in list_spvasm_files(input_dir):
+        if in_file.endswith(".expected.spvasm"):
+            continue
+        out_file = os.path.splitext(
+            corpus_dir + os.sep +
+            in_file[len(input_dir) + 1:].replace(os.sep, '_'))[0] + ".spv"
+        cmd = [
+            spirv_as_path, "--target-env", "spv1.3", in_file, "-o", out_file
+        ]
+        proc = subprocess.Popen(cmd,
+                                stdout=subprocess.PIPE,
+                                stderr=subprocess.PIPE)
+        stdout, stderr = proc.communicate()
+        if proc.returncode != 0:
+            num_errors += 1
+            logged_errors += "Error running " + " ".join(
+                cmd) + ": " + stdout.decode('utf-8') + stderr.decode('utf-8')
+
+    if num_errors > max_tolerated_errors:
+        print("Too many (" + str(num_errors) +
+              ") errors occured while generating the SPIR-V corpus.")
+        print(logged_errors)
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/src/tint/fuzzers/generate_wgsl_corpus.py b/src/tint/fuzzers/generate_wgsl_corpus.py
new file mode 100644
index 0000000..65c564f
--- /dev/null
+++ b/src/tint/fuzzers/generate_wgsl_corpus.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Collect all .wgsl files under a given directory and copy them to a given
+# corpus directory, flattening their file names by replacing path
+# separators with underscores. If the output directory already exists, it
+# will be deleted and re-created. Files ending with ".expected.spvasm" are
+# skipped.
+#
+# The intended use of this script is to generate a corpus of WGSL shaders
+# for fuzzing.
+#
+# Usage:
+#    generate_wgsl_corpus.py <input_dir> <corpus_dir>
+
+import os
+import pathlib
+import shutil
+import sys
+
+
+def list_wgsl_files(root_search_dir):
+    for root, folders, files in os.walk(root_search_dir):
+        for filename in folders + files:
+            if pathlib.Path(filename).suffix == '.wgsl':
+                yield os.path.join(root, filename)
+
+
+def main():
+    if len(sys.argv) != 3:
+        print("Usage: " + sys.argv[0] + " <input dir> <output dir>")
+        return 1
+    input_dir: str = os.path.abspath(sys.argv[1].rstrip(os.sep))
+    corpus_dir: str = os.path.abspath(sys.argv[2])
+    if os.path.exists(corpus_dir):
+        shutil.rmtree(corpus_dir)
+    os.makedirs(corpus_dir)
+    for in_file in list_wgsl_files(input_dir):
+        if in_file.endswith(".expected.wgsl"):
+            continue
+        out_file = in_file[len(input_dir) + 1:].replace(os.sep, '_')
+        shutil.copy(in_file, corpus_dir + os.sep + out_file)
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/src/tint/fuzzers/mersenne_twister_engine.cc b/src/tint/fuzzers/mersenne_twister_engine.cc
new file mode 100644
index 0000000..5acba2b
--- /dev/null
+++ b/src/tint/fuzzers/mersenne_twister_engine.cc
@@ -0,0 +1,59 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/mersenne_twister_engine.h"
+
+#include <algorithm>
+#include <cassert>
+
+#include "src/tint/utils/hash.h"
+
+namespace tint {
+namespace fuzzers {
+
+namespace {
+
+/// Generate integer from uniform distribution
+/// @tparam I - integer type
+/// @param engine - random number engine to use
+/// @param lower - Lower bound of integer generated
+/// @param upper - Upper bound of integer generated
+/// @returns i, where lower <= i < upper
+template <typename I>
+I RandomInteger(std::mt19937_64* engine, I lower, I upper) {
+  assert(lower < upper && "|lower| must be strictly less than |upper|");
+  return std::uniform_int_distribution<I>(lower, upper - 1)(*engine);
+}
+
+}  // namespace
+
+MersenneTwisterEngine::MersenneTwisterEngine(uint64_t seed) : engine_(seed) {}
+
+uint32_t MersenneTwisterEngine::RandomUInt32(uint32_t lower, uint32_t upper) {
+  return RandomInteger(&engine_, lower, upper);
+}
+
+uint64_t MersenneTwisterEngine::RandomUInt64(uint64_t lower, uint64_t upper) {
+  return RandomInteger(&engine_, lower, upper);
+}
+
+void MersenneTwisterEngine::RandomNBytes(uint8_t* dest, size_t n) {
+  assert(dest && "|dest| must not be nullptr");
+  std::generate(
+      dest, dest + n,
+      std::independent_bits_engine<std::mt19937_64, 8, uint8_t>(engine_));
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/mersenne_twister_engine.h b/src/tint/fuzzers/mersenne_twister_engine.h
new file mode 100644
index 0000000..c482953
--- /dev/null
+++ b/src/tint/fuzzers/mersenne_twister_engine.h
@@ -0,0 +1,61 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_MERSENNE_TWISTER_ENGINE_H_
+#define SRC_TINT_FUZZERS_MERSENNE_TWISTER_ENGINE_H_
+
+#include <random>
+
+#include "src/tint/fuzzers/random_generator_engine.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Standard MT based random number generation
+class MersenneTwisterEngine : public RandomGeneratorEngine {
+ public:
+  /// @brief Initializes using provided seed
+  /// @param seed - seed value to use
+  explicit MersenneTwisterEngine(uint64_t seed);
+  ~MersenneTwisterEngine() override = default;
+
+  /// Generate random uint32_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  uint32_t RandomUInt32(uint32_t lower, uint32_t upper) override;
+
+  /// Get random uint64_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  uint64_t RandomUInt64(uint64_t lower, uint64_t upper) override;
+
+  /// Get N bytes of pseudo-random data
+  /// @param dest - memory location to store data
+  /// @param n - number of bytes of data to generate
+  void RandomNBytes(uint8_t* dest, size_t n) override;
+
+ private:
+  // Disallow copy & assign
+  MersenneTwisterEngine(const MersenneTwisterEngine&) = delete;
+  MersenneTwisterEngine& operator=(const MersenneTwisterEngine&) = delete;
+
+  std::mt19937_64 engine_;
+};  // class MersenneTwisterEngine
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_MERSENNE_TWISTER_ENGINE_H_
diff --git a/src/tint/fuzzers/random_generator.cc b/src/tint/fuzzers/random_generator.cc
new file mode 100644
index 0000000..6b3c98a
--- /dev/null
+++ b/src/tint/fuzzers/random_generator.cc
@@ -0,0 +1,124 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/random_generator.h"
+
+#include <algorithm>
+#include <cassert>
+#include <utility>
+
+#include "src/tint/fuzzers/mersenne_twister_engine.h"
+#include "src/tint/fuzzers/random_generator_engine.h"
+#include "src/tint/utils/hash.h"
+
+namespace tint {
+namespace fuzzers {
+
+namespace {
+
+/// Calculate the hash for the contents of a c-style data buffer
+/// This is intentionally not implemented as a generic override of HashCombine
+/// in "src/tint/utils/hash.h", because it conflicts with the vardiac override
+/// for the case where a pointer and an integer are being hashed.
+/// @param data - pointer to buffer to be hashed
+/// @param size - number of elements in buffer
+/// @returns hash of the data in the buffer
+size_t HashBuffer(const uint8_t* data, const size_t size) {
+  size_t hash = 102931;
+  utils::HashCombine(&hash, size);
+  for (size_t i = 0; i < size; i++) {
+    utils::HashCombine(&hash, data[i]);
+  }
+  return hash;
+}
+
+}  // namespace
+
+RandomGenerator::RandomGenerator(std::unique_ptr<RandomGeneratorEngine> engine)
+    : engine_(std::move(engine)) {}
+
+RandomGenerator::RandomGenerator(uint64_t seed)
+    : RandomGenerator(std::make_unique<MersenneTwisterEngine>(seed)) {}
+
+uint32_t RandomGenerator::GetUInt32(uint32_t lower, uint32_t upper) {
+  assert(lower < upper && "|lower| must be strictly less than |upper|");
+  return engine_->RandomUInt32(lower, upper);
+}
+
+uint32_t RandomGenerator::GetUInt32(uint32_t bound) {
+  assert(bound > 0 && "|bound| must be greater than 0");
+  return engine_->RandomUInt32(0u, bound);
+}
+
+uint64_t RandomGenerator::GetUInt64(uint64_t lower, uint64_t upper) {
+  assert(lower < upper && "|lower| must be strictly less than |upper|");
+  return engine_->RandomUInt64(lower, upper);
+}
+
+uint64_t RandomGenerator::GetUInt64(uint64_t bound) {
+  assert(bound > 0 && "|bound| must be greater than 0");
+  return engine_->RandomUInt64(static_cast<uint64_t>(0), bound);
+}
+
+uint8_t RandomGenerator::GetByte() {
+  uint8_t result;
+  engine_->RandomNBytes(&result, 1);
+  return result;
+}
+
+uint32_t RandomGenerator::Get4Bytes() {
+  uint32_t result;
+  engine_->RandomNBytes(reinterpret_cast<uint8_t*>(&result), 4);
+  return result;
+}
+
+void RandomGenerator::GetNBytes(uint8_t* dest, size_t n) {
+  assert(dest && "|dest| must not be nullptr");
+  engine_->RandomNBytes(dest, n);
+}
+
+bool RandomGenerator::GetBool() {
+  return engine_->RandomUInt32(0u, 2u);
+}
+
+bool RandomGenerator::GetWeightedBool(uint32_t percentage) {
+  static const uint32_t kMaxPercentage = 100;
+  assert(percentage <= kMaxPercentage &&
+         "|percentage| needs to be within [0, 100]");
+  return engine_->RandomUInt32(0u, kMaxPercentage) < percentage;
+}
+
+uint64_t RandomGenerator::CalculateSeed(const uint8_t* data, size_t size) {
+  assert(data != nullptr && "|data| must be !nullptr");
+
+  // Number of bytes we want to skip at the start of data for the hash.
+  // Fewer bytes may be skipped when `size` is small.
+  // Has lower precedence than kHashDesiredMinBytes.
+  static const int64_t kHashDesiredLeadingSkipBytes = 5;
+  // Minimum number of bytes we want to use in the hash.
+  // Used for short buffers.
+  static const int64_t kHashDesiredMinBytes = 4;
+  // Maximum number of bytes we want to use in the hash.
+  static const int64_t kHashDesiredMaxBytes = 32;
+  auto size_i64 = static_cast<int64_t>(size);
+  auto hash_begin_i64 =
+      std::min(kHashDesiredLeadingSkipBytes,
+               std::max<int64_t>(size_i64 - kHashDesiredMinBytes, 0));
+  auto hash_end_i64 = std::min(hash_begin_i64 + kHashDesiredMaxBytes, size_i64);
+  auto hash_begin = static_cast<size_t>(hash_begin_i64);
+  auto hash_size = static_cast<size_t>(hash_end_i64) - hash_begin;
+  return HashBuffer(data + hash_begin, hash_size);
+}
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/random_generator.h b/src/tint/fuzzers/random_generator.h
new file mode 100644
index 0000000..bb9a46f
--- /dev/null
+++ b/src/tint/fuzzers/random_generator.h
@@ -0,0 +1,118 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_RANDOM_GENERATOR_H_
+#define SRC_TINT_FUZZERS_RANDOM_GENERATOR_H_
+
+#include <memory>
+#include <random>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator_engine.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Pseudo random generator utility class for fuzzing
+class RandomGenerator {
+ public:
+  /// @brief Initializes using provided engine
+  /// @param engine - engine implementation to use
+  explicit RandomGenerator(std::unique_ptr<RandomGeneratorEngine> engine);
+
+  /// @brief Creates a MersenneTwisterEngine and initializes using that
+  /// @param seed - seed value to use for engine
+  explicit RandomGenerator(uint64_t seed);
+
+  /// Destructor
+  ~RandomGenerator() = default;
+
+  /// Move Constructor
+  RandomGenerator(RandomGenerator&&) = default;
+
+  /// Get uint32_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  uint32_t GetUInt32(uint32_t lower, uint32_t upper);
+
+  /// Get uint32_t value from uniform distribution.
+  /// @param bound - Upper bound of integer generated
+  /// @returns i, where 0 <= i < bound
+  uint32_t GetUInt32(uint32_t bound);
+
+  /// Get uint32_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  uint64_t GetUInt64(uint64_t lower, uint64_t upper);
+
+  /// Get uint64_t value from uniform distribution.
+  /// @param bound - Upper bound of integer generated
+  /// @returns i, where 0 <= i < bound
+  uint64_t GetUInt64(uint64_t bound);
+
+  /// Get 1 byte of pseudo-random data
+  /// Should be more efficient then calling GetNBytes(1);
+  /// @returns 1-byte of random data
+  uint8_t GetByte();
+
+  /// Get 4 bytes of pseudo-random data
+  /// Should be more efficient then calling GetNBytes(4);
+  /// @returns 4-bytes of random data
+  uint32_t Get4Bytes();
+
+  /// Get N bytes of pseudo-random data
+  /// @param dest - memory location to store data
+  /// @param n - number of bytes of data to get
+  void GetNBytes(uint8_t* dest, size_t n);
+
+  /// Get random bool with even odds
+  /// @returns true 50% of the time and false %50 of time.
+  bool GetBool();
+
+  /// Get random bool with weighted odds
+  /// @param percentage - likelihood of true being returned
+  /// @returns true |percentage|% of the time, and false (100 - |percentage|)%
+  /// of the time.
+  bool GetWeightedBool(uint32_t percentage);
+
+  /// Returns a randomly-chosen element from vector v.
+  /// @param v - the vector from which the random element will be selected.
+  /// @return a random element of vector v.
+  template <typename T>
+  inline T GetRandomElement(const std::vector<T>& v) {
+    return v[GetUInt64(0, v.size())];
+  }
+
+  /// Calculate a seed value based on a blob of data.
+  /// Currently hashes bytes near the front of the buffer, after skipping N
+  /// bytes.
+  /// @param data - pointer to data to base calculation off of, must be !nullptr
+  /// @param size - number of elements in |data|, must be > 0
+  /// @returns calculated seed value
+  static uint64_t CalculateSeed(const uint8_t* data, size_t size);
+
+ private:
+  // Disallow copy & assign
+  RandomGenerator(const RandomGenerator&) = delete;
+  RandomGenerator& operator=(const RandomGenerator&) = delete;
+
+  std::unique_ptr<RandomGeneratorEngine> engine_;
+};  // class RandomGenerator
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_RANDOM_GENERATOR_H_
diff --git a/src/tint/fuzzers/random_generator_engine.cc b/src/tint/fuzzers/random_generator_engine.cc
new file mode 100644
index 0000000..2e861e3
--- /dev/null
+++ b/src/tint/fuzzers/random_generator_engine.cc
@@ -0,0 +1,26 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/random_generator_engine.h"
+
+namespace tint {
+namespace fuzzers {
+
+// Not in header to avoid weak vtable warnings from clang
+RandomGeneratorEngine::RandomGeneratorEngine() = default;
+RandomGeneratorEngine::~RandomGeneratorEngine() = default;
+RandomGeneratorEngine::RandomGeneratorEngine(RandomGeneratorEngine&&) = default;
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/random_generator_engine.h b/src/tint/fuzzers/random_generator_engine.h
new file mode 100644
index 0000000..eb9e716
--- /dev/null
+++ b/src/tint/fuzzers/random_generator_engine.h
@@ -0,0 +1,63 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_RANDOM_GENERATOR_ENGINE_H_
+#define SRC_TINT_FUZZERS_RANDOM_GENERATOR_ENGINE_H_
+
+#include <memory>
+#include <random>
+#include <vector>
+
+namespace tint {
+namespace fuzzers {
+
+/// Wrapper interface around STL random number engine
+class RandomGeneratorEngine {
+ public:
+  /// Constructor
+  RandomGeneratorEngine();
+
+  /// Destructor
+  virtual ~RandomGeneratorEngine();
+
+  /// Move Constructor
+  RandomGeneratorEngine(RandomGeneratorEngine&&);
+
+  /// Generates a random uint32_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  virtual uint32_t RandomUInt32(uint32_t lower, uint32_t upper) = 0;
+
+  /// Generates a random uint64_t value from uniform distribution.
+  /// @param lower - lower bound of integer generated
+  /// @param upper - upper bound of integer generated
+  /// @returns i, where lower <= i < upper
+  virtual uint64_t RandomUInt64(uint64_t lower, uint64_t upper) = 0;
+
+  /// Generates N bytes of pseudo-random data
+  /// @param dest - memory location to store data
+  /// @param n - number of bytes of data to generate
+  virtual void RandomNBytes(uint8_t* dest, size_t n) = 0;
+
+ private:
+  // Disallow copy & assign
+  RandomGeneratorEngine(const RandomGeneratorEngine&) = delete;
+  RandomGeneratorEngine& operator=(const RandomGeneratorEngine&) = delete;
+};  // class RandomGeneratorEngine
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_RANDOM_GENERATOR_ENGINE_H_
diff --git a/src/tint/fuzzers/random_generator_test.cc b/src/tint/fuzzers/random_generator_test.cc
new file mode 100644
index 0000000..182e7ab
--- /dev/null
+++ b/src/tint/fuzzers/random_generator_test.cc
@@ -0,0 +1,202 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/random_generator.h"
+
+#include <memory>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/fuzzers/mersenne_twister_engine.h"
+
+namespace tint {
+namespace fuzzers {
+namespace {
+
+/// Implementation of RandomGeneratorEngine that just returns a stream of
+/// monotonically increasing numbers.
+class MonotonicEngine : public RandomGeneratorEngine {
+ public:
+  uint32_t RandomUInt32(uint32_t, uint32_t) override { return next_++; }
+
+  uint64_t RandomUInt64(uint64_t, uint64_t) override { return next_++; }
+
+  void RandomNBytes(uint8_t*, size_t) override {
+    assert(false && "MonotonicDelegate does not implement RandomNBytes");
+  }
+
+ private:
+  uint32_t next_ = 0;
+};
+
+class RandomGeneratorTest : public testing::Test {
+ public:
+  void SetUp() override { rng_ = std::make_unique<RandomGenerator>(0); }
+
+  void TearDown() override {}
+
+ protected:
+  std::unique_ptr<RandomGenerator> rng_;
+};
+
+#ifndef NDEBUG
+TEST_F(RandomGeneratorTest, GetUInt32ReversedBoundsCrashes) {
+  EXPECT_DEATH(rng_->GetUInt32(10, 5), ".*");
+}
+
+TEST_F(RandomGeneratorTest, GetUInt32EmptyBoundsCrashes) {
+  EXPECT_DEATH(rng_->GetUInt32(5, 5), ".*");
+}
+
+TEST_F(RandomGeneratorTest, GetUInt32ZeroBoundCrashes) {
+  EXPECT_DEATH(rng_->GetUInt32(0u), ".*");
+}
+#endif  // NDEBUG
+
+TEST_F(RandomGeneratorTest, GetUInt32SingularReturnsOneValue) {
+  {
+    uint32_t result = rng_->GetUInt32(5u, 6u);
+    ASSERT_EQ(5u, result);
+  }
+  {
+    uint32_t result = rng_->GetUInt32(1u);
+    ASSERT_EQ(0u, result);
+  }
+}
+
+TEST_F(RandomGeneratorTest, GetUInt32StaysInBounds) {
+  {
+    uint32_t result = rng_->GetUInt32(5u, 10u);
+    ASSERT_LE(5u, result);
+    ASSERT_GT(10u, result);
+  }
+  {
+    uint32_t result = rng_->GetUInt32(10u);
+    ASSERT_LE(0u, result);
+    ASSERT_GT(10u, result);
+  }
+}
+
+#ifndef NDEBUG
+TEST_F(RandomGeneratorTest, GetUInt64ReversedBoundsCrashes) {
+  EXPECT_DEATH(rng_->GetUInt64(10, 5), ".*");
+}
+
+TEST_F(RandomGeneratorTest, GetUInt64EmptyBoundsCrashes) {
+  EXPECT_DEATH(rng_->GetUInt64(5, 5), ".*");
+}
+
+TEST_F(RandomGeneratorTest, GetUInt64ZeroBoundCrashes) {
+  EXPECT_DEATH(rng_->GetUInt64(0u), ".*");
+}
+#endif  // NDEBUG
+
+TEST_F(RandomGeneratorTest, GetUInt64SingularReturnsOneValue) {
+  {
+    uint64_t result = rng_->GetUInt64(5u, 6u);
+    ASSERT_EQ(5u, result);
+  }
+  {
+    uint64_t result = rng_->GetUInt64(1u);
+    ASSERT_EQ(0u, result);
+  }
+}
+
+TEST_F(RandomGeneratorTest, GetUInt64StaysInBounds) {
+  {
+    uint64_t result = rng_->GetUInt64(5u, 10u);
+    ASSERT_LE(5u, result);
+    ASSERT_GT(10u, result);
+  }
+  {
+    uint64_t result = rng_->GetUInt64(10u);
+    ASSERT_LE(0u, result);
+    ASSERT_GT(10u, result);
+  }
+}
+
+TEST_F(RandomGeneratorTest, GetByte) {
+  rng_->GetByte();
+}
+
+#ifndef NDEBUG
+TEST_F(RandomGeneratorTest, GetNBytesNullDataBufferCrashes) {
+  EXPECT_DEATH(rng_->GetNBytes(nullptr, 5), ".*");
+}
+#endif  // NDEBUG
+
+TEST_F(RandomGeneratorTest, GetNBytes) {
+  std::vector<uint8_t> data;
+  for (uint32_t i = 25; i < 1000u; i = i + 25) {
+    data.resize(i);
+    rng_->GetNBytes(data.data(), data.size());
+  }
+}
+
+TEST_F(RandomGeneratorTest, GetBool) {
+  rng_->GetBool();
+}
+
+TEST_F(RandomGeneratorTest, GetWeightedBoolZeroAlwaysFalse) {
+  ASSERT_FALSE(rng_->GetWeightedBool(0));
+}
+
+TEST_F(RandomGeneratorTest, GetWeightedBoolHundredAlwaysTrue) {
+  ASSERT_TRUE(rng_->GetWeightedBool(100));
+}
+
+#ifndef NDEBUG
+TEST_F(RandomGeneratorTest, GetWeightedBoolAboveHundredCrashes) {
+  EXPECT_DEATH(rng_->GetWeightedBool(101), ".*");
+  EXPECT_DEATH(rng_->GetWeightedBool(500), ".*");
+}
+#endif  // NDEBUG
+
+TEST_F(RandomGeneratorTest, GetWeightedBool) {
+  for (uint32_t i = 0; i <= 100; i++) {
+    rng_ =
+        std::make_unique<RandomGenerator>(std::make_unique<MonotonicEngine>());
+    for (uint32_t j = 0; j <= 100; j++) {
+      if (j < i) {
+        ASSERT_TRUE(rng_->GetWeightedBool(i));
+      } else {
+        ASSERT_FALSE(rng_->GetWeightedBool(i));
+      }
+    }
+  }
+}
+
+#ifndef NDEBUG
+TEST_F(RandomGeneratorTest, GetRandomElementEmptyVectorCrashes) {
+  std::vector<uint8_t> v;
+  EXPECT_DEATH(rng_->GetRandomElement(v), ".*");
+}
+#endif  // NDEBUG
+
+TEST_F(RandomGeneratorTest, GetRandomElement) {
+  std::vector<uint32_t> v;
+  for (uint32_t i = 25; i < 100u; i = i + 25) {
+    rng_ =
+        std::make_unique<RandomGenerator>(std::make_unique<MonotonicEngine>());
+    v.resize(i);
+    std::iota(v.begin(), v.end(), 0);
+    for (uint32_t j = 0; j < i; j++) {
+      EXPECT_EQ(j, rng_->GetRandomElement(v));
+    }
+  }
+}
+
+}  // namespace
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/shuffle_transform.cc b/src/tint/fuzzers/shuffle_transform.cc
new file mode 100644
index 0000000..4061104
--- /dev/null
+++ b/src/tint/fuzzers/shuffle_transform.cc
@@ -0,0 +1,38 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/shuffle_transform.h"
+
+#include <random>
+
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+ShuffleTransform::ShuffleTransform(size_t seed) : seed_(seed) {}
+
+void ShuffleTransform::Run(CloneContext& ctx,
+                           const tint::transform::DataMap&,
+                           tint::transform::DataMap&) const {
+  auto decls = ctx.src->AST().GlobalDeclarations();
+  auto rng = std::mt19937_64{seed_};
+  std::shuffle(std::begin(decls), std::end(decls), rng);
+  for (auto* decl : decls) {
+    ctx.dst->AST().AddGlobalDeclaration(ctx.Clone(decl));
+  }
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/shuffle_transform.h b/src/tint/fuzzers/shuffle_transform.h
new file mode 100644
index 0000000..4674d35
--- /dev/null
+++ b/src/tint/fuzzers/shuffle_transform.h
@@ -0,0 +1,42 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_SHUFFLE_TRANSFORM_H_
+#define SRC_TINT_FUZZERS_SHUFFLE_TRANSFORM_H_
+
+#include "src/tint/transform/transform.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// ShuffleTransform reorders the module scope declarations into a random order
+class ShuffleTransform : public tint::transform::Transform {
+ public:
+  /// Constructor
+  /// @param seed the random seed to use for the shuffling
+  explicit ShuffleTransform(size_t seed);
+
+ protected:
+  void Run(CloneContext& ctx,
+           const tint::transform::DataMap&,
+           tint::transform::DataMap&) const override;
+
+ private:
+  size_t seed_;
+};
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_SHUFFLE_TRANSFORM_H_
diff --git a/src/tint/fuzzers/tint_all_transforms_fuzzer.cc b/src/tint/fuzzers/tint_all_transforms_fuzzer.cc
new file mode 100644
index 0000000..356a53c
--- /dev/null
+++ b/src/tint/fuzzers/tint_all_transforms_fuzzer.cc
@@ -0,0 +1,85 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  {
+    TransformBuilder tb(data, size);
+    tb.AddTransform<ShuffleTransform>();
+    tb.AddPlatformIndependentPasses();
+
+    fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kSpv);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+    fuzzer.SetDumpInput(GetCliParams().dump_input);
+    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+    fuzzer.Run(data, size);
+  }
+
+#if TINT_BUILD_HLSL_WRITER
+  {
+    TransformBuilder tb(data, size);
+    tb.AddTransform<ShuffleTransform>();
+    tb.AddPlatformIndependentPasses();
+
+    fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kHLSL);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+    fuzzer.SetDumpInput(GetCliParams().dump_input);
+    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+    fuzzer.Run(data, size);
+  }
+#endif  // TINT_BUILD_HLSL_WRITER
+
+#if TINT_BUILD_MSL_WRITER
+  {
+    TransformBuilder tb(data, size);
+    tb.AddTransform<ShuffleTransform>();
+    tb.AddPlatformIndependentPasses();
+
+    fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kMSL);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+    fuzzer.SetDumpInput(GetCliParams().dump_input);
+    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+    fuzzer.Run(data, size);
+  }
+#endif  // TINT_BUILD_MSL_WRITER
+#if TINT_BUILD_SPV_WRITER
+  {
+    TransformBuilder tb(data, size);
+    tb.AddTransform<ShuffleTransform>();
+    tb.AddPlatformIndependentPasses();
+
+    fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kSpv);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+    fuzzer.SetDumpInput(GetCliParams().dump_input);
+    fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+    fuzzer.Run(data, size);
+  }
+#endif  // TINT_BUILD_SPV_WRITER
+
+  return 0;
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_clone_fuzzer.cc b/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
new file mode 100644
index 0000000..5382f24
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_clone_fuzzer.cc
@@ -0,0 +1,116 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <iostream>
+#include <string>
+#include <unordered_set>
+
+#include "src/tint/reader/wgsl/parser_impl.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+#define ASSERT_EQ(A, B)                                  \
+  do {                                                   \
+    decltype(A) assert_a = (A);                          \
+    decltype(B) assert_b = (B);                          \
+    if (assert_a != assert_b) {                          \
+      std::cerr << "ASSERT_EQ(" #A ", " #B ") failed:\n" \
+                << #A << " was: " << assert_a << "\n"    \
+                << #B << " was: " << assert_b << "\n";   \
+      __builtin_trap();                                  \
+    }                                                    \
+  } while (false)
+
+#define ASSERT_TRUE(A)                                 \
+  do {                                                 \
+    decltype(A) assert_a = (A);                        \
+    if (!assert_a) {                                   \
+      std::cerr << "ASSERT_TRUE(" #A ") failed:\n"     \
+                << #A << " was: " << assert_a << "\n"; \
+      __builtin_trap();                                \
+    }                                                  \
+  } while (false)
+
+[[noreturn]] void TintInternalCompilerErrorReporter(
+    const tint::diag::List& diagnostics) {
+  auto printer = tint::diag::Printer::create(stderr, true);
+  tint::diag::Formatter{}.format(diagnostics, printer.get());
+  __builtin_trap();
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  std::string str(reinterpret_cast<const char*>(data), size);
+
+  tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+
+  tint::Source::File file("test.wgsl", str);
+
+  // Parse the wgsl, create the src program
+  tint::reader::wgsl::ParserImpl parser(&file);
+  parser.set_max_errors(1);
+  if (!parser.Parse()) {
+    return 0;
+  }
+  auto src = parser.program();
+  if (!src.IsValid()) {
+    return 0;
+  }
+
+  // Clone the src program to dst
+  tint::Program dst(src.Clone());
+
+  // Expect the printed strings to match
+  ASSERT_EQ(tint::Program::printer(&src), tint::Program::printer(&dst));
+
+  // Check that none of the AST nodes or type pointers in dst are found in src
+  std::unordered_set<const tint::ast::Node*> src_nodes;
+  for (auto* src_node : src.ASTNodes().Objects()) {
+    src_nodes.emplace(src_node);
+  }
+  std::unordered_set<const tint::sem::Type*> src_types;
+  for (auto* src_type : src.Types()) {
+    src_types.emplace(src_type);
+  }
+  for (auto* dst_node : dst.ASTNodes().Objects()) {
+    ASSERT_EQ(src_nodes.count(dst_node), 0u);
+  }
+  for (auto* dst_type : dst.Types()) {
+    ASSERT_EQ(src_types.count(dst_type), 0u);
+  }
+
+  // Regenerate the wgsl for the src program. We use this instead of the
+  // original source so that reformatting doesn't impact the final wgsl
+  // comparison.
+  std::string src_wgsl;
+  tint::writer::wgsl::Options wgsl_options;
+  {
+    auto result = tint::writer::wgsl::Generate(&src, wgsl_options);
+    ASSERT_TRUE(result.success);
+    src_wgsl = result.wgsl;
+
+    // Move the src program to a temporary that'll be dropped, so that the src
+    // program is released before we attempt to print the dst program. This
+    // guarantee that all the source program nodes and types are destructed and
+    // freed. ASAN should error if there's any remaining references in dst when
+    // we try to reconstruct the WGSL.
+    auto tmp = std::move(src);
+  }
+
+  // Print the dst program, check it matches the original source
+  auto result = tint::writer::wgsl::Generate(&dst, wgsl_options);
+  ASSERT_TRUE(result.success);
+  auto dst_wgsl = result.wgsl;
+  ASSERT_EQ(src_wgsl, dst_wgsl);
+
+  return 0;
+}
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
index 2a312be..4c63bab 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
+++ b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2022 The Dawn Authors
+# Copyright 2022 The Dawn & Tint Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,18 +12,63 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import("../../../../scripts/dawn_overrides_with_defaults.gni")
+import("//build_overrides/build.gni")
+import("../../../../tint_overrides_with_defaults.gni")
 
-# Target aliases to ease merging Tint->Dawn
+if (build_with_chromium) {
+  import("//third_party/protobuf/proto_library.gni")
 
-group("tint_ast_fuzzer") {
-  deps = [ "${dawn_tint_dir}/src/tint/fuzzers/tint_ast_fuzzer:tint_ast_fuzzer" ]
-  testonly = true
-}
+  proto_library("tint_ast_fuzzer_proto") {
+    sources = [ "protobufs/tint_ast_fuzzer.proto" ]
+    generate_python = false
+    use_protobuf_full = true
+  }
 
-group("tint_ast_fuzzer_proto") {
-  deps = [
-    "${dawn_tint_dir}/src/tint/fuzzers/tint_ast_fuzzer:tint_ast_fuzzer_proto",
-  ]
-  testonly = true
+  source_set("tint_ast_fuzzer") {
+    public_configs = [
+      "${tint_root_dir}/src/tint:tint_config",
+      "${tint_root_dir}/src/tint:tint_common_config",
+    ]
+
+    include_dirs = [ "${target_gen_dir}/../../../.." ]
+
+    deps = [
+      ":tint_ast_fuzzer_proto",
+      "${tint_root_dir}/src/tint/fuzzers:tint_fuzzer_common_src",
+      "//third_party/protobuf:protobuf_full",
+    ]
+
+    sources = [
+      "cli.cc",
+      "cli.h",
+      "expression_size.cc",
+      "expression_size.h",
+      "fuzzer.cc",
+      "mutation.cc",
+      "mutation.h",
+      "mutation_finder.cc",
+      "mutation_finder.h",
+      "mutation_finders/change_binary_operators.cc",
+      "mutation_finders/change_binary_operators.h",
+      "mutation_finders/replace_identifiers.cc",
+      "mutation_finders/replace_identifiers.h",
+      "mutation_finders/wrap_unary_operators.cc",
+      "mutation_finders/wrap_unary_operators.h",
+      "mutations/change_binary_operator.cc",
+      "mutations/change_binary_operator.h",
+      "mutations/replace_identifier.cc",
+      "mutations/replace_identifier.h",
+      "mutations/wrap_unary_operator.cc",
+      "mutations/wrap_unary_operator.h",
+      "mutator.cc",
+      "mutator.h",
+      "node_id_map.cc",
+      "node_id_map.h",
+      "override_cli_params.h",
+      "probability_context.cc",
+      "probability_context.h",
+      "protobufs/tint_ast_fuzzer.h",
+      "util.h",
+    ]
+  }
 }
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
new file mode 100644
index 0000000..1a45897
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -0,0 +1,124 @@
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function(add_tint_ast_fuzzer NAME)
+  add_executable(${NAME} ${NAME}.cc ${AST_FUZZER_SOURCES})
+  target_link_libraries(${NAME} libtint-fuzz libtint_ast_fuzzer)
+  tint_default_compile_options(${NAME})
+  target_compile_definitions(${NAME} PRIVATE CUSTOM_MUTATOR)
+  target_include_directories(${NAME} PRIVATE ${CMAKE_BINARY_DIR})
+endfunction()
+
+set(PROTOBUF_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/protobufs/tint_ast_fuzzer.proto)
+
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/protobufs)
+
+add_custom_command(
+        OUTPUT
+        ${CMAKE_CURRENT_BINARY_DIR}/protobufs/tint_ast_fuzzer.pb.cc
+        ${CMAKE_CURRENT_BINARY_DIR}/protobufs/tint_ast_fuzzer.pb.h
+        COMMAND
+        "protobuf::protoc" -I=${CMAKE_CURRENT_SOURCE_DIR}/protobufs
+        --cpp_out=${CMAKE_CURRENT_BINARY_DIR}/protobufs ${PROTOBUF_SOURCES}
+        DEPENDS ${PROTOBUF_SOURCES}
+        COMMENT "Generate protobuf sources from proto definition file.")
+
+set(LIBTINT_AST_FUZZER_SOURCES
+        ../mersenne_twister_engine.h
+        ../random_generator.h
+        ../random_generator_engine.h
+        expression_size.h
+        mutation.h
+        mutation_finder.h
+        mutation_finders/change_binary_operators.h
+        mutation_finders/replace_identifiers.h
+        mutation_finders/wrap_unary_operators.h
+        mutations/change_binary_operator.h
+        mutations/replace_identifier.h
+        mutations/wrap_unary_operator.h
+        mutator.h
+        node_id_map.h
+        probability_context.h
+        protobufs/tint_ast_fuzzer.h
+        util.h
+        ${CMAKE_CURRENT_BINARY_DIR}/protobufs/tint_ast_fuzzer.pb.h)
+
+set(LIBTINT_AST_FUZZER_SOURCES ${LIBTINT_AST_FUZZER_SOURCES}
+        ../mersenne_twister_engine.cc
+        ../random_generator.cc
+        ../random_generator_engine.cc
+        expression_size.cc
+        mutation.cc
+        mutation_finder.cc
+        mutation_finders/change_binary_operators.cc
+        mutation_finders/replace_identifiers.cc
+        mutation_finders/wrap_unary_operators.cc
+        mutations/change_binary_operator.cc
+        mutations/replace_identifier.cc
+        mutations/wrap_unary_operator.cc
+        mutator.cc
+        node_id_map.cc
+        probability_context.cc
+        ${CMAKE_CURRENT_BINARY_DIR}/protobufs/tint_ast_fuzzer.pb.cc)
+
+set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/protobufs/tint_ast_fuzzer.pb.cc PROPERTIES COMPILE_FLAGS -w)
+
+# Add static library target.
+add_library(libtint_ast_fuzzer STATIC ${LIBTINT_AST_FUZZER_SOURCES})
+target_link_libraries(libtint_ast_fuzzer protobuf::libprotobuf libtint)
+tint_default_compile_options(libtint_ast_fuzzer)
+target_include_directories(libtint_ast_fuzzer PRIVATE ${CMAKE_BINARY_DIR})
+
+set(AST_FUZZER_SOURCES
+        cli.cc
+        cli.h
+        fuzzer.cc
+        override_cli_params.h
+        ../tint_common_fuzzer.cc
+        ../tint_common_fuzzer.h)
+
+set_source_files_properties(fuzzer.cc PROPERTIES COMPILE_FLAGS -Wno-missing-prototypes)
+
+# Add libfuzzer targets.
+# Targets back-ends according to command line arguments.
+add_tint_ast_fuzzer(tint_ast_fuzzer)
+# Targets back-ends individually.
+add_tint_ast_fuzzer(tint_ast_hlsl_writer_fuzzer)
+add_tint_ast_fuzzer(tint_ast_msl_writer_fuzzer)
+add_tint_ast_fuzzer(tint_ast_spv_writer_fuzzer)
+add_tint_ast_fuzzer(tint_ast_wgsl_writer_fuzzer)
+
+# Add tests.
+if (${TINT_BUILD_TESTS})
+    set(TEST_SOURCES
+            expression_size_test.cc
+            mutations/change_binary_operator_test.cc
+            mutations/replace_identifier_test.cc
+	        mutations/wrap_unary_operator_test.cc)
+
+    add_executable(tint_ast_fuzzer_unittests ${TEST_SOURCES})
+
+    target_include_directories(
+            tint_ast_fuzzer_unittests PRIVATE ${gmock_SOURCE_DIR}/include)
+    target_link_libraries(tint_ast_fuzzer_unittests gmock_main libtint_ast_fuzzer)
+    tint_default_compile_options(tint_ast_fuzzer_unittests)
+    target_compile_options(tint_ast_fuzzer_unittests PRIVATE
+            -Wno-global-constructors
+            -Wno-weak-vtables
+            -Wno-covered-switch-default)
+
+    target_include_directories(tint_ast_fuzzer_unittests PRIVATE ${CMAKE_BINARY_DIR})
+
+    add_test(NAME tint_ast_fuzzer_unittests COMMAND tint_ast_fuzzer_unittests)
+endif ()
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/cli.cc b/src/tint/fuzzers/tint_ast_fuzzer/cli.cc
new file mode 100644
index 0000000..34a7d92
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/cli.cc
@@ -0,0 +1,167 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+
+#include <cstring>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+const char* const kHelpMessage = R"(
+This is a fuzzer for the Tint compiler that works by mutating the AST.
+
+Below is a list of all supported parameters for this fuzzer. You may want to
+run it with -help=1 to check out libfuzzer parameters.
+
+  -tint_enable_all_mutations=
+                       If `false`, the fuzzer will only apply mutations from a
+                       randomly selected subset of mutation types. Otherwise,
+                       all mutation types will be considered. This must be one
+                       of `true` or `false` (without `). By default it's `false`.
+
+  -tint_fuzzing_target=
+                       Specifies the shading language to target during fuzzing.
+                       This must be one or a combination of `wgsl`, `spv`, `hlsl`,
+                       `msl` (without `) separated by commas. By default it's
+                       `wgsl,msl,hlsl,spv`.
+
+  -tint_help
+                       Show this message. Note that there is also a -help=1
+                       parameter that will display libfuzzer's help message.
+
+  -tint_mutation_batch_size=
+                       The number of mutations to apply in a single libfuzzer
+                       mutation session. This must be a numeric value that fits
+                       in type `uint32_t`. By default it's 5.
+)";
+
+bool HasPrefix(const char* str, const char* prefix) {
+  return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+[[noreturn]] void InvalidParam(const char* param) {
+  std::cout << "Invalid value for " << param << std::endl;
+  std::cout << kHelpMessage << std::endl;
+  exit(1);
+}
+
+bool ParseBool(const char* value, bool* out) {
+  if (!strcmp(value, "true")) {
+    *out = true;
+  } else if (!strcmp(value, "false")) {
+    *out = false;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseUint32(const char* value, uint32_t* out) {
+  auto parsed = strtoul(value, nullptr, 10);
+  if (parsed > std::numeric_limits<uint32_t>::max()) {
+    return false;
+  }
+  *out = static_cast<uint32_t>(parsed);
+  return true;
+}
+
+bool ParseFuzzingTarget(const char* value, FuzzingTarget* out) {
+  if (!strcmp(value, "wgsl")) {
+    *out = FuzzingTarget::kWgsl;
+  } else if (!strcmp(value, "spv")) {
+    *out = FuzzingTarget::kSpv;
+  } else if (!strcmp(value, "msl")) {
+    *out = FuzzingTarget::kMsl;
+  } else if (!strcmp(value, "hlsl")) {
+    *out = FuzzingTarget::kHlsl;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+CliParams ParseCliParams(int* argc, char** argv) {
+  CliParams cli_params;
+  auto help = false;
+
+  for (int i = *argc - 1; i > 0; --i) {
+    auto param = argv[i];
+    auto recognized_parameter = true;
+
+    if (HasPrefix(param, "-tint_enable_all_mutations=")) {
+      if (!ParseBool(param + sizeof("-tint_enable_all_mutations=") - 1,
+                     &cli_params.enable_all_mutations)) {
+        InvalidParam(param);
+      }
+    } else if (HasPrefix(param, "-tint_mutation_batch_size=")) {
+      if (!ParseUint32(param + sizeof("-tint_mutation_batch_size=") - 1,
+                       &cli_params.mutation_batch_size)) {
+        InvalidParam(param);
+      }
+    } else if (HasPrefix(param, "-tint_fuzzing_target=")) {
+      auto result = FuzzingTarget::kNone;
+
+      std::stringstream ss(param + sizeof("-tint_fuzzing_target=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto tmp = FuzzingTarget::kNone;
+        if (!ParseFuzzingTarget(value.c_str(), &tmp)) {
+          InvalidParam(param);
+        }
+        result = result | tmp;
+      }
+
+      if (result == FuzzingTarget::kNone) {
+        InvalidParam(param);
+      }
+
+      cli_params.fuzzing_target = result;
+    } else if (!strcmp(param, "-tint_help")) {
+      help = true;
+    } else {
+      recognized_parameter = false;
+    }
+
+    if (recognized_parameter) {
+      // Remove the recognized parameter from the list of all parameters by
+      // swapping it with the last one. This will suppress warnings in the
+      // libFuzzer about unrecognized parameters. By default, libFuzzer thinks
+      // that all user-defined parameters start with two dashes. However, we are
+      // forced to use a single one to make the fuzzer compatible with the
+      // ClusterFuzz.
+      std::swap(argv[i], argv[*argc - 1]);
+      *argc -= 1;
+    }
+  }
+
+  if (help) {
+    std::cout << kHelpMessage << std::endl;
+    exit(0);
+  }
+
+  return cli_params;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/cli.h b/src/tint/fuzzers/tint_ast_fuzzer/cli.h
new file mode 100644
index 0000000..ed1bfaa
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/cli.h
@@ -0,0 +1,72 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_CLI_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_CLI_H_
+
+#include <cstdint>
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// The backend this fuzzer will test.
+enum class FuzzingTarget {
+  kNone = 0,
+  kHlsl = 1 << 0,
+  kMsl = 1 << 1,
+  kSpv = 1 << 2,
+  kWgsl = 1 << 3,
+  kAll = kHlsl | kMsl | kSpv | kWgsl
+};
+
+inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// CLI parameters accepted by the fuzzer. Type -tint_help in the CLI to see the
+/// help message
+struct CliParams {
+  /// Whether to use all mutation finders or only a randomly selected subset of
+  /// them.
+  bool enable_all_mutations = false;
+
+  /// The maximum number of mutations applied during a single mutation session
+  /// (i.e. a call to `ast_fuzzer::Mutate` function).
+  uint32_t mutation_batch_size = 5;
+
+  /// Compiler backends we want to fuzz.
+  FuzzingTarget fuzzing_target = FuzzingTarget::kAll;
+};
+
+/// @brief Parses CLI parameters.
+///
+/// This function will exit the process with non-zero return code if some
+/// parameters are invalid. This function will remove recognized parameters from
+/// `argv` and adjust `argc` accordingly.
+///
+/// @param argc - the total number of parameters.
+/// @param argv - array of all CLI parameters.
+/// @return parsed parameters.
+CliParams ParseCliParams(int* argc, char** argv);
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_CLI_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/expression_size.cc b/src/tint/fuzzers/tint_ast_fuzzer/expression_size.cc
new file mode 100644
index 0000000..fe4a5c4
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/expression_size.cc
@@ -0,0 +1,46 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/expression_size.h"
+
+#include "src/tint/ast/traverse_expressions.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+ExpressionSize::ExpressionSize(const Program& program) {
+  // By construction, all the children of an AST node are encountered before the
+  // node itself when iterating through a program's AST nodes. Computing
+  // expression sizes exploits this property: the size of a compound expression
+  // is computed based on the already-computed sizes of its sub-expressions.
+  for (const auto* node : program.ASTNodes().Objects()) {
+    const auto* expr_ast_node = node->As<ast::Expression>();
+    if (expr_ast_node == nullptr) {
+      continue;
+    }
+    size_t expr_size = 0;
+    diag::List empty;
+    ast::TraverseExpressions(expr_ast_node, empty,
+                             [&](const ast::Expression* expression) {
+                               if (expression == expr_ast_node) {
+                                 expr_size++;
+                                 return ast::TraverseAction::Descend;
+                               }
+                               expr_size += expr_to_size_.at(expression);
+                               return ast::TraverseAction::Skip;
+                             });
+    expr_to_size_[expr_ast_node] = expr_size;
+  }
+}
+
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/expression_size.h b/src/tint/fuzzers/tint_ast_fuzzer/expression_size.h
new file mode 100644
index 0000000..5a6d3dd
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/expression_size.h
@@ -0,0 +1,47 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_EXPRESSION_SIZE_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_EXPRESSION_SIZE_H_
+
+#include <unordered_map>
+
+#include "src/tint/ast/expression.h"
+#include "src/tint/program.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+/// This class computes the size of the subtree rooted at each expression in a
+/// program, and allows these sizes to be subsequently queried.
+class ExpressionSize {
+ public:
+  /// Initializes expression size information for the given program.
+  /// @param program - the program for which expression sizes will be computed;
+  ///     must remain in scope as long as this instance exists.
+  explicit ExpressionSize(const Program& program);
+
+  /// Returns the size of the subtree rooted at the given expression.
+  /// @param expression - the expression whose size should be returned.
+  /// @return the size of the subtree rooted at `expression`.
+  size_t operator()(const ast::Expression* expression) const {
+    return expr_to_size_.at(expression);
+  }
+
+ private:
+  std::unordered_map<const ast::Expression*, size_t> expr_to_size_;
+};
+
+}  // namespace tint::fuzzers::ast_fuzzer
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_EXPRESSION_SIZE_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/expression_size_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/expression_size_test.cc
new file mode 100644
index 0000000..bb6e5e7
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/expression_size_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/expression_size.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/expression.h"
+#include "src/tint/ast/int_literal_expression.h"
+#include "src/tint/program.h"
+#include "src/tint/reader/wgsl/parser.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+TEST(ExpressionSizeTest, Basic) {
+  std::string content = R"(
+    fn main() {
+      let a = (0 + 0) * (0 + 0);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  ExpressionSize expression_size(program);
+  for (const auto* node : program.ASTNodes().Objects()) {
+    const auto* expr = node->As<ast::Expression>();
+    if (expr == nullptr) {
+      continue;
+    }
+    if (expr->Is<ast::IntLiteralExpression>()) {
+      ASSERT_EQ(1, expression_size(expr));
+    } else {
+      const auto* binary_expr = expr->As<ast::BinaryExpression>();
+      ASSERT_TRUE(binary_expr != nullptr);
+      switch (binary_expr->op) {
+        case ast::BinaryOp::kAdd:
+          ASSERT_EQ(3, expression_size(expr));
+          break;
+        case ast::BinaryOp::kMultiply:
+          ASSERT_EQ(7, expression_size(expr));
+          break;
+        default:
+          FAIL();
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc
new file mode 100644
index 0000000..5570e06
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/fuzzer.cc
@@ -0,0 +1,134 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstddef>
+#include <cstdint>
+
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+CliParams cli_params{};
+
+extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
+  // Parse CLI parameters. `ParseCliParams` will call `exit` if some parameter
+  // is invalid.
+  cli_params = ParseCliParams(argc, *argv);
+  // For some fuzz targets it is desirable to force the values of certain CLI
+  // parameters after parsing.
+  OverrideCliParams(cli_params);
+  return 0;
+}
+
+extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data,
+                                          size_t size,
+                                          size_t max_size,
+                                          unsigned seed) {
+  Source::File file("test.wgsl", {reinterpret_cast<char*>(data), size});
+  auto program = reader::wgsl::Parse(&file);
+  if (!program.IsValid()) {
+    std::cout << "Trying to mutate an invalid program:" << std::endl
+              << program.Diagnostics().str() << std::endl;
+    return 0;
+  }
+
+  // Run the mutator.
+  RandomGenerator generator(seed);
+  ProbabilityContext probability_context(&generator);
+  program = Mutate(std::move(program), &probability_context,
+                   cli_params.enable_all_mutations,
+                   cli_params.mutation_batch_size, nullptr);
+
+  if (!program.IsValid()) {
+    std::cout << "Mutator produced invalid WGSL:" << std::endl
+              << "  seed: " << seed << std::endl
+              << program.Diagnostics().str() << std::endl;
+    return 0;
+  }
+
+  auto result = writer::wgsl::Generate(&program, writer::wgsl::Options());
+  if (!result.success) {
+    std::cout << "Can't generate WGSL for a valid tint::Program:" << std::endl
+              << result.error << std::endl;
+    return 0;
+  }
+
+  if (result.wgsl.size() > max_size) {
+    return 0;
+  }
+
+  // No need to worry about the \0 here. The reason is that if \0 is included by
+  // developer by mistake, it will be considered a part of the string and will
+  // cause all sorts of strange bugs. Thus, unless `data` below is used as a raw
+  // C string, the \0 symbol should be ignored.
+  std::memcpy(  // NOLINT - clang-tidy warns about lack of null termination.
+      data, result.wgsl.data(), result.wgsl.size());
+  return result.wgsl.size();
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  if (size == 0) {
+    return 0;
+  }
+
+  struct Target {
+    FuzzingTarget fuzzing_target;
+    OutputFormat output_format;
+    const char* name;
+  };
+
+  Target targets[] = {{FuzzingTarget::kWgsl, OutputFormat::kWGSL, "WGSL"},
+                      {FuzzingTarget::kHlsl, OutputFormat::kHLSL, "HLSL"},
+                      {FuzzingTarget::kMsl, OutputFormat::kMSL, "MSL"},
+                      {FuzzingTarget::kSpv, OutputFormat::kSpv, "SPV"}};
+
+  for (auto target : targets) {
+    if ((target.fuzzing_target & cli_params.fuzzing_target) !=
+        target.fuzzing_target) {
+      continue;
+    }
+
+    TransformBuilder tb(data, size);
+    tb.AddTransform<tint::transform::Robustness>();
+
+    CommonFuzzer fuzzer(InputFormat::kWGSL, target.output_format);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+
+    fuzzer.Run(data, size);
+    if (fuzzer.HasErrors()) {
+      std::cout << "Fuzzing " << target.name << " produced an error"
+                << std::endl;
+      auto printer = tint::diag::Printer::create(stderr, true);
+      tint::diag::Formatter{}.format(fuzzer.Diagnostics(), printer.get());
+    }
+  }
+
+  return 0;
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
new file mode 100644
index 0000000..c3c89a6
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
@@ -0,0 +1,50 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+Mutation::~Mutation() = default;
+
+std::unique_ptr<Mutation> Mutation::FromMessage(
+    const protobufs::Mutation& message) {
+  switch (message.mutation_case()) {
+    case protobufs::Mutation::kReplaceIdentifier:
+      return std::make_unique<MutationReplaceIdentifier>(
+          message.replace_identifier());
+    case protobufs::Mutation::kChangeBinaryOperator:
+      return std::make_unique<MutationChangeBinaryOperator>(
+          message.change_binary_operator());
+    case protobufs::Mutation::kWrapUnaryOperator:
+      return std::make_unique<MutationWrapUnaryOperator>(
+          message.wrap_unary_operator());
+    case protobufs::Mutation::MUTATION_NOT_SET:
+      assert(false && "Mutation is not set");
+      break;
+  }
+  return nullptr;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation.h
new file mode 100644
index 0000000..cc1afdd
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation.h
@@ -0,0 +1,86 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_H_
+
+#include <memory>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h"
+
+#include "src/tint/clone_context.h"
+#include "src/tint/program.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// The base class for all the mutations in the fuzzer. Children must override
+/// three methods:
+/// - `IsApplicable` - checks whether it is possible to apply the mutation
+///   in a manner that will lead to a valid program.
+/// - `Apply` - applies the mutation.
+/// - `ToMessage` - converts the mutation data into a protobuf message.
+class Mutation {
+ public:
+  /// Virtual destructor.
+  virtual ~Mutation();
+
+  /// @brief Determines whether this mutation is applicable to the `program`.
+  ///
+  /// @param program - the program this mutation will be applied to. The program
+  ///     must be valid.
+  /// @param node_id_map - the map from `tint::ast::` nodes to their ids.
+  /// @return `true` if `Apply` method can be called without breaking the
+  ///     semantics of the `program`.
+  /// @return `false` otherwise.
+  virtual bool IsApplicable(const tint::Program& program,
+                            const NodeIdMap& node_id_map) const = 0;
+
+  /// @brief Applies this mutation to the `clone_context`.
+  ///
+  /// Precondition: `IsApplicable` must return `true` when invoked on the same
+  /// `node_id_map` and `clone_context->src` instance of `tint::Program`. A new
+  /// `tint::Program` that arises in `clone_context` must be valid.
+  ///
+  /// @param node_id_map - the map from `tint::ast::` nodes to their ids.
+  /// @param clone_context - the context that will clone the program with some
+  ///     changes introduced by this mutation.
+  /// @param new_node_id_map - this map will store ids for the mutated and
+  ///     cloned program. This argument cannot be a `nullptr` nor can it point
+  ///     to the same object as `node_id_map`.
+  virtual void Apply(const NodeIdMap& node_id_map,
+                     tint::CloneContext* clone_context,
+                     NodeIdMap* new_node_id_map) const = 0;
+
+  /// @return a protobuf message for this mutation.
+  virtual protobufs::Mutation ToMessage() const = 0;
+
+  /// @brief Converts a protobuf message into the mutation instance.
+  ///
+  /// @param message - a protobuf message.
+  /// @return the instance of this class.
+  static std::unique_ptr<Mutation> FromMessage(
+      const protobufs::Mutation& message);
+};
+
+using MutationList = std::vector<std::unique_ptr<Mutation>>;
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.cc
new file mode 100644
index 0000000..7344320
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.cc
@@ -0,0 +1,25 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationFinder::~MutationFinder() = default;
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h
new file mode 100644
index 0000000..d13a440
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h
@@ -0,0 +1,77 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDER_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDER_H_
+
+#include <memory>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
+
+#include "src/tint/program.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Instances of this class traverse the `tint::Program`, looking for
+/// opportunities to apply mutations and return them to the caller.
+///
+/// Ideally, the behaviour of this class (precisely, its `FindMutations` method)
+/// should not be probabilistic. This is useful when mutation finders are used
+/// for test case reduction, because it enables the test case reducer to
+/// systematically explore all available mutations. There may be some
+/// exceptions, however. For example, if a huge number of mutations is returned,
+/// it would make sense to apply only a probabilistically selected subset of
+/// them.
+class MutationFinder {
+ public:
+  /// Virtual destructor.
+  virtual ~MutationFinder();
+
+  /// @brief Traverses the `program`, looking for opportunities to apply
+  /// mutations.
+  ///
+  /// @param program - the program being fuzzed.
+  /// @param node_id_map - a map from `tint::ast::` nodes in the `program` to
+  ///     their unique ids.
+  /// @param probability_context - determines various probabilistic stuff in the
+  ///     mutator. This should ideally be used as less as possible.
+  /// @return all the found mutations.
+  virtual MutationList FindMutations(
+      const tint::Program& program,
+      NodeIdMap* node_id_map,
+      ProbabilityContext* probability_context) const = 0;
+
+  /// @brief Compute a probability of applying a single mutation, returned by
+  /// this class.
+  ///
+  /// @param probability_context - contains information about various
+  ///     non-deterministic stuff in the fuzzer.
+  /// @return a number in the range [0; 100] which is a chance of applying a
+  ///     mutation.
+  virtual uint32_t GetChanceOfApplyingMutation(
+      ProbabilityContext* probability_context) const = 0;
+};
+
+using MutationFinderList = std::vector<std::unique_ptr<MutationFinder>>;
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDER_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc
new file mode 100644
index 0000000..dbfc36e
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc
@@ -0,0 +1,92 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h"
+
+#include <memory>
+#include <vector>
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationList MutationFinderChangeBinaryOperators::FindMutations(
+    const tint::Program& program,
+    NodeIdMap* node_id_map,
+    ProbabilityContext* probability_context) const {
+  MutationList result;
+
+  // Go through each binary expression in the AST and add a mutation that
+  // replaces its operator with some other type-compatible operator.
+
+  const std::vector<ast::BinaryOp> all_binary_operators = {
+      ast::BinaryOp::kAnd,
+      ast::BinaryOp::kOr,
+      ast::BinaryOp::kXor,
+      ast::BinaryOp::kLogicalAnd,
+      ast::BinaryOp::kLogicalOr,
+      ast::BinaryOp::kEqual,
+      ast::BinaryOp::kNotEqual,
+      ast::BinaryOp::kLessThan,
+      ast::BinaryOp::kGreaterThan,
+      ast::BinaryOp::kLessThanEqual,
+      ast::BinaryOp::kGreaterThanEqual,
+      ast::BinaryOp::kShiftLeft,
+      ast::BinaryOp::kShiftRight,
+      ast::BinaryOp::kAdd,
+      ast::BinaryOp::kSubtract,
+      ast::BinaryOp::kMultiply,
+      ast::BinaryOp::kDivide,
+      ast::BinaryOp::kModulo};
+
+  for (const auto* node : program.ASTNodes().Objects()) {
+    const auto* binary_expr = As<ast::BinaryExpression>(node);
+    if (!binary_expr) {
+      continue;
+    }
+
+    // Get vector of all operators this could be replaced with.
+    std::vector<ast::BinaryOp> allowed_replacements;
+    for (auto candidate_op : all_binary_operators) {
+      if (MutationChangeBinaryOperator::CanReplaceBinaryOperator(
+              program, *binary_expr, candidate_op)) {
+        allowed_replacements.push_back(candidate_op);
+      }
+    }
+
+    if (!allowed_replacements.empty()) {
+      // Choose an available replacement operator at random.
+      const ast::BinaryOp replacement =
+          allowed_replacements[probability_context->GetRandomIndex(
+              allowed_replacements)];
+      // Add a mutation according to the chosen replacement.
+      result.push_back(std::make_unique<MutationChangeBinaryOperator>(
+          node_id_map->GetId(binary_expr), replacement));
+    }
+  }
+
+  return result;
+}
+
+uint32_t MutationFinderChangeBinaryOperators::GetChanceOfApplyingMutation(
+    ProbabilityContext* probability_context) const {
+  return probability_context->GetChanceOfChangingBinaryOperators();
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h
new file mode 100644
index 0000000..460196a
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h
@@ -0,0 +1,42 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Looks for opportunities to apply `MutationChangeBinaryOperator`.
+///
+/// Concretely, for each binary expression in the module, tries to replace it
+/// with a different, type-compatible operator.
+class MutationFinderChangeBinaryOperators : public MutationFinder {
+ public:
+  MutationList FindMutations(
+      const tint::Program& program,
+      NodeIdMap* node_id_map,
+      ProbabilityContext* probability_context) const override;
+  uint32_t GetChanceOfApplyingMutation(
+      ProbabilityContext* probability_context) const override;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.cc
new file mode 100644
index 0000000..59e0fa2
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.cc
@@ -0,0 +1,79 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h"
+
+#include <memory>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/util.h"
+
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/statement.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationList MutationFinderReplaceIdentifiers::FindMutations(
+    const tint::Program& program,
+    NodeIdMap* node_id_map,
+    ProbabilityContext* probability_context) const {
+  MutationList result;
+
+  // Go through each variable in the AST and for each user of that variable, try
+  // to replace it with some other variable usage.
+
+  for (const auto* node : program.SemNodes().Objects()) {
+    const auto* sem_variable = tint::As<sem::Variable>(node);
+    if (!sem_variable) {
+      continue;
+    }
+
+    // Iterate over all users of `sem_variable`.
+    for (const auto* user : sem_variable->Users()) {
+      // Get all variables that can be used to replace the `user` of
+      // `sem_variable`.
+      auto candidate_variables = util::GetAllVarsInScope(
+          program, user->Stmt(), [user](const sem::Variable* var) {
+            return var != user->Variable() && var->Type() == user->Type();
+          });
+
+      if (candidate_variables.empty()) {
+        // No suitable replacements have been found.
+        continue;
+      }
+
+      const auto* replacement =
+          candidate_variables[probability_context->GetRandomIndex(
+              candidate_variables)];
+
+      result.push_back(std::make_unique<MutationReplaceIdentifier>(
+          node_id_map->GetId(user->Declaration()),
+          node_id_map->GetId(replacement->Declaration())));
+    }
+  }
+
+  return result;
+}
+
+uint32_t MutationFinderReplaceIdentifiers::GetChanceOfApplyingMutation(
+    ProbabilityContext* probability_context) const {
+  return probability_context->GetChanceOfReplacingIdentifiers();
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h
new file mode 100644
index 0000000..2d8d70e
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h
@@ -0,0 +1,42 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_REPLACE_IDENTIFIERS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_REPLACE_IDENTIFIERS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Looks for opportunities to apply `MutationReplaceIdentifier`.
+///
+/// Concretely, for each variable in the module, tries to replace its users with
+/// the uses of some other variables.
+class MutationFinderReplaceIdentifiers : public MutationFinder {
+ public:
+  MutationList FindMutations(
+      const tint::Program& program,
+      NodeIdMap* node_id_map,
+      ProbabilityContext* probability_context) const override;
+  uint32_t GetChanceOfApplyingMutation(
+      ProbabilityContext* probability_context) const override;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_REPLACE_IDENTIFIERS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.cc
new file mode 100644
index 0000000..52cbc45
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.cc
@@ -0,0 +1,92 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h"
+
+#include <memory>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/expression_size.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/util.h"
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/statement.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+namespace {
+const size_t kMaxExpressionSize = 50;
+}  // namespace
+
+MutationList MutationFinderWrapUnaryOperators::FindMutations(
+    const tint::Program& program,
+    NodeIdMap* node_id_map,
+    ProbabilityContext* probability_context) const {
+  MutationList result;
+
+  ExpressionSize expression_size(program);
+
+  // Iterate through all ast nodes and for each expression node, try to wrap
+  // the inside a valid unary operator based on the type of the expression.
+  for (const auto* node : program.ASTNodes().Objects()) {
+    const auto* expr_ast_node = tint::As<ast::Expression>(node);
+
+    // Transformation applies only when the node represents a valid expression.
+    if (!expr_ast_node) {
+      continue;
+    }
+
+    if (expression_size(expr_ast_node) > kMaxExpressionSize) {
+      continue;
+    }
+
+    const auto* expr_sem_node =
+        tint::As<sem::Expression>(program.Sem().Get(expr_ast_node));
+
+    // Transformation applies only when the semantic node for the given
+    // expression is present.
+    if (!expr_sem_node) {
+      continue;
+    }
+
+    std::vector<ast::UnaryOp> valid_operators =
+        MutationWrapUnaryOperator::GetValidUnaryWrapper(*expr_sem_node);
+
+    // Transformation only applies when there are available unary operators
+    // for the given expression.
+    if (valid_operators.empty()) {
+      continue;
+    }
+
+    ast::UnaryOp unary_op_wrapper =
+        valid_operators[probability_context->GetRandomIndex(valid_operators)];
+
+    result.push_back(std::make_unique<MutationWrapUnaryOperator>(
+        node_id_map->GetId(expr_ast_node), node_id_map->TakeFreshId(),
+        unary_op_wrapper));
+  }
+
+  return result;
+}
+
+uint32_t MutationFinderWrapUnaryOperators::GetChanceOfApplyingMutation(
+    ProbabilityContext* probability_context) const {
+  return probability_context->GetChanceOfWrappingUnaryOperators();
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h
new file mode 100644
index 0000000..02538fc
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h
@@ -0,0 +1,43 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_WRAP_UNARY_OPERATORS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_WRAP_UNARY_OPERATORS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Looks for opportunities to apply
+/// `MutationFinderWrapUnaryOperators`.
+///
+/// For each expression in the module, try to wrap it within
+/// a unary operator.
+class MutationFinderWrapUnaryOperators : public MutationFinder {
+ public:
+  MutationList FindMutations(
+      const tint::Program& program,
+      NodeIdMap* node_id_map,
+      ProbabilityContext* probability_context) const override;
+  uint32_t GetChanceOfApplyingMutation(
+      ProbabilityContext* probability_context) const override;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_WRAP_UNARY_OPERATORS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
new file mode 100644
index 0000000..60a2a1c
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
@@ -0,0 +1,489 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
+
+#include <utility>
+
+#include "src/tint/sem/reference_type.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+namespace {
+
+bool IsSuitableForShift(const sem::Type* lhs_type, const sem::Type* rhs_type) {
+  // `a << b` requires b to be an unsigned scalar or vector, and `a` to be an
+  // integer scalar or vector with the same width as `b`. Similar for `a >> b`.
+
+  if (rhs_type->is_unsigned_integer_scalar()) {
+    return lhs_type->is_integer_scalar();
+  }
+  if (rhs_type->is_unsigned_integer_vector()) {
+    return lhs_type->is_unsigned_integer_vector();
+  }
+  return false;
+}
+
+bool CanReplaceAddSubtractWith(const sem::Type* lhs_type,
+                               const sem::Type* rhs_type,
+                               ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in an '+' or
+  // '-' expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      // '+' and '-' are fully type compatible.
+      return true;
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators do not have a mixed vector-scalar form, and only work
+      // on integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kMultiply:
+      // '+' and '*' are largely type-compatible, but for matrices they are only
+      // type-compatible if the matrices are square.
+      return !lhs_type->is_float_matrix() || lhs_type->is_square_float_matrix();
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      // '/' is not defined for matrices.
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceMultiplyWith(const sem::Type* lhs_type,
+                            const sem::Type* rhs_type,
+                            ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in a '*'
+  // expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kMultiply:
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      // '*' is type-compatible with '+' and '-' for square matrices, and for
+      // numeric scalars/vectors.
+      if (lhs_type->is_square_float_matrix() &&
+          rhs_type->is_square_float_matrix()) {
+        return true;
+      }
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators require homogeneous integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      // '/' is not defined for matrices.
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceDivideOrModuloWith(const sem::Type* lhs_type,
+                                  const sem::Type* rhs_type,
+                                  ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in a '/'
+  // expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      // These operators work in all contexts where '/' works.
+      return true;
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators require homogeneous integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceLogicalAndLogicalOrWith(ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // These operators all work whenever '&&' and '||' work.
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceAndOrWith(const sem::Type* lhs_type,
+                         const sem::Type* rhs_type,
+                         ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      // '&' and '|' work in all the same contexts.
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kXor:
+      // '&' and '|' can be applied to booleans. In all other contexts,
+      // integer numeric operators work.
+      return !lhs_type->is_bool_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      // '&' and '|' can be applied to booleans, and for boolean scalar
+      // scalar contexts, their logical counterparts work.
+      return lhs_type->Is<sem::Bool>();
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // '&' and '|' can be applied to booleans, and in these contexts equality
+      // comparison operators also work.
+      return lhs_type->is_bool_scalar_or_vector();
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceXorWith(const sem::Type* lhs_type,
+                       const sem::Type* rhs_type,
+                       ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // '^' only works on integer types, and in any such context, all other
+      // integer operators also work.
+      return true;
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceShiftLeftShiftRightWith(const sem::Type* lhs_type,
+                                       const sem::Type* rhs_type,
+                                       ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      // These operators are type-compatible.
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // Shift operators allow mixing of signed and unsigned arguments, but in
+      // the case where the arguments are homogeneous, they are type-compatible
+      // with other numeric operators.
+      return lhs_type == rhs_type;
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceEqualNotEqualWith(const sem::Type* lhs_type,
+                                 ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // These operators are type-compatible.
+      return true;
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+      // An equality comparison between numeric types can be changed to an
+      // ordered comparison.
+      return lhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      // An equality comparison between boolean scalars can be turned into a
+      // logical operation.
+      return lhs_type->Is<sem::Bool>();
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      // An equality comparison between boolean scalars or vectors can be turned
+      // into a component-wise non-short-circuit logical operation.
+      return lhs_type->is_bool_scalar_or_vector();
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceLessThanLessThanEqualGreaterThanGreaterThanEqualWith(
+    ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+      // Ordered comparison operators can be interchanged, and equality
+      // operators can be used in their place.
+      return true;
+    default:
+      return false;
+  }
+}
+}  // namespace
+
+MutationChangeBinaryOperator::MutationChangeBinaryOperator(
+    protobufs::MutationChangeBinaryOperator message)
+    : message_(std::move(message)) {}
+
+MutationChangeBinaryOperator::MutationChangeBinaryOperator(
+    uint32_t binary_expr_id,
+    ast::BinaryOp new_operator) {
+  message_.set_binary_expr_id(binary_expr_id);
+  message_.set_new_operator(static_cast<uint32_t>(new_operator));
+}
+
+bool MutationChangeBinaryOperator::CanReplaceBinaryOperator(
+    const Program& program,
+    const ast::BinaryExpression& binary_expr,
+    ast::BinaryOp new_operator) {
+  if (new_operator == binary_expr.op) {
+    // An operator should not be replaced with itself, as this would be a no-op.
+    return false;
+  }
+
+  // Get the types of the operators.
+  const auto* lhs_type = program.Sem().Get(binary_expr.lhs)->Type();
+  const auto* rhs_type = program.Sem().Get(binary_expr.rhs)->Type();
+
+  // If these are reference types, unwrap them to get the pointee type.
+  const sem::Type* lhs_basic_type =
+      lhs_type->Is<sem::Reference>()
+          ? lhs_type->As<sem::Reference>()->StoreType()
+          : lhs_type;
+  const sem::Type* rhs_basic_type =
+      rhs_type->Is<sem::Reference>()
+          ? rhs_type->As<sem::Reference>()->StoreType()
+          : rhs_type;
+
+  switch (binary_expr.op) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      return CanReplaceAddSubtractWith(lhs_basic_type, rhs_basic_type,
+                                       new_operator);
+    case ast::BinaryOp::kMultiply:
+      return CanReplaceMultiplyWith(lhs_basic_type, rhs_basic_type,
+                                    new_operator);
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      return CanReplaceDivideOrModuloWith(lhs_basic_type, rhs_basic_type,
+                                          new_operator);
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      return CanReplaceAndOrWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kXor:
+      return CanReplaceXorWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return CanReplaceShiftLeftShiftRightWith(lhs_basic_type, rhs_basic_type,
+                                               new_operator);
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      return CanReplaceLogicalAndLogicalOrWith(new_operator);
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      return CanReplaceEqualNotEqualWith(lhs_basic_type, new_operator);
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+    case ast::BinaryOp::kNone:
+      return CanReplaceLessThanLessThanEqualGreaterThanGreaterThanEqualWith(
+          new_operator);
+      assert(false && "Unreachable");
+      return false;
+  }
+}
+
+bool MutationChangeBinaryOperator::IsApplicable(
+    const Program& program,
+    const NodeIdMap& node_id_map) const {
+  const auto* binary_expr_node =
+      As<ast::BinaryExpression>(node_id_map.GetNode(message_.binary_expr_id()));
+  if (binary_expr_node == nullptr) {
+    // Either the id does not exist, or does not correspond to a binary
+    // expression.
+    return false;
+  }
+  // Check whether the replacement is acceptable.
+  const auto new_operator = static_cast<ast::BinaryOp>(message_.new_operator());
+  return CanReplaceBinaryOperator(program, *binary_expr_node, new_operator);
+}
+
+void MutationChangeBinaryOperator::Apply(const NodeIdMap& node_id_map,
+                                         CloneContext* clone_context,
+                                         NodeIdMap* new_node_id_map) const {
+  // Get the node whose operator is to be replaced.
+  const auto* binary_expr_node =
+      As<ast::BinaryExpression>(node_id_map.GetNode(message_.binary_expr_id()));
+
+  // Clone the binary expression, with the appropriate new operator.
+  const ast::BinaryExpression* cloned_replacement;
+  switch (static_cast<ast::BinaryOp>(message_.new_operator())) {
+    case ast::BinaryOp::kAnd:
+      cloned_replacement =
+          clone_context->dst->And(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kOr:
+      cloned_replacement =
+          clone_context->dst->Or(clone_context->Clone(binary_expr_node->lhs),
+                                 clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kXor:
+      cloned_replacement =
+          clone_context->dst->Xor(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLogicalAnd:
+      cloned_replacement = clone_context->dst->LogicalAnd(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLogicalOr:
+      cloned_replacement = clone_context->dst->LogicalOr(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kEqual:
+      cloned_replacement = clone_context->dst->Equal(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kNotEqual:
+      cloned_replacement = clone_context->dst->NotEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLessThan:
+      cloned_replacement = clone_context->dst->LessThan(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kGreaterThan:
+      cloned_replacement = clone_context->dst->GreaterThan(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLessThanEqual:
+      cloned_replacement = clone_context->dst->LessThanEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kGreaterThanEqual:
+      cloned_replacement = clone_context->dst->GreaterThanEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kShiftLeft:
+      cloned_replacement =
+          clone_context->dst->Shl(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kShiftRight:
+      cloned_replacement =
+          clone_context->dst->Shr(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kAdd:
+      cloned_replacement =
+          clone_context->dst->Add(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kSubtract:
+      cloned_replacement =
+          clone_context->dst->Sub(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kMultiply:
+      cloned_replacement =
+          clone_context->dst->Mul(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kDivide:
+      cloned_replacement =
+          clone_context->dst->Div(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kModulo:
+      cloned_replacement =
+          clone_context->dst->Mod(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kNone:
+      cloned_replacement = nullptr;
+      assert(false && "Unreachable");
+  }
+  // Set things up so that the original binary expression will be replaced with
+  // its clone, and update the id mapping.
+  clone_context->Replace(binary_expr_node, cloned_replacement);
+  new_node_id_map->Add(cloned_replacement, message_.binary_expr_id());
+}
+
+protobufs::Mutation MutationChangeBinaryOperator::ToMessage() const {
+  protobufs::Mutation mutation;
+  *mutation.mutable_change_binary_operator() = message_;
+  return mutation;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
new file mode 100644
index 0000000..73ac22b
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
@@ -0,0 +1,85 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/program.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// @see MutationChangeBinaryOperator::Apply
+class MutationChangeBinaryOperator : public Mutation {
+ public:
+  /// @brief Constructs an instance of this mutation from a protobuf message.
+  /// @param message - protobuf message
+  explicit MutationChangeBinaryOperator(
+      protobufs::MutationChangeBinaryOperator message);
+
+  /// @brief Constructor.
+  /// @param binary_expr_id - the id of a binary expression.
+  /// @param new_operator - a new binary operator to replace the one used in the
+  /// expression.
+  MutationChangeBinaryOperator(uint32_t binary_expr_id,
+                               ast::BinaryOp new_operator);
+
+  /// @copybrief Mutation::IsApplicable
+  ///
+  /// The mutation is applicable iff:
+  /// - `binary_expr_id` is a valid id of an `ast::BinaryExpression`.
+  /// - `new_operator` is type-compatible with the arguments of the binary
+  /// expression.
+  ///
+  /// @copydetails Mutation::IsApplicable
+  bool IsApplicable(const tint::Program& program,
+                    const NodeIdMap& node_id_map) const override;
+
+  /// @copybrief Mutation::Apply
+  ///
+  /// Replaces binary operator in the binary expression corresponding to
+  /// `binary_expr_id` with `new_operator`.
+  ///
+  /// @copydetails Mutation::Apply
+  void Apply(const NodeIdMap& node_id_map,
+             tint::CloneContext* clone_context,
+             NodeIdMap* new_node_id_map) const override;
+
+  protobufs::Mutation ToMessage() const override;
+
+  /// @brief Determines whether replacing the operator of a binary expression
+  ///     with another operator would preserve well-typedness.
+  /// @param program - the program that owns the binary expression.
+  /// @param binary_expr - the binary expression being considered for mutation.
+  /// @param new_operator - a new binary operator to be checked as a candidate
+  ///     replacement for the binary expression's operator.
+  /// @return `true` if and only if the replacement would be well-typed.
+  static bool CanReplaceBinaryOperator(const Program& program,
+                                       const ast::BinaryExpression& binary_expr,
+                                       ast::BinaryOp new_operator);
+
+ private:
+  protobufs::MutationChangeBinaryOperator message_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
new file mode 100644
index 0000000..d9994e3
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
@@ -0,0 +1,724 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
+
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+std::string OpToString(ast::BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kNone:
+      assert(false && "Unreachable");
+      return "";
+    case ast::BinaryOp::kAnd:
+      return "&";
+    case ast::BinaryOp::kOr:
+      return "|";
+    case ast::BinaryOp::kXor:
+      return "^";
+    case ast::BinaryOp::kLogicalAnd:
+      return "&&";
+    case ast::BinaryOp::kLogicalOr:
+      return "||";
+    case ast::BinaryOp::kEqual:
+      return "==";
+    case ast::BinaryOp::kNotEqual:
+      return "!=";
+    case ast::BinaryOp::kLessThan:
+      return "<";
+    case ast::BinaryOp::kGreaterThan:
+      return ">";
+    case ast::BinaryOp::kLessThanEqual:
+      return "<=";
+    case ast::BinaryOp::kGreaterThanEqual:
+      return ">=";
+    case ast::BinaryOp::kShiftLeft:
+      return "<<";
+    case ast::BinaryOp::kShiftRight:
+      return ">>";
+    case ast::BinaryOp::kAdd:
+      return "+";
+    case ast::BinaryOp::kSubtract:
+      return "-";
+    case ast::BinaryOp::kMultiply:
+      return "*";
+    case ast::BinaryOp::kDivide:
+      return "/";
+    case ast::BinaryOp::kModulo:
+      return "%";
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, NotApplicable_Simple) {
+  std::string content = R"(
+    fn main() {
+      let a : i32 = 1 + 2;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_stmts = program.AST().Functions()[0]->body->statements;
+
+  const auto* a_var =
+      main_fn_stmts[0]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(a_var, nullptr);
+
+  auto a_var_id = node_id_map.GetId(a_var);
+
+  const auto* sum_expr = a_var->constructor->As<ast::BinaryExpression>();
+  ASSERT_NE(sum_expr, nullptr);
+
+  auto sum_expr_id = node_id_map.GetId(sum_expr);
+  ASSERT_NE(sum_expr_id, 0);
+
+  // binary_expr_id is invalid.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kSubtract)
+                   .IsApplicable(program, node_id_map));
+
+  // binary_expr_id is not a binary expression.
+  EXPECT_FALSE(MutationChangeBinaryOperator(a_var_id, ast::BinaryOp::kSubtract)
+                   .IsApplicable(program, node_id_map));
+
+  // new_operator is applicable to the argument types.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kLogicalAnd)
+                   .IsApplicable(program, node_id_map));
+
+  // new_operator does not have the right result type.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kLessThan)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ChangeBinaryOperatorTest, Applicable_Simple) {
+  std::string shader = R"(fn main() {
+  let a : i32 = (1 + 2);
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_stmts = program.AST().Functions()[0]->body->statements;
+
+  const auto* a_var =
+      main_fn_stmts[0]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(a_var, nullptr);
+
+  const auto* sum_expr = a_var->constructor->As<ast::BinaryExpression>();
+  ASSERT_NE(sum_expr, nullptr);
+
+  auto sum_expr_id = node_id_map.GetId(sum_expr);
+  ASSERT_NE(sum_expr_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationChangeBinaryOperator(sum_expr_id, ast::BinaryOp::kSubtract),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  let a : i32 = (1 - 2);
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+void CheckMutations(
+    const std::string& lhs_type,
+    const std::string& rhs_type,
+    const std::string& result_type,
+    ast::BinaryOp original_operator,
+    const std::unordered_set<ast::BinaryOp>& allowed_replacement_operators) {
+  std::stringstream shader;
+  shader << "fn foo(a : " << lhs_type << ", b : " << rhs_type + ") {\n"
+         << "  let r : " << result_type
+         << " = (a " + OpToString(original_operator) << " b);\n}\n";
+
+  const std::vector<ast::BinaryOp> all_operators = {
+      ast::BinaryOp::kAnd,
+      ast::BinaryOp::kOr,
+      ast::BinaryOp::kXor,
+      ast::BinaryOp::kLogicalAnd,
+      ast::BinaryOp::kLogicalOr,
+      ast::BinaryOp::kEqual,
+      ast::BinaryOp::kNotEqual,
+      ast::BinaryOp::kLessThan,
+      ast::BinaryOp::kGreaterThan,
+      ast::BinaryOp::kLessThanEqual,
+      ast::BinaryOp::kGreaterThanEqual,
+      ast::BinaryOp::kShiftLeft,
+      ast::BinaryOp::kShiftRight,
+      ast::BinaryOp::kAdd,
+      ast::BinaryOp::kSubtract,
+      ast::BinaryOp::kMultiply,
+      ast::BinaryOp::kDivide,
+      ast::BinaryOp::kModulo};
+
+  for (auto new_operator : all_operators) {
+    Source::File file("test.wgsl", shader.str());
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    NodeIdMap node_id_map(program);
+
+    const auto& stmts = program.AST().Functions()[0]->body->statements;
+
+    const auto* r_var = stmts[0]->As<ast::VariableDeclStatement>()->variable;
+    ASSERT_NE(r_var, nullptr);
+
+    const auto* binary_expr = r_var->constructor->As<ast::BinaryExpression>();
+    ASSERT_NE(binary_expr, nullptr);
+
+    auto binary_expr_id = node_id_map.GetId(binary_expr);
+    ASSERT_NE(binary_expr_id, 0);
+
+    MutationChangeBinaryOperator mutation(binary_expr_id, new_operator);
+
+    std::stringstream expected_shader;
+    expected_shader << "fn foo(a : " << lhs_type << ", b : " << rhs_type
+                    << ") {\n"
+                    << "  let r : " << result_type << " = (a "
+                    << OpToString(new_operator) << " b);\n}\n";
+
+    if (allowed_replacement_operators.count(new_operator) == 0) {
+      ASSERT_FALSE(mutation.IsApplicable(program, node_id_map));
+      if (new_operator != binary_expr->op) {
+        Source::File invalid_file("test.wgsl", expected_shader.str());
+        auto invalid_program = reader::wgsl::Parse(&invalid_file);
+        ASSERT_FALSE(invalid_program.IsValid()) << program.Diagnostics().str();
+      }
+    } else {
+      ASSERT_TRUE(MaybeApplyMutation(program, mutation, node_id_map, &program,
+                                     &node_id_map, nullptr));
+      ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+      writer::wgsl::Options options;
+      auto result = writer::wgsl::Generate(&program, options);
+      ASSERT_TRUE(result.success) << result.error;
+
+      ASSERT_EQ(expected_shader.str(), result.wgsl);
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, AddSubtract) {
+  for (auto op : {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract}) {
+    const ast::BinaryOp other_op = op == ast::BinaryOp::kAdd
+                                       ? ast::BinaryOp::kSubtract
+                                       : ast::BinaryOp::kAdd;
+    for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      CheckMutations(
+          type, type, type, op,
+          {other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+           ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+           ast::BinaryOp::kXor});
+    }
+    for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      CheckMutations(
+          type, type, type, op,
+          {other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+           ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+           ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+           ast::BinaryOp::kShiftRight});
+    }
+    for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+      CheckMutations(type, type, type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      std::string scalar_type = "i32";
+      CheckMutations(vector_type, scalar_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+      CheckMutations(scalar_type, vector_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      std::string scalar_type = "u32";
+      CheckMutations(vector_type, scalar_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+      CheckMutations(scalar_type, vector_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+      std::string scalar_type = "f32";
+      CheckMutations(vector_type, scalar_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+      CheckMutations(scalar_type, vector_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string square_matrix_type :
+         {"mat2x2<f32>", "mat3x3<f32>", "mat4x4<f32>"}) {
+      CheckMutations(square_matrix_type, square_matrix_type, square_matrix_type,
+                     op, {other_op, ast::BinaryOp::kMultiply});
+    }
+    for (std::string non_square_matrix_type :
+         {"mat2x3<f32>", "mat2x4<f32>", "mat3x2<f32>", "mat3x4<f32>",
+          "mat4x2<f32>", "mat4x3<f32>"}) {
+      CheckMutations(non_square_matrix_type, non_square_matrix_type,
+                     non_square_matrix_type, op, {other_op});
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, Mul) {
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kMultiply,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract, ast::BinaryOp::kDivide,
+         ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+         ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kMultiply,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract, ast::BinaryOp::kDivide,
+         ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+         ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    std::string scalar_type = "f32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string square_matrix_type :
+       {"mat2x2<f32>", "mat3x3<f32>", "mat4x4<f32>"}) {
+    CheckMutations(square_matrix_type, square_matrix_type, square_matrix_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract});
+  }
+
+  CheckMutations("vec2<f32>", "mat2x2<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec2<f32>", "mat3x2<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec2<f32>", "mat4x2<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x2<f32>", "vec2<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x2<f32>", "mat3x2<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x2<f32>", "mat4x2<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x3<f32>", "vec2<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat2x2<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat3x2<f32>", "mat3x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat4x2<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x4<f32>", "vec2<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat2x2<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat3x2<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat4x2<f32>", "mat4x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("vec3<f32>", "mat2x3<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec3<f32>", "mat3x3<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec3<f32>", "mat4x3<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x2<f32>", "vec3<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat2x3<f32>", "mat2x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat3x3<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat4x3<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x3<f32>", "vec3<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x3<f32>", "mat2x3<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x3<f32>", "mat4x3<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x4<f32>", "vec3<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat2x3<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat3x3<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat4x3<f32>", "mat4x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("vec4<f32>", "mat2x4<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec4<f32>", "mat3x4<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec4<f32>", "mat4x4<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x2<f32>", "vec4<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat2x4<f32>", "mat2x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat3x4<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat4x4<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x3<f32>", "vec4<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat2x4<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat3x4<f32>", "mat3x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat4x4<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x4<f32>", "vec4<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x4<f32>", "mat2x4<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x4<f32>", "mat3x4<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+}
+
+TEST(ChangeBinaryOperatorTest, DivideAndModulo) {
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kDivide,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kDivide,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    std::string scalar_type = "f32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kModulo,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kModulo,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, AndOrXor) {
+  for (auto op :
+       {ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kXor}) {
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators_signed{
+        ast::BinaryOp::kAdd,      ast::BinaryOp::kSubtract,
+        ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+        ast::BinaryOp::kModulo,   ast::BinaryOp::kAnd,
+        ast::BinaryOp::kOr,       ast::BinaryOp::kXor};
+    allowed_replacement_operators_signed.erase(op);
+    for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      CheckMutations(type, type, type, op,
+                     allowed_replacement_operators_signed);
+    }
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators_unsigned{
+        ast::BinaryOp::kAdd,        ast::BinaryOp::kSubtract,
+        ast::BinaryOp::kMultiply,   ast::BinaryOp::kDivide,
+        ast::BinaryOp::kModulo,     ast::BinaryOp::kShiftLeft,
+        ast::BinaryOp::kShiftRight, ast::BinaryOp::kAnd,
+        ast::BinaryOp::kOr,         ast::BinaryOp::kXor};
+    allowed_replacement_operators_unsigned.erase(op);
+    for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      CheckMutations(type, type, type, op,
+                     allowed_replacement_operators_unsigned);
+    }
+    if (op != ast::BinaryOp::kXor) {
+      for (std::string type :
+           {"bool", "vec2<bool>", "vec3<bool>", "vec4<bool>"}) {
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators_bool{
+            ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kEqual,
+            ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators_bool.erase(op);
+        if (type == "bool") {
+          allowed_replacement_operators_bool.insert(ast::BinaryOp::kLogicalAnd);
+          allowed_replacement_operators_bool.insert(ast::BinaryOp::kLogicalOr);
+        }
+        CheckMutations(type, type, type, op,
+                       allowed_replacement_operators_bool);
+      }
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, EqualNotEqual) {
+  for (auto op : {ast::BinaryOp::kEqual, ast::BinaryOp::kNotEqual}) {
+    for (std::string element_type : {"i32", "u32", "f32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream argument_type;
+        std::stringstream result_type;
+        if (element_count == 1) {
+          argument_type << element_type;
+          result_type << "bool";
+        } else {
+          argument_type << "vec" << element_count << "<" << element_type << ">";
+          result_type << "vec" << element_count << "<bool>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kLessThan,    ast::BinaryOp::kLessThanEqual,
+            ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual,
+            ast::BinaryOp::kEqual,       ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators.erase(op);
+        CheckMutations(argument_type.str(), argument_type.str(),
+                       result_type.str(), op, allowed_replacement_operators);
+      }
+    }
+    {
+      std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+          ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr,
+          ast::BinaryOp::kAnd,        ast::BinaryOp::kOr,
+          ast::BinaryOp::kEqual,      ast::BinaryOp::kNotEqual};
+      allowed_replacement_operators.erase(op);
+      CheckMutations("bool", "bool", "bool", op, allowed_replacement_operators);
+    }
+    for (size_t element_count = 2; element_count <= 4; element_count++) {
+      std::stringstream argument_and_result_type;
+      argument_and_result_type << "vec" << element_count << "<bool>";
+      std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+          ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kEqual,
+          ast::BinaryOp::kNotEqual};
+      allowed_replacement_operators.erase(op);
+      CheckMutations(
+          argument_and_result_type.str(), argument_and_result_type.str(),
+          argument_and_result_type.str(), op, allowed_replacement_operators);
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest,
+     LessThanLessThanEqualGreaterThanGreaterThanEqual) {
+  for (auto op :
+       {ast::BinaryOp::kLessThan, ast::BinaryOp::kLessThanEqual,
+        ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual}) {
+    for (std::string element_type : {"i32", "u32", "f32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream argument_type;
+        std::stringstream result_type;
+        if (element_count == 1) {
+          argument_type << element_type;
+          result_type << "bool";
+        } else {
+          argument_type << "vec" << element_count << "<" << element_type << ">";
+          result_type << "vec" << element_count << "<bool>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kLessThan,    ast::BinaryOp::kLessThanEqual,
+            ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual,
+            ast::BinaryOp::kEqual,       ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators.erase(op);
+        CheckMutations(argument_type.str(), argument_type.str(),
+                       result_type.str(), op, allowed_replacement_operators);
+      }
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, LogicalAndLogicalOr) {
+  for (auto op : {ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr}) {
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+        ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr,
+        ast::BinaryOp::kAnd,        ast::BinaryOp::kOr,
+        ast::BinaryOp::kEqual,      ast::BinaryOp::kNotEqual};
+    allowed_replacement_operators.erase(op);
+    CheckMutations("bool", "bool", "bool", op, allowed_replacement_operators);
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, ShiftLeftShiftRight) {
+  for (auto op : {ast::BinaryOp::kShiftLeft, ast::BinaryOp::kShiftRight}) {
+    for (std::string lhs_element_type : {"i32", "u32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream lhs_and_result_type;
+        std::stringstream rhs_type;
+        if (element_count == 1) {
+          lhs_and_result_type << lhs_element_type;
+          rhs_type << "u32";
+        } else {
+          lhs_and_result_type << "vec" << element_count << "<"
+                              << lhs_element_type << ">";
+          rhs_type << "vec" << element_count << "<u32>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kShiftLeft, ast::BinaryOp::kShiftRight};
+        allowed_replacement_operators.erase(op);
+        if (lhs_element_type == "u32") {
+          allowed_replacement_operators.insert(ast::BinaryOp::kAdd);
+          allowed_replacement_operators.insert(ast::BinaryOp::kSubtract);
+          allowed_replacement_operators.insert(ast::BinaryOp::kMultiply);
+          allowed_replacement_operators.insert(ast::BinaryOp::kDivide);
+          allowed_replacement_operators.insert(ast::BinaryOp::kModulo);
+          allowed_replacement_operators.insert(ast::BinaryOp::kAnd);
+          allowed_replacement_operators.insert(ast::BinaryOp::kOr);
+          allowed_replacement_operators.insert(ast::BinaryOp::kXor);
+        }
+        CheckMutations(lhs_and_result_type.str(), rhs_type.str(),
+                       lhs_and_result_type.str(), op,
+                       allowed_replacement_operators);
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc
new file mode 100644
index 0000000..7f09dcc
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.cc
@@ -0,0 +1,106 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
+
+#include <utility>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/util.h"
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationReplaceIdentifier::MutationReplaceIdentifier(
+    protobufs::MutationReplaceIdentifier message)
+    : message_(std::move(message)) {}
+
+MutationReplaceIdentifier::MutationReplaceIdentifier(uint32_t use_id,
+                                                     uint32_t replacement_id) {
+  message_.set_use_id(use_id);
+  message_.set_replacement_id(replacement_id);
+}
+
+bool MutationReplaceIdentifier::IsApplicable(
+    const tint::Program& program,
+    const NodeIdMap& node_id_map) const {
+  const auto* use_ast_node = tint::As<ast::IdentifierExpression>(
+      node_id_map.GetNode(message_.use_id()));
+  if (!use_ast_node) {
+    // Either the `use_id` is invalid or the node is not an
+    // `IdentifierExpression`.
+    return false;
+  }
+
+  const auto* use_sem_node =
+      tint::As<sem::VariableUser>(program.Sem().Get(use_ast_node));
+  if (!use_sem_node) {
+    // Either the semantic information is not present for a `use_node` or that
+    // node is not a variable user.
+    return false;
+  }
+
+  const auto* replacement_ast_node =
+      tint::As<ast::Variable>(node_id_map.GetNode(message_.replacement_id()));
+  if (!replacement_ast_node) {
+    // Either the `replacement_id` is invalid or is not an id of a variable.
+    return false;
+  }
+
+  const auto* replacement_sem_node = program.Sem().Get(replacement_ast_node);
+  if (!replacement_sem_node) {
+    return false;
+  }
+
+  if (replacement_sem_node == use_sem_node->Variable()) {
+    return false;
+  }
+
+  auto in_scope =
+      util::GetAllVarsInScope(program, use_sem_node->Stmt(),
+                              [replacement_sem_node](const sem::Variable* var) {
+                                return var == replacement_sem_node;
+                              });
+  if (in_scope.empty()) {
+    // The replacement variable is not in scope.
+    return false;
+  }
+
+  return use_sem_node->Type() == replacement_sem_node->Type();
+}
+
+void MutationReplaceIdentifier::Apply(const NodeIdMap& node_id_map,
+                                      tint::CloneContext* clone_context,
+                                      NodeIdMap* new_node_id_map) const {
+  const auto* use_node = node_id_map.GetNode(message_.use_id());
+  const auto* replacement_var =
+      tint::As<ast::Variable>(node_id_map.GetNode(message_.replacement_id()));
+
+  auto* cloned_replacement =
+      clone_context->dst->Expr(clone_context->Clone(use_node->source),
+                               clone_context->Clone(replacement_var->symbol));
+  clone_context->Replace(use_node, cloned_replacement);
+  new_node_id_map->Add(cloned_replacement, message_.use_id());
+}
+
+protobufs::Mutation MutationReplaceIdentifier::ToMessage() const {
+  protobufs::Mutation mutation;
+  *mutation.mutable_replace_identifier() = message_;
+  return mutation;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h
new file mode 100644
index 0000000..553b8b4
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h
@@ -0,0 +1,77 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_REPLACE_IDENTIFIER_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_REPLACE_IDENTIFIER_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// @see MutationReplaceIdentifier::Apply
+class MutationReplaceIdentifier : public Mutation {
+ public:
+  /// @brief Constructs an instance of this mutation from a protobuf message.
+  /// @param message - protobuf message
+  explicit MutationReplaceIdentifier(
+      protobufs::MutationReplaceIdentifier message);
+
+  /// @brief Constructor.
+  /// @param use_id - the id of a variable user.
+  /// @param replacement_id - the id of a variable to replace the `use_id`.
+  MutationReplaceIdentifier(uint32_t use_id, uint32_t replacement_id);
+
+  /// @copybrief Mutation::IsApplicable
+  ///
+  /// The mutation is applicable iff:
+  /// - `use_id` is a valid id of an `ast::IdentifierExpression`, that
+  ///   references a variable.
+  /// - `replacement_id` is a valid id of an `ast::Variable`.
+  /// - The identifier expression doesn't reference the variable of a
+  ///   `replacement_id`.
+  /// - The variable with `replacement_id` is in scope of an identifier
+  ///   expression with `use_id`.
+  /// - The identifier expression and the variable have the same type.
+  ///
+  /// @copydetails Mutation::IsApplicable
+  bool IsApplicable(const tint::Program& program,
+                    const NodeIdMap& node_id_map) const override;
+
+  /// @copybrief Mutation::Apply
+  ///
+  /// Replaces the use of an identifier expression with `use_id` with a newly
+  /// created identifier expression, that references a variable with
+  /// `replacement_id`. The newly created identifier expression will have the
+  /// same id as the old one (i.e. `use_id`).
+  ///
+  /// @copydetails Mutation::Apply
+  void Apply(const NodeIdMap& node_id_map,
+             tint::CloneContext* clone_context,
+             NodeIdMap* new_node_id_map) const override;
+
+  protobufs::Mutation ToMessage() const override;
+
+ private:
+  protobufs::MutationReplaceIdentifier message_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_REPLACE_IDENTIFIER_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
new file mode 100644
index 0000000..bdb730c
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
@@ -0,0 +1,668 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+TEST(ReplaceIdentifierTest, NotApplicable_Simple) {
+  std::string content = R"(
+    fn main() {
+      let a = 5;
+      let c = 6;
+      let b = a + 5;
+
+      let d = vec2<i32>(1, 2);
+      let e = d.x;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_stmts = program.AST().Functions()[0]->body->statements;
+
+  const auto* a_var =
+      main_fn_stmts[0]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(a_var, nullptr);
+
+  const auto* b_var =
+      main_fn_stmts[2]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(b_var, nullptr);
+
+  const auto* e_var =
+      main_fn_stmts[4]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(e_var, nullptr);
+
+  auto a_var_id = node_id_map.GetId(a_var);
+  ASSERT_NE(a_var_id, 0);
+
+  auto b_var_id = node_id_map.GetId(b_var);
+  ASSERT_NE(b_var_id, 0);
+
+  const auto* sum_expr = b_var->constructor->As<ast::BinaryExpression>();
+  ASSERT_NE(sum_expr, nullptr);
+
+  auto a_ident_id = node_id_map.GetId(sum_expr->lhs);
+  ASSERT_NE(a_ident_id, 0);
+
+  auto sum_expr_id = node_id_map.GetId(sum_expr);
+  ASSERT_NE(sum_expr_id, 0);
+
+  auto e_var_id = node_id_map.GetId(e_var);
+  ASSERT_NE(e_var_id, 0);
+
+  auto vec_member_access_id = node_id_map.GetId(
+      e_var->constructor->As<ast::MemberAccessorExpression>()->member);
+  ASSERT_NE(vec_member_access_id, 0);
+
+  // use_id is invalid.
+  EXPECT_FALSE(MutationReplaceIdentifier(0, a_var_id)
+                   .IsApplicable(program, node_id_map));
+
+  // use_id is not an identifier expression.
+  EXPECT_FALSE(MutationReplaceIdentifier(sum_expr_id, a_var_id)
+                   .IsApplicable(program, node_id_map));
+
+  // use_id is an identifier but not a variable user.
+  EXPECT_FALSE(MutationReplaceIdentifier(vec_member_access_id, a_var_id)
+                   .IsApplicable(program, node_id_map));
+
+  // replacement_id is invalid.
+  EXPECT_FALSE(MutationReplaceIdentifier(a_ident_id, 0)
+                   .IsApplicable(program, node_id_map));
+
+  // replacement_id is not a variable.
+  EXPECT_FALSE(MutationReplaceIdentifier(a_ident_id, sum_expr_id)
+                   .IsApplicable(program, node_id_map));
+
+  // Can't replace a variable with itself.
+  EXPECT_FALSE(MutationReplaceIdentifier(a_ident_id, a_var_id)
+                   .IsApplicable(program, node_id_map));
+
+  // Replacement is not in scope.
+  EXPECT_FALSE(MutationReplaceIdentifier(a_ident_id, b_var_id)
+                   .IsApplicable(program, node_id_map));
+  EXPECT_FALSE(MutationReplaceIdentifier(a_ident_id, e_var_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, GlobalVarNotInScope) {
+  // Can't use the global variable if it's not in scope.
+  std::string shader = R"(
+var<private> a: i32;
+
+fn f() {
+  a = 3;
+}
+
+var<private> b: i32;
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[1]);
+  ASSERT_NE(replacement_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable1) {
+  // Can't replace `a` with `b` since the store type is wrong (the same storage
+  // class though).
+  std::string shader = R"(
+var<private> a: i32;
+var<private> b: u32;
+fn f() {
+  *&a = 4;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[1]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable2) {
+  // Can't replace `a` with `b` since the store type is wrong (the storage
+  // class is different though).
+  std::string shader = R"(
+var<private> a: i32;
+fn f() {
+  var b: u32;
+  *&a = 4;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST()
+                                              .Functions()[0]
+                                              ->body->statements[0]
+                                              ->As<ast::VariableDeclStatement>()
+                                              ->variable);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[1]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable3) {
+  // Can't replace `a` with `b` since the latter is not a reference (the store
+  // type is the same, though).
+  std::string shader = R"(
+var<private> a: i32;
+fn f() {
+  let b = 45;
+  *&a = 4;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST()
+                                              .Functions()[0]
+                                              ->body->statements[0]
+                                              ->As<ast::VariableDeclStatement>()
+                                              ->variable);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[1]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable4) {
+  // Can't replace `a` with `b` since the latter is not a reference (the store
+  // type is the same, though).
+  std::string shader = R"(
+var<private> a: i32;
+fn f(b: i32) {
+  *&a = 4;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id =
+      node_id_map.GetId(program.AST().Functions()[0]->params[0]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable5) {
+  // Can't replace `a` with `b` since the latter has a wrong access mode
+  // (`read` for uniform storage class).
+  std::string shader = R"(
+struct S {
+  a: i32;
+};
+
+var<private> a: S;
+@group(1) @binding(1) var<uniform> b: S;
+fn f() {
+  *&a = S(4);
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[1]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable6) {
+  // Can't replace `ptr_b` with `a` since the latter is not a pointer.
+  std::string shader = R"(
+struct S {
+  a: i32;
+};
+
+var<private> a: S;
+@group(1) @binding(1) var<uniform> b: S;
+fn f() {
+  let ptr_b = &b;
+  *&a = *ptr_b;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[0]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[1]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->rhs->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable8) {
+  // Can't replace `ptr_b` with `c` since the latter has a wrong access mode and
+  // storage class.
+  std::string shader = R"(
+struct S {
+  a: i32;
+};
+
+var<private> a: S;
+@group(1) @binding(1) var<uniform> b: S;
+@group(1) @binding(2) var<storage, write> c: S;
+fn f() {
+  let ptr_b = &b;
+  *&a = *ptr_b;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[2]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[1]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->rhs->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable9) {
+  // Can't replace `b` with `e` since the latter is not a reference.
+  std::string shader = R"(
+struct S {
+  a: i32;
+};
+
+var<private> a: S;
+let e = 3;
+@group(1) @binding(1) var<uniform> b: S;
+fn f() {
+  *&a = *&b;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[1]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->rhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable10) {
+  // Can't replace `b` with `e` since the latter has a wrong access mode.
+  std::string shader = R"(
+struct S {
+  a: i32;
+};
+
+var<private> a: S;
+@group(0) @binding(0) var<storage, write> e: S;
+@group(1) @binding(1) var<uniform> b: S;
+fn f() {
+  *&a = *&b;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[1]);
+  ASSERT_NE(replacement_id, 0);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[0]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->rhs->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, Applicable1) {
+  // Can replace `a` with `b` (same storage class).
+  std::string shader = R"(
+fn f() {
+  var b : vec2<u32>;
+  var a = vec2<u32>(34u, 45u);
+  (*&a)[1] = 3u;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[2]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::IndexAccessorExpression>()
+                                      ->object->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id = node_id_map.GetId(program.AST()
+                                              .Functions()[0]
+                                              ->body->statements[0]
+                                              ->As<ast::VariableDeclStatement>()
+                                              ->variable);
+  ASSERT_NE(replacement_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program, MutationReplaceIdentifier(use_id, replacement_id), node_id_map,
+      &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn f() {
+  var b : vec2<u32>;
+  var a = vec2<u32>(34u, 45u);
+  (*(&(b)))[1] = 3u;
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(ReplaceIdentifierTest, Applicable2) {
+  // Can replace `ptr_a` with `b` - the function parameter.
+  std::string shader = R"(
+fn f(b: ptr<function, vec2<u32>>) {
+  var a = vec2<u32>(34u, 45u);
+  let ptr_a = &a;
+  (*ptr_a)[1] = 3u;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[2]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::IndexAccessorExpression>()
+                                      ->object->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id =
+      node_id_map.GetId(program.AST().Functions()[0]->params[0]);
+  ASSERT_NE(replacement_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program, MutationReplaceIdentifier(use_id, replacement_id), node_id_map,
+      &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn f(b : ptr<function, vec2<u32>>) {
+  var a = vec2<u32>(34u, 45u);
+  let ptr_a = &(a);
+  (*(b))[1] = 3u;
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable12) {
+  // Can't replace `a` with `b` (both are references with different storage
+  // class).
+  std::string shader = R"(
+var<private> b : vec2<u32>;
+fn f() {
+  var a = vec2<u32>(34u, 45u);
+  (*&a)[1] = 3u;
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(program.AST()
+                                      .Functions()[0]
+                                      ->body->statements[1]
+                                      ->As<ast::AssignmentStatement>()
+                                      ->lhs->As<ast::IndexAccessorExpression>()
+                                      ->object->As<ast::UnaryOpExpression>()
+                                      ->expr->As<ast::UnaryOpExpression>()
+                                      ->expr);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[0]);
+  ASSERT_NE(replacement_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable13) {
+  // Can't replace `a` with `b` (both are references with different storage
+  // class).
+  std::string shader = R"(
+var<private> b : vec2<u32>;
+fn f() {
+  var a = vec2<u32>(34u, 45u);
+  let c = (*&a)[1];
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(
+      program.AST()
+          .Functions()[0]
+          ->body->statements[1]
+          ->As<ast::VariableDeclStatement>()
+          ->variable->constructor->As<ast::IndexAccessorExpression>()
+          ->object->As<ast::UnaryOpExpression>()
+          ->expr->As<ast::UnaryOpExpression>()
+          ->expr);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id = node_id_map.GetId(program.AST().GlobalVariables()[0]);
+  ASSERT_NE(replacement_id, 0);
+
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ReplaceIdentifierTest, NotApplicable14) {
+  // Can't replace `ptr_a` with `ptr_b` (both are pointers with different
+  // storage class).
+  std::string shader = R"(
+var<private> b: vec2<u32>;
+fn f() {
+  var a = vec2<u32>(34u, 45u);
+  let ptr_a = &a;
+  let ptr_b = &b;
+  let c = (*ptr_a)[1];
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  auto use_id = node_id_map.GetId(
+      program.AST()
+          .Functions()[0]
+          ->body->statements[3]
+          ->As<ast::VariableDeclStatement>()
+          ->variable->constructor->As<ast::IndexAccessorExpression>()
+          ->object->As<ast::UnaryOpExpression>()
+          ->expr);
+  ASSERT_NE(use_id, 0);
+
+  auto replacement_id = node_id_map.GetId(program.AST()
+                                              .Functions()[0]
+                                              ->body->statements[2]
+                                              ->As<ast::VariableDeclStatement>()
+                                              ->variable);
+  ASSERT_NE(replacement_id, 0);
+  ASSERT_FALSE(MutationReplaceIdentifier(use_id, replacement_id)
+                   .IsApplicable(program, node_id_map));
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
new file mode 100644
index 0000000..1f5d5ae
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
@@ -0,0 +1,127 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h"
+
+#include <utility>
+#include <vector>
+
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationWrapUnaryOperator::MutationWrapUnaryOperator(
+    protobufs::MutationWrapUnaryOperator message)
+    : message_(std::move(message)) {}
+
+MutationWrapUnaryOperator::MutationWrapUnaryOperator(
+    uint32_t expression_id,
+    uint32_t fresh_id,
+    ast::UnaryOp unary_op_wrapper) {
+  message_.set_expression_id(expression_id);
+  message_.set_fresh_id(fresh_id);
+  message_.set_unary_op_wrapper(static_cast<uint32_t>(unary_op_wrapper));
+}
+
+bool MutationWrapUnaryOperator::IsApplicable(
+    const tint::Program& program,
+    const NodeIdMap& node_id_map) const {
+  // Check if id that will be assigned is fresh.
+  if (!node_id_map.IdIsFreshAndValid(message_.fresh_id())) {
+    return false;
+  }
+
+  const auto* expression_ast_node =
+      tint::As<ast::Expression>(node_id_map.GetNode(message_.expression_id()));
+
+  if (!expression_ast_node) {
+    // Either the node is not present with the given id or
+    // the node is not a valid expression type.
+    return false;
+  }
+
+  const auto* expression_sem_node =
+      tint::As<sem::Expression>(program.Sem().Get(expression_ast_node));
+
+  if (!expression_sem_node) {
+    // Semantic information for the expression ast node is not present
+    // or the semantic node is not a valid expression type node.
+    return false;
+  }
+
+  ast::UnaryOp unary_op_wrapper =
+      static_cast<ast::UnaryOp>(message_.unary_op_wrapper());
+
+  std::vector<ast::UnaryOp> valid_ops =
+      GetValidUnaryWrapper(*expression_sem_node);
+
+  // There is no available unary operator or |unary_op_wrapper| is a
+  // type that is not allowed for the given expression.
+  if (std::find(valid_ops.begin(), valid_ops.end(), unary_op_wrapper) ==
+      valid_ops.end()) {
+    return false;
+  }
+
+  return true;
+}
+
+void MutationWrapUnaryOperator::Apply(const NodeIdMap& node_id_map,
+                                      tint::CloneContext* clone_context,
+                                      NodeIdMap* new_node_id_map) const {
+  auto* expression_node =
+      tint::As<ast::Expression>(node_id_map.GetNode(message_.expression_id()));
+
+  auto* replacement_expression_node =
+      clone_context->dst->create<ast::UnaryOpExpression>(
+          static_cast<ast::UnaryOp>(message_.unary_op_wrapper()),
+          clone_context->Clone(expression_node));
+
+  clone_context->Replace(expression_node, replacement_expression_node);
+
+  new_node_id_map->Add(replacement_expression_node, message_.fresh_id());
+}
+
+protobufs::Mutation MutationWrapUnaryOperator::ToMessage() const {
+  protobufs::Mutation mutation;
+  *mutation.mutable_wrap_unary_operator() = message_;
+  return mutation;
+}
+
+std::vector<ast::UnaryOp> MutationWrapUnaryOperator::GetValidUnaryWrapper(
+    const sem::Expression& expr) {
+  const auto* expr_type = expr.Type();
+  if (expr_type->is_bool_scalar_or_vector()) {
+    return {ast::UnaryOp::kNot};
+  }
+
+  if (expr_type->is_signed_scalar_or_vector()) {
+    return {ast::UnaryOp::kNegation, ast::UnaryOp::kComplement};
+  }
+
+  if (expr_type->is_unsigned_scalar_or_vector()) {
+    return {ast::UnaryOp::kComplement};
+  }
+
+  if (expr_type->is_float_scalar_or_vector()) {
+    return {ast::UnaryOp::kNegation};
+  }
+
+  return {};
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h
new file mode 100644
index 0000000..25fec00
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h
@@ -0,0 +1,85 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_WRAP_UNARY_OPERATOR_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_WRAP_UNARY_OPERATOR_H_
+
+#include <vector>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// @see MutationWrapUnaryOperator::Apply
+class MutationWrapUnaryOperator : public Mutation {
+ public:
+  /// @brief Constructs an instance of this mutation from a protobuf message.
+  /// @param message - protobuf message
+  explicit MutationWrapUnaryOperator(
+      protobufs::MutationWrapUnaryOperator message);
+
+  /// @brief Constructor.
+  /// @param expression_id - the id of an expression.
+  /// @param fresh_id - a fresh id for the created expression node with
+  /// unary operator wrapper.
+  /// @param unary_op_wrapper - a `ast::UnaryOp` instance.
+  MutationWrapUnaryOperator(uint32_t expression_id,
+                            uint32_t fresh_id,
+                            ast::UnaryOp unary_op_wrapper);
+
+  /// @copybrief Mutation::IsApplicable
+  ///
+  /// The mutation is applicable iff:
+  /// - `expression_id` must refer to a valid expression that can be wrapped
+  ///    with unary operator.
+  /// - `fresh_id` must be fresh.
+  /// - `unary_op_wrapper` is a unary expression that is valid based on the
+  ///   type of the given expression.
+  ///
+  /// @copydetails Mutation::IsApplicable
+  bool IsApplicable(const tint::Program& program,
+                    const NodeIdMap& node_id_map) const override;
+
+  /// @copybrief Mutation::Apply
+  ///
+  /// Wrap an expression in a unary operator that is valid based on
+  /// the type of the expression.
+  ///
+  /// @copydetails Mutation::Apply
+  void Apply(const NodeIdMap& node_id_map,
+             tint::CloneContext* clone_context,
+             NodeIdMap* new_node_id_map) const override;
+
+  protobufs::Mutation ToMessage() const override;
+
+  /// Return list of unary operator wrappers allowed for the given
+  /// expression.
+  /// @param expr - an `ast::Expression` instance from node id map.
+  /// @return a list of unary operators.
+  static std::vector<ast::UnaryOp> GetValidUnaryWrapper(
+      const sem::Expression& expr);
+
+ private:
+  protobufs::MutationWrapUnaryOperator message_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_WRAP_UNARY_OPERATOR_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc
new file mode 100644
index 0000000..4b8435e
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator_test.cc
@@ -0,0 +1,548 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+TEST(WrapUnaryOperatorTest, Applicable1) {
+  std::string content = R"(
+    fn main() {
+      var a = 5;
+      if (a < 5) {
+        a = 6;
+      }
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  auto expression_id = node_id_map.GetId(
+      main_fn_statements[1]->As<ast::IfStatement>()->condition);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNot),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  var a = 5;
+  if (!((a < 5))) {
+    a = 6;
+  }
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable2) {
+  std::string content = R"(
+    fn main() {
+      let a = vec3<bool>(true, false, true);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNot),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  let a = !(vec3<bool>(true, false, true));
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable3) {
+  std::string content = R"(
+    fn main() {
+      var a : u32;
+      a = 6u;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[1]->As<ast::AssignmentStatement>()->rhs;
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kComplement),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  var a : u32;
+  a = ~(6u);
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable4) {
+  std::string content = R"(
+    fn main() -> vec2<bool> {
+      var a = (vec2<u32> (1u, 2u) == vec2<u32> (1u, 2u));
+      return a;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::BinaryExpression>()
+                         ->lhs;
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kComplement),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() -> vec2<bool> {
+  var a = (~(vec2<u32>(1u, 2u)) == vec2<u32>(1u, 2u));
+  return a;
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable5) {
+  std::string content = R"(
+    fn main() {
+      let a : f32 = -(1.0);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::UnaryOpExpression>()
+                         ->expr;
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  let a : f32 = -(-(1.0));
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable6) {
+  std::string content = R"(
+    fn main() {
+      var a : vec4<f32> = vec4<f32>(-1.0, -1.0, -1.0, -1.0);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  var a : vec4<f32> = -(vec4<f32>(-1.0, -1.0, -1.0, -1.0));
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable7) {
+  std::string content = R"(
+    fn main() {
+      var a = 1;
+      for(var i : i32 = 1; i < 5; i = i + 1) {
+        a = a + 1;
+      }
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[1]
+                         ->As<ast::ForLoopStatement>()
+                         ->initializer->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  var a = 1;
+  for(var i : i32 = -(1); (i < 5); i = (i + 1)) {
+    a = (a + 1);
+  }
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, Applicable8) {
+  std::string content = R"(
+    fn main() {
+      var a : vec4<i32> = vec4<i32>(1, 0, -1, 0);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kComplement),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  var a : vec4<i32> = ~(vec4<i32>(1, 0, -1, 0));
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable1) {
+  std::string content = R"(
+    fn main() {
+      let a = mat2x3<f32>(vec3<f32>(1.,0.,1.), vec3<f32>(0.,1.,0.));
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  // There is no unary operator that can be applied to matrix type.
+  ASSERT_FALSE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable2) {
+  std::string content = R"(
+    fn main() {
+      let a = 1;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  // Not cannot be applied to integer types.
+  ASSERT_FALSE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNot),
+      node_id_map, &program, &node_id_map, nullptr));
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable3) {
+  std::string content = R"(
+    fn main() {
+      let a = vec2<u32>(1u, 2u);
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  // Negation cannot be applied to unsigned integer scalar or vectors.
+  ASSERT_FALSE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable4) {
+  std::string content = R"(
+    fn main() {
+      let a = 1.5;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  // Cannot wrap float types with complement operator.
+  ASSERT_FALSE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(expression_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kComplement),
+      node_id_map, &program, &node_id_map, nullptr));
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable5) {
+  std::string content = R"(
+    fn main() {
+      let a = 1.5;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* expr = main_fn_statements[0]
+                         ->As<ast::VariableDeclStatement>()
+                         ->variable->constructor->As<ast::Expression>();
+
+  const auto expression_id = node_id_map.GetId(expr);
+  ASSERT_NE(expression_id, 0);
+
+  // Id for the replacement expression is not fresh.
+  ASSERT_FALSE(
+      MaybeApplyMutation(program,
+                         MutationWrapUnaryOperator(expression_id, expression_id,
+                                                   ast::UnaryOp::kNegation),
+                         node_id_map, &program, &node_id_map, nullptr));
+}
+
+TEST(WrapUnaryOperatorTest, NotApplicable6) {
+  std::string content = R"(
+    fn main() {
+      let a = 1.5;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_statements =
+      program.AST().Functions()[0]->body->statements;
+
+  const auto* statement =
+      main_fn_statements[0]->As<ast::VariableDeclStatement>();
+
+  const auto statement_id = node_id_map.GetId(statement);
+  ASSERT_NE(statement_id, 0);
+
+  // The id provided for the expression is not a valid expression type.
+  ASSERT_FALSE(MaybeApplyMutation(
+      program,
+      MutationWrapUnaryOperator(statement_id, node_id_map.TakeFreshId(),
+                                ast::UnaryOp::kNegation),
+      node_id_map, &program, &node_id_map, nullptr));
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
new file mode 100644
index 0000000..ab73a3a
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -0,0 +1,189 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+
+#include <cassert>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/program_builder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+template <typename T, typename... Args>
+void MaybeAddFinder(bool enable_all_mutations,
+                    ProbabilityContext* probability_context,
+                    MutationFinderList* finders,
+                    Args&&... args) {
+  if (enable_all_mutations || probability_context->RandomBool()) {
+    finders->push_back(std::make_unique<T>(std::forward<Args>(args)...));
+  }
+}
+
+MutationFinderList CreateMutationFinders(
+    ProbabilityContext* probability_context,
+    bool enable_all_mutations) {
+  MutationFinderList result;
+  do {
+    MaybeAddFinder<MutationFinderChangeBinaryOperators>(
+        enable_all_mutations, probability_context, &result);
+    MaybeAddFinder<MutationFinderReplaceIdentifiers>(
+        enable_all_mutations, probability_context, &result);
+    MaybeAddFinder<MutationFinderWrapUnaryOperators>(
+        enable_all_mutations, probability_context, &result);
+  } while (result.empty());
+  return result;
+}
+
+}  // namespace
+
+bool MaybeApplyMutation(const tint::Program& program,
+                        const Mutation& mutation,
+                        const NodeIdMap& node_id_map,
+                        tint::Program* out_program,
+                        NodeIdMap* out_node_id_map,
+                        protobufs::MutationSequence* mutation_sequence) {
+  assert(out_program && "`out_program` may not be a nullptr");
+  assert(out_node_id_map && "`out_node_id_map` may not be a nullptr");
+
+  if (!mutation.IsApplicable(program, node_id_map)) {
+    return false;
+  }
+
+  // The mutated `program` will be copied into the `mutated` program builder.
+  tint::ProgramBuilder mutated;
+  tint::CloneContext clone_context(&mutated, &program);
+  NodeIdMap new_node_id_map;
+  clone_context.ReplaceAll(
+      [&node_id_map, &new_node_id_map, &clone_context](const ast::Node* node) {
+        // Make sure all `tint::ast::` nodes' ids are preserved.
+        auto* cloned = tint::As<ast::Node>(node->Clone(&clone_context));
+        new_node_id_map.Add(cloned, node_id_map.GetId(node));
+        return cloned;
+      });
+
+  mutation.Apply(node_id_map, &clone_context, &new_node_id_map);
+  if (mutation_sequence) {
+    *mutation_sequence->add_mutation() = mutation.ToMessage();
+  }
+
+  clone_context.Clone();
+  *out_program = tint::Program(std::move(mutated));
+  *out_node_id_map = std::move(new_node_id_map);
+  return true;
+}
+
+tint::Program Replay(tint::Program program,
+                     const protobufs::MutationSequence& mutation_sequence) {
+  assert(program.IsValid() && "Initial program is invalid");
+
+  NodeIdMap node_id_map(program);
+  for (const auto& mutation_message : mutation_sequence.mutation()) {
+    auto mutation = Mutation::FromMessage(mutation_message);
+    auto status = MaybeApplyMutation(program, *mutation, node_id_map, &program,
+                                     &node_id_map, nullptr);
+    (void)status;  // `status` will be unused in release mode.
+    assert(status && "`mutation` is inapplicable - it's most likely a bug");
+    if (!program.IsValid()) {
+      // `mutation` has a bug.
+      break;
+    }
+  }
+
+  return program;
+}
+
+tint::Program Mutate(tint::Program program,
+                     ProbabilityContext* probability_context,
+                     bool enable_all_mutations,
+                     uint32_t max_applied_mutations,
+                     protobufs::MutationSequence* mutation_sequence) {
+  assert(max_applied_mutations != 0 &&
+         "Maximum number of mutations is invalid");
+  assert(program.IsValid() && "Initial program is invalid");
+
+  // The number of allowed failed attempts to apply mutations. If this number is
+  // exceeded, the mutator is considered stuck and the mutation session is
+  // stopped.
+  const uint32_t kMaxFailureToApply = 10;
+
+  auto finders =
+      CreateMutationFinders(probability_context, enable_all_mutations);
+  NodeIdMap node_id_map(program);
+
+  // Total number of applied mutations during this call to `Mutate`.
+  uint32_t applied_mutations = 0;
+
+  // The number of consecutively failed attempts to apply mutations.
+  uint32_t failure_to_apply = 0;
+
+  // Apply mutations as long as the `program` is valid, the limit on the number
+  // of mutations is not reached and the mutator is not stuck (i.e. unable to
+  // apply any mutations for some time).
+  while (program.IsValid() && applied_mutations < max_applied_mutations &&
+         failure_to_apply < kMaxFailureToApply) {
+    // Get all applicable mutations from some mutation finder.
+    const auto& mutation_finder =
+        finders[probability_context->GetRandomIndex(finders)];
+    auto mutations = mutation_finder->FindMutations(program, &node_id_map,
+                                                    probability_context);
+
+    const auto old_applied_mutations = applied_mutations;
+    for (const auto& mutation : mutations) {
+      if (!probability_context->ChoosePercentage(
+              mutation_finder->GetChanceOfApplyingMutation(
+                  probability_context))) {
+        // Skip this `mutation` probabilistically.
+        continue;
+      }
+
+      if (!MaybeApplyMutation(program, *mutation, node_id_map, &program,
+                              &node_id_map, mutation_sequence)) {
+        // This `mutation` is inapplicable. This may happen if some of the
+        // earlier mutations cancelled this one.
+        continue;
+      }
+
+      applied_mutations++;
+      if (!program.IsValid()) {
+        // This `mutation` has a bug.
+        return program;
+      }
+    }
+
+    if (old_applied_mutations == applied_mutations) {
+      // No mutation was applied. Increase the counter to prevent an infinite
+      // loop.
+      failure_to_apply++;
+    } else {
+      failure_to_apply = 0;
+    }
+  }
+
+  return program;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutator.h
new file mode 100644
index 0000000..d700b70
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.h
@@ -0,0 +1,101 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATOR_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h"
+
+#include "src/tint/program.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+/// @file
+
+/// @brief Tries to apply a `mutation` to the `program`.
+///
+/// If the `mutation` is inapplicable, this function will return `false` and
+/// `out_program`, `out_node_id_map` and `mutation_sequence` won't be modified.
+///
+/// The `mutation` is required to produce a valid program when the
+/// `Mutation::Apply` method is called. This guarantees that this function
+/// returns a valid program as well.
+///
+/// @param program - the initial program (must be valid).
+/// @param mutation - the mutation that will be applied.
+/// @param node_id_map - a map from `tint::ast::` nodes in the `program` to
+///     their unique ids.
+/// @param out_program - the resulting mutated program will be written through
+///     this pointer. It may *not* be a `nullptr`. It _may_ point to `program`,
+///     so that a program can be updated in place.
+/// @param out_node_id_map - will contain new ids for the AST nodes in the
+///     mutated program. It may *not* be a `nullptr`. It _may_ point to
+///     `node_id_map`, so that a map can be updated in place.
+/// @param mutation_sequence - the message about this mutation will be recorded
+///     here. It may be a `nullptr`, in which case it's ignored.
+/// @return `true` if the `mutation` was applied.
+/// @return `false` if the `mutation` is inapplicable.
+bool MaybeApplyMutation(const tint::Program& program,
+                        const Mutation& mutation,
+                        const NodeIdMap& node_id_map,
+                        tint::Program* out_program,
+                        NodeIdMap* out_node_id_map,
+                        protobufs::MutationSequence* mutation_sequence);
+
+/// @brief Applies mutations from `mutations_sequence` to the `program`.
+///
+/// All mutations in `mutation_sequence` must be applicable. Additionally, all
+/// mutations must produce a valid program when the `Mutation::Apply` method is
+/// called. This guarantees that this function returns a valid program as well.
+///
+/// @param program - the initial program - must be valid.
+/// @param mutation_sequence - a sequence of mutations.
+/// @return the mutated program.
+tint::Program Replay(tint::Program program,
+                     const protobufs::MutationSequence& mutation_sequence);
+
+/// @brief Applies up to `max_applied_mutations` mutations to the `program`.
+///
+/// All applied mutations must produce valid programs. This guarantees that the
+/// returned program is valid as well. The returned program may be identical to
+/// the initial `program` if no mutation was applied.
+///
+/// @param program - initial program - must be valid.
+/// @param probability_context - contains information about various
+///     probabilistic behaviour of the fuzzer.
+/// @param enable_all_mutations - if `false`, only mutations from a
+///     probabilistically selected set of mutation types are applied. If `true`,
+///     all mutation types are considered.
+/// @param max_applied_mutations - the maximum number of applied mutations. This
+///     may not be 0.
+/// @param mutation_sequence - applied mutations will be recorded into this
+///     protobuf message. This argument may be `nullptr`, in which case it's
+///     ignored.
+/// @return the mutated program.
+tint::Program Mutate(tint::Program program,
+                     ProbabilityContext* probability_context,
+                     bool enable_all_mutations,
+                     uint32_t max_applied_mutations,
+                     protobufs::MutationSequence* mutation_sequence);
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.cc b/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.cc
new file mode 100644
index 0000000..117fd2d
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.cc
@@ -0,0 +1,65 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+
+#include <cassert>
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+NodeIdMap::NodeIdMap() = default;
+
+NodeIdMap::NodeIdMap(const Program& program) : NodeIdMap() {
+  for (const auto* node : program.ASTNodes().Objects()) {
+    Add(node, TakeFreshId());
+  }
+}
+
+NodeIdMap::IdType NodeIdMap::GetId(const ast::Node* node) const {
+  auto it = node_to_id_.find(node);
+  return it == node_to_id_.end() ? 0 : it->second;
+}
+
+const ast::Node* NodeIdMap::GetNode(IdType id) const {
+  auto it = id_to_node_.find(id);
+  return it == id_to_node_.end() ? nullptr : it->second;
+}
+
+void NodeIdMap::Add(const ast::Node* node, IdType id) {
+  assert(!node_to_id_.count(node) && "The node already exists in the map");
+  assert(IdIsFreshAndValid(id) && "Id already exists in the map or Id is zero");
+  assert(node && "`node` can't be a nullptr");
+
+  node_to_id_[node] = id;
+  id_to_node_[id] = node;
+
+  if (id >= fresh_id_) {
+    fresh_id_ = id + 1;
+  }
+}
+
+bool NodeIdMap::IdIsFreshAndValid(IdType id) const {
+  return id && !id_to_node_.count(id);
+}
+
+NodeIdMap::IdType NodeIdMap::TakeFreshId() {
+  assert(fresh_id_ != 0 && "`NodeIdMap` id has overflowed");
+  return fresh_id_++;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h b/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h
new file mode 100644
index 0000000..1aae93f
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_NODE_ID_MAP_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_NODE_ID_MAP_H_
+
+#include <unordered_map>
+
+#include "src/tint/program.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Contains a one-to-one mapping between the nodes in the AST of the program
+/// and their ids.
+///
+/// The motivation for having this mapping is:
+/// - To be able to uniquely identify a node in the AST. This will be used
+///   to record transformations in the protobuf messages.
+/// - When the AST is being modified, only the mapping for the modified nodes
+///   must be affected. That is, if some node is unchanged, it must have the
+///   same id defined in this class.
+///
+/// This class achieves these goals partially. Concretely, the only way to
+/// change the AST is by cloning it since all instances of `tint::ast::` classes
+/// are immutable. This will invalidate all the pointers to the AST nodes which
+/// are used in this class. To overcome this, a new instance of this class is
+/// created with all the cloned nodes and the old instance is discarded.
+class NodeIdMap {
+ public:
+  /// Type of the id used by this map.
+  using IdType = uint32_t;
+
+  /// Creates an empty map.
+  NodeIdMap();
+
+  /// @brief Initializes this instance with all the nodes in the `program`.
+  /// @param program - must be valid.
+  explicit NodeIdMap(const Program& program);
+
+  /// @brief Returns a node for the given `id`.
+  /// @param id - any value is accepted.
+  /// @return a pointer to some node if `id` exists in this map.
+  /// @return `nullptr` otherwise.
+  const ast::Node* GetNode(IdType id) const;
+
+  /// @brief Returns an id of the given `node`.
+  /// @param node - can be a `nullptr`.
+  /// @return not equal to 0 if `node` exists in this map.
+  /// @return 0 otherwise.
+  IdType GetId(const ast::Node* node) const;
+
+  /// @brief Adds a mapping from `node` to `id` to this map.
+  /// @param node - may not be a `nullptr` and can't be present in this map.
+  /// @param id - may not be 0 and can't be present in this map.
+  void Add(const ast::Node* node, IdType id);
+
+  /// @brief Returns whether the id is fresh by checking if it exists in
+  /// the id map and the id is not 0.
+  /// @param id - an id that is used to check in the map.
+  /// @return true the given id is fresh and valid (non-zero).
+  /// @return false otherwise.
+  bool IdIsFreshAndValid(IdType id) const;
+
+  /// @brief Returns an id that is guaranteed to be unoccupied in this map.
+  ///
+  /// This will effectively increase the counter. This means that two
+  /// consecutive calls to this method will return different ids.
+  ///
+  /// @return an unoccupied id.
+  IdType TakeFreshId();
+
+ private:
+  IdType fresh_id_ = 1;
+
+  std::unordered_map<const ast::Node*, IdType> node_to_id_;
+  std::unordered_map<IdType, const ast::Node*> id_to_node_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_NODE_ID_MAP_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h b/src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h
new file mode 100644
index 0000000..31b2946
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h
@@ -0,0 +1,36 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_OVERRIDE_CLI_PARAMS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_OVERRIDE_CLI_PARAMS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// @brief Allows CLI parameters to be overridden.
+///
+/// This function allows fuzz targets to override particular CLI parameters,
+/// for example forcing a particular back-end to be targeted.
+///
+/// @param cli_params - the parsed CLI parameters to be updated.
+void OverrideCliParams(CliParams& cli_params);
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_OVERRIDE_CLI_PARAMS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
new file mode 100644
index 0000000..1d9461f
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
@@ -0,0 +1,50 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
+
+#include <cassert>
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+const std::pair<uint32_t, uint32_t> kChanceOfChangingBinaryOperators = {30, 90};
+const std::pair<uint32_t, uint32_t> kChanceOfReplacingIdentifiers = {30, 70};
+const std::pair<uint32_t, uint32_t> kChanceOfWrappingUnaryOperators = {30, 70};
+
+}  // namespace
+
+ProbabilityContext::ProbabilityContext(RandomGenerator* generator)
+    : generator_(generator),
+      chance_of_changing_binary_operators_(
+          RandomFromRange(kChanceOfChangingBinaryOperators)),
+      chance_of_replacing_identifiers_(
+          RandomFromRange(kChanceOfReplacingIdentifiers)),
+      chance_of_wrapping_unary_operators_(
+          RandomFromRange(kChanceOfWrappingUnaryOperators)) {
+  assert(generator != nullptr && "generator must not be nullptr");
+}
+
+uint32_t ProbabilityContext::RandomFromRange(
+    std::pair<uint32_t, uint32_t> range) {
+  assert(range.first <= range.second && "Range must be non-decreasing");
+  return generator_->GetUInt32(
+      range.first, range.second + 1);  // + 1 need since range is inclusive.
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
new file mode 100644
index 0000000..eacc1bd
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
@@ -0,0 +1,89 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROBABILITY_CONTEXT_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROBABILITY_CONTEXT_H_
+
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// This class is intended to be used by the `MutationFinder`s to introduce some
+/// variance to the mutation process.
+class ProbabilityContext {
+ public:
+  /// Initializes this instance with a random number generator.
+  /// @param generator - must not be a `nullptr`. Must remain in scope as long
+  /// as this
+  ///     instance exists.
+  explicit ProbabilityContext(RandomGenerator* generator);
+
+  /// Get random bool with even odds
+  /// @returns true 50% of the time and false %50 of time.
+  bool RandomBool() { return generator_->GetBool(); }
+
+  /// Get random bool with weighted odds
+  /// @param percentage - likelihood of true being returned
+  /// @returns true |percentage|% of the time, and false (100 - |percentage|)%
+  /// of the time.
+  bool ChoosePercentage(uint32_t percentage) {
+    return generator_->GetWeightedBool(percentage);
+  }
+
+  /// Returns a random value in the range `[0; arr.size())`.
+  /// @tparam T - type of the elements in the vector.
+  /// @param arr - may not be empty.
+  /// @return the random index in the `arr`.
+  template <typename T>
+  size_t GetRandomIndex(const std::vector<T>& arr) {
+    return static_cast<size_t>(generator_->GetUInt64(arr.size()));
+  }
+
+  /// @return the probability of replacing some binary operator with another.
+  uint32_t GetChanceOfChangingBinaryOperators() const {
+    return chance_of_changing_binary_operators_;
+  }
+
+  /// @return the probability of replacing some identifier with some other one.
+  uint32_t GetChanceOfReplacingIdentifiers() const {
+    return chance_of_replacing_identifiers_;
+  }
+
+  /// @return the probability of wrapping an expression in a unary operator.
+  uint32_t GetChanceOfWrappingUnaryOperators() const {
+    return chance_of_wrapping_unary_operators_;
+  }
+
+ private:
+  /// @param range - a pair of integers `a` and `b` s.t. `a <= b`.
+  /// @return an random number in the range `[a; b]`.
+  uint32_t RandomFromRange(std::pair<uint32_t, uint32_t> range);
+
+  RandomGenerator* generator_;
+
+  uint32_t chance_of_changing_binary_operators_;
+  uint32_t chance_of_replacing_identifiers_;
+  uint32_t chance_of_wrapping_unary_operators_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROBABILITY_CONTEXT_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h
new file mode 100644
index 0000000..54586b1
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.h
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROTOBUFS_TINT_AST_FUZZER_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROTOBUFS_TINT_AST_FUZZER_H_
+
+// Compilation of the protobuf library and its autogenerated code can produce
+// warnings. Ignore them since we can't control them.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#pragma clang diagnostic ignored "-Wreserved-id-macro"
+#pragma clang diagnostic ignored "-Wsign-conversion"
+#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
+#pragma clang diagnostic ignored "-Wextra-semi-stmt"
+#pragma clang diagnostic ignored "-Winconsistent-missing-destructor-override"
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#pragma clang diagnostic ignored "-Wsuggest-destructor-override"
+#pragma clang diagnostic ignored "-Wreserved-identifier"
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.pb.h"
+
+#pragma clang diagnostic pop
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_PROTOBUFS_TINT_AST_FUZZER_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto
new file mode 100644
index 0000000..0a5951f
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto
@@ -0,0 +1,76 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto3";
+
+package tint.fuzzers.ast_fuzzer.protobufs;
+
+message Mutation {
+  oneof mutation {
+    MutationReplaceIdentifier replace_identifier = 1;
+    MutationChangeBinaryOperator change_binary_operator = 2;
+    MutationWrapUnaryOperator wrap_unary_operator = 3;
+  };
+}
+
+message MutationSequence {
+  repeated Mutation mutation = 1;
+}
+
+message MutatorState {
+  // Contains the state of the fuzzer.
+
+  // The program that is being fuzzed. This can be either
+  // the original program (if mutation sequence is available) or
+  // the mutated version (if mutations are being recorded).
+  string program = 1;
+
+  // The sequence of mutations that was applied to the `program`.
+  // This may not have any mutations if they are not being recorded.
+  MutationSequence mutation_sequence = 2;
+}
+
+message MutationReplaceIdentifier {
+  // This transformation replaces a use of one variable with another.
+
+  // The id of the use of a variable in the AST.
+  uint32 use_id = 1;
+
+  // The id of a definition of a variable to replace the use with.
+  uint32 replacement_id = 2;
+}
+
+message MutationChangeBinaryOperator {
+  // This transformation replaces one binary operator with another.
+
+  // The id of a binary expression in the AST.
+  uint32 binary_expr_id = 1;
+
+  // A BinaryOp representing the new binary operator.
+  uint32 new_operator = 2;
+}
+
+message MutationWrapUnaryOperator {
+  // This transformation wraps an expression with a allowed unary
+  // expression operator.
+
+  // The id of the expression.
+  uint32 expression_id = 1;
+
+  // A fresh id for the created unary expression.
+  uint32 fresh_id = 2;
+
+  // The unary operator to wrap the expression with.
+  uint32 unary_op_wrapper = 3;
+}
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_fuzzer.cc
new file mode 100644
index 0000000..77b00c4
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_fuzzer.cc
@@ -0,0 +1,28 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+void OverrideCliParams(CliParams& /*unused*/) {
+  // Leave the CLI parameters unchanged.
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc
new file mode 100644
index 0000000..a1d886e
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_hlsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kHlsl;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc
new file mode 100644
index 0000000..8354ac3
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_msl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kMsl;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc
new file mode 100644
index 0000000..266ef6a
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_spv_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kSpv;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc
new file mode 100644
index 0000000..ede740f
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/tint_ast_wgsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kWgsl;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/util.h b/src/tint/fuzzers/tint_ast_fuzzer/util.h
new file mode 100644
index 0000000..019fdec
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/util.h
@@ -0,0 +1,109 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_UTIL_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_UTIL_H_
+
+#include <vector>
+
+#include "src/tint/ast/module.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/castable.h"
+#include "src/tint/program.h"
+#include "src/tint/sem/block_statement.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/statement.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace util {
+/// @file
+
+/// @brief Returns all in-scope variables (including formal function parameters)
+/// related to statement `curr_stmt`.
+///
+/// These variables are additionally filtered by applying a predicate `pred`.
+///
+/// @tparam Pred - a predicate that accepts a `const sem::Variable*` and returns
+///     `bool`.
+/// @param program - the program to look for variables in.
+/// @param curr_stmt - the current statement. Everything below it is not in
+///     scope.
+/// @param pred - a predicate (e.g. a function pointer, functor, lambda etc) of
+///     type `Pred`.
+/// @return a vector of all variables that can be accessed from `curr_stmt`.
+template <typename Pred>
+std::vector<const sem::Variable*> GetAllVarsInScope(
+    const tint::Program& program,
+    const sem::Statement* curr_stmt,
+    Pred&& pred) {
+  std::vector<const sem::Variable*> result;
+
+  // Walk up the hierarchy of blocks in which `curr_stmt` is contained.
+  for (const auto* block = curr_stmt->Block(); block;
+       block = tint::As<sem::BlockStatement>(block->Parent())) {
+    for (const auto* stmt : block->Declaration()->statements) {
+      if (stmt == curr_stmt->Declaration()) {
+        // `curr_stmt` was found. This is only possible if `block is the
+        // enclosing block of `curr_stmt` since the AST nodes are not shared.
+        // Because of all this, skip the iteration of the inner loop since
+        // the rest of the instructions in the `block` are not visible from the
+        // `curr_stmt`.
+        break;
+      }
+
+      if (const auto* var_node = tint::As<ast::VariableDeclStatement>(stmt)) {
+        const auto* sem_var = program.Sem().Get(var_node->variable);
+        if (pred(sem_var)) {
+          result.push_back(sem_var);
+        }
+      }
+    }
+  }
+
+  // Process function parameters.
+  for (const auto* param : curr_stmt->Function()->Parameters()) {
+    if (pred(param)) {
+      result.push_back(param);
+    }
+  }
+
+  // Global variables do not belong to any ast::BlockStatement.
+  for (const auto* global_decl : program.AST().GlobalDeclarations()) {
+    if (global_decl == curr_stmt->Function()->Declaration()) {
+      // The same situation as in the previous loop. The current function has
+      // been reached. If there are any variables declared below, they won't be
+      // visible in this function. Thus, exit the loop.
+      break;
+    }
+
+    if (const auto* global_var = tint::As<ast::Variable>(global_decl)) {
+      const auto* sem_node = program.Sem().Get(global_var);
+      if (pred(sem_node)) {
+        result.push_back(sem_node);
+      }
+    }
+  }
+
+  return result;
+}
+
+}  // namespace util
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_UTIL_H_
diff --git a/src/tint/fuzzers/tint_binding_remapper_fuzzer.cc b/src/tint/fuzzers/tint_binding_remapper_fuzzer.cc
new file mode 100644
index 0000000..f736cca
--- /dev/null
+++ b/src/tint/fuzzers/tint_binding_remapper_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<transform::BindingRemapper>();
+
+  fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_black_box_fuzz_target.cc b/src/tint/fuzzers/tint_black_box_fuzz_target.cc
new file mode 100644
index 0000000..426b58a
--- /dev/null
+++ b/src/tint/fuzzers/tint_black_box_fuzz_target.cc
@@ -0,0 +1,157 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+
+namespace {
+
+/// Controls the target language in which code will be generated.
+enum class TargetLanguage {
+  kHlsl,
+  kMsl,
+  kSpv,
+  kWgsl,
+  kTargetLanguageMax,
+};
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
+  if (!buffer) {
+    std::cerr << "The buffer pointer was null" << std::endl;
+    return false;
+  }
+
+  FILE* file = nullptr;
+#if defined(_MSC_VER)
+  fopen_s(&file, input_file.c_str(), "rb");
+#else
+  file = fopen(input_file.c_str(), "rb");
+#endif
+  if (!file) {
+    std::cerr << "Failed to open " << input_file << std::endl;
+    return false;
+  }
+
+  fseek(file, 0, SEEK_END);
+  const auto file_size = static_cast<size_t>(ftell(file));
+  if (0 != (file_size % sizeof(T))) {
+    std::cerr << "File " << input_file
+              << " does not contain an integral number of objects: "
+              << file_size << " bytes in the file, require " << sizeof(T)
+              << " bytes per object" << std::endl;
+    fclose(file);
+    return false;
+  }
+  fseek(file, 0, SEEK_SET);
+
+  buffer->clear();
+  buffer->resize(file_size / sizeof(T));
+
+  size_t bytes_read = fread(buffer->data(), 1, file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    std::cerr << "Failed to read " << input_file << std::endl;
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+  if (argc < 2 || argc > 3) {
+    std::cerr << "Usage: " << argv[0] << " <input file> [hlsl|msl|spv|wgsl]"
+              << std::endl;
+    return 1;
+  }
+
+  std::string input_filename(argv[1]);
+
+  std::vector<uint8_t> data;
+  if (!ReadFile<uint8_t>(input_filename, &data)) {
+    return 1;
+  }
+
+  if (data.empty()) {
+    return 0;
+  }
+
+  tint::fuzzers::DataBuilder builder(data.data(), data.size());
+
+  TargetLanguage target_language;
+
+  if (argc == 3) {
+    std::string target_language_string = argv[2];
+    if (target_language_string == "hlsl") {
+      target_language = TargetLanguage::kHlsl;
+    } else if (target_language_string == "msl") {
+      target_language = TargetLanguage::kMsl;
+    } else if (target_language_string == "spv") {
+      target_language = TargetLanguage::kSpv;
+    } else {
+      assert(target_language_string == "wgsl" && "Unknown target language.");
+      target_language = TargetLanguage::kWgsl;
+    }
+  } else {
+    target_language = builder.enum_class<TargetLanguage>(
+        static_cast<uint32_t>(TargetLanguage::kTargetLanguageMax));
+  }
+
+  switch (target_language) {
+    case TargetLanguage::kHlsl: {
+      tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
+                                         tint::fuzzers::OutputFormat::kHLSL);
+      return fuzzer.Run(data.data(), data.size());
+    }
+    case TargetLanguage::kMsl: {
+      tint::writer::msl::Options options;
+      GenerateMslOptions(&builder, &options);
+      tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
+                                         tint::fuzzers::OutputFormat::kMSL);
+      fuzzer.SetOptionsMsl(options);
+      return fuzzer.Run(data.data(), data.size());
+    }
+    case TargetLanguage::kSpv: {
+      tint::writer::spirv::Options options;
+      GenerateSpirvOptions(&builder, &options);
+      tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
+                                         tint::fuzzers::OutputFormat::kSpv);
+      fuzzer.SetOptionsSpirv(options);
+      return fuzzer.Run(data.data(), data.size());
+    }
+    case TargetLanguage::kWgsl: {
+      tint::fuzzers::CommonFuzzer fuzzer(tint::fuzzers::InputFormat::kWGSL,
+                                         tint::fuzzers::OutputFormat::kWGSL);
+      return fuzzer.Run(data.data(), data.size());
+    }
+    default:
+      std::cerr << "Aborting due to unknown target language; fuzzer must be "
+                   "misconfigured."
+                << std::endl;
+      abort();
+  }
+}
diff --git a/src/tint/fuzzers/tint_common_fuzzer.cc b/src/tint/fuzzers/tint_common_fuzzer.cc
new file mode 100644
index 0000000..b8bfe08
--- /dev/null
+++ b/src/tint/fuzzers/tint_common_fuzzer.cc
@@ -0,0 +1,350 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+
+#include <cassert>
+#include <cstring>
+#include <fstream>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#if TINT_BUILD_SPV_READER
+#include "spirv-tools/libspirv.hpp"
+#endif  // TINT_BUILD_SPV_READER
+
+#include "src/tint/ast/module.h"
+#include "src/tint/diagnostic/formatter.h"
+#include "src/tint/program.h"
+#include "src/tint/utils/hash.h"
+
+namespace tint {
+namespace fuzzers {
+
+namespace {
+
+// A macro is used to avoid FATAL_ERROR creating its own stack frame. This leads
+// to better de-duplication of bug reports, because ClusterFuzz only uses the
+// top few stack frames for de-duplication, and a FATAL_ERROR stack frame
+// provides no useful information.
+#define FATAL_ERROR(diags, msg_string)                        \
+  do {                                                        \
+    std::string msg = msg_string;                             \
+    auto printer = tint::diag::Printer::create(stderr, true); \
+    if (!msg.empty()) {                                       \
+      printer->write(msg + "\n", {diag::Color::kRed, true});  \
+    }                                                         \
+    tint::diag::Formatter().format(diags, printer.get());     \
+    __builtin_trap();                                         \
+  } while (false)
+
+[[noreturn]] void TintInternalCompilerErrorReporter(
+    const tint::diag::List& diagnostics) {
+  FATAL_ERROR(diagnostics, "");
+}
+
+// Wrapping in a macro, so it can be a one-liner in the code, but not
+// introduce another level in the stack trace. This will help with de-duping
+// ClusterFuzz issues.
+#define CHECK_INSPECTOR(program, inspector)                    \
+  do {                                                         \
+    if ((inspector).has_error()) {                             \
+      if (!enforce_validity) {                                 \
+        return;                                                \
+      }                                                        \
+      FATAL_ERROR((program)->Diagnostics(),                    \
+                  "Inspector failed: " + (inspector).error()); \
+    }                                                          \
+  } while (false)
+
+// Wrapping in a macro to make code more readable and help with issue de-duping.
+#define VALIDITY_ERROR(diags, msg_string) \
+  do {                                    \
+    if (!enforce_validity) {              \
+      return 0;                           \
+    }                                     \
+    FATAL_ERROR(diags, msg_string);       \
+  } while (false)
+
+bool SPIRVToolsValidationCheck(const tint::Program& program,
+                               const std::vector<uint32_t>& spirv) {
+  spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
+  const tint::diag::List& diags = program.Diagnostics();
+  tools.SetMessageConsumer([diags](spv_message_level_t, const char*,
+                                   const spv_position_t& pos, const char* msg) {
+    std::stringstream out;
+    out << "Unexpected spirv-val error:\n"
+        << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg
+        << std::endl;
+
+    auto printer = tint::diag::Printer::create(stderr, true);
+    printer->write(out.str(), {diag::Color::kYellow, false});
+    tint::diag::Formatter().format(diags, printer.get());
+  });
+
+  return tools.Validate(spirv.data(), spirv.size(),
+                        spvtools::ValidatorOptions());
+}
+
+}  // namespace
+
+void GenerateSpirvOptions(DataBuilder* b, writer::spirv::Options* options) {
+  *options = b->build<writer::spirv::Options>();
+}
+
+void GenerateWgslOptions(DataBuilder* b, writer::wgsl::Options* options) {
+  *options = b->build<writer::wgsl::Options>();
+}
+
+void GenerateHlslOptions(DataBuilder* b, writer::hlsl::Options* options) {
+  *options = b->build<writer::hlsl::Options>();
+}
+
+void GenerateMslOptions(DataBuilder* b, writer::msl::Options* options) {
+  *options = b->build<writer::msl::Options>();
+}
+
+CommonFuzzer::CommonFuzzer(InputFormat input, OutputFormat output)
+    : input_(input), output_(output) {}
+
+CommonFuzzer::~CommonFuzzer() = default;
+
+int CommonFuzzer::Run(const uint8_t* data, size_t size) {
+  tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+
+#if TINT_BUILD_WGSL_WRITER
+  tint::Program::printer = [](const tint::Program* program) {
+    auto result = tint::writer::wgsl::Generate(program, {});
+    if (!result.error.empty()) {
+      return "error: " + result.error;
+    }
+    return result.wgsl;
+  };
+#endif  // TINT_BUILD_WGSL_WRITER
+
+  Program program;
+
+#if TINT_BUILD_SPV_READER
+  std::vector<uint32_t> spirv_input(size / sizeof(uint32_t));
+
+#endif  // TINT_BUILD_SPV_READER
+
+#if TINT_BUILD_WGSL_READER || TINT_BUILD_SPV_READER
+  auto dump_input_data = [&](auto& content, const char* extension) {
+    size_t hash = utils::Hash(content);
+    auto filename = "fuzzer_input_" + std::to_string(hash) + extension;  //
+    std::ofstream fout(filename, std::ios::binary);
+    fout.write(reinterpret_cast<const char*>(data),
+               static_cast<std::streamsize>(size));
+    std::cout << "Dumped input data to " << filename << std::endl;
+  };
+#endif
+
+  switch (input_) {
+#if TINT_BUILD_WGSL_READER
+    case InputFormat::kWGSL: {
+      // Clear any existing diagnostics, as these will hold pointers to file_,
+      // which we are about to release.
+      diagnostics_ = {};
+      std::string str(reinterpret_cast<const char*>(data), size);
+      file_ = std::make_unique<Source::File>("test.wgsl", str);
+      if (dump_input_) {
+        dump_input_data(str, ".wgsl");
+      }
+      program = reader::wgsl::Parse(file_.get());
+      break;
+    }
+#endif  // TINT_BUILD_WGSL_READER
+#if TINT_BUILD_SPV_READER
+    case InputFormat::kSpv: {
+      // `spirv_input` has been initialized with the capacity to store `size /
+      // sizeof(uint32_t)` uint32_t values. If `size` is not a multiple of
+      // sizeof(uint32_t) then not all of `data` can be copied into
+      // `spirv_input`, and any trailing bytes are discarded.
+      std::memcpy(spirv_input.data(), data,
+                  spirv_input.size() * sizeof(uint32_t));
+      if (spirv_input.empty()) {
+        return 0;
+      }
+      if (dump_input_) {
+        dump_input_data(spirv_input, ".spv");
+      }
+      program = reader::spirv::Parse(spirv_input);
+      break;
+    }
+#endif  // TINT_BUILD_SPV_READER
+  }
+
+  if (!program.IsValid()) {
+    diagnostics_ = program.Diagnostics();
+    return 0;
+  }
+
+#if TINT_BUILD_SPV_READER
+  if (input_ == InputFormat::kSpv &&
+      !SPIRVToolsValidationCheck(program, spirv_input)) {
+    FATAL_ERROR(
+        program.Diagnostics(),
+        "Fuzzing detected invalid input spirv not being caught by Tint");
+  }
+#endif  // TINT_BUILD_SPV_READER
+
+  RunInspector(&program);
+  diagnostics_ = program.Diagnostics();
+
+  if (transform_manager_) {
+    auto out = transform_manager_->Run(&program, *transform_inputs_);
+    if (!out.program.IsValid()) {
+      // Transforms can produce error messages for bad input.
+      // Catch ICEs and errors from non transform systems.
+      for (const auto& diag : out.program.Diagnostics()) {
+        if (diag.severity > diag::Severity::Error ||
+            diag.system != diag::System::Transform) {
+          VALIDITY_ERROR(program.Diagnostics(),
+                         "Fuzzing detected valid input program being "
+                         "transformed into an invalid output program");
+        }
+      }
+    }
+
+    program = std::move(out.program);
+    RunInspector(&program);
+  }
+
+  switch (output_) {
+    case OutputFormat::kWGSL: {
+#if TINT_BUILD_WGSL_WRITER
+      auto result = writer::wgsl::Generate(&program, options_wgsl_);
+      generated_wgsl_ = std::move(result.wgsl);
+      if (!result.success) {
+        VALIDITY_ERROR(
+            program.Diagnostics(),
+            "WGSL writer errored on validated input:\n" + result.error);
+      }
+#endif  // TINT_BUILD_WGSL_WRITER
+      break;
+    }
+    case OutputFormat::kSpv: {
+#if TINT_BUILD_SPV_WRITER
+      auto result = writer::spirv::Generate(&program, options_spirv_);
+      generated_spirv_ = std::move(result.spirv);
+      if (!result.success) {
+        VALIDITY_ERROR(
+            program.Diagnostics(),
+            "SPIR-V writer errored on validated input:\n" + result.error);
+      }
+
+      if (!SPIRVToolsValidationCheck(program, generated_spirv_)) {
+        VALIDITY_ERROR(program.Diagnostics(),
+                       "Fuzzing detected invalid spirv being emitted by Tint");
+      }
+
+#endif  // TINT_BUILD_SPV_WRITER
+      break;
+    }
+    case OutputFormat::kHLSL: {
+#if TINT_BUILD_HLSL_WRITER
+      auto result = writer::hlsl::Generate(&program, options_hlsl_);
+      generated_hlsl_ = std::move(result.hlsl);
+      if (!result.success) {
+        VALIDITY_ERROR(
+            program.Diagnostics(),
+            "HLSL writer errored on validated input:\n" + result.error);
+      }
+#endif  // TINT_BUILD_HLSL_WRITER
+      break;
+    }
+    case OutputFormat::kMSL: {
+#if TINT_BUILD_MSL_WRITER
+      auto result = writer::msl::Generate(&program, options_msl_);
+      generated_msl_ = std::move(result.msl);
+      if (!result.success) {
+        VALIDITY_ERROR(
+            program.Diagnostics(),
+            "MSL writer errored on validated input:\n" + result.error);
+      }
+#endif  // TINT_BUILD_MSL_WRITER
+      break;
+    }
+  }
+
+  return 0;
+}
+
+void CommonFuzzer::RunInspector(Program* program) {
+  inspector::Inspector inspector(program);
+  diagnostics_ = program->Diagnostics();
+
+  auto entry_points = inspector.GetEntryPoints();
+  CHECK_INSPECTOR(program, inspector);
+
+  auto constant_ids = inspector.GetConstantIDs();
+  CHECK_INSPECTOR(program, inspector);
+
+  auto constant_name_to_id = inspector.GetConstantNameToIdMap();
+  CHECK_INSPECTOR(program, inspector);
+
+  for (auto& ep : entry_points) {
+    inspector.GetStorageSize(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetUniformBufferResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetStorageBufferResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetReadOnlyStorageBufferResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetSamplerResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetComparisonSamplerResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetSampledTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetMultisampledTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetWriteOnlyStorageTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetDepthTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetDepthMultisampledTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetExternalTextureResourceBindings(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetSamplerTextureUses(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+
+    inspector.GetWorkgroupStorageSize(ep.name);
+    CHECK_INSPECTOR(program, inspector);
+  }
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_common_fuzzer.h b/src/tint/fuzzers/tint_common_fuzzer.h
new file mode 100644
index 0000000..50bfb61
--- /dev/null
+++ b/src/tint/fuzzers/tint_common_fuzzer.h
@@ -0,0 +1,162 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_COMMON_FUZZER_H_
+#define SRC_TINT_FUZZERS_TINT_COMMON_FUZZER_H_
+
+#include <cassert>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "include/tint/tint.h"
+
+#include "src/tint/fuzzers/data_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+// TODO(crbug.com/tint/1356): Add using shader reflection to generate options
+//                            that are potentially valid for Generate*Options
+//                            functions.
+/// Generates random set of options for SPIRV generation
+void GenerateSpirvOptions(DataBuilder* b, writer::spirv::Options* options);
+
+/// Generates random set of options for WGSL generation
+void GenerateWgslOptions(DataBuilder* b, writer::wgsl::Options* options);
+
+/// Generates random set of options for HLSL generation
+void GenerateHlslOptions(DataBuilder* b, writer::hlsl::Options* options);
+
+/// Generates random set of options for MSL generation
+void GenerateMslOptions(DataBuilder* b, writer::msl::Options* options);
+
+/// Shader language the fuzzer is reading
+enum class InputFormat { kWGSL, kSpv };
+
+/// Shader language the fuzzer is emitting
+enum class OutputFormat { kWGSL, kSpv, kHLSL, kMSL };
+
+/// Generic runner for reading and emitting shaders using Tint, used by most
+/// fuzzers to share common code.
+class CommonFuzzer {
+ public:
+  /// Constructor
+  /// @param input shader language being read
+  /// @param output shader language being emitted
+  CommonFuzzer(InputFormat input, OutputFormat output);
+
+  /// Destructor
+  ~CommonFuzzer();
+
+  /// @param tm manager for transforms to run
+  /// @param inputs data for transforms to run
+  void SetTransformManager(transform::Manager* tm, transform::DataMap* inputs) {
+    assert((!tm || inputs) && "DataMap must be !nullptr if Manager !nullptr");
+    transform_manager_ = tm;
+    transform_inputs_ = inputs;
+  }
+
+  /// @param enabled if the input shader for run should be outputted to the log
+  void SetDumpInput(bool enabled) { dump_input_ = enabled; }
+
+  /// @param enabled if the shader being valid after parsing is being enforced.
+  /// If false, invalidation of the shader will cause an early exit, but not
+  /// throw an error.
+  /// If true invalidation will throw an error that is caught by libFuzzer and
+  /// will generate a crash report.
+  void SetEnforceValidity(bool enabled) { enforce_validity = enabled; }
+
+  /// Convert given shader from input to output format.
+  /// Will also apply provided transforms and run the inspector over the result.
+  /// @param data buffer of data that will interpreted as a byte array or string
+  ///             depending on the shader input format.
+  /// @param size number of elements in buffer
+  /// @returns 0, this is what libFuzzer expects
+  int Run(const uint8_t* data, size_t size);
+
+  /// @returns diagnostic messages generated while Run() is executed.
+  const tint::diag::List& Diagnostics() const { return diagnostics_; }
+
+  /// @returns if there are any errors in the diagnostic messages
+  bool HasErrors() const { return diagnostics_.contains_errors(); }
+
+  /// @returns generated SPIR-V binary, if SPIR-V was emitted.
+  const std::vector<uint32_t>& GetGeneratedSpirv() const {
+    return generated_spirv_;
+  }
+
+  /// @returns generated WGSL string, if WGSL was emitted.
+  const std::string& GetGeneratedWgsl() const { return generated_wgsl_; }
+
+  /// @returns generated HLSL string, if HLSL was emitted.
+  const std::string& GetGeneratedHlsl() const { return generated_hlsl_; }
+
+  /// @returns generated MSL string, if HLSL was emitted.
+  const std::string& GetGeneratedMsl() const { return generated_msl_; }
+
+  /// @param options SPIR-V emission options
+  void SetOptionsSpirv(const writer::spirv::Options& options) {
+    options_spirv_ = options;
+  }
+
+  /// @param options WGSL emission options
+  void SetOptionsWgsl(const writer::wgsl::Options& options) {
+    options_wgsl_ = options;
+  }
+
+  /// @param options HLSL emission options
+  void SetOptionsHlsl(const writer::hlsl::Options& options) {
+    options_hlsl_ = options;
+  }
+
+  /// @param options MSL emission options
+  void SetOptionsMsl(const writer::msl::Options& options) {
+    options_msl_ = options;
+  }
+
+ private:
+  InputFormat input_;
+  OutputFormat output_;
+  transform::Manager* transform_manager_ = nullptr;
+  transform::DataMap* transform_inputs_ = nullptr;
+  bool dump_input_ = false;
+  tint::diag::List diagnostics_;
+  bool enforce_validity = false;
+
+  std::vector<uint32_t> generated_spirv_;
+  std::string generated_wgsl_;
+  std::string generated_hlsl_;
+  std::string generated_msl_;
+
+  writer::spirv::Options options_spirv_;
+  writer::wgsl::Options options_wgsl_;
+  writer::hlsl::Options options_hlsl_;
+  writer::msl::Options options_msl_;
+
+#if TINT_BUILD_WGSL_READER
+  /// The source file needs to live at least as long as #diagnostics_
+  std::unique_ptr<Source::File> file_;
+#endif  // TINT_BUILD_WGSL_READER
+
+  /// Runs a series of reflection operations to exercise the Inspector API.
+  void RunInspector(Program* program);
+};
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_COMMON_FUZZER_H_
diff --git a/src/tint/fuzzers/tint_first_index_offset_fuzzer.cc b/src/tint/fuzzers/tint_first_index_offset_fuzzer.cc
new file mode 100644
index 0000000..f8d437c
--- /dev/null
+++ b/src/tint/fuzzers/tint_first_index_offset_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<transform::FirstIndexOffset>();
+
+  fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_reader_writer_fuzzer.h b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
new file mode 100644
index 0000000..e4a4e37
--- /dev/null
+++ b/src/tint/fuzzers/tint_reader_writer_fuzzer.h
@@ -0,0 +1,72 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_READER_WRITER_FUZZER_H_
+#define SRC_TINT_FUZZERS_TINT_READER_WRITER_FUZZER_H_
+
+#include <memory>
+
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Wrapper around the common fuzzing class for tint_*_reader_*_writter fuzzers
+class ReaderWriterFuzzer : public CommonFuzzer {
+ public:
+  /// Constructor
+  /// Pass through to the CommonFuzzer constructor
+  /// @param input shader language being read
+  /// @param output shader language being emitted
+  ReaderWriterFuzzer(InputFormat input, OutputFormat output)
+      : CommonFuzzer(input, output) {}
+
+  /// Destructor
+  ~ReaderWriterFuzzer() {}
+
+  /// Pass through to the CommonFuzzer setter, but records if it has been
+  /// invoked.
+  /// @param tm manager for transforms to run
+  /// @param inputs data for transforms to run
+  void SetTransformManager(transform::Manager* tm, transform::DataMap* inputs) {
+    tm_set_ = true;
+    CommonFuzzer::SetTransformManager(tm, inputs);
+  }
+
+  /// Pass through to the CommonFuzzer implementation, but will setup a
+  /// robustness transform, if no other transforms have been set.
+  /// @param data buffer of data that will interpreted as a byte array or string
+  ///             depending on the shader input format.
+  /// @param size number of elements in buffer
+  /// @returns 0, this is what libFuzzer expects
+  int Run(const uint8_t* data, size_t size) {
+    if (!tm_set_) {
+      tb_ = std::make_unique<TransformBuilder>(data, size);
+      tb_->AddTransform<tint::transform::Robustness>();
+      SetTransformManager(tb_->manager(), tb_->data_map());
+    }
+
+    return CommonFuzzer::Run(data, size);
+  }
+
+ private:
+  bool tm_set_ = false;
+  std::unique_ptr<TransformBuilder> tb_;
+};
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_READER_WRITER_FUZZER_H_
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn
new file mode 100644
index 0000000..e8d647d
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2021 The Tint Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+import("../../../../tint_overrides_with_defaults.gni")
+
+if (build_with_chromium) {
+  source_set("tint_regex_fuzzer") {
+    public_configs = [
+      "${tint_root_dir}/src/tint:tint_config",
+      "${tint_root_dir}/src/tint:tint_common_config",
+    ]
+
+    deps = [ "${tint_root_dir}/src/tint/fuzzers:tint_fuzzer_common_src" ]
+
+    sources = [
+      "cli.cc",
+      "cli.h",
+      "fuzzer.cc",
+      "override_cli_params.h",
+      "wgsl_mutator.cc",
+      "wgsl_mutator.h",
+    ]
+  }
+}
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
new file mode 100644
index 0000000..bcd0885
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/CMakeLists.txt
@@ -0,0 +1,75 @@
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+function(add_tint_regex_fuzzer NAME)
+  add_executable(${NAME} ${NAME}.cc ${REGEX_FUZZER_SOURCES})
+  target_link_libraries(${NAME} libtint-fuzz libtint_regex_fuzzer)
+  tint_default_compile_options(${NAME})
+  target_compile_definitions(${NAME} PRIVATE CUSTOM_MUTATOR)
+  target_include_directories(${NAME} PRIVATE ${CMAKE_BINARY_DIR})
+endfunction()
+
+set(LIBTINT_REGEX_FUZZER_SOURCES
+        ../mersenne_twister_engine.cc
+        ../mersenne_twister_engine.h
+        ../random_generator.cc
+        ../random_generator.h
+        ../random_generator_engine.cc
+        ../random_generator_engine.h
+        wgsl_mutator.cc
+        wgsl_mutator.h)
+
+# Add static library target.
+add_library(libtint_regex_fuzzer STATIC ${LIBTINT_REGEX_FUZZER_SOURCES})
+tint_default_compile_options(libtint_regex_fuzzer)
+
+set(REGEX_FUZZER_SOURCES
+        cli.cc
+        cli.h
+        fuzzer.cc
+        override_cli_params.h
+        ../tint_common_fuzzer.cc
+        ../tint_common_fuzzer.h)
+
+set_source_files_properties(fuzzer.cc PROPERTIES COMPILE_FLAGS -Wno-missing-prototypes)
+
+# Add libfuzzer targets.
+# Targets back-ends according to command line arguments.
+add_tint_regex_fuzzer(tint_regex_fuzzer)
+# Targets back-ends individually.
+add_tint_regex_fuzzer(tint_regex_hlsl_writer_fuzzer)
+add_tint_regex_fuzzer(tint_regex_msl_writer_fuzzer)
+add_tint_regex_fuzzer(tint_regex_spv_writer_fuzzer)
+add_tint_regex_fuzzer(tint_regex_wgsl_writer_fuzzer)
+
+# Add tests.
+if (${TINT_BUILD_TESTS})
+    set(TEST_SOURCES
+            regex_fuzzer_tests.cc)
+
+    add_executable(tint_regex_fuzzer_unittests ${TEST_SOURCES})
+
+    target_include_directories(
+            tint_regex_fuzzer_unittests PRIVATE ${gmock_SOURCE_DIR}/include)
+    target_link_libraries(tint_regex_fuzzer_unittests gmock_main libtint_regex_fuzzer)
+    tint_default_compile_options(tint_regex_fuzzer_unittests)
+    target_compile_options(tint_regex_fuzzer_unittests PRIVATE
+            -Wno-global-constructors
+            -Wno-weak-vtables
+            -Wno-covered-switch-default)
+
+    target_include_directories(tint_regex_fuzzer_unittests PRIVATE ${CMAKE_BINARY_DIR})
+
+    add_test(NAME tint_regex_fuzzer_unittests COMMAND tint_regex_fuzzer_unittests)
+endif ()
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/CPPLINT.cfg b/src/tint/fuzzers/tint_regex_fuzzer/CPPLINT.cfg
new file mode 100644
index 0000000..96988ad
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/CPPLINT.cfg
@@ -0,0 +1 @@
+filter=-build/c++11
\ No newline at end of file
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/cli.cc b/src/tint/fuzzers/tint_regex_fuzzer/cli.cc
new file mode 100644
index 0000000..1fd3e10
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/cli.cc
@@ -0,0 +1,126 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+
+#include <cstring>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+namespace {
+
+const char* const kHelpMessage = R"(
+This is a fuzzer for the Tint compiler that works by mutating a WGSL shader.
+
+Below is a list of all supported parameters for this fuzzer. You may want to
+run it with -help=1 to check out libfuzzer parameters.
+
+  -tint_fuzzing_target=
+                       Specifies the shading language to target during fuzzing.
+                       This must be one or a combination of `wgsl`, `spv`, `hlsl`,
+                       `msl` (without `) separated by commas. By default it's
+                       `wgsl,msl,hlsl,spv`.
+
+  -tint_help
+                       Show this message. Note that there is also a -help=1
+                       parameter that will display libfuzzer's help message.
+)";
+
+bool HasPrefix(const char* str, const char* prefix) {
+  return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+[[noreturn]] void InvalidParam(const char* param) {
+  std::cout << "Invalid value for " << param << std::endl;
+  std::cout << kHelpMessage << std::endl;
+  exit(1);
+}
+
+bool ParseFuzzingTarget(const char* value, FuzzingTarget* out) {
+  if (!strcmp(value, "wgsl")) {
+    *out = FuzzingTarget::kWgsl;
+  } else if (!strcmp(value, "spv")) {
+    *out = FuzzingTarget::kSpv;
+  } else if (!strcmp(value, "msl")) {
+    *out = FuzzingTarget::kMsl;
+  } else if (!strcmp(value, "hlsl")) {
+    *out = FuzzingTarget::kHlsl;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+CliParams ParseCliParams(int* argc, char** argv) {
+  CliParams cli_params;
+  auto help = false;
+
+  for (int i = *argc - 1; i > 0; --i) {
+    auto param = argv[i];
+    auto recognized_parameter = true;
+
+    if (HasPrefix(param, "-tint_fuzzing_target=")) {
+      auto result = FuzzingTarget::kNone;
+
+      std::stringstream ss(param + sizeof("-tint_fuzzing_target=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto tmp = FuzzingTarget::kNone;
+        if (!ParseFuzzingTarget(value.c_str(), &tmp)) {
+          InvalidParam(param);
+        }
+        result = result | tmp;
+      }
+
+      if (result == FuzzingTarget::kNone) {
+        InvalidParam(param);
+      }
+
+      cli_params.fuzzing_target = result;
+    } else if (!strcmp(param, "-tint_help")) {
+      help = true;
+    } else {
+      recognized_parameter = false;
+    }
+
+    if (recognized_parameter) {
+      // Remove the recognized parameter from the list of all parameters by
+      // swapping it with the last one. This will suppress warnings in the
+      // libFuzzer about unrecognized parameters. By default, libFuzzer thinks
+      // that all user-defined parameters start with two dashes. However, we are
+      // forced to use a single one to make the fuzzer compatible with the
+      // ClusterFuzz.
+      std::swap(argv[i], argv[*argc - 1]);
+      *argc -= 1;
+    }
+  }
+
+  if (help) {
+    std::cout << kHelpMessage << std::endl;
+    exit(0);
+  }
+
+  return cli_params;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/cli.h b/src/tint/fuzzers/tint_regex_fuzzer/cli.h
new file mode 100644
index 0000000..55048c5
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/cli.h
@@ -0,0 +1,64 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_CLI_H_
+#define SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_CLI_H_
+
+#include <cstdint>
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+/// The backend this fuzzer will test.
+enum class FuzzingTarget {
+  kNone = 0,
+  kHlsl = 1 << 0,
+  kMsl = 1 << 1,
+  kSpv = 1 << 2,
+  kWgsl = 1 << 3,
+  kAll = kHlsl | kMsl | kSpv | kWgsl
+};
+
+inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// CLI parameters accepted by the fuzzer. Type -tint_help in the CLI to see the
+/// help message
+struct CliParams {
+  /// Compiler backends we want to fuzz.
+  FuzzingTarget fuzzing_target = FuzzingTarget::kAll;
+};
+
+/// @brief Parses CLI parameters.
+///
+/// This function will exit the process with non-zero return code if some
+/// parameters are invalid. This function will remove recognized parameters from
+/// `argv` and adjust `argc` accordingly.
+///
+/// @param argc - the total number of parameters.
+/// @param argv - array of all CLI parameters.
+/// @return parsed parameters.
+CliParams ParseCliParams(int* argc, char** argv);
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_CLI_H_
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc
new file mode 100644
index 0000000..bc8dc78
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc
@@ -0,0 +1,156 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h"
+#include "src/tint/fuzzers/transform_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+namespace {
+
+CliParams cli_params{};
+
+enum class MutationKind {
+  kSwapIntervals,
+  kDeleteInterval,
+  kDuplicateInterval,
+  kReplaceIdentifier,
+  kReplaceLiteral,
+  kInsertReturnStatement,
+  kNumMutationKinds
+};
+
+extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
+  // Parse CLI parameters. `ParseCliParams` will call `exit` if some parameter
+  // is invalid.
+  cli_params = ParseCliParams(argc, *argv);
+  // For some fuzz targets it is desirable to force the values of certain CLI
+  // parameters after parsing.
+  OverrideCliParams(cli_params);
+  return 0;
+}
+
+extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data,
+                                          size_t size,
+                                          size_t max_size,
+                                          unsigned seed) {
+  std::string wgsl_code(data, data + size);
+  const std::vector<std::string> delimiters{";"};
+  RandomGenerator generator(seed);
+
+  std::string delimiter =
+      delimiters[generator.GetUInt32(static_cast<uint32_t>(delimiters.size()))];
+
+  MutationKind mutation_kind = static_cast<MutationKind>(generator.GetUInt32(
+      static_cast<uint32_t>(MutationKind::kNumMutationKinds)));
+
+  switch (mutation_kind) {
+    case MutationKind::kSwapIntervals:
+      if (!SwapRandomIntervals(delimiter, wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    case MutationKind::kDeleteInterval:
+      if (!DeleteRandomInterval(delimiter, wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    case MutationKind::kDuplicateInterval:
+      if (!DuplicateRandomInterval(delimiter, wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    case MutationKind::kReplaceIdentifier:
+      if (!ReplaceRandomIdentifier(wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    case MutationKind::kReplaceLiteral:
+      if (!ReplaceRandomIntLiteral(wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    case MutationKind::kInsertReturnStatement:
+      if (!InsertReturnStatement(wgsl_code, generator)) {
+        return 0;
+      }
+      break;
+
+    default:
+      assert(false && "Unreachable");
+      return 0;
+  }
+
+  if (wgsl_code.size() > max_size) {
+    return 0;
+  }
+
+  memcpy(data, wgsl_code.c_str(), wgsl_code.size());
+  return wgsl_code.size();
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  if (size == 0) {
+    return 0;
+  }
+
+  struct Target {
+    FuzzingTarget fuzzing_target;
+    OutputFormat output_format;
+    const char* name;
+  };
+
+  Target targets[] = {{FuzzingTarget::kWgsl, OutputFormat::kWGSL, "WGSL"},
+                      {FuzzingTarget::kHlsl, OutputFormat::kHLSL, "HLSL"},
+                      {FuzzingTarget::kMsl, OutputFormat::kMSL, "MSL"},
+                      {FuzzingTarget::kSpv, OutputFormat::kSpv, "SPV"}};
+
+  for (auto target : targets) {
+    if ((target.fuzzing_target & cli_params.fuzzing_target) !=
+        target.fuzzing_target) {
+      continue;
+    }
+
+    TransformBuilder tb(data, size);
+    tb.AddTransform<tint::transform::Robustness>();
+
+    CommonFuzzer fuzzer(InputFormat::kWGSL, target.output_format);
+    fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+
+    fuzzer.Run(data, size);
+  }
+
+  return 0;
+}
+
+}  // namespace
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h b/src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h
new file mode 100644
index 0000000..445f524
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h
@@ -0,0 +1,36 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_OVERRIDE_CLI_PARAMS_H_
+#define SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_OVERRIDE_CLI_PARAMS_H_
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+/// @brief Allows CLI parameters to be overridden.
+///
+/// This function allows fuzz targets to override particular CLI parameters,
+/// for example forcing a particular back-end to be targeted.
+///
+/// @param cli_params - the parsed CLI parameters to be updated.
+void OverrideCliParams(CliParams& cli_params);
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_OVERRIDE_CLI_PARAMS_H_
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc b/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc
new file mode 100644
index 0000000..d58942d
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc
@@ -0,0 +1,521 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+namespace {
+
+// Swaps two non-consecutive regions in the edge
+TEST(SwapRegionsTest, SwapIntervalsEdgeNonConsecutive) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;";
+  std::string all_regions = R1 + R2 + R3;
+
+  // this call should swap R1 with R3.
+  SwapIntervals(0, R1.length(), R1.length() + R2.length(), R3.length(),
+                all_regions);
+
+  ASSERT_EQ(R3 + R2 + R1, all_regions);
+}
+
+// Swaps two non-consecutive regions not in the edge
+TEST(SwapRegionsTest, SwapIntervalsNonConsecutiveNonEdge) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // this call should swap R2 with R4.
+  SwapIntervals(R1.length(), R2.length(),
+                R1.length() + R2.length() + R3.length(), R4.length(),
+                all_regions);
+
+  ASSERT_EQ(R1 + R4 + R3 + R2 + R5, all_regions);
+}
+
+// Swaps two consecutive regions not in the edge (sorrounded by other
+// regions)
+TEST(SwapRegionsTest, SwapIntervalsConsecutiveEdge) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4;
+
+  // this call should swap R2 with R3.
+  SwapIntervals(R1.length(), R2.length(), R1.length() + R2.length(),
+                R3.length(), all_regions);
+
+  ASSERT_EQ(R1 + R3 + R2 + R4, all_regions);
+}
+
+// Swaps two consecutive regions not in the edge (not sorrounded by other
+// regions)
+TEST(SwapRegionsTest, SwapIntervalsConsecutiveNonEdge) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // this call should swap R4 with R5.
+  SwapIntervals(R1.length() + R2.length() + R3.length(), R4.length(),
+                R1.length() + R2.length() + R3.length() + R4.length(),
+                R5.length(), all_regions);
+
+  ASSERT_EQ(R1 + R2 + R3 + R5 + R4, all_regions);
+}
+
+// Deletes the first region.
+TEST(DeleteRegionTest, DeleteFirstRegion) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should delete R1.
+  DeleteInterval(0, R1.length(), all_regions);
+
+  ASSERT_EQ(";" + R2 + R3 + R4 + R5, all_regions);
+}
+
+// Deletes the last region.
+TEST(DeleteRegionTest, DeleteLastRegion) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should delete R5.
+  DeleteInterval(R1.length() + R2.length() + R3.length() + R4.length(),
+                 R5.length(), all_regions);
+
+  ASSERT_EQ(R1 + R2 + R3 + R4 + ";", all_regions);
+}
+
+// Deletes the middle region.
+TEST(DeleteRegionTest, DeleteMiddleRegion) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should delete R3.
+  DeleteInterval(R1.length() + R2.length(), R3.length(), all_regions);
+
+  ASSERT_EQ(R1 + R2 + ";" + R4 + R5, all_regions);
+}
+
+TEST(InsertRegionTest, InsertRegionTest1) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should insert R2 after R4.
+  DuplicateInterval(R1.length(), R2.length(),
+                    R1.length() + R2.length() + R3.length() + R4.length() - 1,
+                    all_regions);
+
+  ASSERT_EQ(R1 + R2 + R3 + R4 + R2.substr(1, R2.size() - 1) + R5, all_regions);
+}
+
+TEST(InsertRegionTest, InsertRegionTest2) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should insert R3 after R1.
+  DuplicateInterval(R1.length() + R2.length(), R3.length(), R1.length() - 1,
+                    all_regions);
+
+  ASSERT_EQ(R1 + R3.substr(1, R3.length() - 1) + R2 + R3 + R4 + R5,
+            all_regions);
+}
+
+TEST(InsertRegionTest, InsertRegionTest3) {
+  std::string R1 = ";region1;", R2 = ";regionregion2;",
+              R3 = ";regionregionregion3;", R4 = ";regionregionregionregion4;",
+              R5 = ";regionregionregionregionregion5;";
+
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // This call should insert R2 after R5.
+  DuplicateInterval(R1.length(), R2.length(), all_regions.length() - 1,
+                    all_regions);
+
+  ASSERT_EQ(R1 + R2 + R3 + R4 + R5 + R2.substr(1, R2.length() - 1),
+            all_regions);
+}
+
+TEST(ReplaceIdentifierTest, ReplaceIdentifierTest1) {
+  std::string R1 = "|region1|", R2 = "; region2;",
+              R3 = "---------region3---------", R4 = "++region4++",
+              R5 = "***region5***";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // Replaces R3 with R1.
+  ReplaceRegion(0, R1.length(), R1.length() + R2.length(), R3.length(),
+                all_regions);
+
+  ASSERT_EQ(R1 + R2 + R1 + R4 + R5, all_regions);
+}
+
+TEST(ReplaceIdentifierTest, ReplaceIdentifierTest2) {
+  std::string R1 = "|region1|", R2 = "; region2;",
+              R3 = "---------region3---------", R4 = "++region4++",
+              R5 = "***region5***";
+  std::string all_regions = R1 + R2 + R3 + R4 + R5;
+
+  // Replaces R5 with R3.
+  ReplaceRegion(R1.length() + R2.length(), R3.length(),
+                R1.length() + R2.length() + R3.length() + R4.length(),
+                R5.length(), all_regions);
+
+  ASSERT_EQ(R1 + R2 + R3 + R4 + R3, all_regions);
+}
+
+TEST(GetIdentifierTest, GetIdentifierTest1) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+      }
+      @stage(vertex)
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+         clamp_0acf8f();"
+         return vec4<f32>();
+      }
+      @stage(fragment)
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @stage(compute) @workgroup_size(1)
+      fn compute_main() {"
+        var<private> foo: f32 = 0.0;
+        clamp_0acf8f();
+      })";
+
+  std::vector<std::pair<size_t, size_t>> identifiers_pos =
+      GetIdentifiers(wgsl_code);
+
+  std::vector<std::pair<size_t, size_t>> ground_truth = {
+      std::make_pair(3, 12),   std::make_pair(28, 3),  std::make_pair(37, 4),
+      std::make_pair(49, 5),   std::make_pair(60, 3),  std::make_pair(68, 4),
+      std::make_pair(81, 4),   std::make_pair(110, 5), std::make_pair(130, 2),
+      std::make_pair(140, 4),  std::make_pair(151, 7), std::make_pair(169, 4),
+      std::make_pair(190, 12), std::make_pair(216, 6), std::make_pair(228, 3),
+      std::make_pair(251, 5),  std::make_pair(273, 2), std::make_pair(285, 4),
+      std::make_pair(302, 12), std::make_pair(333, 5), std::make_pair(349, 14),
+      std::make_pair(373, 2),  std::make_pair(384, 4), std::make_pair(402, 3),
+      std::make_pair(415, 3),  std::make_pair(420, 3), std::make_pair(439, 12)};
+
+  ASSERT_EQ(ground_truth, identifiers_pos);
+}
+
+TEST(TestGetLiteralsValues, TestGetLiteralsValues1) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+      }
+      @stage(vertex)
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+        clamp_0acf8f();
+        var foo_1: i32 = 3;
+        return vec4<f32>();
+      }
+      @stage(fragment)
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @stage(compute) @workgroup_size(1)
+      fn compute_main() {
+        var<private> foo: f32 = 0.0;
+        var foo_2: i32 = 10;
+        clamp_0acf8f();
+      }
+      foo_1 = 5 + 7;
+      var foo_3 : i32 = -20;)";
+
+  std::vector<std::pair<size_t, size_t>> literals_pos =
+      GetIntLiterals(wgsl_code);
+
+  std::vector<std::string> ground_truth = {"3", "10", "5", "7", "-20"};
+
+  std::vector<std::string> result;
+
+  for (auto pos : literals_pos) {
+    result.push_back(wgsl_code.substr(pos.first, pos.second));
+  }
+
+  ASSERT_EQ(ground_truth, result);
+}
+
+TEST(InsertReturnTest, FindClosingBrace) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+        if(false){
+
+        } else{
+          var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+          }
+        }
+        @stage(vertex)
+        fn vertex_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f();
+          var foo_1: i32 = 3;
+          return vec4<f32>();
+        }
+        @stage(fragment)
+        fn fragment_main() {
+          clamp_0acf8f();
+        }
+        @stage(compute) @workgroup_size(1)
+        fn compute_main() {
+          var<private> foo: f32 = 0.0;
+          var foo_2: i32 = 10;
+          clamp_0acf8f();
+        }
+        foo_1 = 5 + 7;
+        var foo_3 : i32 = -20;
+      )";
+  size_t opening_bracket_pos = 18;
+  size_t closing_bracket_pos = FindClosingBrace(opening_bracket_pos, wgsl_code);
+
+  // The -1 is needed since the function body starts after the left bracket.
+  std::string function_body = wgsl_code.substr(
+      opening_bracket_pos + 1, closing_bracket_pos - opening_bracket_pos - 1);
+  std::string expected =
+      R"(
+        if(false){
+
+        } else{
+          var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+          }
+        )";
+  ASSERT_EQ(expected, function_body);
+}
+
+TEST(InsertReturnTest, FindClosingBraceFailing) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+      // This comment } causes the test to fail.
+      "if(false){
+
+      } else{
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+        }
+      }
+      @stage(vertex)
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+        clamp_0acf8f();
+        var foo_1: i32 = 3;
+        return vec4<f32>();
+      }
+      @stage(fragment)
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @stage(compute) @workgroup_size(1)
+      fn compute_main() {
+        var<private> foo: f32 = 0.0;
+        var foo_2: i32 = 10;
+        clamp_0acf8f();
+      }
+      foo_1 = 5 + 7;
+      var foo_3 : i32 = -20;)";
+  size_t opening_bracket_pos = 18;
+  size_t closing_bracket_pos = FindClosingBrace(opening_bracket_pos, wgsl_code);
+
+  // The -1 is needed since the function body starts after the left bracket.
+  std::string function_body = wgsl_code.substr(
+      opening_bracket_pos + 1, closing_bracket_pos - opening_bracket_pos - 1);
+  std::string expected =
+      R"(// This comment } causes the test to fail.
+      "if(false){
+
+      } else{
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+        })";
+  ASSERT_NE(expected, function_body);
+}
+
+TEST(TestInsertReturn, TestInsertReturn1) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+      }
+      @stage(vertex)
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+        clamp_0acf8f();
+        var foo_1: i32 = 3;
+        return vec4<f32>();
+      }
+      @stage(fragment)
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @stage(compute) @workgroup_size(1)
+      fn compute_main() {
+        var<private> foo: f32 = 0.0;
+        var foo_2: i32 = 10;
+        clamp_0acf8f();
+      }
+      foo_1 = 5 + 7;
+      var foo_3 : i32 = -20;)";
+
+  std::vector<size_t> semicolon_pos;
+  for (size_t pos = wgsl_code.find(";", 0); pos != std::string::npos;
+       pos = wgsl_code.find(";", pos + 1)) {
+    semicolon_pos.push_back(pos);
+  }
+
+  // should insert a return true statement after the first semicolon of the
+  // first function the the WGSL-like string above.
+  wgsl_code.insert(semicolon_pos[0] + 1, "return true;");
+
+  std::string expected_wgsl_code =
+      R"(fn clamp_0acf8f() {
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());return true;
+      }
+      @stage(vertex)
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+        clamp_0acf8f();
+        var foo_1: i32 = 3;
+        return vec4<f32>();
+      }
+      @stage(fragment)
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @stage(compute) @workgroup_size(1)
+      fn compute_main() {
+        var<private> foo: f32 = 0.0;
+        var foo_2: i32 = 10;
+        clamp_0acf8f();
+      }
+      foo_1 = 5 + 7;
+      var foo_3 : i32 = -20;)";
+
+  ASSERT_EQ(expected_wgsl_code, wgsl_code);
+}
+
+TEST(TestInsertReturn, TestFunctionPositions) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+          var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+        }
+        @stage(vertex)
+        fn vertex_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f();
+          var foo_1: i32 = 3;
+          return vec4<f32>();
+        }
+        @stage(fragment)
+        fn fragment_main() {
+          clamp_0acf8f();
+        }
+        @stage(compute) @workgroup_size(1)
+        fn compute_main() {
+          var<private> foo: f32 = 0.0;
+          var foo_2: i32 = 10;
+          clamp_0acf8f();
+        }
+        fn vert_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f();
+          var foo_1: i32 = 3;
+          return vec4<f32>();
+        }
+        foo_1 = 5 + 7;
+        var foo_3 : i32 = -20;)";
+
+  std::vector<size_t> function_positions = GetFunctionBodyPositions(wgsl_code);
+  std::vector<size_t> expected_positions = {187, 607};
+  ASSERT_EQ(expected_positions, function_positions);
+}
+
+TEST(TestInsertReturn, TestMissingSemicolon) {
+  std::string wgsl_code =
+      R"(fn clamp_0acf8f() {
+          var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>())
+        }
+        @stage(vertex)
+        fn vertex_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f()
+          var foo_1: i32 = 3
+          return vec4<f32>()
+        }
+        @stage(fragment)
+        fn fragment_main() {
+          clamp_0acf8f();
+        }
+        @stage(compute) @workgroup_size(1)
+        fn compute_main() {
+          var<private> foo: f32 = 0.0;
+          var foo_2: i32 = 10;
+          clamp_0acf8f();
+        }
+        fn vert_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f()
+          var foo_1: i32 = 3
+          return vec4<f32>()
+        }
+        foo_1 = 5 + 7;
+        var foo_3 : i32 = -20;)";
+
+  RandomGenerator generator(0);
+  InsertReturnStatement(wgsl_code, generator);
+
+  // No semicolons found in the function's body, so wgsl_code
+  // should remain unchanged.
+  std::string expected_wgsl_code =
+      R"(fn clamp_0acf8f() {
+          var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>())
+        }
+        @stage(vertex)
+        fn vertex_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f()
+          var foo_1: i32 = 3
+          return vec4<f32>()
+        }
+        @stage(fragment)
+        fn fragment_main() {
+          clamp_0acf8f();
+        }
+        @stage(compute) @workgroup_size(1)
+        fn compute_main() {
+          var<private> foo: f32 = 0.0;
+          var foo_2: i32 = 10;
+          clamp_0acf8f();
+        }
+        fn vert_main() -> @builtin(position) vec4<f32> {
+          clamp_0acf8f()
+          var foo_1: i32 = 3
+          return vec4<f32>()
+        }
+        foo_1 = 5 + 7;
+        var foo_3 : i32 = -20;)";
+  ASSERT_EQ(expected_wgsl_code, wgsl_code);
+}
+
+}  // namespace
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_fuzzer.cc
new file mode 100644
index 0000000..045ecfd
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_fuzzer.cc
@@ -0,0 +1,28 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+void OverrideCliParams(CliParams& /*unused*/) {
+  // Leave the CLI parameters unchanged.
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_hlsl_writer_fuzzer.cc
new file mode 100644
index 0000000..e89dd97
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_hlsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kHlsl;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_msl_writer_fuzzer.cc
new file mode 100644
index 0000000..23afc86
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_msl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kMsl;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_spv_writer_fuzzer.cc
new file mode 100644
index 0000000..18a1a3e
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_spv_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kSpv;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_wgsl_writer_fuzzer.cc
new file mode 100644
index 0000000..8fbd395
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/tint_regex_wgsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_regex_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+void OverrideCliParams(CliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kWgsl;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc
new file mode 100644
index 0000000..b8d9160
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc
@@ -0,0 +1,358 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h"
+
+#include <cassert>
+#include <cstring>
+#include <map>
+#include <regex>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+std::vector<size_t> FindDelimiterIndices(const std::string& delimiter,
+                                         const std::string& wgsl_code) {
+  std::vector<size_t> result;
+  for (size_t pos = wgsl_code.find(delimiter, 0); pos != std::string::npos;
+       pos = wgsl_code.find(delimiter, pos + 1)) {
+    result.push_back(pos);
+  }
+
+  return result;
+}
+
+std::vector<std::pair<size_t, size_t>> GetIdentifiers(
+    const std::string& wgsl_code) {
+  std::vector<std::pair<size_t, size_t>> result;
+
+  // This regular expression works by looking for a character that
+  // is not part of an identifier followed by a WGSL identifier, followed
+  // by a character which cannot be part of a WGSL identifer. The regex
+  // for the WGSL identifier is obtained from:
+  // https://www.w3.org/TR/WGSL/#identifiers.
+  std::regex wgsl_identifier_regex(
+      "[^a-zA-Z]([a-zA-Z][0-9a-zA-Z_]*)[^0-9a-zA-Z_]");
+
+  std::smatch match;
+
+  std::string::const_iterator search_start(wgsl_code.cbegin());
+  std::string prefix;
+
+  while (regex_search(search_start, wgsl_code.cend(), match,
+                      wgsl_identifier_regex) == true) {
+    prefix += match.prefix();
+    result.push_back(std::make_pair(prefix.size() + 1, match.str(1).size()));
+    prefix += match.str(0);
+    search_start = match.suffix().first;
+  }
+  return result;
+}
+
+std::vector<std::pair<size_t, size_t>> GetIntLiterals(const std::string& s) {
+  std::vector<std::pair<size_t, size_t>> result;
+
+  // Looks for integer literals in decimal or hexadecimal form.
+  // Regex obtained here: https://www.w3.org/TR/WGSL/#literals
+  std::regex int_literal_regex("-?0x[0-9a-fA-F]+ | 0 | -?[1-9][0-9]*");
+  std::regex uint_literal_regex("0x[0-9a-fA-F]+u | 0u | [1-9][0-9]*u");
+  std::smatch match;
+
+  std::string::const_iterator search_start(s.cbegin());
+  std::string prefix = "";
+
+  while (regex_search(search_start, s.cend(), match, int_literal_regex) ||
+         regex_search(search_start, s.cend(), match, uint_literal_regex)) {
+    prefix += match.prefix();
+    result.push_back(
+        std::make_pair(prefix.size() + 1, match.str(0).size() - 1));
+    prefix += match.str(0);
+    search_start = match.suffix().first;
+  }
+  return result;
+}
+
+size_t FindClosingBrace(size_t opening_bracket_pos,
+                        const std::string& wgsl_code) {
+  size_t open_bracket_count = 1;
+  size_t pos = opening_bracket_pos + 1;
+  while (open_bracket_count >= 1 && pos < wgsl_code.size()) {
+    if (wgsl_code[pos] == '{') {
+      ++open_bracket_count;
+    } else if (wgsl_code[pos] == '}') {
+      --open_bracket_count;
+    }
+    ++pos;
+  }
+  return (pos == wgsl_code.size() && open_bracket_count >= 1) ? 0 : pos - 1;
+}
+
+std::vector<size_t> GetFunctionBodyPositions(const std::string& wgsl_code) {
+  // Finds all the functions with a non-void return value.
+  std::regex function_regex("fn.*?->.*?\\{");
+  std::smatch match;
+  std::vector<size_t> result;
+
+  auto search_start(wgsl_code.cbegin());
+  std::string prefix = "";
+
+  while (std::regex_search(search_start, wgsl_code.cend(), match,
+                           function_regex)) {
+    result.push_back(
+        static_cast<size_t>(match.suffix().first - wgsl_code.cbegin() - 1L));
+    search_start = match.suffix().first;
+  }
+  return result;
+}
+
+bool InsertReturnStatement(std::string& wgsl_code, RandomGenerator& generator) {
+  std::vector<size_t> function_body_positions =
+      GetFunctionBodyPositions(wgsl_code);
+
+  // No function was found in wgsl_code.
+  if (function_body_positions.empty()) {
+    return false;
+  }
+
+  // Pick a random function's opening bracket, find the corresponding closing
+  // bracket, and find a semi-colon within the function body.
+  size_t left_bracket_pos = generator.GetRandomElement(function_body_positions);
+
+  size_t right_bracket_pos = FindClosingBrace(left_bracket_pos, wgsl_code);
+
+  if (right_bracket_pos == 0) {
+    return false;
+  }
+
+  std::vector<size_t> semicolon_positions;
+  for (size_t pos = wgsl_code.find(";", left_bracket_pos + 1);
+       pos < right_bracket_pos; pos = wgsl_code.find(";", pos + 1)) {
+    semicolon_positions.push_back(pos);
+  }
+
+  if (semicolon_positions.empty()) {
+    return false;
+  }
+
+  size_t semicolon_position = generator.GetRandomElement(semicolon_positions);
+
+  // Get all identifiers and integer literals to use as potential return values.
+  std::vector<std::pair<size_t, size_t>> identifiers =
+      GetIdentifiers(wgsl_code);
+  auto return_values = identifiers;
+  std::vector<std::pair<size_t, size_t>> int_literals =
+      GetIntLiterals(wgsl_code);
+  return_values.insert(return_values.end(), int_literals.begin(),
+                       int_literals.end());
+  std::pair<size_t, size_t> return_value =
+      generator.GetRandomElement(return_values);
+  std::string return_statement =
+      "return " + wgsl_code.substr(return_value.first, return_value.second) +
+      ";";
+
+  // Insert the return statement immediately after the semicolon.
+  wgsl_code.insert(semicolon_position + 1, return_statement);
+  return true;
+}
+
+void SwapIntervals(size_t idx1,
+                   size_t reg1_len,
+                   size_t idx2,
+                   size_t reg2_len,
+                   std::string& wgsl_code) {
+  std::string region_1 = wgsl_code.substr(idx1 + 1, reg1_len - 1);
+
+  std::string region_2 = wgsl_code.substr(idx2 + 1, reg2_len - 1);
+
+  // The second transformation is done first as it doesn't affect idx2.
+  wgsl_code.replace(idx2 + 1, region_2.size(), region_1);
+
+  wgsl_code.replace(idx1 + 1, region_1.size(), region_2);
+}
+
+void DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code) {
+  wgsl_code.erase(idx1 + 1, reg_len - 1);
+}
+
+void DuplicateInterval(size_t idx1,
+                       size_t reg1_len,
+                       size_t idx2,
+                       std::string& wgsl_code) {
+  std::string region = wgsl_code.substr(idx1 + 1, reg1_len - 1);
+  wgsl_code.insert(idx2 + 1, region);
+}
+
+void ReplaceRegion(size_t idx1,
+                   size_t id1_len,
+                   size_t idx2,
+                   size_t id2_len,
+                   std::string& wgsl_code) {
+  std::string region_1 = wgsl_code.substr(idx1, id1_len);
+  std::string region_2 = wgsl_code.substr(idx2, id2_len);
+  wgsl_code.replace(idx2, region_2.size(), region_1);
+}
+
+void ReplaceInterval(size_t start_index,
+                     size_t length,
+                     std::string replacement_text,
+                     std::string& wgsl_code) {
+  std::string region_1 = wgsl_code.substr(start_index, length);
+  wgsl_code.replace(start_index, length, replacement_text);
+}
+
+bool SwapRandomIntervals(const std::string& delimiter,
+                         std::string& wgsl_code,
+                         RandomGenerator& generator) {
+  std::vector<size_t> delimiter_positions =
+      FindDelimiterIndices(delimiter, wgsl_code);
+
+  // Need to have at least 3 indices.
+  if (delimiter_positions.size() < 3) {
+    return false;
+  }
+
+  // Choose indices:
+  //   interval_1_start < interval_1_end <= interval_2_start < interval_2_end
+  uint32_t interval_1_start = generator.GetUInt32(
+      static_cast<uint32_t>(delimiter_positions.size()) - 2u);
+  uint32_t interval_1_end = generator.GetUInt32(
+      interval_1_start + 1u,
+      static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+  uint32_t interval_2_start = generator.GetUInt32(
+      interval_1_end, static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+  uint32_t interval_2_end = generator.GetUInt32(
+      interval_2_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
+
+  SwapIntervals(delimiter_positions[interval_1_start],
+                delimiter_positions[interval_1_end] -
+                    delimiter_positions[interval_1_start],
+                delimiter_positions[interval_2_start],
+                delimiter_positions[interval_2_end] -
+                    delimiter_positions[interval_2_start],
+                wgsl_code);
+
+  return true;
+}
+
+bool DeleteRandomInterval(const std::string& delimiter,
+                          std::string& wgsl_code,
+                          RandomGenerator& generator) {
+  std::vector<size_t> delimiter_positions =
+      FindDelimiterIndices(delimiter, wgsl_code);
+
+  // Need to have at least 2 indices.
+  if (delimiter_positions.size() < 2) {
+    return false;
+  }
+
+  uint32_t interval_start = generator.GetUInt32(
+      static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+  uint32_t interval_end = generator.GetUInt32(
+      interval_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
+
+  DeleteInterval(
+      delimiter_positions[interval_start],
+      delimiter_positions[interval_end] - delimiter_positions[interval_start],
+      wgsl_code);
+
+  return true;
+}
+
+bool DuplicateRandomInterval(const std::string& delimiter,
+                             std::string& wgsl_code,
+                             RandomGenerator& generator) {
+  std::vector<size_t> delimiter_positions =
+      FindDelimiterIndices(delimiter, wgsl_code);
+
+  // Need to have at least 2 indices
+  if (delimiter_positions.size() < 2) {
+    return false;
+  }
+
+  uint32_t interval_start = generator.GetUInt32(
+      static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+  uint32_t interval_end = generator.GetUInt32(
+      interval_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
+  uint32_t duplication_point =
+      generator.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()));
+
+  DuplicateInterval(
+      delimiter_positions[interval_start],
+      delimiter_positions[interval_end] - delimiter_positions[interval_start],
+      delimiter_positions[duplication_point], wgsl_code);
+
+  return true;
+}
+
+bool ReplaceRandomIdentifier(std::string& wgsl_code,
+                             RandomGenerator& generator) {
+  std::vector<std::pair<size_t, size_t>> identifiers =
+      GetIdentifiers(wgsl_code);
+
+  // Need at least 2 identifiers
+  if (identifiers.size() < 2) {
+    return false;
+  }
+
+  uint32_t id1_index =
+      generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+  uint32_t id2_index =
+      generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+
+  // The two identifiers must be different
+  while (id1_index == id2_index) {
+    id2_index = generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+  }
+
+  ReplaceRegion(identifiers[id1_index].first, identifiers[id1_index].second,
+                identifiers[id2_index].first, identifiers[id2_index].second,
+                wgsl_code);
+
+  return true;
+}
+
+bool ReplaceRandomIntLiteral(std::string& wgsl_code,
+                             RandomGenerator& generator) {
+  std::vector<std::pair<size_t, size_t>> literals = GetIntLiterals(wgsl_code);
+
+  // Need at least one integer literal
+  if (literals.size() < 1) {
+    return false;
+  }
+
+  uint32_t literal_index =
+      generator.GetUInt32(static_cast<uint32_t>(literals.size()));
+
+  // INT_MAX = 2147483647, INT_MIN = -2147483648
+  std::vector<std::string> boundary_values = {
+      "2147483647", "-2147483648", "1", "-1", "0", "4294967295"};
+
+  uint32_t boundary_index =
+      generator.GetUInt32(static_cast<uint32_t>(boundary_values.size()));
+
+  ReplaceInterval(literals[literal_index].first, literals[literal_index].second,
+                  boundary_values[boundary_index], wgsl_code);
+
+  return true;
+}
+
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h
new file mode 100644
index 0000000..47e6adb
--- /dev/null
+++ b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h
@@ -0,0 +1,186 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace regex_fuzzer {
+
+/// A function that given a delimiter, returns a vector that contains
+/// all the positions of the delimiter in the WGSL code.
+/// @param delimiter - the delimiter of the enclosed region.
+/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+/// @return a vector with the positions of the delimiter in the WGSL code.
+std::vector<size_t> FindDelimiterIndices(const std::string& delimiter,
+                                         const std::string& wgsl_code);
+
+/// A function that finds all the identifiers in a WGSL-like string.
+/// @param wgsl_code - the WGSL-like string where the identifiers will be found.
+/// @return a vector with the positions and the length of all the
+/// identifiers in wgsl_code.
+std::vector<std::pair<size_t, size_t>> GetIdentifiers(
+    const std::string& wgsl_code);
+
+/// A function that returns returns the starting position
+/// and the length of all the integer literals in a WGSL-like string.
+/// @param wgsl_code - the WGSL-like string where the int literals
+/// will be found.
+/// @return a vector with the starting positions and the length
+/// of all the integer literals.
+std::vector<std::pair<size_t, size_t>> GetIntLiterals(
+    const std::string& wgsl_code);
+
+/// Finds a possible closing brace corresponding to the opening
+/// brace at position opening_bracket_pos.
+/// @param opening_bracket_pos - the position of the opening brace.
+/// @param wgsl_code - the WGSL-like string where the closing brace.
+/// @return the position of the closing bracket or 0 if there is no closing
+/// brace.
+size_t FindClosingBrace(size_t opening_bracket_pos,
+                        const std::string& wgsl_code);
+
+/// Returns the starting_position of the bodies of the functions
+/// that follow the regular expression: fn.*?->.*?\\{, which searches for the
+/// keyword fn followed by the function name, its return type and opening brace.
+/// @param wgsl_code - the WGSL-like string where the functions will be
+/// searched.
+/// @return a vector with the starting position of the function bodies in
+/// wgsl_code.
+std::vector<size_t> GetFunctionBodyPositions(const std::string& wgsl_code);
+
+/// Given 4 indices, idx1, idx2, idx3 and idx4 it swaps the regions
+/// in the interval (idx1, idx2] with the region in the interval (idx3, idx4]
+/// in wgsl_text.
+/// @param idx1 - starting index of the first region.
+/// @param reg1_len - length of the first region.
+/// @param idx2 - starting index of the second region.
+/// @param reg2_len - length of the second region.
+/// @param wgsl_code - the string where the swap will occur.
+void SwapIntervals(size_t idx1,
+                   size_t reg1_len,
+                   size_t idx2,
+                   size_t reg2_len,
+                   std::string& wgsl_code);
+
+/// Given index idx1 it delets the region of length interval_len
+/// starting at index idx1;
+/// @param idx1 - starting index of the first region.
+/// @param reg_len - terminating index of the second region.
+/// @param wgsl_code - the string where the swap will occur.
+void DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code);
+
+/// Given 2 indices, idx1, idx2, it inserts the region of length
+/// reg1_len starting at idx1 after idx2.
+/// @param idx1 - starting index of region.
+/// @param reg1_len - length of the region.
+/// @param idx2 - the position where the region will be inserted.
+/// @param wgsl_code - the string where the swap will occur.
+void DuplicateInterval(size_t idx1,
+                       size_t reg1_len,
+                       size_t idx2,
+                       std::string& wgsl_code);
+
+/// Replaces a region of a WGSL-like string of length id2_len starting
+/// at position idx2 with a region of length id1_len starting at
+/// position idx1.
+/// @param idx1 - starting position of the first region.
+/// @param id1_len - length of the first region.
+/// @param idx2 - starting position of the second region.
+/// @param id2_len - length of the second region.
+/// @param wgsl_code - the string where the replacement will occur.
+void ReplaceRegion(size_t idx1,
+                   size_t id1_len,
+                   size_t idx2,
+                   size_t id2_len,
+                   std::string& wgsl_code);
+
+/// Replaces an interval of length `length` starting at start_index
+/// with the `replacement_text`.
+/// @param start_index - starting position of the interval to be replaced.
+/// @param length - length of the interval to be replaced.
+/// @param replacement_text - the interval that will be used as a replacement.
+/// @param wgsl_code - the WGSL-like string where the replacement will occur.
+void ReplaceInterval(size_t start_index,
+                     size_t length,
+                     std::string replacement_text,
+                     std::string& wgsl_code);
+
+/// A function that, given WGSL-like string and a delimiter,
+/// generates another WGSL-like string by picking two random regions
+/// enclosed by the delimiter and swapping them.
+/// @param delimiter - the delimiter that will be used to find enclosed regions.
+/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+/// @param generator - the random number generator.
+/// @return true if a swap happened or false otherwise.
+bool SwapRandomIntervals(const std::string& delimiter,
+                         std::string& wgsl_code,
+                         RandomGenerator& generator);
+
+/// A function that, given a WGSL-like string and a delimiter,
+/// generates another WGSL-like string by deleting a random
+/// region enclosed by the delimiter.
+/// @param delimiter - the delimiter that will be used to find enclosed regions.
+/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+/// @param generator - the random number generator.
+/// @return true if a deletion happened or false otherwise.
+bool DeleteRandomInterval(const std::string& delimiter,
+                          std::string& wgsl_code,
+                          RandomGenerator& generator);
+
+/// A function that, given a WGSL-like string and a delimiter,
+/// generates another WGSL-like string by duplicating a random
+/// region enclosed by the delimiter.
+/// @param delimiter - the delimiter that will be used to find enclosed regions.
+/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+/// @param generator - the random number generator.
+/// @return true if a duplication happened or false otherwise.
+bool DuplicateRandomInterval(const std::string& delimiter,
+                             std::string& wgsl_code,
+                             RandomGenerator& generator);
+
+/// Replaces a randomly-chosen identifier in wgsl_code.
+/// @param wgsl_code - WGSL-like string where the replacement will occur.
+/// @param generator - the random number generator.
+/// @return true if a replacement happened or false otherwise.
+bool ReplaceRandomIdentifier(std::string& wgsl_code,
+                             RandomGenerator& generator);
+
+/// Replaces the value of a randomly-chosen integer with one of
+/// the values in the set {INT_MAX, INT_MIN, 0, -1}.
+/// @param wgsl_code - WGSL-like string where the replacement will occur.
+/// @param generator - the random number generator.
+/// @return true if a replacement happened or false otherwise.
+bool ReplaceRandomIntLiteral(std::string& wgsl_code,
+                             RandomGenerator& generator);
+
+/// Inserts a return statement in a randomly chosen function of a
+/// WGSL-like string. The return value is a randomly-chosen identifier
+/// or literal in the string.
+/// @param wgsl_code - WGSL-like string that will be mutated.
+/// @param generator - the random number generator.
+/// @return true if the mutation was succesful or false otherwise.
+bool InsertReturnStatement(std::string& wgsl_code, RandomGenerator& generator);
+}  // namespace regex_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_renamer_fuzzer.cc b/src/tint/fuzzers/tint_renamer_fuzzer.cc
new file mode 100644
index 0000000..02e539c
--- /dev/null
+++ b/src/tint/fuzzers/tint_renamer_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<transform::Renamer>();
+
+  fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_robustness_fuzzer.cc b/src/tint/fuzzers/tint_robustness_fuzzer.cc
new file mode 100644
index 0000000..22f0ad1
--- /dev/null
+++ b/src/tint/fuzzers/tint_robustness_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<tint::transform::Robustness>();
+
+  tint::fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_single_entry_point_fuzzer.cc b/src/tint/fuzzers/tint_single_entry_point_fuzzer.cc
new file mode 100644
index 0000000..84146d8
--- /dev/null
+++ b/src/tint/fuzzers/tint_single_entry_point_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<transform::SingleEntryPoint>();
+
+  fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
new file mode 100644
index 0000000..c83333a
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/CMakeLists.txt
@@ -0,0 +1,106 @@
+# Copyright 2021 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(FUZZER_SOURCES
+        ../mersenne_twister_engine.cc
+        ../random_generator.cc
+        ../random_generator_engine.cc
+        cli.cc
+        fuzzer.cc
+        mutator.cc
+        mutator_cache.cc
+        spirv_fuzz_mutator.cc
+        spirv_opt_mutator.cc
+        spirv_reduce_mutator.cc
+        util.cc)
+
+set(FUZZER_SOURCES ${FUZZER_SOURCES}
+        ../mersenne_twister_engine.h
+        ../random_generator.h
+        ../random_generator_engine.h
+        cli.h
+        mutator.h
+        mutator_cache.h
+        override_cli_params.h
+        spirv_fuzz_mutator.h
+        spirv_opt_mutator.h
+        spirv_reduce_mutator.h
+        util.h)
+
+set(FUZZER_SOURCES ${FUZZER_SOURCES}
+        ../tint_common_fuzzer.cc
+        ../tint_common_fuzzer.h)
+
+function(configure_spirv_tools_fuzzer_target NAME SOURCES)
+    add_executable(${NAME} ${SOURCES})
+    target_link_libraries(${NAME} SPIRV-Tools SPIRV-Tools-opt SPIRV-Tools-fuzz SPIRV-Tools-reduce)
+    tint_default_compile_options(${NAME})
+    target_compile_options(${NAME} PRIVATE
+            -Wno-missing-prototypes
+            -Wno-zero-as-null-pointer-constant
+            -Wno-reserved-id-macro
+            -Wno-sign-conversion
+            -Wno-extra-semi-stmt
+            -Wno-inconsistent-missing-destructor-override
+            -Wno-newline-eof
+            -Wno-old-style-cast
+            -Wno-weak-vtables
+            -Wno-undef)
+    target_include_directories(${NAME} PRIVATE
+            ${spirv-tools_SOURCE_DIR}
+            ${spirv-tools_BINARY_DIR})
+endfunction()
+
+function(add_tint_spirv_tools_fuzzer NAME)
+  set(FUZZER_TARGET_SOURCES ${NAME}.cc ${FUZZER_SOURCES})
+  configure_spirv_tools_fuzzer_target(${NAME} "${FUZZER_TARGET_SOURCES}")
+  target_link_libraries(${NAME} libtint-fuzz)
+  target_compile_definitions(tint_spirv_tools_fuzzer PUBLIC CUSTOM_MUTATOR)
+  target_compile_definitions(tint_spirv_tools_fuzzer PRIVATE TARGET_FUZZER)
+endfunction()
+
+# Add libfuzzer targets.
+# Targets back-ends according to command line arguments.
+add_tint_spirv_tools_fuzzer(tint_spirv_tools_fuzzer)
+# Targets back-ends individually.
+add_tint_spirv_tools_fuzzer(tint_spirv_tools_hlsl_writer_fuzzer)
+add_tint_spirv_tools_fuzzer(tint_spirv_tools_msl_writer_fuzzer)
+add_tint_spirv_tools_fuzzer(tint_spirv_tools_spv_writer_fuzzer)
+add_tint_spirv_tools_fuzzer(tint_spirv_tools_wgsl_writer_fuzzer)
+
+set(DEBUGGER_SOURCES
+        ../mersenne_twister_engine.cc
+        ../random_generator.cc
+        ../random_generator_engine.cc
+        cli.cc
+        mutator.cc
+        mutator_debugger.cc
+        spirv_fuzz_mutator.cc
+        spirv_opt_mutator.cc
+        spirv_reduce_mutator.cc
+        util.cc)
+
+set(DEBUGGER_SOURCES ${DEBUGGER_SOURCES}
+        ../mersenne_twister_engine.h
+        ../random_generator.h
+        ../random_generator_engine.h
+        cli.h
+        mutator.h
+        spirv_fuzz_mutator.h
+        spirv_opt_mutator.h
+        spirv_reduce_mutator.h
+        util.h)
+
+configure_spirv_tools_fuzzer_target(tint_spirv_tools_mutator_debugger "${DEBUGGER_SOURCES}")
+target_compile_definitions(tint_spirv_tools_mutator_debugger PRIVATE TARGET_DEBUGGER)
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc
new file mode 100644
index 0000000..7659fcc
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.cc
@@ -0,0 +1,484 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+
+#include <fstream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "source/opt/build_module.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace {
+
+const char* const kMutatorParameters = R"(
+Mutators' parameters:
+
+  -tint_donors=
+                       A path to the text file with a list of paths to the
+                       SPIR-V donor files. Check out the doc for the spirv-fuzz
+                       to learn more about donor binaries. Donors are not used
+                       by default.
+
+  -tint_enable_all_fuzzer_passes=
+                       Whether to use all fuzzer passes or a randomly selected subset
+                       of them. This must be one of `true` or `false` (without `).
+                       By default it's `false`.
+
+  -tint_enable_all_reduce_passes=
+                       Whether to use all reduction passes or a randomly selected subset
+                       of them. This must be one of `true` or `false` (without `).
+                       By default it's `false`.
+
+  -tint_opt_batch_size=
+                       The maximum number of spirv-opt optimizations that
+                       will be applied in a single mutation session (i.e.
+                       a call to LLVMFuzzerCustomMutator). This must fit in
+                       uint32_t. By default it's 6.
+
+  -tint_reduction_batch_size=
+                       The maximum number of spirv-reduce reductions that
+                       will be applied in a single mutation session (i.e.
+                       a call to LLVMFuzzerCustomMutator). This must fit in
+                       uint32_t. By default it's 3.
+
+  -tint_repeated_pass_strategy=
+                       The strategy that will be used to recommend the next fuzzer
+                       pass. This must be one of `simple`, `looped` or `random`
+                       (without `). By default it's `simple`. Check out the doc for
+                       spirv-fuzz to learn more.
+
+  -tint_transformation_batch_size=
+                       The maximum number of spirv-fuzz transformations
+                       that will be applied during a single mutation
+                       session (i.e. a call to LLVMFuzzerCustomMutator).
+                       This must fit in uint32_t. By default it's 3.
+
+  -tint_validate_after_each_fuzzer_pass=
+                       Whether to validate SPIR-V binary after each fuzzer pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+
+  -tint_validate_after_each_opt_pass=
+                       Whether to validate SPIR-V binary after each optimization pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+
+  -tint_validate_after_each_reduce_pass=
+                       Whether to validate SPIR-V binary after each reduction pass.
+                       This must be one of `true` or `false` (without `).
+                       By default it's `true`. Switch this to `false` if you experience
+                       bad performance.
+)";
+
+const char* const kFuzzerHelpMessage = R"(
+This fuzzer uses SPIR-V binaries to fuzz the Tint compiler. It uses SPIRV-Tools
+to mutate those binaries. The fuzzer works on a corpus of SPIR-V shaders.
+For each shader from the corpus it uses one of `spirv-fuzz`, `spirv-reduce` or
+`spirv-opt` to mutate it and then runs the shader through the Tint compiler in
+two steps:
+- Converts the mutated shader to WGSL.
+- Converts WGSL to some target language specified in the CLI arguments.
+
+Below is a list of all supported parameters for this fuzzer. You may want to
+run it with -help=1 to check out libfuzzer parameters.
+
+Fuzzer parameters:
+
+  -tint_error_dir
+                       The directory that will be used to output invalid SPIR-V
+                       binaries to. This is especially useful during debugging
+                       mutators. The directory must have the following subdirectories:
+                       - spv/ - will be used to output errors, produced during
+                         the conversion from the SPIR-V to WGSL.
+                       - wgsl/ - will be used to output errors, produced during
+                         the conversion from the WGSL to `--fuzzing_target`.
+                       - mutator/ - will be used to output errors, produced by
+                         the mutators.
+                       By default invalid files are not printed out.
+
+  -tint_fuzzing_target
+                       The type of backend to target during fuzzing. This must
+                       be one or a combination of `wgsl`, `spv`, `msl` or `hlsl`
+                       (without `) separated by commas. By default it's
+                       `wgsl,spv,msl,hlsl`.
+
+  -tint_help
+                       Show this message. Note that there is also a -help=1
+                       parameter that will display libfuzzer's help message.
+
+  -tint_mutator_cache_size=
+                       The maximum size of the cache that stores
+                       mutation sessions. This must fit in uint32_t.
+                       By default it's 20.
+
+  -tint_mutator_type=
+                       Determines types of the mutators to run. This must be one or
+                       a combination of `fuzz`, `opt`, `reduce` (without `) separated by
+                       comma. If a combination is specified, each element in the
+                       combination will have an equal chance of mutating a SPIR-V
+                       binary during a mutation session (i.e. if no mutator exists
+                       for that binary in the mutator cache). By default, the
+                       parameter's value is `fuzz,opt,reduce`.
+)";
+
+const char* const kMutatorDebuggerHelpMessage = R"(
+This tool is used to debug *mutators*. It uses CLI arguments similar to the
+ones used by the fuzzer. To debug some mutator you just need to specify the
+mutator type, the seed and the path to the SPIR-V binary that triggered the
+error. This tool will run the mutator on the binary until the error is
+produced or the mutator returns `kLimitReached`.
+
+Note that this is different from debugging the fuzzer by specifying input
+files to test. The difference is that the latter will not execute any
+mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
+tool is useful when one of the SPIRV-Tools mutators crashes or produces an
+invalid binary in LLVMFuzzerCustomMutator.
+
+Debugger parameters:
+
+  --help
+                       Show this message.
+
+  --mutator_type=
+                       Determines the type of the mutator to debug. This must be
+                       one of `fuzz`, `reduce` or `opt` (without `). This parameter
+                       is REQUIRED.
+
+  --original_binary=
+                       The path to the SPIR-V binary that the faulty mutator was
+                       initialized with. This will be dumped on errors by the fuzzer
+                       if `--error_dir` is specified. This parameter is REQUIRED.
+
+  --seed=
+                       The seed for the random number generator that was used to
+                       initialize the mutator. This value is usually printed to
+                       the console when the mutator produces an invalid binary.
+                       It is also dumped into the log file if `--error_dir` is
+                       specified. This must fit in uint32_t. This parameter is
+                       REQUIRED.
+)";
+
+void PrintHelpMessage(const char* help_message) {
+  std::cout << help_message << std::endl << kMutatorParameters << std::endl;
+}
+
+[[noreturn]] void InvalidParameter(const char* help_message,
+                                   const char* param) {
+  std::cout << "Invalid value for " << param << std::endl;
+  PrintHelpMessage(help_message);
+  exit(1);
+}
+
+bool ParseUint32(const char* param, uint32_t* out) {
+  uint64_t value = static_cast<uint64_t>(strtoul(param, nullptr, 10));
+  if (value > static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
+    return false;
+  }
+  *out = static_cast<uint32_t>(value);
+  return true;
+}
+
+std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> ParseDonors(
+    const char* file_name) {
+  std::ifstream fin(file_name);
+  if (!fin) {
+    std::cout << "Can't open donors list file: " << file_name << std::endl;
+    exit(1);
+  }
+
+  std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> result;
+  for (std::string donor_file_name; fin >> donor_file_name;) {
+    if (!std::ifstream(donor_file_name)) {
+      std::cout << "Can't open donor file: " << donor_file_name << std::endl;
+      exit(1);
+    }
+
+    result.emplace_back([donor_file_name] {
+      std::vector<uint32_t> binary;
+      if (!util::ReadBinary(donor_file_name, &binary)) {
+        std::cout << "Failed to read donor from: " << donor_file_name
+                  << std::endl;
+        exit(1);
+      }
+      return spvtools::BuildModule(
+          kDefaultTargetEnv, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+          binary.data(), binary.size());
+    });
+  }
+
+  return result;
+}
+
+bool ParseRepeatedPassStrategy(const char* param,
+                               spvtools::fuzz::RepeatedPassStrategy* out) {
+  if (!strcmp(param, "simple")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kSimple;
+  } else if (!strcmp(param, "looped")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kLoopedWithRecommendations;
+  } else if (!strcmp(param, "random")) {
+    *out = spvtools::fuzz::RepeatedPassStrategy::kRandomWithRecommendations;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseBool(const char* param, bool* out) {
+  if (!strcmp(param, "true")) {
+    *out = true;
+  } else if (!strcmp(param, "false")) {
+    *out = false;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseMutatorType(const char* param, MutatorType* out) {
+  if (!strcmp(param, "fuzz")) {
+    *out = MutatorType::kFuzz;
+  } else if (!strcmp(param, "opt")) {
+    *out = MutatorType::kOpt;
+  } else if (!strcmp(param, "reduce")) {
+    *out = MutatorType::kReduce;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool ParseFuzzingTarget(const char* param, FuzzingTarget* out) {
+  if (!strcmp(param, "wgsl")) {
+    *out = FuzzingTarget::kWgsl;
+  } else if (!strcmp(param, "spv")) {
+    *out = FuzzingTarget::kSpv;
+  } else if (!strcmp(param, "msl")) {
+    *out = FuzzingTarget::kMsl;
+  } else if (!strcmp(param, "hlsl")) {
+    *out = FuzzingTarget::kHlsl;
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool HasPrefix(const char* str, const char* prefix) {
+  return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+bool ParseMutatorCliParam(const char* param,
+                          const char* help_message,
+                          MutatorCliParams* out) {
+  if (HasPrefix(param, "-tint_transformation_batch_size=")) {
+    if (!ParseUint32(param + sizeof("-tint_transformation_batch_size=") - 1,
+                     &out->transformation_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_reduction_batch_size=")) {
+    if (!ParseUint32(param + sizeof("-tint_reduction_batch_size=") - 1,
+                     &out->reduction_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_opt_batch_size=")) {
+    if (!ParseUint32(param + sizeof("-tint_opt_batch_size=") - 1,
+                     &out->opt_batch_size)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_donors=")) {
+    out->donors = ParseDonors(param + sizeof("-tint_donors=") - 1);
+  } else if (HasPrefix(param, "-tint_repeated_pass_strategy=")) {
+    if (!ParseRepeatedPassStrategy(
+            param + sizeof("-tint_repeated_pass_strategy=") - 1,
+            &out->repeated_pass_strategy)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_enable_all_fuzzer_passes=")) {
+    if (!ParseBool(param + sizeof("-tint_enable_all_fuzzer_passes=") - 1,
+                   &out->enable_all_fuzzer_passes)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_enable_all_reduce_passes=")) {
+    if (!ParseBool(param + sizeof("-tint_enable_all_reduce_passes=") - 1,
+                   &out->enable_all_reduce_passes)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_validate_after_each_opt_pass=")) {
+    if (!ParseBool(param + sizeof("-tint_validate_after_each_opt_pass=") - 1,
+                   &out->validate_after_each_opt_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_validate_after_each_fuzzer_pass=")) {
+    if (!ParseBool(param + sizeof("-tint_validate_after_each_fuzzer_pass=") - 1,
+                   &out->validate_after_each_fuzzer_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  } else if (HasPrefix(param, "-tint_validate_after_each_reduce_pass=")) {
+    if (!ParseBool(param + sizeof("-tint_validate_after_each_reduce_pass=") - 1,
+                   &out->validate_after_each_reduce_pass)) {
+      InvalidParameter(help_message, param);
+    }
+  } else {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace
+
+FuzzerCliParams ParseFuzzerCliParams(int* argc, char** argv) {
+  FuzzerCliParams cli_params;
+  const auto* help_message = kFuzzerHelpMessage;
+  auto help = false;
+
+  for (int i = *argc - 1; i > 0; --i) {
+    auto param = argv[i];
+    auto recognized_param = true;
+
+    if (HasPrefix(param, "-tint_mutator_cache_size=")) {
+      if (!ParseUint32(param + sizeof("-tint_mutator_cache_size=") - 1,
+                       &cli_params.mutator_cache_size)) {
+        InvalidParameter(help_message, param);
+      }
+    } else if (HasPrefix(param, "-tint_mutator_type=")) {
+      auto result = MutatorType::kNone;
+
+      std::stringstream ss(param + sizeof("-tint_mutator_type=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto out = MutatorType::kNone;
+        if (!ParseMutatorType(value.c_str(), &out)) {
+          InvalidParameter(help_message, param);
+        }
+        result = result | out;
+      }
+
+      if (result == MutatorType::kNone) {
+        InvalidParameter(help_message, param);
+      }
+
+      cli_params.mutator_type = result;
+    } else if (HasPrefix(param, "-tint_fuzzing_target=")) {
+      auto result = FuzzingTarget::kNone;
+
+      std::stringstream ss(param + sizeof("-tint_fuzzing_target=") - 1);
+      for (std::string value; std::getline(ss, value, ',');) {
+        auto tmp = FuzzingTarget::kNone;
+        if (!ParseFuzzingTarget(value.c_str(), &tmp)) {
+          InvalidParameter(help_message, param);
+        }
+        result = result | tmp;
+      }
+
+      if (result == FuzzingTarget::kNone) {
+        InvalidParameter(help_message, param);
+      }
+
+      cli_params.fuzzing_target = result;
+    } else if (HasPrefix(param, "-tint_error_dir=")) {
+      cli_params.error_dir = param + sizeof("-tint_error_dir=") - 1;
+    } else if (!strcmp(param, "-tint_help")) {
+      help = true;
+    } else {
+      recognized_param =
+          ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
+    }
+
+    if (recognized_param) {
+      // Remove the recognized parameter from the list of all parameters by
+      // swapping it with the last one. This will suppress warnings in the
+      // libFuzzer about unrecognized parameters. By default, libFuzzer thinks
+      // that all user-defined parameters start with two dashes. However, we are
+      // forced to use a single one to make the fuzzer compatible with the
+      // ClusterFuzz.
+      std::swap(argv[i], argv[*argc - 1]);
+      *argc -= 1;
+    }
+  }
+
+  if (help) {
+    PrintHelpMessage(help_message);
+    exit(0);
+  }
+
+  return cli_params;
+}
+
+MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(
+    int argc,
+    const char* const* argv) {
+  MutatorDebuggerCliParams cli_params;
+  bool seed_param_present = false;
+  bool original_binary_param_present = false;
+  bool mutator_type_param_present = false;
+  const auto* help_message = kMutatorDebuggerHelpMessage;
+  auto help = false;
+
+  for (int i = 0; i < argc; ++i) {
+    auto param = argv[i];
+    ParseMutatorCliParam(param, help_message, &cli_params.mutator_params);
+
+    if (HasPrefix(param, "--mutator_type=")) {
+      if (!ParseMutatorType(param + sizeof("--mutator_type=") - 1,
+                            &cli_params.mutator_type)) {
+        InvalidParameter(help_message, param);
+      }
+      mutator_type_param_present = true;
+    } else if (HasPrefix(param, "--original_binary=")) {
+      if (!util::ReadBinary(param + sizeof("--original_binary=") - 1,
+                            &cli_params.original_binary)) {
+        InvalidParameter(help_message, param);
+      }
+      original_binary_param_present = true;
+    } else if (HasPrefix(param, "--seed=")) {
+      if (!ParseUint32(param + sizeof("--seed=") - 1, &cli_params.seed)) {
+        InvalidParameter(help_message, param);
+      }
+      seed_param_present = true;
+    } else if (!strcmp(param, "--help")) {
+      help = true;
+    }
+  }
+
+  if (help) {
+    PrintHelpMessage(help_message);
+    exit(0);
+  }
+
+  std::pair<bool, const char*> required_params[] = {
+      {seed_param_present, "--seed"},
+      {original_binary_param_present, "--original_binary"},
+      {mutator_type_param_present, "--mutator_type"}};
+
+  for (auto required_param : required_params) {
+    if (!required_param.first) {
+      std::cout << required_param.second << " is missing" << std::endl;
+      exit(1);
+    }
+  }
+
+  return cli_params;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h
new file mode 100644
index 0000000..6e6d60b
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h
@@ -0,0 +1,167 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
+
+#include <string>
+#include <vector>
+
+#include "source/fuzz/fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Default SPIR-V environment that will be used during fuzzing.
+const auto kDefaultTargetEnv = SPV_ENV_VULKAN_1_1;
+
+/// The type of the mutator to run.
+enum class MutatorType {
+  kNone = 0,
+  kFuzz = 1 << 0,
+  kReduce = 1 << 1,
+  kOpt = 1 << 2,
+  kAll = kFuzz | kReduce | kOpt
+};
+
+inline MutatorType operator|(MutatorType a, MutatorType b) {
+  return static_cast<MutatorType>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline MutatorType operator&(MutatorType a, MutatorType b) {
+  return static_cast<MutatorType>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// Shading language to target during fuzzing.
+enum class FuzzingTarget {
+  kNone = 0,
+  kHlsl = 1 << 0,
+  kMsl = 1 << 1,
+  kSpv = 1 << 2,
+  kWgsl = 1 << 3,
+  kAll = kHlsl | kMsl | kSpv | kWgsl
+};
+
+inline FuzzingTarget operator|(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+inline FuzzingTarget operator&(FuzzingTarget a, FuzzingTarget b) {
+  return static_cast<FuzzingTarget>(static_cast<int>(a) & static_cast<int>(b));
+}
+
+/// These parameters are accepted by various mutators and thus they are accepted
+/// by both the fuzzer and the mutator debugger.
+struct MutatorCliParams {
+  /// SPIR-V target environment for fuzzing.
+  spv_target_env target_env = kDefaultTargetEnv;
+
+  /// The number of spirv-fuzz transformations to apply at a time.
+  uint32_t transformation_batch_size = 3;
+
+  /// The number of spirv-reduce reductions to apply at a time.
+  uint32_t reduction_batch_size = 3;
+
+  /// The number of spirv-opt optimizations to apply at a time.
+  uint32_t opt_batch_size = 6;
+
+  /// The vector of donors to use in spirv-fuzz (see the doc for spirv-fuzz to
+  /// learn more).
+  std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier> donors = {};
+
+  /// The strategy to use during fuzzing in spirv-fuzz (see the doc for
+  /// spirv-fuzz to learn more).
+  spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy =
+      spvtools::fuzz::RepeatedPassStrategy::kSimple;
+
+  /// Whether to use all fuzzer passes or a randomly selected subset of them.
+  bool enable_all_fuzzer_passes = false;
+
+  /// Whether to use all reduction passes or a randomly selected subset of them.
+  bool enable_all_reduce_passes = false;
+
+  /// Whether to validate the SPIR-V binary after each optimization pass.
+  bool validate_after_each_opt_pass = true;
+
+  /// Whether to validate the SPIR-V binary after each fuzzer pass.
+  bool validate_after_each_fuzzer_pass = true;
+
+  /// Whether to validate the SPIR-V binary after each reduction pass.
+  bool validate_after_each_reduce_pass = true;
+};
+
+/// Parameters specific to the fuzzer. Type `-tint_help` in the CLI to learn
+/// more.
+struct FuzzerCliParams {
+  /// The size of the cache that records ongoing mutation sessions.
+  uint32_t mutator_cache_size = 20;
+
+  /// The type of the mutator to run.
+  MutatorType mutator_type = MutatorType::kAll;
+
+  /// Tint backend to fuzz.
+  FuzzingTarget fuzzing_target = FuzzingTarget::kAll;
+
+  /// The path to the directory, that will be used to output buggy shaders.
+  std::string error_dir = "";
+
+  /// Parameters for various mutators.
+  MutatorCliParams mutator_params;
+};
+
+/// Parameters specific to the mutator debugger. Type `--help` in the CLI to
+/// learn more.
+struct MutatorDebuggerCliParams {
+  /// The type of the mutator to debug.
+  MutatorType mutator_type = MutatorType::kNone;
+
+  /// The seed that was used to initialize the mutator.
+  uint32_t seed = 0;
+
+  /// The binary that triggered a bug in the mutator.
+  std::vector<uint32_t> original_binary;
+
+  /// Parameters for various mutators.
+  MutatorCliParams mutator_params;
+};
+
+/// Parses CLI parameters for the fuzzer. This function exits with an error code
+/// and a message is printed to the console if some parameter has invalid
+/// format. You can pass `-tint_help` to check out all available parameters.
+/// This function will remove recognized parameters from the `argv` and adjust
+/// the `argc` accordingly.
+///
+/// @param argc - the number of parameters (identical to the `argc` in `main`
+///     function).
+/// @param argv - array of C strings of parameters.
+/// @return the parsed parameters.
+FuzzerCliParams ParseFuzzerCliParams(int* argc, char** argv);
+
+/// Parses CLI parameters for the mutator debugger. This function exits with an
+/// error code and a message is printed to the console if some parameter has
+/// invalid format. You can pass `--help` to check out all available parameters.
+///
+/// @param argc - the number of parameters (identical to the `argc` in `main`
+///     function).
+/// @param argv - array of C strings of parameters.
+/// @return the parsed parameters.
+MutatorDebuggerCliParams ParseMutatorDebuggerCliParams(int argc,
+                                                       const char* const* argv);
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_CLI_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
new file mode 100644
index 0000000..d17d743
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/fuzzer.cc
@@ -0,0 +1,263 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "spirv-tools/libspirv.hpp"
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace {
+
+struct Context {
+  FuzzerCliParams params;
+  std::unique_ptr<MutatorCache> mutator_cache;
+};
+
+Context* context = nullptr;
+
+extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
+  auto params = ParseFuzzerCliParams(argc, *argv);
+  auto mutator_cache =
+      params.mutator_cache_size
+          ? std::make_unique<MutatorCache>(params.mutator_cache_size)
+          : nullptr;
+  context = new Context{std::move(params), std::move(mutator_cache)};
+  OverrideCliParams(context->params);
+  return 0;
+}
+
+std::unique_ptr<Mutator> CreateMutator(const std::vector<uint32_t>& binary,
+                                       unsigned seed) {
+  std::vector<MutatorType> types;
+  types.reserve(3);
+
+  // Determine which mutator we will be using for `binary` at random.
+  auto cli_mutator_type = context->params.mutator_type;
+  if ((MutatorType::kFuzz & cli_mutator_type) == MutatorType::kFuzz) {
+    types.push_back(MutatorType::kFuzz);
+  }
+  if ((MutatorType::kReduce & cli_mutator_type) == MutatorType::kReduce) {
+    types.push_back(MutatorType::kReduce);
+  }
+  if ((MutatorType::kOpt & cli_mutator_type) == MutatorType::kOpt) {
+    types.push_back(MutatorType::kOpt);
+  }
+
+  assert(!types.empty() && "At least one mutator type must be specified");
+  RandomGenerator generator(seed);
+  auto mutator_type =
+      types[generator.GetUInt32(static_cast<uint32_t>(types.size()))];
+
+  const auto& mutator_params = context->params.mutator_params;
+  switch (mutator_type) {
+    case MutatorType::kFuzz:
+      return std::make_unique<SpirvFuzzMutator>(
+          mutator_params.target_env, binary, seed, mutator_params.donors,
+          mutator_params.enable_all_fuzzer_passes,
+          mutator_params.repeated_pass_strategy,
+          mutator_params.validate_after_each_fuzzer_pass,
+          mutator_params.transformation_batch_size);
+    case MutatorType::kReduce:
+      return std::make_unique<SpirvReduceMutator>(
+          mutator_params.target_env, binary, seed,
+          mutator_params.reduction_batch_size,
+          mutator_params.enable_all_reduce_passes,
+          mutator_params.validate_after_each_reduce_pass);
+    case MutatorType::kOpt:
+      return std::make_unique<SpirvOptMutator>(
+          mutator_params.target_env, seed, binary,
+          mutator_params.validate_after_each_opt_pass,
+          mutator_params.opt_batch_size);
+    default:
+      assert(false && "All mutator types must be handled above");
+      return nullptr;
+  }
+}
+
+void CLIMessageConsumer(spv_message_level_t level,
+                        const char*,
+                        const spv_position_t& position,
+                        const char* message) {
+  switch (level) {
+    case SPV_MSG_FATAL:
+    case SPV_MSG_INTERNAL_ERROR:
+    case SPV_MSG_ERROR:
+      std::cerr << "error: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    case SPV_MSG_WARNING:
+      std::cout << "warning: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    case SPV_MSG_INFO:
+      std::cout << "info: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    default:
+      break;
+  }
+}
+
+bool IsValid(const std::vector<uint32_t>& binary) {
+  spvtools::SpirvTools tools(context->params.mutator_params.target_env);
+  tools.SetMessageConsumer(CLIMessageConsumer);
+  return tools.IsValid() && tools.Validate(binary.data(), binary.size(),
+                                           spvtools::ValidatorOptions());
+}
+
+extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data,
+                                          size_t size,
+                                          size_t max_size,
+                                          unsigned seed) {
+  if ((size % sizeof(uint32_t)) != 0) {
+    // A valid SPIR-V binary's size must be a multiple of the size of a 32-bit
+    // word, and the SPIR-V Tools fuzzer is only designed to work with valid
+    // binaries.
+    return 0;
+  }
+
+  std::vector<uint32_t> binary(size / sizeof(uint32_t));
+  std::memcpy(binary.data(), data, size);
+
+  MutatorCache placeholder_cache(1);
+  auto* mutator_cache = context->mutator_cache.get();
+  if (!mutator_cache) {
+    // Use a placeholder cache if the user has decided not to use a real cache.
+    // The placeholder cache will be destroyed when we return from this function
+    // but it will save us from writing all the `if (mutator_cache)` below.
+    mutator_cache = &placeholder_cache;
+  }
+
+  if (!mutator_cache->Get(binary)) {
+    // This is an unknown binary, so its validity must be checked before
+    // proceeding.
+    if (!IsValid(binary)) {
+      return 0;
+    }
+    // Assign a mutator to the binary if it doesn't have one yet.
+    mutator_cache->Put(binary, CreateMutator(binary, seed));
+  }
+
+  auto* mutator = mutator_cache->Get(binary);
+  assert(mutator && "Mutator must be present in the cache");
+
+  auto result = mutator->Mutate();
+
+  if (result.GetStatus() == Mutator::Status::kInvalid) {
+    // The binary is invalid - log the error and remove the mutator from the
+    // cache.
+    util::LogMutatorError(*mutator, context->params.error_dir);
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  if (!result.IsChanged()) {
+    // The mutator didn't change the binary this time. This could be due to the
+    // fact that we've reached the number of mutations we can apply (e.g. the
+    // number of transformations in spirv-fuzz) or the mutator was just unlucky.
+    // Either way, there is no harm in destroying mutator and maybe trying again
+    // later (i.e. if libfuzzer decides to do so).
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  // At this point the binary is valid and was changed by the mutator.
+
+  auto mutated = mutator->GetBinary();
+  auto mutated_bytes_size = mutated.size() * sizeof(uint32_t);
+  if (mutated_bytes_size > max_size) {
+    // The binary is too big. It's unlikely that we'll reduce its size by
+    // applying the mutator one more time.
+    mutator_cache->Remove(binary);
+    return 0;
+  }
+
+  if (result.GetStatus() == Mutator::Status::kComplete) {
+    // Reassign the mutator to the mutated binary in the cache so that we can
+    // access later.
+    mutator_cache->Put(mutated, mutator_cache->Remove(binary));
+  } else {
+    // If the binary is valid and was changed but is not `kComplete`, then the
+    // mutator has reached some limit on the number of mutations.
+    mutator_cache->Remove(binary);
+  }
+
+  std::memcpy(data, mutated.data(), mutated_bytes_size);
+  return mutated_bytes_size;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  if (size == 0) {
+    return 0;
+  }
+
+  if ((size % sizeof(uint32_t)) != 0) {
+    // The SPIR-V Tools fuzzer has been designed to work with valid
+    // SPIR-V binaries, whose sizes should be multiples of the size of a 32-bit
+    // word.
+    return 0;
+  }
+
+  CommonFuzzer spv_to_wgsl(InputFormat::kSpv, OutputFormat::kWGSL);
+  spv_to_wgsl.Run(data, size);
+  if (spv_to_wgsl.HasErrors()) {
+    auto error = spv_to_wgsl.Diagnostics().str();
+    util::LogSpvError(error, data, size,
+                      context ? context->params.error_dir : "");
+    return 0;
+  }
+
+  const auto& wgsl = spv_to_wgsl.GetGeneratedWgsl();
+
+  std::pair<FuzzingTarget, OutputFormat> targets[] = {
+      {FuzzingTarget::kHlsl, OutputFormat::kHLSL},
+      {FuzzingTarget::kMsl, OutputFormat::kMSL},
+      {FuzzingTarget::kSpv, OutputFormat::kSpv},
+      {FuzzingTarget::kWgsl, OutputFormat::kWGSL}};
+
+  for (auto target : targets) {
+    if ((target.first & context->params.fuzzing_target) != target.first) {
+      continue;
+    }
+
+    CommonFuzzer fuzzer(InputFormat::kWGSL, target.second);
+    fuzzer.Run(reinterpret_cast<const uint8_t*>(wgsl.data()), wgsl.size());
+    if (fuzzer.HasErrors()) {
+      auto error = spv_to_wgsl.Diagnostics().str();
+      util::LogWgslError(error, data, size, wgsl, target.second,
+                         context->params.error_dir);
+    }
+  }
+
+  return 0;
+}
+
+}  // namespace
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
new file mode 100644
index 0000000..05dcc41
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.cc
@@ -0,0 +1,34 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+// We need to define constructor here so that vtable is produced in this
+// translation unit (see -Wweak-vtables clang flag).
+Mutator::~Mutator() = default;
+
+Mutator::Result::Result(Status status, bool is_changed)
+    : status_(status), is_changed_(is_changed) {
+  assert((is_changed || status == Status::kStuck ||
+          status == Status::kLimitReached) &&
+         "Returning invalid result state");
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h
new file mode 100644
index 0000000..9ff8fd9
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h
@@ -0,0 +1,108 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
+
+#include <cassert>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// This is an interface that is used to define custom mutators based on the
+/// SPIR-V tools.
+class Mutator {
+ public:
+  /// The status of the mutation.
+  enum class Status {
+    /// Binary is valid, the limit is not reached - can mutate further.
+    kComplete,
+
+    /// The binary is valid, the limit of mutations has been reached -
+    /// can't mutate further.
+    kLimitReached,
+
+    /// The binary is valid, the limit is not reached but the mutator has spent
+    /// too much time without mutating anything - better to restart to make sure
+    /// we can make any progress.
+    kStuck,
+
+    /// The binary is invalid - this is likely a bug in the mutator - must
+    /// abort.
+    kInvalid
+  };
+
+  /// Represents the result of the mutation. The following states are possible:
+  /// - if `IsChanged() == false`, then `GetStatus()` can be either
+  ///   `kLimitReached` or `kStuck`.
+  /// - otherwise, any value of `Status` is possible.
+  class Result {
+   public:
+    /// Constructor.
+    /// @param status - the status of the mutation.
+    /// @param is_changed - whether the module was changed during mutation.
+    Result(Status status, bool is_changed);
+
+    /// @return the status of the mutation.
+    Status GetStatus() const { return status_; }
+
+    /// @return whether the module was changed during mutation.
+    bool IsChanged() const { return is_changed_; }
+
+   private:
+    Status status_;
+    bool is_changed_;
+  };
+
+  /// Virtual destructor.
+  virtual ~Mutator();
+
+  /// Causes the mutator to apply a mutation. This method can be called
+  /// multiple times as long as the previous call didn't return
+  /// `Status::kInvalid`.
+  ///
+  /// @return the status of the mutation (e.g. success, error etc) and whether
+  ///     the binary was changed during mutation.
+  virtual Result Mutate() = 0;
+
+  /// Returns the mutated binary. The returned binary is guaranteed to be valid
+  /// iff the previous call to the `Mutate` method returned didn't return
+  /// `Status::kInvalid`.
+  ///
+  /// @return the mutated SPIR-V binary. It might be identical to the original
+  ///     binary if `Result::IsChanged` returns `false`.
+  virtual std::vector<uint32_t> GetBinary() const = 0;
+
+  /// Returns errors, produced by the mutator.
+  ///
+  /// @param path - the directory to which the errors are printed to. No files
+  ///     are created if the `path` is nullptr.
+  /// @param count - the number of the error. Files for this error will be
+  ///     prefixed with `count`.
+  virtual void LogErrors(const std::string* path, uint32_t count) const = 0;
+
+  /// @return errors encountered during the mutation. The returned string is
+  ///     if there were no errors during mutation.
+  virtual std::string GetErrors() const = 0;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
new file mode 100644
index 0000000..4ce1ad2
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.cc
@@ -0,0 +1,78 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+MutatorCache::MutatorCache(size_t max_size)
+    : map_(), entries_(), max_size_(max_size) {
+  assert(max_size && "`max_size` may not be 0");
+}
+
+MutatorCache::Value::pointer MutatorCache::Get(const Key& key) {
+  auto it = map_.find(key);
+  if (it == map_.end()) {
+    return nullptr;
+  }
+  UpdateUsage(it);
+  return entries_.front().second.get();
+}
+
+void MutatorCache::Put(const Key& key, Value value) {
+  assert(value && "Mutator cache can't have nullptr unique_ptr");
+  auto it = map_.find(key);
+  if (it != map_.end()) {
+    it->second->second = std::move(value);
+    UpdateUsage(it);
+  } else {
+    if (map_.size() == max_size_) {
+      Remove(*entries_.back().first);
+    }
+
+    entries_.emplace_front(nullptr, std::move(value));
+    auto pair = map_.emplace(key, entries_.begin());
+    assert(pair.second && "The key must be unique");
+    entries_.front().first = &pair.first->first;
+  }
+}
+
+MutatorCache::Value MutatorCache::Remove(const Key& key) {
+  auto it = map_.find(key);
+  if (it == map_.end()) {
+    return nullptr;
+  }
+  auto result = std::move(it->second->second);
+  entries_.erase(it->second);
+  map_.erase(it);
+  return result;
+}
+
+size_t MutatorCache::KeyHash::operator()(
+    const std::vector<uint32_t>& vec) const {
+  return std::hash<std::u32string>()({vec.begin(), vec.end()});
+}
+
+void MutatorCache::UpdateUsage(Map::iterator it) {
+  auto entry = std::move(*it->second);
+  entries_.erase(it->second);
+  entries_.push_front(std::move(entry));
+  it->second = entries_.begin();
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
new file mode 100644
index 0000000..7318b5c
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_cache.h
@@ -0,0 +1,99 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
+
+#include <cassert>
+#include <list>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Implementation of a fixed size LRU cache. That is, when the number of
+/// elements reaches a certain threshold, the element that wasn't used for the
+/// longest period of time is removed from the cache when a new element is
+/// inserted. All operations have amortized constant time complexity.
+class MutatorCache {
+ public:
+  /// SPIR-V binary that is being mutated.
+  using Key = std::vector<uint32_t>;
+
+  /// Mutator that is used to mutate the `Key`.
+  using Value = std::unique_ptr<Mutator>;
+
+  /// Constructor.
+  /// @param max_size - the maximum number of elements the cache can store. May
+  ///     not be equal to 0.
+  explicit MutatorCache(size_t max_size);
+
+  /// Retrieves a pointer to a value, associated with a given `key`.
+  ///
+  /// If the key is present in the cache, its usage is updated and the
+  /// (non-null) pointer to the value is returned. Otherwise, `nullptr` is
+  /// returned.
+  ///
+  /// @param key - may not exist in this cache.
+  /// @return non-`nullptr` pointer to a value if `key` exists in the cache.
+  /// @return `nullptr` if `key` doesn't exist in this cache.
+  Value::pointer Get(const Key& key);
+
+  /// Inserts a `key`-`value` pair into the cache.
+  ///
+  /// If the `key` is already present, the `value` replaces the old value and
+  /// the usage of `key` is updated. If the `key` is not present, then:
+  /// - if the number of elements in the cache is equal to `max_size`, the
+  ///   key-value pair, where the usage of the key wasn't updated for the
+  ///   longest period of time, is removed from the cache.
+  /// - a new `key`-`value` pair is inserted into the cache.
+  ///
+  /// @param key - a key.
+  /// @param value - may not be a `nullptr`.
+  void Put(const Key& key, Value value);
+
+  /// Removes `key` and an associated value from the cache.
+  ///
+  /// @param key - a key.
+  /// @return a non-`nullptr` pointer to the removed value, associated with
+  ///     `key`.
+  /// @return `nullptr` if `key` is not present in the cache.
+  Value Remove(const Key& key);
+
+ private:
+  struct KeyHash {
+    size_t operator()(const std::vector<uint32_t>& vec) const;
+  };
+
+  using Entry = std::pair<const Key*, Value>;
+  using Map = std::unordered_map<Key, std::list<Entry>::iterator, KeyHash>;
+
+  void UpdateUsage(Map::iterator it);
+
+  Map map_;
+  std::list<Entry> entries_;
+  const size_t max_size_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_MUTATOR_CACHE_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
new file mode 100644
index 0000000..aaa85fa
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator_debugger.cc
@@ -0,0 +1,84 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <memory>
+#include <string>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+/// This tool is used to debug *mutators*. It uses CLI arguments similar to the
+/// ones used by the fuzzer. To debug some mutator you just need to specify the
+/// mutator type, the seed and the path to the SPIR-V binary that triggered the
+/// error. This tool will run the mutator on the binary until the error is
+/// produced or the mutator returns `kLimitReached`.
+///
+/// Note that this is different from debugging the fuzzer by specifying input
+/// files to test. The difference is that the latter will not execute any
+/// mutator (it will only run the LLVMFuzzerTestOneInput function) whereas this
+/// tool is useful when one of the spirv-tools mutators crashes or produces an
+/// invalid binary in LLVMFuzzerCustomMutator.
+int main(int argc, const char** argv) {
+  auto params =
+      tint::fuzzers::spvtools_fuzzer::ParseMutatorDebuggerCliParams(argc, argv);
+
+  std::unique_ptr<tint::fuzzers::spvtools_fuzzer::Mutator> mutator;
+  const auto& mutator_params = params.mutator_params;
+  switch (params.mutator_type) {
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kFuzz:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvFuzzMutator>(
+              mutator_params.target_env, params.original_binary, params.seed,
+              mutator_params.donors, mutator_params.enable_all_fuzzer_passes,
+              mutator_params.repeated_pass_strategy,
+              mutator_params.validate_after_each_fuzzer_pass,
+              mutator_params.transformation_batch_size);
+      break;
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kReduce:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvReduceMutator>(
+              mutator_params.target_env, params.original_binary, params.seed,
+              mutator_params.reduction_batch_size,
+              mutator_params.enable_all_reduce_passes,
+              mutator_params.validate_after_each_reduce_pass);
+      break;
+    case tint::fuzzers::spvtools_fuzzer::MutatorType::kOpt:
+      mutator =
+          std::make_unique<tint::fuzzers::spvtools_fuzzer::SpirvOptMutator>(
+              mutator_params.target_env, params.seed, params.original_binary,
+              mutator_params.validate_after_each_opt_pass,
+              mutator_params.opt_batch_size);
+      break;
+    default:
+      assert(false && "All mutator types must've been handled");
+      return 1;
+  }
+
+  while (true) {
+    auto result = mutator->Mutate();
+    if (result.GetStatus() ==
+        tint::fuzzers::spvtools_fuzzer::Mutator::Status::kInvalid) {
+      std::cerr << mutator->GetErrors() << std::endl;
+      return 0;
+    }
+    if (result.GetStatus() ==
+        tint::fuzzers::spvtools_fuzzer::Mutator::Status::kLimitReached) {
+      break;
+    }
+  }
+}
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h
new file mode 100644
index 0000000..2aa2086
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h
@@ -0,0 +1,36 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// @brief Allows CLI parameters to be overridden.
+///
+/// This function allows fuzz targets to override particular CLI parameters,
+/// for example forcing a particular back-end to be targeted.
+///
+/// @param cli_params - the parsed CLI parameters to be updated.
+void OverrideCliParams(FuzzerCliParams& cli_params);
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_OVERRIDE_CLI_PARAMS_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
new file mode 100644
index 0000000..05fd2a3
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.cc
@@ -0,0 +1,127 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"
+
+#include <fstream>
+#include <utility>
+
+#include "source/opt/build_module.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvFuzzMutator::SpirvFuzzMutator(
+    spv_target_env target_env,
+    std::vector<uint32_t> binary,
+    unsigned seed,
+    const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
+    bool enable_all_passes,
+    spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
+    bool validate_after_each_pass,
+    uint32_t transformation_batch_size)
+    : transformation_batch_size_(transformation_batch_size),
+      errors_(std::make_unique<std::stringstream>()),
+      fuzzer_(nullptr),
+      validator_options_(),
+      original_binary_(std::move(binary)),
+      seed_(seed) {
+  auto ir_context = spvtools::BuildModule(
+      target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+      original_binary_.data(), original_binary_.size());
+  assert(ir_context && "|binary| is invalid");
+
+  auto transformation_context =
+      std::make_unique<spvtools::fuzz::TransformationContext>(
+          std::make_unique<spvtools::fuzz::FactManager>(ir_context.get()),
+          validator_options_);
+
+  auto fuzzer_context = std::make_unique<spvtools::fuzz::FuzzerContext>(
+      std::make_unique<spvtools::fuzz::PseudoRandomGenerator>(seed),
+      spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), false);
+  fuzzer_ = std::make_unique<spvtools::fuzz::Fuzzer>(
+      std::move(ir_context), std::move(transformation_context),
+      std::move(fuzzer_context), util::GetBufferMessageConsumer(errors_.get()),
+      donors, enable_all_passes, repeated_pass_strategy,
+      validate_after_each_pass, validator_options_);
+}
+
+Mutator::Result SpirvFuzzMutator::Mutate() {
+  // The assertion will fail in |fuzzer_->Run| if the previous fuzzing led to
+  // invalid module.
+  auto result = fuzzer_->Run(transformation_batch_size_);
+  switch (result.status) {
+    case spvtools::fuzz::Fuzzer::Status::kComplete:
+      return {Mutator::Status::kComplete, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kModuleTooBig:
+    case spvtools::fuzz::Fuzzer::Status::kTransformationLimitReached:
+      return {Mutator::Status::kLimitReached, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kFuzzerStuck:
+      return {Mutator::Status::kStuck, result.is_changed};
+    case spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule:
+      return {Mutator::Status::kInvalid, result.is_changed};
+  }
+}
+
+std::vector<uint32_t> SpirvFuzzMutator::GetBinary() const {
+  std::vector<uint32_t> result;
+  fuzzer_->GetIRContext()->module()->ToBinary(&result, true);
+  return result;
+}
+
+std::string SpirvFuzzMutator::GetErrors() const {
+  return errors_->str();
+}
+
+void SpirvFuzzMutator::LogErrors(const std::string* path,
+                                 uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvFuzzMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".fuzzer.log") << "seed: " << seed_ << std::endl
+                                          << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".fuzzer.invalid.spv", GetBinary());
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".fuzzer.original.spv", original_binary_);
+
+    // Write transformations.
+    google::protobuf::util::JsonOptions options;
+    options.add_whitespace = true;
+    std::string json;
+    google::protobuf::util::MessageToJsonString(
+        fuzzer_->GetTransformationSequence(), &json, options);
+    std::ofstream(prefix + ".fuzzer.transformations.json") << json << std::endl;
+
+    std::ofstream binary_transformations(
+        prefix + ".fuzzer.transformations.binary",
+        std::ios::binary | std::ios::out);
+    fuzzer_->GetTransformationSequence().SerializeToOstream(
+        &binary_transformations);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
new file mode 100644
index 0000000..073662e
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h
@@ -0,0 +1,95 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "source/fuzz/fuzzer.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/pseudo_random_generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// The mutator that uses spirv-fuzz to mutate SPIR-V.
+///
+/// The initial `binary` must be valid according to `target_env`. All other
+/// parameters (except for the `seed` which just initializes the RNG) are from
+/// the `spvtools::fuzz::Fuzzer` class.
+class SpirvFuzzMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - the target environment for the `binary`.
+  /// @param binary - the SPIR-V binary. Must be valid.
+  /// @param seed - seed for the RNG.
+  /// @param donors - vector of donor suppliers.
+  /// @param enable_all_passes - whether to use all fuzzer passes.
+  /// @param repeated_pass_strategy - the strategy to use when selecting the
+  ///     next fuzzer pass.
+  /// @param validate_after_each_pass - whether to validate the binary after
+  ///     each fuzzer pass.
+  /// @param transformation_batch_size - the maximum number of transformations
+  ///     that will be applied during a single call to `Mutate`. It it's equal
+  ///     to 0 then we apply as much transformations as we can until the
+  ///     threshold in the spvtools::fuzz::Fuzzer is reached (see the doc for
+  ///     that class for more info).
+  SpirvFuzzMutator(
+      spv_target_env target_env,
+      std::vector<uint32_t> binary,
+      uint32_t seed,
+      const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
+      bool enable_all_passes,
+      spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
+      bool validate_after_each_pass,
+      uint32_t transformation_batch_size);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  // The number of transformations that will be applied during a single call to
+  // the `Mutate` method. Is this only a lower bound since transformations are
+  // applied in batches by fuzzer passes (see docs for the
+  // `spvtools::fuzz::Fuzzer` for more info).
+  const uint32_t transformation_batch_size_;
+
+  // The errors produced by the `spvtools::fuzz::Fuzzer`.
+  std::unique_ptr<std::stringstream> errors_;
+  std::unique_ptr<spvtools::fuzz::Fuzzer> fuzzer_;
+  spvtools::ValidatorOptions validator_options_;
+
+  // The following fields are useful for debugging.
+
+  // The binary that the mutator is constructed with.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed that the mutator is constructed with.
+  const uint32_t seed_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_FUZZ_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
new file mode 100644
index 0000000..4e17ad0
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.cc
@@ -0,0 +1,159 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h"
+
+#include <fstream>
+#include <iostream>
+#include <unordered_set>
+#include <utility>
+
+#include "spirv-tools/optimizer.hpp"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvOptMutator::SpirvOptMutator(spv_target_env target_env,
+                                 uint32_t seed,
+                                 std::vector<uint32_t> binary,
+                                 bool validate_after_each_opt,
+                                 uint32_t opt_batch_size)
+    : num_executions_(0),
+      is_valid_(true),
+      target_env_(target_env),
+      original_binary_(std::move(binary)),
+      seed_(seed),
+      opt_passes_({"--combine-access-chains",
+                   "--loop-unroll",
+                   "--merge-blocks",
+                   "--cfg-cleanup",
+                   "--eliminate-dead-functions",
+                   "--merge-return",
+                   "--wrap-opkill",
+                   "--eliminate-dead-code-aggressive",
+                   "--if-conversion",
+                   "--eliminate-local-single-store",
+                   "--eliminate-local-single-block",
+                   "--eliminate-dead-branches",
+                   "--scalar-replacement=0",
+                   "--eliminate-dead-inserts",
+                   "--eliminate-dead-members",
+                   "--simplify-instructions",
+                   "--private-to-local",
+                   "--ssa-rewrite",
+                   "--ccp",
+                   "--reduce-load-size",
+                   "--vector-dce",
+                   "--scalar-replacement=100",
+                   "--inline-entry-points-exhaustive",
+                   "--redundancy-elimination",
+                   "--convert-local-access-chains",
+                   "--copy-propagate-arrays",
+                   "--fix-storage-class"}),
+      optimized_binary_(),
+      validate_after_each_opt_(validate_after_each_opt),
+      opt_batch_size_(opt_batch_size),
+      generator_(seed) {
+  assert(spvtools::SpirvTools(target_env).Validate(original_binary_) &&
+         "Initial binary is invalid");
+  assert(!opt_passes_.empty() && "Must be at least one pass");
+}
+
+SpirvOptMutator::Result SpirvOptMutator::Mutate() {
+  assert(is_valid_ && "The optimizer is not longer valid");
+
+  const uint32_t kMaxNumExecutions = 100;
+  const uint32_t kMaxNumStuck = 10;
+
+  if (num_executions_ == kMaxNumExecutions) {
+    // We've applied this mutator many times already. Indicate to the user that
+    // it might be better to try a different mutator.
+    return {Status::kLimitReached, false};
+  }
+
+  num_executions_++;
+
+  // Get the input binary. If this is the first time we run this mutator, use
+  // the `original_binary_`. Otherwise, one of the following will be true:
+  // - the `optimized_binary_` is not empty.
+  // - the previous call to the `Mutate` method returned `kStuck`.
+  auto binary = num_executions_ == 1 ? original_binary_ : optimized_binary_;
+  optimized_binary_.clear();
+
+  assert(!binary.empty() && "Can't run the optimizer on an empty binary");
+
+  // Number of times spirv-opt wasn't able to produce any new result.
+  uint32_t num_stuck = 0;
+  do {
+    // Randomly select `opt_batch_size` optimization passes. If `opt_batch_size`
+    // is equal to 0, we will use the number of passes equal to the number of
+    // all available passes.
+    auto num_of_passes = opt_batch_size_ ? opt_batch_size_ : opt_passes_.size();
+    std::vector<std::string> passes;
+
+    while (passes.size() < num_of_passes) {
+      auto idx =
+          generator_.GetUInt32(static_cast<uint32_t>(opt_passes_.size()));
+      passes.push_back(opt_passes_[idx]);
+    }
+
+    // Run the `binary` into the `optimized_binary_`.
+    spvtools::Optimizer optimizer(target_env_);
+    optimizer.SetMessageConsumer(util::GetBufferMessageConsumer(&errors_));
+    optimizer.SetValidateAfterAll(validate_after_each_opt_);
+    optimizer.RegisterPassesFromFlags(passes);
+    if (!optimizer.Run(binary.data(), binary.size(), &optimized_binary_)) {
+      is_valid_ = false;
+      return {Status::kInvalid, true};
+    }
+  } while (optimized_binary_.empty() && ++num_stuck < kMaxNumStuck);
+
+  return {optimized_binary_.empty() ? Status::kStuck : Status::kComplete,
+          !optimized_binary_.empty()};
+}
+
+std::vector<uint32_t> SpirvOptMutator::GetBinary() const {
+  return optimized_binary_;
+}
+
+std::string SpirvOptMutator::GetErrors() const {
+  return errors_.str();
+}
+
+void SpirvOptMutator::LogErrors(const std::string* path, uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvOptMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".opt.log") << "seed: " << seed_ << std::endl
+                                       << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".opt.invalid.spv", optimized_binary_);
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".opt.original.spv", original_binary_);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
new file mode 100644
index 0000000..6c209cd
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_opt_mutator.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "spirv-tools/libspirv.h"
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Mutates the SPIR-V module using the spirv-opt tool.
+///
+/// The initial `binary` must be valid according to `target_env`. On each call
+/// to the `Mutate` method the mutator selects `opt_batch_size` random
+/// optimization passes (with substitutions) and applies them to the binary.
+class SpirvOptMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - target environment for the `binary`.
+  /// @param seed - seed for the RNG.
+  /// @param binary - SPIR-V binary. Must be valid.
+  /// @param validate_after_each_opt - whether to validate the binary after each
+  ///     optimization pass.
+  /// @param opt_batch_size - the maximum number of optimization passes that
+  ///     will be applied in a single call to `Mutate`. If it's equal to 0 then
+  ///     all available optimization passes are applied.
+  SpirvOptMutator(spv_target_env target_env,
+                  uint32_t seed,
+                  std::vector<uint32_t> binary,
+                  bool validate_after_each_opt,
+                  uint32_t opt_batch_size);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  // Number of times this mutator was executed.
+  uint32_t num_executions_;
+
+  // Whether the last execution left it in a valid state.
+  bool is_valid_;
+
+  // Target environment for the SPIR-V binary.
+  const spv_target_env target_env_;
+
+  // The original SPIR-V binary. Useful for debugging.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed for the RNG. Useful for debugging.
+  const uint32_t seed_;
+
+  // All the optimization passes available.
+  const std::vector<std::string> opt_passes_;
+
+  // The result of the optimization.
+  std::vector<uint32_t> optimized_binary_;
+
+  // Whether we need to validate the binary after each optimization pass.
+  const bool validate_after_each_opt_;
+
+  // The number of optimization passes to apply at once.
+  const uint32_t opt_batch_size_;
+
+  // All the errors produced by the optimizer.
+  std::stringstream errors_;
+
+  // The random number generator initialized with `seed_`.
+  RandomGenerator generator_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_OPT_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
new file mode 100644
index 0000000..93d2e8b
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.cc
@@ -0,0 +1,190 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h"
+
+#include <fstream>
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/opt/build_module.h"
+#include "source/reduce/conditional_branch_to_simple_conditional_branch_opportunity_finder.h"
+#include "source/reduce/merge_blocks_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_const_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_dominating_id_reduction_opportunity_finder.h"
+#include "source/reduce/operand_to_undef_reduction_opportunity_finder.h"
+#include "source/reduce/remove_block_reduction_opportunity_finder.h"
+#include "source/reduce/remove_function_reduction_opportunity_finder.h"
+#include "source/reduce/remove_selection_reduction_opportunity_finder.h"
+#include "source/reduce/remove_unused_instruction_reduction_opportunity_finder.h"
+#include "source/reduce/remove_unused_struct_member_reduction_opportunity_finder.h"
+#include "source/reduce/simple_conditional_branch_to_branch_opportunity_finder.h"
+#include "source/reduce/structured_loop_to_selection_reduction_opportunity_finder.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+SpirvReduceMutator::SpirvReduceMutator(spv_target_env target_env,
+                                       std::vector<uint32_t> binary,
+                                       uint32_t seed,
+                                       uint32_t reductions_batch_size,
+                                       bool enable_all_reductions,
+                                       bool validate_after_each_reduction)
+    : ir_context_(nullptr),
+      finders_(),
+      generator_(seed),
+      errors_(),
+      is_valid_(true),
+      reductions_batch_size_(reductions_batch_size),
+      total_applied_reductions_(0),
+      enable_all_reductions_(enable_all_reductions),
+      validate_after_each_reduction_(validate_after_each_reduction),
+      original_binary_(std::move(binary)),
+      seed_(seed) {
+  ir_context_ = spvtools::BuildModule(
+      target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
+      original_binary_.data(), original_binary_.size());
+  assert(ir_context_ && "|binary| is invalid");
+
+  do {
+    MaybeAddFinder<
+        spvtools::reduce::
+            ConditionalBranchToSimpleConditionalBranchOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::MergeBlocksReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToConstReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToDominatingIdReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::OperandToUndefReductionOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::RemoveBlockReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveFunctionReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveSelectionReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::RemoveUnusedInstructionReductionOpportunityFinder>(
+        true);
+    MaybeAddFinder<
+        spvtools::reduce::RemoveUnusedStructMemberReductionOpportunityFinder>();
+    MaybeAddFinder<
+        spvtools::reduce::SimpleConditionalBranchToBranchOpportunityFinder>();
+    MaybeAddFinder<spvtools::reduce::
+                       StructuredLoopToSelectionReductionOpportunityFinder>();
+  } while (finders_.empty());
+}
+
+Mutator::Result SpirvReduceMutator::Mutate() {
+  assert(is_valid_ && "Can't mutate invalid module");
+
+  // The upper limit on the number of applied reduction passes.
+  const uint32_t kMaxAppliedReductions = 500;
+  const auto old_applied_reductions = total_applied_reductions_;
+
+  // The upper limit on the number of failed attempts to apply reductions (i.e.
+  // when no reduction was returned by the reduction finder).
+  const uint32_t kMaxConsecutiveFailures = 10;
+  uint32_t num_consecutive_failures = 0;
+
+  // Iterate while we haven't exceeded the limit on the total number of applied
+  // reductions, the limit on the number of reductions applied at once and limit
+  // on the number of consecutive failed attempts.
+  while (total_applied_reductions_ < kMaxAppliedReductions &&
+         (reductions_batch_size_ == 0 ||
+          total_applied_reductions_ - old_applied_reductions <
+              reductions_batch_size_) &&
+         num_consecutive_failures < kMaxConsecutiveFailures) {
+    // Select an opportunity finder and get some reduction opportunities from
+    // it.
+    auto finder = GetRandomElement(&finders_);
+    auto reduction_opportunities =
+        finder->GetAvailableOpportunities(ir_context_.get(), 0);
+
+    if (reduction_opportunities.empty()) {
+      // There is nothing to reduce. We increase the counter to make sure we
+      // don't stuck in this situation.
+      num_consecutive_failures++;
+    } else {
+      // Apply a random reduction opportunity. The latter should be applicable.
+      auto opportunity = GetRandomElement(&reduction_opportunities);
+      assert(opportunity->PreconditionHolds() && "Preconditions should hold");
+      total_applied_reductions_++;
+      num_consecutive_failures = 0;
+      if (!ApplyReduction(opportunity)) {
+        // The module became invalid as a result of the applied reduction.
+        is_valid_ = false;
+        return {Mutator::Status::kInvalid,
+                total_applied_reductions_ != old_applied_reductions};
+      }
+    }
+  }
+
+  auto is_changed = total_applied_reductions_ != old_applied_reductions;
+  if (total_applied_reductions_ == kMaxAppliedReductions) {
+    return {Mutator::Status::kLimitReached, is_changed};
+  }
+
+  if (num_consecutive_failures == kMaxConsecutiveFailures) {
+    return {Mutator::Status::kStuck, is_changed};
+  }
+
+  assert(is_changed && "This is the only way left to break the loop");
+  return {Mutator::Status::kComplete, is_changed};
+}
+
+bool SpirvReduceMutator::ApplyReduction(
+    spvtools::reduce::ReductionOpportunity* reduction_opportunity) {
+  reduction_opportunity->TryToApply();
+  return !validate_after_each_reduction_ ||
+         spvtools::fuzz::fuzzerutil::IsValidAndWellFormed(
+             ir_context_.get(), spvtools::ValidatorOptions(),
+             util::GetBufferMessageConsumer(&errors_));
+}
+
+std::vector<uint32_t> SpirvReduceMutator::GetBinary() const {
+  std::vector<uint32_t> result;
+  ir_context_->module()->ToBinary(&result, true);
+  return result;
+}
+
+std::string SpirvReduceMutator::GetErrors() const {
+  return errors_.str();
+}
+
+void SpirvReduceMutator::LogErrors(const std::string* path,
+                                   uint32_t count) const {
+  auto message = GetErrors();
+  std::cout << count << " | SpirvReduceMutator (seed: " << seed_ << ")"
+            << std::endl;
+  std::cout << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(count);
+
+    // Write errors to file.
+    std::ofstream(prefix + ".reducer.log") << "seed: " << seed_ << std::endl
+                                           << message << std::endl;
+
+    // Write the invalid SPIR-V binary.
+    util::WriteBinary(prefix + ".reducer.invalid.spv", GetBinary());
+
+    // Write the original SPIR-V binary.
+    util::WriteBinary(prefix + ".reducer.original.spv", original_binary_);
+  }
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
new file mode 100644
index 0000000..9699b01
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/spirv_reduce_mutator.h
@@ -0,0 +1,132 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "src/tint/fuzzers/random_generator.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "source/reduce/reduction_opportunity_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+/// Mutates SPIR-V binary by running spirv-reduce tool.
+///
+/// The initial `binary` must be valid according to `target_env`. Applies at
+/// most `reductions_batch_size` reductions at a time. This parameter is ignored
+/// if its value is 0. Uses a random subset of reduction opportunity finders by
+/// default. This can be overridden with the `enable_all_reductions` parameter.
+class SpirvReduceMutator : public Mutator {
+ public:
+  /// Constructor.
+  /// @param target_env - the target environment for the `binary`.
+  /// @param binary - SPIR-V binary. Must be valid.
+  /// @param seed - the seed for the RNG.
+  /// @param reductions_batch_size - the number of reduction passes that will be
+  ///     applied during a single call to `Mutate`. If it's equal to 0 then we
+  ///     apply the passes until we reach the threshold for the total number of
+  ///     applied passes.
+  /// @param enable_all_reductions - whether to use all reduction passes or only
+  ///     a randomly selected subset of them.
+  /// @param validate_after_each_reduction - whether to validate after each
+  ///     applied reduction.
+  SpirvReduceMutator(spv_target_env target_env,
+                     std::vector<uint32_t> binary,
+                     uint32_t seed,
+                     uint32_t reductions_batch_size,
+                     bool enable_all_reductions,
+                     bool validate_after_each_reduction);
+
+  Result Mutate() override;
+  std::vector<uint32_t> GetBinary() const override;
+  void LogErrors(const std::string* path, uint32_t count) const override;
+  std::string GetErrors() const override;
+
+ private:
+  template <typename T, typename... Args>
+  void MaybeAddFinder(Args&&... args) {
+    if (enable_all_reductions_ || generator_.GetBool()) {
+      finders_.push_back(std::make_unique<T>(std::forward<Args>(args)...));
+    }
+  }
+
+  template <typename T>
+  T* GetRandomElement(std::vector<T>* arr) {
+    assert(!arr->empty() && "Can't get random element from an empty vector");
+    auto index = generator_.GetUInt32(static_cast<uint32_t>(arr->size()));
+    return &(*arr)[index];
+  }
+
+  template <typename T>
+  T* GetRandomElement(std::vector<std::unique_ptr<T>>* arr) {
+    assert(!arr->empty() && "Can't get random element from an empty vector");
+    auto index = generator_.GetUInt32(static_cast<uint32_t>(arr->size()));
+    return (*arr)[index].get();
+  }
+
+  bool ApplyReduction(
+      spvtools::reduce::ReductionOpportunity* reduction_opportunity);
+
+  // The SPIR-V binary that is being reduced.
+  std::unique_ptr<spvtools::opt::IRContext> ir_context_;
+
+  // The selected subset of reduction opportunity finders.
+  std::vector<std::unique_ptr<spvtools::reduce::ReductionOpportunityFinder>>
+      finders_;
+
+  // Random number generator initialized with `seed_`.
+  RandomGenerator generator_;
+
+  // All the errors produced by the reducer.
+  std::stringstream errors_;
+
+  // Whether the last call to the `Mutate` method produced the valid binary.
+  bool is_valid_;
+
+  // The number of reductions to apply on a single call to `Mutate`.
+  const uint32_t reductions_batch_size_;
+
+  // The total number of applied reductions.
+  uint32_t total_applied_reductions_;
+
+  // Whether we want to use all the reduction opportunity finders and not just a
+  // subset of them.
+  const bool enable_all_reductions_;
+
+  // Whether we want to validate all the binary after each reduction.
+  const bool validate_after_each_reduction_;
+
+  // The original binary that was used to initialize this mutator.
+  // Useful for debugging.
+  const std::vector<uint32_t> original_binary_;
+
+  // The seed that was used to initialize the random number generator.
+  // Useful for debugging.
+  const uint32_t seed_;
+};
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_SPIRV_REDUCE_MUTATOR_H_
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc
new file mode 100644
index 0000000..8c14547
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_fuzzer.cc
@@ -0,0 +1,30 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+void OverrideCliParams(FuzzerCliParams& /*unused*/) {
+  // Leave the CLI parameters unchanged.
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc
new file mode 100644
index 0000000..08111d9
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_hlsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+void OverrideCliParams(FuzzerCliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kHlsl;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc
new file mode 100644
index 0000000..86a493e
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_msl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+void OverrideCliParams(FuzzerCliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kMsl;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc
new file mode 100644
index 0000000..2cbfef7
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_spv_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+void OverrideCliParams(FuzzerCliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kSpv;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc
new file mode 100644
index 0000000..4538973
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/tint_spirv_tools_wgsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cassert>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/cli.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/override_cli_params.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+
+void OverrideCliParams(FuzzerCliParams& cli_params) {
+  assert(cli_params.fuzzing_target == FuzzingTarget::kAll &&
+         "The fuzzing target should not have been set by a CLI parameter: it "
+         "should have its default value.");
+  cli_params.fuzzing_target = FuzzingTarget::kWgsl;
+}
+
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc
new file mode 100644
index 0000000..a20a1f4
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.cc
@@ -0,0 +1,157 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <fstream>
+#include <iostream>
+
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace util {
+namespace {
+
+bool WriteBinary(const std::string& path, const uint8_t* data, size_t size) {
+  std::ofstream spv(path, std::ios::binary);
+  return spv && spv.write(reinterpret_cast<const char*>(data),
+                          static_cast<std::streamsize>(size));
+}
+
+void LogError(uint32_t index,
+              const std::string& type,
+              const std::string& message,
+              const std::string* path,
+              const uint8_t* data,
+              size_t size,
+              const std::string* wgsl) {
+  std::cout << index << " | " << type << ": " << message << std::endl;
+
+  if (path) {
+    auto prefix = *path + std::to_string(index);
+    std::ofstream(prefix + ".log") << message << std::endl;
+
+    WriteBinary(prefix + ".spv", data, size);
+
+    if (wgsl) {
+      std::ofstream(prefix + ".wgsl") << *wgsl << std::endl;
+    }
+  }
+}
+
+}  // namespace
+
+spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer) {
+  return [buffer](spv_message_level_t level, const char*,
+                  const spv_position_t& position, const char* message) {
+    std::string status;
+    switch (level) {
+      case SPV_MSG_FATAL:
+      case SPV_MSG_INTERNAL_ERROR:
+      case SPV_MSG_ERROR:
+        status = "ERROR";
+        break;
+      case SPV_MSG_WARNING:
+      case SPV_MSG_INFO:
+      case SPV_MSG_DEBUG:
+        status = "INFO";
+        break;
+    }
+    *buffer << status << " " << position.line << ":" << position.column << ":"
+            << position.index << ": " << message << std::endl;
+  };
+}
+
+void LogMutatorError(const Mutator& mutator, const std::string& error_dir) {
+  static uint32_t mutator_count = 0;
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/mutator/";
+  mutator.LogErrors(error_dir.empty() ? nullptr : &error_path, mutator_count++);
+}
+
+void LogWgslError(const std::string& message,
+                  const uint8_t* data,
+                  size_t size,
+                  const std::string& wgsl,
+                  OutputFormat output_format,
+                  const std::string& error_dir) {
+  static uint32_t wgsl_count = 0;
+  std::string error_type;
+  switch (output_format) {
+    case OutputFormat::kSpv:
+      error_type = "WGSL -> SPV";
+      break;
+    case OutputFormat::kMSL:
+      error_type = "WGSL -> MSL";
+      break;
+    case OutputFormat::kHLSL:
+      error_type = "WGSL -> HLSL";
+      break;
+    case OutputFormat::kWGSL:
+      error_type = "WGSL -> WGSL";
+      break;
+  }
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/wgsl/";
+  LogError(wgsl_count++, error_type, message,
+           error_dir.empty() ? nullptr : &error_path, data, size, &wgsl);
+}
+
+void LogSpvError(const std::string& message,
+                 const uint8_t* data,
+                 size_t size,
+                 const std::string& error_dir) {
+  static uint32_t spv_count = 0;
+  auto error_path = error_dir.empty() ? error_dir : error_dir + "/spv/";
+  LogError(spv_count++, "SPV -> WGSL", message,
+           error_dir.empty() ? nullptr : &error_path, data, size, nullptr);
+}
+
+bool ReadBinary(const std::string& path, std::vector<uint32_t>* out) {
+  if (!out) {
+    return false;
+  }
+
+  std::ifstream file(path, std::ios::binary | std::ios::ate);
+  if (!file) {
+    return false;
+  }
+
+  size_t size = static_cast<size_t>(file.tellg());
+  if (!file) {
+    return false;
+  }
+
+  file.seekg(0);
+  if (!file) {
+    return false;
+  }
+
+  std::vector<char> binary(size);
+  if (!file.read(binary.data(), size)) {
+    return false;
+  }
+
+  out->resize(binary.size() / sizeof(uint32_t));
+  std::memcpy(out->data(), binary.data(), binary.size());
+  return true;
+}
+
+bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary) {
+  return WriteBinary(path, reinterpret_cast<const uint8_t*>(binary.data()),
+                     binary.size() * sizeof(uint32_t));
+}
+
+}  // namespace util
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h
new file mode 100644
index 0000000..301e3ba
--- /dev/null
+++ b/src/tint/fuzzers/tint_spirv_tools_fuzzer/util.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
+#define SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
+
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/tint_spirv_tools_fuzzer/mutator.h"
+
+#include "spirv-tools/libspirv.hpp"
+
+namespace tint {
+namespace fuzzers {
+namespace spvtools_fuzzer {
+namespace util {
+
+/// @param buffer will be used to output errors by the returned message
+///     consumer. Must remain in scope as long as the returned consumer is in
+///     scope.
+/// @return the message consumer that will print errors to the `buffer`.
+spvtools::MessageConsumer GetBufferMessageConsumer(std::stringstream* buffer);
+
+/// Output errors from the SPV -> WGSL conversion.
+///
+/// @param message - the error message.
+/// @param data - invalid SPIR-V binary.
+/// @param size - the size of `data`.
+/// @param error_dir - the directory, to which the binary will be printed to.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `spv/` subdirectory.
+void LogSpvError(const std::string& message,
+                 const uint8_t* data,
+                 size_t size,
+                 const std::string& error_dir);
+
+/// Output errors from the WGSL -> `output_format` conversion.
+///
+/// @param message - the error message.
+/// @param data - the SPIR-V binary that generated the WGSL binary.
+/// @param size - the size of `data`.
+/// @param wgsl - the invalid WGSL binary.
+/// @param output_format - the format which we attempted to convert `wgsl` to.
+/// @param error_dir - the directory, to which the binary will be printed out.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `wgsl/` subdirectory.
+void LogWgslError(const std::string& message,
+                  const uint8_t* data,
+                  size_t size,
+                  const std::string& wgsl,
+                  OutputFormat output_format,
+                  const std::string& error_dir);
+
+/// Output errors produced by the mutator.
+///
+/// @param mutator - the mutator with invalid state.
+/// @param error_dir - the directory, to which invalid files will be printed to.
+///     If it's empty, the invalid binary and supplemental files will not be
+///     printed. Otherwise, it must have a `mutator/` subdirectory.
+void LogMutatorError(const Mutator& mutator, const std::string& error_dir);
+
+/// Reads SPIR-V binary from `path` into `out`. Returns `true` if successful and
+/// `false` otherwise (in this case, `out` is unchanged).
+///
+/// @param path - the path to the SPIR-V binary.
+/// @param out - may be a `nullptr`. In this case, `false` is returned.
+/// @return `true` if successful and `false` otherwise.
+bool ReadBinary(const std::string& path, std::vector<uint32_t>* out);
+
+/// Writes `binary` into `path`.
+///
+/// @param path - the path to write `binary` to.
+/// @param binary - SPIR-V binary.
+/// @return whether the operation was successful.
+bool WriteBinary(const std::string& path, const std::vector<uint32_t>& binary);
+
+}  // namespace util
+}  // namespace spvtools_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_SPIRV_TOOLS_FUZZER_UTIL_H_
diff --git a/src/tint/fuzzers/tint_spv_reader_fuzzer.cc b/src/tint/fuzzers/tint_spv_reader_fuzzer.cc
new file mode 100644
index 0000000..0daab5d
--- /dev/null
+++ b/src/tint/fuzzers/tint_spv_reader_fuzzer.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::CommonFuzzer fuzzer(InputFormat::kSpv, OutputFormat::kNone);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spv_reader_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spv_reader_hlsl_writer_fuzzer.cc
new file mode 100644
index 0000000..5f2f2c7
--- /dev/null
+++ b/src/tint/fuzzers/tint_spv_reader_hlsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kSpv,
+                                           OutputFormat::kHLSL);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spv_reader_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spv_reader_msl_writer_fuzzer.cc
new file mode 100644
index 0000000..0b1446b
--- /dev/null
+++ b/src/tint/fuzzers/tint_spv_reader_msl_writer_fuzzer.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  DataBuilder db(data, size);
+  writer::msl::Options options;
+  GenerateMslOptions(&db, &options);
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kSpv,
+                                           OutputFormat::kMSL);
+  fuzzer.SetOptionsMsl(options);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spv_reader_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_spv_reader_spv_writer_fuzzer.cc
new file mode 100644
index 0000000..b3cc6b0
--- /dev/null
+++ b/src/tint/fuzzers/tint_spv_reader_spv_writer_fuzzer.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  DataBuilder db(data, size);
+  writer::spirv::Options options;
+  GenerateSpirvOptions(&db, &options);
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kSpv,
+                                           OutputFormat::kSpv);
+  fuzzer.SetOptionsSpirv(options);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_spv_reader_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_spv_reader_wgsl_writer_fuzzer.cc
new file mode 100644
index 0000000..eff349d
--- /dev/null
+++ b/src/tint/fuzzers/tint_spv_reader_wgsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <vector>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kSpv,
+                                           OutputFormat::kWGSL);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_vertex_pulling_fuzzer.cc b/src/tint/fuzzers/tint_vertex_pulling_fuzzer.cc
new file mode 100644
index 0000000..e1ad99e
--- /dev/null
+++ b/src/tint/fuzzers/tint_vertex_pulling_fuzzer.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+#include "src/tint/fuzzers/transform_builder.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  TransformBuilder tb(data, size);
+  tb.AddTransform<transform::VertexPulling>();
+
+  tint::fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kWGSL);
+  fuzzer.SetTransformManager(tb.manager(), tb.data_map());
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_wgsl_reader_fuzzer.cc b/src/tint/fuzzers/tint_wgsl_reader_fuzzer.cc
new file mode 100644
index 0000000..d1b1f27
--- /dev/null
+++ b/src/tint/fuzzers/tint_wgsl_reader_fuzzer.cc
@@ -0,0 +1,30 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_common_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::CommonFuzzer fuzzer(InputFormat::kWGSL, OutputFormat::kNone);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_wgsl_reader_hlsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_wgsl_reader_hlsl_writer_fuzzer.cc
new file mode 100644
index 0000000..d5a15a7
--- /dev/null
+++ b/src/tint/fuzzers/tint_wgsl_reader_hlsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL,
+                                           OutputFormat::kHLSL);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_wgsl_reader_msl_writer_fuzzer.cc b/src/tint/fuzzers/tint_wgsl_reader_msl_writer_fuzzer.cc
new file mode 100644
index 0000000..58fdf8b
--- /dev/null
+++ b/src/tint/fuzzers/tint_wgsl_reader_msl_writer_fuzzer.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  DataBuilder db(data, size);
+  writer::msl::Options options;
+  GenerateMslOptions(&db, &options);
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL,
+                                           OutputFormat::kMSL);
+  fuzzer.SetOptionsMsl(options);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_wgsl_reader_spv_writer_fuzzer.cc b/src/tint/fuzzers/tint_wgsl_reader_spv_writer_fuzzer.cc
new file mode 100644
index 0000000..f4932dd
--- /dev/null
+++ b/src/tint/fuzzers/tint_wgsl_reader_spv_writer_fuzzer.cc
@@ -0,0 +1,37 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  DataBuilder db(data, size);
+  writer::spirv::Options options;
+  GenerateSpirvOptions(&db, &options);
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL,
+                                           OutputFormat::kSpv);
+  fuzzer.SetOptionsSpirv(options);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc b/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
new file mode 100644
index 0000000..8cb7eaa
--- /dev/null
+++ b/src/tint/fuzzers/tint_wgsl_reader_wgsl_writer_fuzzer.cc
@@ -0,0 +1,33 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "src/tint/fuzzers/fuzzer_init.h"
+#include "src/tint/fuzzers/tint_reader_writer_fuzzer.h"
+
+namespace tint {
+namespace fuzzers {
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  tint::fuzzers::ReaderWriterFuzzer fuzzer(InputFormat::kWGSL,
+                                           OutputFormat::kWGSL);
+  fuzzer.SetDumpInput(GetCliParams().dump_input);
+  fuzzer.SetEnforceValidity(GetCliParams().enforce_validity);
+
+  return fuzzer.Run(data, size);
+}
+
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/transform_builder.h b/src/tint/fuzzers/transform_builder.h
new file mode 100644
index 0000000..7c5073d
--- /dev/null
+++ b/src/tint/fuzzers/transform_builder.h
@@ -0,0 +1,228 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TRANSFORM_BUILDER_H_
+#define SRC_TINT_FUZZERS_TRANSFORM_BUILDER_H_
+
+#include <string>
+#include <vector>
+
+#include "include/tint/tint.h"
+
+#include "src/tint/fuzzers/data_builder.h"
+#include "src/tint/fuzzers/shuffle_transform.h"
+
+namespace tint {
+namespace fuzzers {
+
+/// Fuzzer utility class to build inputs for transforms and setup the transform
+/// manager.
+class TransformBuilder {
+ public:
+  /// @brief Initializes the internal builder using a seed value
+  /// @param seed - seed value passed to engine
+  explicit TransformBuilder(uint64_t seed) : builder_(seed) {}
+
+  /// @brief Initializes the internal builder using seed data
+  /// @param data - data fuzzer to calculate seed from
+  /// @param size - size of data buffer
+  explicit TransformBuilder(const uint8_t* data, size_t size)
+      : builder_(data, size) {
+    assert(data != nullptr && "|data| must be !nullptr");
+  }
+
+  ~TransformBuilder() = default;
+
+  /// @returns manager for transforms
+  transform::Manager* manager() { return &manager_; }
+
+  /// @returns data for transforms
+  transform::DataMap* data_map() { return &data_map_; }
+
+  /// Adds a transform and needed data to |manager_| and |data_map_|.
+  /// @tparam T - A class that inherits from transform::Transform and has an
+  ///             explicit specialization in AddTransformImpl.
+  template <typename T>
+  void AddTransform() {
+    static_assert(std::is_base_of<transform::Transform, T>::value,
+                  "T is not a transform::Transform");
+    AddTransformImpl<T>::impl(this);
+  }
+
+  /// Helper that invokes Add*Transform for all of the platform independent
+  /// passes.
+  void AddPlatformIndependentPasses() {
+    AddTransform<transform::Robustness>();
+    AddTransform<transform::FirstIndexOffset>();
+    AddTransform<transform::BindingRemapper>();
+    AddTransform<transform::Renamer>();
+    AddTransform<transform::SingleEntryPoint>();
+    AddTransform<transform::VertexPulling>();
+  }
+
+ private:
+  DataBuilder builder_;
+  transform::Manager manager_;
+  transform::DataMap data_map_;
+
+  DataBuilder* builder() { return &builder_; }
+
+  /// Implementation of AddTransform, specialized for each transform that is
+  /// implemented. Default implementation intentionally deleted to cause compile
+  /// error if unimplemented type passed in.
+  /// @tparam T - A fuzzer transform
+  template <typename T>
+  struct AddTransformImpl;
+
+  /// Implementation of AddTransform for ShuffleTransform
+  template <>
+  struct AddTransformImpl<ShuffleTransform> {
+    /// Add instance of ShuffleTransform to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      tb->manager()->Add<ShuffleTransform>(tb->builder_.build<size_t>());
+    }
+  };
+
+  /// Implementation of AddTransform for transform::Robustness
+  template <>
+  struct AddTransformImpl<transform::Robustness> {
+    /// Add instance of transform::Robustness to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      tb->manager()->Add<transform::Robustness>();
+    }
+  };
+
+  /// Implementation of AddTransform for transform::FirstIndexOffset
+  template <>
+  struct AddTransformImpl<transform::FirstIndexOffset> {
+    /// Add instance of transform::FirstIndexOffset to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      struct Config {
+        uint32_t group;
+        uint32_t binding;
+      };
+
+      Config config = tb->builder()->build<Config>();
+
+      tb->data_map()->Add<tint::transform::FirstIndexOffset::BindingPoint>(
+          config.binding, config.group);
+      tb->manager()->Add<transform::FirstIndexOffset>();
+    }
+  };
+
+  /// Implementation of AddTransform for transform::BindingRemapper
+  template <>
+  struct AddTransformImpl<transform::BindingRemapper> {
+    /// Add instance of transform::BindingRemapper to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      struct Config {
+        uint8_t old_group;
+        uint8_t old_binding;
+        uint8_t new_group;
+        uint8_t new_binding;
+        ast::Access new_access;
+      };
+
+      std::vector<Config> configs = tb->builder()->vector<Config>();
+      transform::BindingRemapper::BindingPoints binding_points;
+      transform::BindingRemapper::AccessControls accesses;
+      for (const auto& config : configs) {
+        binding_points[{config.old_binding, config.old_group}] = {
+            config.new_binding, config.new_group};
+        accesses[{config.old_binding, config.old_group}] = config.new_access;
+      }
+
+      tb->data_map()->Add<transform::BindingRemapper::Remappings>(
+          binding_points, accesses, tb->builder()->build<bool>());
+      tb->manager()->Add<transform::BindingRemapper>();
+    }
+  };
+
+  /// Implementation of AddTransform for transform::Renamer
+  template <>
+  struct AddTransformImpl<transform::Renamer> {
+    /// Add instance of transform::Renamer to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      tb->manager()->Add<transform::Renamer>();
+    }
+  };
+
+  /// Implementation of AddTransform for transform::SingleEntryPoint
+  template <>
+  struct AddTransformImpl<transform::SingleEntryPoint> {
+    /// Add instance of transform::SingleEntryPoint to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      auto input = tb->builder()->build<std::string>();
+      transform::SingleEntryPoint::Config cfg(input);
+
+      tb->data_map()->Add<transform::SingleEntryPoint::Config>(cfg);
+      tb->manager()->Add<transform::SingleEntryPoint>();
+    }
+  };  // struct AddTransformImpl<transform::SingleEntryPoint>
+
+  /// Implementation of AddTransform for transform::VertexPulling
+  template <>
+  struct AddTransformImpl<transform::VertexPulling> {
+    /// Add instance of transform::VertexPulling to TransformBuilder
+    /// @param tb - TransformBuilder to add transform to
+    static void impl(TransformBuilder* tb) {
+      transform::VertexPulling::Config cfg;
+      cfg.entry_point_name = tb->builder()->build<std::string>();
+      cfg.vertex_state =
+          tb->builder()->vector<transform::VertexBufferLayoutDescriptor>(
+              GenerateVertexBufferLayoutDescriptor);
+      cfg.pulling_group = tb->builder()->build<uint32_t>();
+
+      tb->data_map()->Add<transform::VertexPulling::Config>(cfg);
+      tb->manager()->Add<transform::VertexPulling>();
+    }
+
+   private:
+    /// Generate an instance of transform::VertexAttributeDescriptor
+    /// @param b - DataBuilder to use
+    static transform::VertexAttributeDescriptor
+    GenerateVertexAttributeDescriptor(DataBuilder* b) {
+      transform::VertexAttributeDescriptor desc{};
+      desc.format = b->enum_class<transform::VertexFormat>(
+          static_cast<uint8_t>(transform::VertexFormat::kLastEntry) + 1);
+      desc.offset = b->build<uint32_t>();
+      desc.shader_location = b->build<uint32_t>();
+      return desc;
+    }
+
+    /// Generate an instance of VertexBufferLayoutDescriptor
+    /// @param b - DataBuilder to use
+    static transform::VertexBufferLayoutDescriptor
+    GenerateVertexBufferLayoutDescriptor(DataBuilder* b) {
+      transform::VertexBufferLayoutDescriptor desc;
+      desc.array_stride = b->build<uint32_t>();
+      desc.step_mode = b->enum_class<transform::VertexStepMode>(
+          static_cast<uint8_t>(transform::VertexStepMode::kLastEntry) + 1);
+      desc.attributes = b->vector<transform::VertexAttributeDescriptor>(
+          GenerateVertexAttributeDescriptor);
+      return desc;
+    }
+  };
+};  // class TransformBuilder
+
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TRANSFORM_BUILDER_H_
diff --git a/src/tint/inspector/entry_point.cc b/src/tint/inspector/entry_point.cc
new file mode 100644
index 0000000..9b0b46f
--- /dev/null
+++ b/src/tint/inspector/entry_point.cc
@@ -0,0 +1,70 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/entry_point.h"
+
+namespace tint {
+namespace inspector {
+
+StageVariable::StageVariable() = default;
+StageVariable::StageVariable(const StageVariable& other)
+    : name(other.name),
+      has_location_attribute(other.has_location_attribute),
+      location_attribute(other.location_attribute),
+      has_location_decoration(has_location_attribute),
+      location_decoration(location_attribute),
+      component_type(other.component_type),
+      composition_type(other.composition_type),
+      interpolation_type(other.interpolation_type),
+      interpolation_sampling(other.interpolation_sampling) {}
+
+StageVariable::~StageVariable() = default;
+
+EntryPoint::EntryPoint() = default;
+EntryPoint::EntryPoint(EntryPoint&) = default;
+EntryPoint::EntryPoint(EntryPoint&&) = default;
+EntryPoint::~EntryPoint() = default;
+
+InterpolationType ASTToInspectorInterpolationType(
+    ast::InterpolationType ast_type) {
+  switch (ast_type) {
+    case ast::InterpolationType::kPerspective:
+      return InterpolationType::kPerspective;
+    case ast::InterpolationType::kLinear:
+      return InterpolationType::kLinear;
+    case ast::InterpolationType::kFlat:
+      return InterpolationType::kFlat;
+  }
+
+  return InterpolationType::kUnknown;
+}
+
+InterpolationSampling ASTToInspectorInterpolationSampling(
+    ast::InterpolationSampling sampling) {
+  switch (sampling) {
+    case ast::InterpolationSampling::kNone:
+      return InterpolationSampling::kNone;
+    case ast::InterpolationSampling::kCenter:
+      return InterpolationSampling::kCenter;
+    case ast::InterpolationSampling::kCentroid:
+      return InterpolationSampling::kCentroid;
+    case ast::InterpolationSampling::kSample:
+      return InterpolationSampling::kSample;
+  }
+
+  return InterpolationSampling::kUnknown;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/entry_point.h b/src/tint/inspector/entry_point.h
new file mode 100644
index 0000000..46b87dd
--- /dev/null
+++ b/src/tint/inspector/entry_point.h
@@ -0,0 +1,187 @@
+// Copyright 2020 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_INSPECTOR_ENTRY_POINT_H_
+#define SRC_TINT_INSPECTOR_ENTRY_POINT_H_
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "src/tint/ast/interpolate_attribute.h"
+#include "src/tint/ast/pipeline_stage.h"
+
+namespace tint {
+namespace inspector {
+
+/// Base component type of a stage variable.
+enum class ComponentType {
+  kUnknown = -1,
+  kFloat,
+  kUInt,
+  kSInt,
+};
+
+/// Composition of components of a stage variable.
+enum class CompositionType {
+  kUnknown = -1,
+  kScalar,
+  kVec2,
+  kVec3,
+  kVec4,
+};
+
+/// Type of interpolation of a stage variable.
+enum class InterpolationType { kUnknown = -1, kPerspective, kLinear, kFlat };
+
+/// Type of interpolation sampling of a stage variable.
+enum class InterpolationSampling {
+  kUnknown = -1,
+  kNone,
+  kCenter,
+  kCentroid,
+  kSample
+};
+
+/// Reflection data about an entry point input or output.
+struct StageVariable {
+  /// Constructor
+  StageVariable();
+  /// Copy constructor
+  /// @param other the StageVariable to copy
+  StageVariable(const StageVariable& other);
+  /// Destructor
+  ~StageVariable();
+
+  /// Name of the variable in the shader.
+  std::string name;
+  /// Is location attribute present
+  bool has_location_attribute = false;
+  /// Value of the location attribute, only valid if #has_location_attribute is
+  /// true.
+  uint32_t location_attribute;
+  /// Is Location attribute present
+  /// [DEPRECATED]: Use #has_location_attribute
+  bool& has_location_decoration = has_location_attribute;
+  /// Value of Location Decoration, only valid if #has_location_decoration is
+  /// true.
+  /// [DEPRECATED]: Use #location_attribute
+  uint32_t& location_decoration = location_attribute;
+  /// Scalar type that the variable is composed of.
+  ComponentType component_type = ComponentType::kUnknown;
+  /// How the scalars are composed for the variable.
+  CompositionType composition_type = CompositionType::kUnknown;
+  /// Interpolation type of the variable.
+  InterpolationType interpolation_type = InterpolationType::kUnknown;
+  /// Interpolation sampling of the variable.
+  InterpolationSampling interpolation_sampling =
+      InterpolationSampling::kUnknown;
+};
+
+/// Convert from internal ast::InterpolationType to public ::InterpolationType.
+/// @param ast_type internal value to convert from
+/// @returns the publicly visible equivalent
+InterpolationType ASTToInspectorInterpolationType(
+    ast::InterpolationType ast_type);
+
+/// Convert from internal ast::InterpolationSampling to public
+/// ::InterpolationSampling
+/// @param sampling internal value to convert from
+/// @returns the publicly visible equivalent
+InterpolationSampling ASTToInspectorInterpolationSampling(
+    ast::InterpolationSampling sampling);
+
+/// Reflection data about a pipeline overridable constant referenced by an entry
+/// point
+struct OverridableConstant {
+  /// Name of the constant
+  std::string name;
+
+  /// ID of the constant
+  uint16_t numeric_id;
+
+  /// Type of the scalar
+  enum class Type {
+    kBool,
+    kFloat32,
+    kUint32,
+    kInt32,
+  };
+
+  /// Type of the scalar
+  Type type;
+
+  /// Does this pipeline overridable constant have an initializer?
+  bool is_initialized = false;
+
+  /// Does this pipeline overridable constant have a numeric ID specified
+  /// explicitly?
+  bool is_numeric_id_specified = false;
+};
+
+/// Reflection data for an entry point in the shader.
+struct EntryPoint {
+  /// Constructors
+  EntryPoint();
+  /// Copy Constructor
+  EntryPoint(EntryPoint&);
+  /// Move Constructor
+  EntryPoint(EntryPoint&&);
+  ~EntryPoint();
+
+  /// The entry point name
+  std::string name;
+  /// Remapped entry point name in the backend
+  std::string remapped_name;
+  /// The entry point stage
+  ast::PipelineStage stage = ast::PipelineStage::kNone;
+  /// The workgroup x size
+  uint32_t workgroup_size_x = 0;
+  /// The workgroup y size
+  uint32_t workgroup_size_y = 0;
+  /// The workgroup z size
+  uint32_t workgroup_size_z = 0;
+  /// List of the input variable accessed via this entry point.
+  std::vector<StageVariable> input_variables;
+  /// List of the output variable accessed via this entry point.
+  std::vector<StageVariable> output_variables;
+  /// List of the pipeline overridable constants accessed via this entry point.
+  std::vector<OverridableConstant> overridable_constants;
+  /// Does the entry point use the sample_mask builtin as an input builtin
+  /// variable.
+  bool input_sample_mask_used = false;
+  /// Does the entry point use the sample_mask builtin as an output builtin
+  /// variable.
+  bool output_sample_mask_used = false;
+  /// Does the entry point use the position builtin as an input builtin
+  /// variable.
+  bool input_position_used = false;
+  /// Does the entry point use the front_facing builtin
+  bool front_facing_used = false;
+  /// Does the entry point use the sample_index builtin
+  bool sample_index_used = false;
+  /// Does the entry point use the num_workgroups builtin
+  bool num_workgroups_used = false;
+
+  /// @returns the size of the workgroup in {x,y,z} format
+  std::tuple<uint32_t, uint32_t, uint32_t> workgroup_size() {
+    return std::tuple<uint32_t, uint32_t, uint32_t>(
+        workgroup_size_x, workgroup_size_y, workgroup_size_z);
+  }
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_ENTRY_POINT_H_
diff --git a/src/tint/inspector/inspector.cc b/src/tint/inspector/inspector.cc
new file mode 100644
index 0000000..d4f9b5e
--- /dev/null
+++ b/src/tint/inspector/inspector.cc
@@ -0,0 +1,956 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/inspector.h"
+
+#include <limits>
+#include <utility>
+
+#include "src/tint/ast/bool_literal_expression.h"
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/float_literal_expression.h"
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/interpolate_attribute.h"
+#include "src/tint/ast/location_attribute.h"
+#include "src/tint/ast/module.h"
+#include "src/tint/ast/sint_literal_expression.h"
+#include "src/tint/ast/uint_literal_expression.h"
+#include "src/tint/sem/array.h"
+#include "src/tint/sem/call.h"
+#include "src/tint/sem/depth_multisampled_texture_type.h"
+#include "src/tint/sem/f32_type.h"
+#include "src/tint/sem/function.h"
+#include "src/tint/sem/i32_type.h"
+#include "src/tint/sem/matrix_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/statement.h"
+#include "src/tint/sem/storage_texture_type.h"
+#include "src/tint/sem/struct.h"
+#include "src/tint/sem/u32_type.h"
+#include "src/tint/sem/variable.h"
+#include "src/tint/sem/vector_type.h"
+#include "src/tint/sem/void_type.h"
+#include "src/tint/utils/math.h"
+#include "src/tint/utils/unique_vector.h"
+
+namespace tint {
+namespace inspector {
+
+namespace {
+
+void AppendResourceBindings(std::vector<ResourceBinding>* dest,
+                            const std::vector<ResourceBinding>& orig) {
+  TINT_ASSERT(Inspector, dest);
+  if (!dest) {
+    return;
+  }
+
+  dest->reserve(dest->size() + orig.size());
+  dest->insert(dest->end(), orig.begin(), orig.end());
+}
+
+std::tuple<ComponentType, CompositionType> CalculateComponentAndComposition(
+    const sem::Type* type) {
+  if (type->is_float_scalar()) {
+    return {ComponentType::kFloat, CompositionType::kScalar};
+  } else if (type->is_float_vector()) {
+    auto* vec = type->As<sem::Vector>();
+    if (vec->Width() == 2) {
+      return {ComponentType::kFloat, CompositionType::kVec2};
+    } else if (vec->Width() == 3) {
+      return {ComponentType::kFloat, CompositionType::kVec3};
+    } else if (vec->Width() == 4) {
+      return {ComponentType::kFloat, CompositionType::kVec4};
+    }
+  } else if (type->is_unsigned_integer_scalar()) {
+    return {ComponentType::kUInt, CompositionType::kScalar};
+  } else if (type->is_unsigned_integer_vector()) {
+    auto* vec = type->As<sem::Vector>();
+    if (vec->Width() == 2) {
+      return {ComponentType::kUInt, CompositionType::kVec2};
+    } else if (vec->Width() == 3) {
+      return {ComponentType::kUInt, CompositionType::kVec3};
+    } else if (vec->Width() == 4) {
+      return {ComponentType::kUInt, CompositionType::kVec4};
+    }
+  } else if (type->is_signed_integer_scalar()) {
+    return {ComponentType::kSInt, CompositionType::kScalar};
+  } else if (type->is_signed_integer_vector()) {
+    auto* vec = type->As<sem::Vector>();
+    if (vec->Width() == 2) {
+      return {ComponentType::kSInt, CompositionType::kVec2};
+    } else if (vec->Width() == 3) {
+      return {ComponentType::kSInt, CompositionType::kVec3};
+    } else if (vec->Width() == 4) {
+      return {ComponentType::kSInt, CompositionType::kVec4};
+    }
+  }
+  return {ComponentType::kUnknown, CompositionType::kUnknown};
+}
+
+std::tuple<InterpolationType, InterpolationSampling> CalculateInterpolationData(
+    const sem::Type* type,
+    const ast::AttributeList& attributes) {
+  auto* interpolation_attribute =
+      ast::GetAttribute<ast::InterpolateAttribute>(attributes);
+  if (type->is_integer_scalar_or_vector()) {
+    return {InterpolationType::kFlat, InterpolationSampling::kNone};
+  }
+
+  if (!interpolation_attribute) {
+    return {InterpolationType::kPerspective, InterpolationSampling::kCenter};
+  }
+
+  auto interpolation_type = interpolation_attribute->type;
+  auto sampling = interpolation_attribute->sampling;
+  if (interpolation_type != ast::InterpolationType::kFlat &&
+      sampling == ast::InterpolationSampling::kNone) {
+    sampling = ast::InterpolationSampling::kCenter;
+  }
+  return {ASTToInspectorInterpolationType(interpolation_type),
+          ASTToInspectorInterpolationSampling(sampling)};
+}
+
+}  // namespace
+
+Inspector::Inspector(const Program* program) : program_(program) {}
+
+Inspector::~Inspector() = default;
+
+std::vector<EntryPoint> Inspector::GetEntryPoints() {
+  std::vector<EntryPoint> result;
+
+  for (auto* func : program_->AST().Functions()) {
+    if (!func->IsEntryPoint()) {
+      continue;
+    }
+
+    auto* sem = program_->Sem().Get(func);
+
+    EntryPoint entry_point;
+    entry_point.name = program_->Symbols().NameFor(func->symbol);
+    entry_point.remapped_name = program_->Symbols().NameFor(func->symbol);
+    entry_point.stage = func->PipelineStage();
+
+    auto wgsize = sem->WorkgroupSize();
+    entry_point.workgroup_size_x = wgsize[0].value;
+    entry_point.workgroup_size_y = wgsize[1].value;
+    entry_point.workgroup_size_z = wgsize[2].value;
+    if (wgsize[0].overridable_const || wgsize[1].overridable_const ||
+        wgsize[2].overridable_const) {
+      // TODO(crbug.com/tint/713): Handle overridable constants.
+      TINT_ASSERT(Inspector, false);
+    }
+
+    for (auto* param : sem->Parameters()) {
+      AddEntryPointInOutVariables(
+          program_->Symbols().NameFor(param->Declaration()->symbol),
+          param->Type(), param->Declaration()->attributes,
+          entry_point.input_variables);
+
+      entry_point.input_position_used |=
+          ContainsBuiltin(ast::Builtin::kPosition, param->Type(),
+                          param->Declaration()->attributes);
+      entry_point.front_facing_used |=
+          ContainsBuiltin(ast::Builtin::kFrontFacing, param->Type(),
+                          param->Declaration()->attributes);
+      entry_point.sample_index_used |=
+          ContainsBuiltin(ast::Builtin::kSampleIndex, param->Type(),
+                          param->Declaration()->attributes);
+      entry_point.input_sample_mask_used |=
+          ContainsBuiltin(ast::Builtin::kSampleMask, param->Type(),
+                          param->Declaration()->attributes);
+      entry_point.num_workgroups_used |=
+          ContainsBuiltin(ast::Builtin::kNumWorkgroups, param->Type(),
+                          param->Declaration()->attributes);
+    }
+
+    if (!sem->ReturnType()->Is<sem::Void>()) {
+      AddEntryPointInOutVariables("<retval>", sem->ReturnType(),
+                                  func->return_type_attributes,
+                                  entry_point.output_variables);
+
+      entry_point.output_sample_mask_used =
+          ContainsBuiltin(ast::Builtin::kSampleMask, sem->ReturnType(),
+                          func->return_type_attributes);
+    }
+
+    for (auto* var : sem->TransitivelyReferencedGlobals()) {
+      auto* decl = var->Declaration();
+
+      auto name = program_->Symbols().NameFor(decl->symbol);
+
+      auto* global = var->As<sem::GlobalVariable>();
+      if (global && global->IsOverridable()) {
+        OverridableConstant overridable_constant;
+        overridable_constant.name = name;
+        overridable_constant.numeric_id = global->ConstantId();
+        auto* type = var->Type();
+        TINT_ASSERT(Inspector, type->is_scalar());
+        if (type->is_bool_scalar_or_vector()) {
+          overridable_constant.type = OverridableConstant::Type::kBool;
+        } else if (type->is_float_scalar()) {
+          overridable_constant.type = OverridableConstant::Type::kFloat32;
+        } else if (type->is_signed_integer_scalar()) {
+          overridable_constant.type = OverridableConstant::Type::kInt32;
+        } else if (type->is_unsigned_integer_scalar()) {
+          overridable_constant.type = OverridableConstant::Type::kUint32;
+        } else {
+          TINT_UNREACHABLE(Inspector, diagnostics_);
+        }
+
+        overridable_constant.is_initialized =
+            global->Declaration()->constructor;
+        overridable_constant.is_numeric_id_specified =
+            ast::HasAttribute<ast::IdAttribute>(
+                global->Declaration()->attributes);
+
+        entry_point.overridable_constants.push_back(overridable_constant);
+      }
+    }
+
+    result.push_back(std::move(entry_point));
+  }
+
+  return result;
+}
+
+std::map<uint32_t, Scalar> Inspector::GetConstantIDs() {
+  std::map<uint32_t, Scalar> result;
+  for (auto* var : program_->AST().GlobalVariables()) {
+    auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    if (!global || !global->IsOverridable()) {
+      continue;
+    }
+
+    // If there are conflicting defintions for a constant id, that is invalid
+    // WGSL, so the resolver should catch it. Thus here the inspector just
+    // assumes all definitions of the constant id are the same, so only needs
+    // to find the first reference to constant id.
+    uint32_t constant_id = global->ConstantId();
+    if (result.find(constant_id) != result.end()) {
+      continue;
+    }
+
+    if (!var->constructor) {
+      result[constant_id] = Scalar();
+      continue;
+    }
+
+    auto* literal = var->constructor->As<ast::LiteralExpression>();
+    if (!literal) {
+      // This is invalid WGSL, but handling gracefully.
+      result[constant_id] = Scalar();
+      continue;
+    }
+
+    if (auto* l = literal->As<ast::BoolLiteralExpression>()) {
+      result[constant_id] = Scalar(l->value);
+      continue;
+    }
+
+    if (auto* l = literal->As<ast::UintLiteralExpression>()) {
+      result[constant_id] = Scalar(l->value);
+      continue;
+    }
+
+    if (auto* l = literal->As<ast::SintLiteralExpression>()) {
+      result[constant_id] = Scalar(l->value);
+      continue;
+    }
+
+    if (auto* l = literal->As<ast::FloatLiteralExpression>()) {
+      result[constant_id] = Scalar(l->value);
+      continue;
+    }
+
+    result[constant_id] = Scalar();
+  }
+
+  return result;
+}
+
+std::map<std::string, uint32_t> Inspector::GetConstantNameToIdMap() {
+  std::map<std::string, uint32_t> result;
+  for (auto* var : program_->AST().GlobalVariables()) {
+    auto* global = program_->Sem().Get<sem::GlobalVariable>(var);
+    if (global && global->IsOverridable()) {
+      auto name = program_->Symbols().NameFor(var->symbol);
+      result[name] = global->ConstantId();
+    }
+  }
+  return result;
+}
+
+uint32_t Inspector::GetStorageSize(const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return 0;
+  }
+
+  size_t size = 0;
+  auto* func_sem = program_->Sem().Get(func);
+  for (auto& ruv : func_sem->TransitivelyReferencedUniformVariables()) {
+    size += ruv.first->Type()->UnwrapRef()->Size();
+  }
+  for (auto& rsv : func_sem->TransitivelyReferencedStorageBufferVariables()) {
+    size += rsv.first->Type()->UnwrapRef()->Size();
+  }
+
+  if (static_cast<uint64_t>(size) >
+      static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
+    return std::numeric_limits<uint32_t>::max();
+  }
+  return static_cast<uint32_t>(size);
+}
+
+std::vector<ResourceBinding> Inspector::GetResourceBindings(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+  for (auto fn : {
+           &Inspector::GetUniformBufferResourceBindings,
+           &Inspector::GetStorageBufferResourceBindings,
+           &Inspector::GetReadOnlyStorageBufferResourceBindings,
+           &Inspector::GetSamplerResourceBindings,
+           &Inspector::GetComparisonSamplerResourceBindings,
+           &Inspector::GetSampledTextureResourceBindings,
+           &Inspector::GetMultisampledTextureResourceBindings,
+           &Inspector::GetWriteOnlyStorageTextureResourceBindings,
+           &Inspector::GetDepthTextureResourceBindings,
+           &Inspector::GetDepthMultisampledTextureResourceBindings,
+           &Inspector::GetExternalTextureResourceBindings,
+       }) {
+    AppendResourceBindings(&result, (this->*fn)(entry_point));
+  }
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetUniformBufferResourceBindings(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+
+  auto* func_sem = program_->Sem().Get(func);
+  for (auto& ruv : func_sem->TransitivelyReferencedUniformVariables()) {
+    auto* var = ruv.first;
+    auto binding_info = ruv.second;
+
+    auto* unwrapped_type = var->Type()->UnwrapRef();
+
+    ResourceBinding entry;
+    entry.resource_type = ResourceBinding::ResourceType::kUniformBuffer;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+    entry.size = unwrapped_type->Size();
+    entry.size_no_padding = entry.size;
+    if (auto* str = unwrapped_type->As<sem::Struct>()) {
+      entry.size_no_padding = str->SizeNoPadding();
+    } else {
+      entry.size_no_padding = entry.size;
+    }
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetStorageBufferResourceBindings(
+    const std::string& entry_point) {
+  return GetStorageBufferResourceBindingsImpl(entry_point, false);
+}
+
+std::vector<ResourceBinding>
+Inspector::GetReadOnlyStorageBufferResourceBindings(
+    const std::string& entry_point) {
+  return GetStorageBufferResourceBindingsImpl(entry_point, true);
+}
+
+std::vector<ResourceBinding> Inspector::GetSamplerResourceBindings(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+
+  auto* func_sem = program_->Sem().Get(func);
+  for (auto& rs : func_sem->TransitivelyReferencedSamplerVariables()) {
+    auto binding_info = rs.second;
+
+    ResourceBinding entry;
+    entry.resource_type = ResourceBinding::ResourceType::kSampler;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetComparisonSamplerResourceBindings(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+
+  auto* func_sem = program_->Sem().Get(func);
+  for (auto& rcs :
+       func_sem->TransitivelyReferencedComparisonSamplerVariables()) {
+    auto binding_info = rcs.second;
+
+    ResourceBinding entry;
+    entry.resource_type = ResourceBinding::ResourceType::kComparisonSampler;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetSampledTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetSampledTextureResourceBindingsImpl(entry_point, false);
+}
+
+std::vector<ResourceBinding> Inspector::GetMultisampledTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetSampledTextureResourceBindingsImpl(entry_point, true);
+}
+
+std::vector<ResourceBinding>
+Inspector::GetWriteOnlyStorageTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetStorageTextureResourceBindingsImpl(entry_point);
+}
+
+std::vector<ResourceBinding> Inspector::GetTextureResourceBindings(
+    const std::string& entry_point,
+    const tint::TypeInfo* texture_type,
+    ResourceBinding::ResourceType resource_type) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+  auto* func_sem = program_->Sem().Get(func);
+  for (auto& ref :
+       func_sem->TransitivelyReferencedVariablesOfType(texture_type)) {
+    auto* var = ref.first;
+    auto binding_info = ref.second;
+
+    ResourceBinding entry;
+    entry.resource_type = resource_type;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+
+    auto* tex = var->Type()->UnwrapRef()->As<sem::Texture>();
+    entry.dim =
+        TypeTextureDimensionToResourceBindingTextureDimension(tex->dim());
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetDepthTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetTextureResourceBindings(
+      entry_point, &TypeInfo::Of<sem::DepthTexture>(),
+      ResourceBinding::ResourceType::kDepthTexture);
+}
+
+std::vector<ResourceBinding>
+Inspector::GetDepthMultisampledTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetTextureResourceBindings(
+      entry_point, &TypeInfo::Of<sem::DepthMultisampledTexture>(),
+      ResourceBinding::ResourceType::kDepthMultisampledTexture);
+}
+
+std::vector<ResourceBinding> Inspector::GetExternalTextureResourceBindings(
+    const std::string& entry_point) {
+  return GetTextureResourceBindings(
+      entry_point, &TypeInfo::Of<sem::ExternalTexture>(),
+      ResourceBinding::ResourceType::kExternalTexture);
+}
+
+std::vector<sem::SamplerTexturePair> Inspector::GetSamplerTextureUses(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  GenerateSamplerTargets();
+
+  auto it = sampler_targets_->find(entry_point);
+  if (it == sampler_targets_->end()) {
+    return {};
+  }
+  return it->second;
+}
+
+std::vector<sem::SamplerTexturePair> Inspector::GetSamplerTextureUses(
+    const std::string& entry_point,
+    const sem::BindingPoint& placeholder) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+  auto* func_sem = program_->Sem().Get(func);
+
+  std::vector<sem::SamplerTexturePair> new_pairs;
+  for (auto pair : func_sem->TextureSamplerPairs()) {
+    auto* texture = pair.first->As<sem::GlobalVariable>();
+    auto* sampler =
+        pair.second ? pair.second->As<sem::GlobalVariable>() : nullptr;
+    SamplerTexturePair new_pair;
+    new_pair.sampler_binding_point =
+        sampler ? sampler->BindingPoint() : placeholder;
+    new_pair.texture_binding_point = texture->BindingPoint();
+    new_pairs.push_back(new_pair);
+  }
+  return new_pairs;
+}
+
+uint32_t Inspector::GetWorkgroupStorageSize(const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return 0;
+  }
+
+  uint32_t total_size = 0;
+  auto* func_sem = program_->Sem().Get(func);
+  for (const sem::Variable* var : func_sem->TransitivelyReferencedGlobals()) {
+    if (var->StorageClass() == ast::StorageClass::kWorkgroup) {
+      auto* ty = var->Type()->UnwrapRef();
+      uint32_t align = ty->Align();
+      uint32_t size = ty->Size();
+
+      // This essentially matches std430 layout rules from GLSL, which are in
+      // turn specified as an upper bound for Vulkan layout sizing. Since D3D
+      // and Metal are even less specific, we assume Vulkan behavior as a
+      // good-enough approximation everywhere.
+      total_size += utils::RoundUp(align, size);
+    }
+  }
+
+  return total_size;
+}
+
+const ast::Function* Inspector::FindEntryPointByName(const std::string& name) {
+  auto* func = program_->AST().Functions().Find(program_->Symbols().Get(name));
+  if (!func) {
+    diagnostics_.add_error(diag::System::Inspector, name + " was not found!");
+    return nullptr;
+  }
+
+  if (!func->IsEntryPoint()) {
+    diagnostics_.add_error(diag::System::Inspector,
+                           name + " is not an entry point!");
+    return nullptr;
+  }
+
+  return func;
+}
+
+void Inspector::AddEntryPointInOutVariables(
+    std::string name,
+    const sem::Type* type,
+    const ast::AttributeList& attributes,
+    std::vector<StageVariable>& variables) const {
+  // Skip builtins.
+  if (ast::HasAttribute<ast::BuiltinAttribute>(attributes)) {
+    return;
+  }
+
+  auto* unwrapped_type = type->UnwrapRef();
+
+  if (auto* struct_ty = unwrapped_type->As<sem::Struct>()) {
+    // Recurse into members.
+    for (auto* member : struct_ty->Members()) {
+      AddEntryPointInOutVariables(
+          name + "." +
+              program_->Symbols().NameFor(member->Declaration()->symbol),
+          member->Type(), member->Declaration()->attributes, variables);
+    }
+    return;
+  }
+
+  // Base case: add the variable.
+
+  StageVariable stage_variable;
+  stage_variable.name = name;
+  std::tie(stage_variable.component_type, stage_variable.composition_type) =
+      CalculateComponentAndComposition(type);
+
+  auto* location = ast::GetAttribute<ast::LocationAttribute>(attributes);
+  TINT_ASSERT(Inspector, location != nullptr);
+  stage_variable.has_location_attribute = true;
+  stage_variable.location_attribute = location->value;
+
+  std::tie(stage_variable.interpolation_type,
+           stage_variable.interpolation_sampling) =
+      CalculateInterpolationData(type, attributes);
+
+  variables.push_back(stage_variable);
+}
+
+bool Inspector::ContainsBuiltin(ast::Builtin builtin,
+                                const sem::Type* type,
+                                const ast::AttributeList& attributes) const {
+  auto* unwrapped_type = type->UnwrapRef();
+
+  if (auto* struct_ty = unwrapped_type->As<sem::Struct>()) {
+    // Recurse into members.
+    for (auto* member : struct_ty->Members()) {
+      if (ContainsBuiltin(builtin, member->Type(),
+                          member->Declaration()->attributes)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Base case: check for builtin
+  auto* builtin_declaration =
+      ast::GetAttribute<ast::BuiltinAttribute>(attributes);
+  if (!builtin_declaration || builtin_declaration->builtin != builtin) {
+    return false;
+  }
+
+  return true;
+}
+
+std::vector<ResourceBinding> Inspector::GetStorageBufferResourceBindingsImpl(
+    const std::string& entry_point,
+    bool read_only) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  auto* func_sem = program_->Sem().Get(func);
+  std::vector<ResourceBinding> result;
+  for (auto& rsv : func_sem->TransitivelyReferencedStorageBufferVariables()) {
+    auto* var = rsv.first;
+    auto binding_info = rsv.second;
+
+    if (read_only != (var->Access() == ast::Access::kRead)) {
+      continue;
+    }
+
+    auto* unwrapped_type = var->Type()->UnwrapRef();
+
+    ResourceBinding entry;
+    entry.resource_type =
+        read_only ? ResourceBinding::ResourceType::kReadOnlyStorageBuffer
+                  : ResourceBinding::ResourceType::kStorageBuffer;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+    entry.size = unwrapped_type->Size();
+    if (auto* str = unwrapped_type->As<sem::Struct>()) {
+      entry.size_no_padding = str->SizeNoPadding();
+    } else {
+      entry.size_no_padding = entry.size;
+    }
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetSampledTextureResourceBindingsImpl(
+    const std::string& entry_point,
+    bool multisampled_only) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  std::vector<ResourceBinding> result;
+  auto* func_sem = program_->Sem().Get(func);
+  auto referenced_variables =
+      multisampled_only
+          ? func_sem->TransitivelyReferencedMultisampledTextureVariables()
+          : func_sem->TransitivelyReferencedSampledTextureVariables();
+  for (auto& ref : referenced_variables) {
+    auto* var = ref.first;
+    auto binding_info = ref.second;
+
+    ResourceBinding entry;
+    entry.resource_type =
+        multisampled_only ? ResourceBinding::ResourceType::kMultisampledTexture
+                          : ResourceBinding::ResourceType::kSampledTexture;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+
+    auto* texture_type = var->Type()->UnwrapRef()->As<sem::Texture>();
+    entry.dim = TypeTextureDimensionToResourceBindingTextureDimension(
+        texture_type->dim());
+
+    const sem::Type* base_type = nullptr;
+    if (multisampled_only) {
+      base_type = texture_type->As<sem::MultisampledTexture>()->type();
+    } else {
+      base_type = texture_type->As<sem::SampledTexture>()->type();
+    }
+    entry.sampled_kind = BaseTypeToSampledKind(base_type);
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+std::vector<ResourceBinding> Inspector::GetStorageTextureResourceBindingsImpl(
+    const std::string& entry_point) {
+  auto* func = FindEntryPointByName(entry_point);
+  if (!func) {
+    return {};
+  }
+
+  auto* func_sem = program_->Sem().Get(func);
+  std::vector<ResourceBinding> result;
+  for (auto& ref :
+       func_sem->TransitivelyReferencedVariablesOfType<sem::StorageTexture>()) {
+    auto* var = ref.first;
+    auto binding_info = ref.second;
+
+    auto* texture_type = var->Type()->UnwrapRef()->As<sem::StorageTexture>();
+
+    ResourceBinding entry;
+    entry.resource_type =
+        ResourceBinding::ResourceType::kWriteOnlyStorageTexture;
+    entry.bind_group = binding_info.group->value;
+    entry.binding = binding_info.binding->value;
+
+    entry.dim = TypeTextureDimensionToResourceBindingTextureDimension(
+        texture_type->dim());
+
+    auto* base_type = texture_type->type();
+    entry.sampled_kind = BaseTypeToSampledKind(base_type);
+    entry.image_format = TypeTexelFormatToResourceBindingTexelFormat(
+        texture_type->texel_format());
+
+    result.push_back(entry);
+  }
+
+  return result;
+}
+
+void Inspector::GenerateSamplerTargets() {
+  // Do not re-generate, since |program_| should not change during the lifetime
+  // of the inspector.
+  if (sampler_targets_ != nullptr) {
+    return;
+  }
+
+  sampler_targets_ = std::make_unique<std::unordered_map<
+      std::string, utils::UniqueVector<sem::SamplerTexturePair>>>();
+
+  auto& sem = program_->Sem();
+
+  for (auto* node : program_->ASTNodes().Objects()) {
+    auto* c = node->As<ast::CallExpression>();
+    if (!c) {
+      continue;
+    }
+
+    auto* call = sem.Get(c);
+    if (!call) {
+      continue;
+    }
+
+    auto* i = call->Target()->As<sem::Builtin>();
+    if (!i) {
+      continue;
+    }
+
+    const auto& signature = i->Signature();
+    int sampler_index = signature.IndexOf(sem::ParameterUsage::kSampler);
+    if (sampler_index == -1) {
+      continue;
+    }
+
+    int texture_index = signature.IndexOf(sem::ParameterUsage::kTexture);
+    if (texture_index == -1) {
+      continue;
+    }
+
+    auto* call_func = call->Stmt()->Function();
+    std::vector<const sem::Function*> entry_points;
+    if (call_func->Declaration()->IsEntryPoint()) {
+      entry_points = {call_func};
+    } else {
+      entry_points = call_func->AncestorEntryPoints();
+    }
+
+    if (entry_points.empty()) {
+      continue;
+    }
+
+    auto* t = c->args[texture_index];
+    auto* s = c->args[sampler_index];
+
+    GetOriginatingResources(
+        std::array<const ast::Expression*, 2>{t, s},
+        [&](std::array<const sem::GlobalVariable*, 2> globals) {
+          auto* texture = globals[0];
+          sem::BindingPoint texture_binding_point = {
+              texture->Declaration()->BindingPoint().group->value,
+              texture->Declaration()->BindingPoint().binding->value};
+
+          auto* sampler = globals[1];
+          sem::BindingPoint sampler_binding_point = {
+              sampler->Declaration()->BindingPoint().group->value,
+              sampler->Declaration()->BindingPoint().binding->value};
+
+          for (auto* entry_point : entry_points) {
+            const auto& ep_name =
+                program_->Symbols().NameFor(entry_point->Declaration()->symbol);
+            (*sampler_targets_)[ep_name].add(
+                {sampler_binding_point, texture_binding_point});
+          }
+        });
+  }
+}
+
+template <size_t N, typename F>
+void Inspector::GetOriginatingResources(
+    std::array<const ast::Expression*, N> exprs,
+    F&& callback) {
+  if (!program_->IsValid()) {
+    TINT_ICE(Inspector, diagnostics_)
+        << "attempting to get originating resources in invalid program";
+    return;
+  }
+
+  auto& sem = program_->Sem();
+
+  std::array<const sem::GlobalVariable*, N> globals{};
+  std::array<const sem::Parameter*, N> parameters{};
+  utils::UniqueVector<const ast::CallExpression*> callsites;
+
+  for (size_t i = 0; i < N; i++) {
+    auto*& expr = exprs[i];
+    // Resolve each of the expressions
+    while (true) {
+      if (auto* user = sem.Get<sem::VariableUser>(expr)) {
+        auto* var = user->Variable();
+
+        if (auto* global = tint::As<sem::GlobalVariable>(var)) {
+          // Found the global resource declaration.
+          globals[i] = global;
+          break;  // Done with this expression.
+        }
+
+        if (auto* local = tint::As<sem::LocalVariable>(var)) {
+          // Chase the variable
+          expr = local->Declaration()->constructor;
+          if (!expr) {
+            TINT_ICE(Inspector, diagnostics_)
+                << "resource variable had no initializer";
+            return;
+          }
+          continue;  // Continue chasing the expression in this function
+        }
+
+        if (auto* param = tint::As<sem::Parameter>(var)) {
+          // Gather each of the callers of this function
+          auto* func = tint::As<sem::Function>(param->Owner());
+          if (func->CallSites().empty()) {
+            // One or more of the expressions is a parameter, but this function
+            // is not called. Ignore.
+            return;
+          }
+          for (auto* call : func->CallSites()) {
+            callsites.add(call->Declaration());
+          }
+          // Need to evaluate each function call with the group of
+          // expressions, so move on to the next expression.
+          parameters[i] = param;
+          break;
+        }
+
+        TINT_ICE(Inspector, diagnostics_)
+            << "unexpected variable type " << var->TypeInfo().name;
+      }
+
+      if (auto* unary = tint::As<ast::UnaryOpExpression>(expr)) {
+        switch (unary->op) {
+          case ast::UnaryOp::kAddressOf:
+          case ast::UnaryOp::kIndirection:
+            // `*` and `&` are the only valid unary ops for a resource type,
+            // and must be balanced in order for the program to have passed
+            // validation. Just skip past these.
+            expr = unary->expr;
+            continue;
+          default: {
+            TINT_ICE(Inspector, diagnostics_)
+                << "unexpected unary op on resource: " << unary->op;
+            return;
+          }
+        }
+      }
+
+      TINT_ICE(Inspector, diagnostics_)
+          << "cannot resolve originating resource with expression type "
+          << expr->TypeInfo().name;
+      return;
+    }
+  }
+
+  if (callsites.size()) {
+    for (auto* call_expr : callsites) {
+      // Make a copy of the expressions for this callsite
+      std::array<const ast::Expression*, N> call_exprs = exprs;
+      // Patch all the parameter expressions with their argument
+      for (size_t i = 0; i < N; i++) {
+        if (auto* param = parameters[i]) {
+          call_exprs[i] = call_expr->args[param->Index()];
+        }
+      }
+      // Now call GetOriginatingResources() with from the callsite
+      GetOriginatingResources(call_exprs, callback);
+    }
+  } else {
+    // All the expressions resolved to globals
+    callback(globals);
+  }
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/inspector.h b/src/tint/inspector/inspector.h
new file mode 100644
index 0000000..df0ccab
--- /dev/null
+++ b/src/tint/inspector/inspector.h
@@ -0,0 +1,234 @@
+// Copyright 2020 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_INSPECTOR_INSPECTOR_H_
+#define SRC_TINT_INSPECTOR_INSPECTOR_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <unordered_map>
+#include <vector>
+
+#include "src/tint/inspector/entry_point.h"
+#include "src/tint/inspector/resource_binding.h"
+#include "src/tint/inspector/scalar.h"
+#include "src/tint/program.h"
+#include "src/tint/sem/sampler_texture_pair.h"
+#include "src/tint/utils/unique_vector.h"
+
+namespace tint {
+namespace inspector {
+
+/// A temporary alias to sem::SamplerTexturePair. [DEPRECATED]
+using SamplerTexturePair = sem::SamplerTexturePair;
+
+/// Extracts information from a program
+class Inspector {
+ public:
+  /// Constructor
+  /// @param program Shader program to extract information from.
+  explicit Inspector(const Program* program);
+
+  /// Destructor
+  ~Inspector();
+
+  /// @returns error messages from the Inspector
+  std::string error() { return diagnostics_.str(); }
+  /// @returns true if an error was encountered
+  bool has_error() const { return diagnostics_.contains_errors(); }
+
+  /// @returns vector of entry point information
+  std::vector<EntryPoint> GetEntryPoints();
+
+  /// @returns map of const_id to initial value
+  std::map<uint32_t, Scalar> GetConstantIDs();
+
+  /// @returns map of module-constant name to pipeline constant ID
+  std::map<std::string, uint32_t> GetConstantNameToIdMap();
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns the total size of shared storage required by an entry point,
+  ///          including all uniform storage buffers.
+  uint32_t GetStorageSize(const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the resource bindings.
+  std::vector<ResourceBinding> GetResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for uniform buffers.
+  std::vector<ResourceBinding> GetUniformBufferResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for storage buffers.
+  std::vector<ResourceBinding> GetStorageBufferResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for read-only storage buffers.
+  std::vector<ResourceBinding> GetReadOnlyStorageBufferResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for regular samplers.
+  std::vector<ResourceBinding> GetSamplerResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for comparison samplers.
+  std::vector<ResourceBinding> GetComparisonSamplerResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for sampled textures.
+  std::vector<ResourceBinding> GetSampledTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for multisampled textures.
+  std::vector<ResourceBinding> GetMultisampledTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for write-only storage textures.
+  std::vector<ResourceBinding> GetWriteOnlyStorageTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for depth textures.
+  std::vector<ResourceBinding> GetDepthTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for depth textures.
+  std::vector<ResourceBinding> GetDepthMultisampledTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for external textures.
+  std::vector<ResourceBinding> GetExternalTextureResourceBindings(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the sampler/texture sampling pairs that are used
+  /// by that entry point.
+  std::vector<sem::SamplerTexturePair> GetSamplerTextureUses(
+      const std::string& entry_point);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @param placeholder the sampler binding point to use for texture-only
+  /// access (e.g., textureLoad)
+  /// @returns vector of all of the sampler/texture sampling pairs that are used
+  /// by that entry point.
+  std::vector<sem::SamplerTexturePair> GetSamplerTextureUses(
+      const std::string& entry_point,
+      const sem::BindingPoint& placeholder);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns the total size in bytes of all Workgroup storage-class storage
+  /// referenced transitively by the entry point.
+  uint32_t GetWorkgroupStorageSize(const std::string& entry_point);
+
+ private:
+  const Program* program_;
+  diag::List diagnostics_;
+  std::unique_ptr<
+      std::unordered_map<std::string,
+                         utils::UniqueVector<sem::SamplerTexturePair>>>
+      sampler_targets_;
+
+  /// @param name name of the entry point to find
+  /// @returns a pointer to the entry point if it exists, otherwise returns
+  ///          nullptr and sets the error string.
+  const ast::Function* FindEntryPointByName(const std::string& name);
+
+  /// Recursively add entry point IO variables.
+  /// If `type` is a struct, recurse into members, appending the member name.
+  /// Otherwise, add the variable unless it is a builtin.
+  /// @param name the name of the variable being added
+  /// @param type the type of the variable
+  /// @param attributes the variable attributes
+  /// @param variables the list to add the variables to
+  void AddEntryPointInOutVariables(std::string name,
+                                   const sem::Type* type,
+                                   const ast::AttributeList& attributes,
+                                   std::vector<StageVariable>& variables) const;
+
+  /// Recursively determine if the type contains builtin.
+  /// If `type` is a struct, recurse into members to check for the attribute.
+  /// Otherwise, check `attributes` for the attribute.
+  bool ContainsBuiltin(ast::Builtin builtin,
+                       const sem::Type* type,
+                       const ast::AttributeList& attributes) const;
+
+  /// Gathers all the texture resource bindings of the given type for the given
+  /// entry point.
+  /// @param entry_point name of the entry point to get information about.
+  /// @param texture_type the type of the textures to gather.
+  /// @param resource_type the ResourceBinding::ResourceType for the given
+  /// texture type.
+  /// @returns vector of all of the bindings for depth textures.
+  std::vector<ResourceBinding> GetTextureResourceBindings(
+      const std::string& entry_point,
+      const tint::TypeInfo* texture_type,
+      ResourceBinding::ResourceType resource_type);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @param read_only if true get only read-only bindings, if false get
+  ///                  write-only bindings.
+  /// @returns vector of all of the bindings for the requested storage buffers.
+  std::vector<ResourceBinding> GetStorageBufferResourceBindingsImpl(
+      const std::string& entry_point,
+      bool read_only);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @param multisampled_only only get multisampled textures if true, otherwise
+  ///                          only get sampled textures.
+  /// @returns vector of all of the bindings for the request storage buffers.
+  std::vector<ResourceBinding> GetSampledTextureResourceBindingsImpl(
+      const std::string& entry_point,
+      bool multisampled_only);
+
+  /// @param entry_point name of the entry point to get information about.
+  /// @returns vector of all of the bindings for the requested storage textures.
+  std::vector<ResourceBinding> GetStorageTextureResourceBindingsImpl(
+      const std::string& entry_point);
+
+  /// Constructs |sampler_targets_| if it hasn't already been instantiated.
+  void GenerateSamplerTargets();
+
+  /// For a N-uple of expressions, resolve to the appropriate global resources
+  /// and call 'cb'.
+  /// 'cb' may be called multiple times.
+  /// Assumes that not being able to resolve the resources is an error, so will
+  /// invoke TINT_ICE when that occurs.
+  /// @tparam N number of expressions in the n-uple
+  /// @tparam F type of the callback provided.
+  /// @param exprs N-uple of expressions to resolve.
+  /// @param cb is a callback function with the signature:
+  /// `void(std::array<const sem::GlobalVariable*, N>)`, which is invoked
+  /// whenever a set of expressions are resolved to globals.
+  template <size_t N, typename F>
+  void GetOriginatingResources(std::array<const ast::Expression*, N> exprs,
+                               F&& cb);
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_INSPECTOR_H_
diff --git a/src/tint/inspector/inspector_test.cc b/src/tint/inspector/inspector_test.cc
new file mode 100644
index 0000000..dc8ba8f
--- /dev/null
+++ b/src/tint/inspector/inspector_test.cc
@@ -0,0 +1,3031 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/disable_validation_attribute.h"
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/workgroup_attribute.h"
+#include "src/tint/inspector/test_inspector_builder.h"
+#include "src/tint/inspector/test_inspector_runner.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/external_texture_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/variable.h"
+#include "tint/tint.h"
+
+namespace tint {
+namespace inspector {
+namespace {
+
+// All the tests that descend from InspectorBuilder are expected to define their
+// test state via building up the AST through InspectorBuilder and then generate
+// the program with ::Build.
+// The returned Inspector from ::Build can then be used to test expecations.
+//
+// All the tests that descend from InspectorRunner are expected to define their
+// test state via a WGSL shader, which will be parsed to generate a Program and
+// Inspector in ::Initialize.
+// The returned Inspector from ::Initialize can then be used to test
+// expecations.
+
+class InspectorGetEntryPointTest : public InspectorBuilder,
+                                   public testing::Test {};
+
+typedef std::tuple<inspector::ComponentType, inspector::CompositionType>
+    InspectorGetEntryPointComponentAndCompositionTestParams;
+class InspectorGetEntryPointComponentAndCompositionTest
+    : public InspectorBuilder,
+      public testing::TestWithParam<
+          InspectorGetEntryPointComponentAndCompositionTestParams> {};
+struct InspectorGetEntryPointInterpolateTestParams {
+  ast::InterpolationType in_type;
+  ast::InterpolationSampling in_sampling;
+  inspector::InterpolationType out_type;
+  inspector::InterpolationSampling out_sampling;
+};
+class InspectorGetEntryPointInterpolateTest
+    : public InspectorBuilder,
+      public testing::TestWithParam<
+          InspectorGetEntryPointInterpolateTestParams> {};
+class InspectorGetConstantIDsTest : public InspectorBuilder,
+                                    public testing::Test {};
+class InspectorGetConstantNameToIdMapTest : public InspectorBuilder,
+                                            public testing::Test {};
+class InspectorGetStorageSizeTest : public InspectorBuilder,
+                                    public testing::Test {};
+class InspectorGetResourceBindingsTest : public InspectorBuilder,
+                                         public testing::Test {};
+class InspectorGetUniformBufferResourceBindingsTest : public InspectorBuilder,
+                                                      public testing::Test {};
+class InspectorGetStorageBufferResourceBindingsTest : public InspectorBuilder,
+                                                      public testing::Test {};
+class InspectorGetReadOnlyStorageBufferResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+class InspectorGetSamplerResourceBindingsTest : public InspectorBuilder,
+                                                public testing::Test {};
+class InspectorGetComparisonSamplerResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+class InspectorGetSampledTextureResourceBindingsTest : public InspectorBuilder,
+                                                       public testing::Test {};
+class InspectorGetSampledArrayTextureResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+struct GetSampledTextureTestParams {
+  ast::TextureDimension type_dim;
+  inspector::ResourceBinding::TextureDimension inspector_dim;
+  inspector::ResourceBinding::SampledKind sampled_kind;
+};
+class InspectorGetSampledTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetSampledTextureTestParams> {};
+class InspectorGetSampledArrayTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetSampledTextureTestParams> {};
+class InspectorGetMultisampledTextureResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+class InspectorGetMultisampledArrayTextureResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+typedef GetSampledTextureTestParams GetMultisampledTextureTestParams;
+class InspectorGetMultisampledArrayTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetMultisampledTextureTestParams> {};
+class InspectorGetMultisampledTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetMultisampledTextureTestParams> {};
+class InspectorGetStorageTextureResourceBindingsTest : public InspectorBuilder,
+                                                       public testing::Test {};
+struct GetDepthTextureTestParams {
+  ast::TextureDimension type_dim;
+  inspector::ResourceBinding::TextureDimension inspector_dim;
+};
+class InspectorGetDepthTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetDepthTextureTestParams> {};
+
+class InspectorGetDepthMultisampledTextureResourceBindingsTest
+    : public InspectorBuilder,
+      public testing::Test {};
+
+typedef std::tuple<ast::TextureDimension, ResourceBinding::TextureDimension>
+    DimensionParams;
+typedef std::tuple<ast::TexelFormat,
+                   ResourceBinding::TexelFormat,
+                   ResourceBinding::SampledKind>
+    TexelFormatParams;
+typedef std::tuple<DimensionParams, TexelFormatParams>
+    GetStorageTextureTestParams;
+class InspectorGetStorageTextureResourceBindingsTestWithParam
+    : public InspectorBuilder,
+      public testing::TestWithParam<GetStorageTextureTestParams> {};
+
+class InspectorGetExternalTextureResourceBindingsTest : public InspectorBuilder,
+                                                        public testing::Test {};
+
+class InspectorGetSamplerTextureUsesTest : public InspectorRunner,
+                                           public testing::Test {};
+
+class InspectorGetWorkgroupStorageSizeTest : public InspectorBuilder,
+                                             public testing::Test {};
+
+// This is a catch all for shaders that have demonstrated regressions/crashes in
+// the wild.
+class InspectorRegressionTest : public InspectorRunner, public testing::Test {};
+
+TEST_F(InspectorGetEntryPointTest, NoFunctions) {
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, NoEntryPoints) {
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, OneEntryPoint) {
+  MakeEmptyBodyFunction("foo", ast::AttributeList{
+                                   Stage(ast::PipelineStage::kFragment),
+                               });
+
+  // TODO(dsinclair): Update to run the namer transform when available.
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ("foo", result[0].name);
+  EXPECT_EQ("foo", result[0].remapped_name);
+  EXPECT_EQ(ast::PipelineStage::kFragment, result[0].stage);
+}
+
+TEST_F(InspectorGetEntryPointTest, MultipleEntryPoints) {
+  MakeEmptyBodyFunction("foo", ast::AttributeList{
+                                   Stage(ast::PipelineStage::kFragment),
+                               });
+
+  MakeEmptyBodyFunction("bar",
+                        ast::AttributeList{Stage(ast::PipelineStage::kCompute),
+                                           WorkgroupSize(1)});
+
+  // TODO(dsinclair): Update to run the namer transform when available.
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(2u, result.size());
+  EXPECT_EQ("foo", result[0].name);
+  EXPECT_EQ("foo", result[0].remapped_name);
+  EXPECT_EQ(ast::PipelineStage::kFragment, result[0].stage);
+  EXPECT_EQ("bar", result[1].name);
+  EXPECT_EQ("bar", result[1].remapped_name);
+  EXPECT_EQ(ast::PipelineStage::kCompute, result[1].stage);
+}
+
+TEST_F(InspectorGetEntryPointTest, MixFunctionsAndEntryPoints) {
+  MakeEmptyBodyFunction("func", {});
+
+  MakeCallerBodyFunction("foo", {"func"},
+                         ast::AttributeList{Stage(ast::PipelineStage::kCompute),
+                                            WorkgroupSize(1)});
+
+  MakeCallerBodyFunction("bar", {"func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  // TODO(dsinclair): Update to run the namer transform when available.
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  EXPECT_FALSE(inspector.has_error());
+
+  ASSERT_EQ(2u, result.size());
+  EXPECT_EQ("foo", result[0].name);
+  EXPECT_EQ("foo", result[0].remapped_name);
+  EXPECT_EQ(ast::PipelineStage::kCompute, result[0].stage);
+  EXPECT_EQ("bar", result[1].name);
+  EXPECT_EQ("bar", result[1].remapped_name);
+  EXPECT_EQ(ast::PipelineStage::kFragment, result[1].stage);
+}
+
+TEST_F(InspectorGetEntryPointTest, DefaultWorkgroupSize) {
+  MakeEmptyBodyFunction("foo",
+                        ast::AttributeList{Stage(ast::PipelineStage::kCompute),
+                                           WorkgroupSize(8, 2, 1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+  uint32_t x, y, z;
+  std::tie(x, y, z) = result[0].workgroup_size();
+  EXPECT_EQ(8u, x);
+  EXPECT_EQ(2u, y);
+  EXPECT_EQ(1u, z);
+}
+
+TEST_F(InspectorGetEntryPointTest, NonDefaultWorkgroupSize) {
+  MakeEmptyBodyFunction(
+      "foo", {Stage(ast::PipelineStage::kCompute), WorkgroupSize(8, 2, 1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+  uint32_t x, y, z;
+  std::tie(x, y, z) = result[0].workgroup_size();
+  EXPECT_EQ(8u, x);
+  EXPECT_EQ(2u, y);
+  EXPECT_EQ(1u, z);
+}
+
+TEST_F(InspectorGetEntryPointTest, NoInOutVariables) {
+  MakeEmptyBodyFunction("func", {});
+
+  MakeCallerBodyFunction("foo", {"func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].input_variables.size());
+  EXPECT_EQ(0u, result[0].output_variables.size());
+}
+
+TEST_P(InspectorGetEntryPointComponentAndCompositionTest, Test) {
+  ComponentType component;
+  CompositionType composition;
+  std::tie(component, composition) = GetParam();
+  std::function<const ast::Type*()> tint_type =
+      GetTypeFunction(component, composition);
+
+  auto* in_var = Param("in_var", tint_type(), {Location(0u), Flat()});
+  Func("foo", {in_var}, tint_type(), {Return("in_var")},
+       {Stage(ast::PipelineStage::kFragment)}, {Location(0u)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  ASSERT_EQ(1u, result[0].input_variables.size());
+  EXPECT_EQ("in_var", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(component, result[0].input_variables[0].component_type);
+
+  ASSERT_EQ(1u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(component, result[0].output_variables[0].component_type);
+}
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetEntryPointTest,
+    InspectorGetEntryPointComponentAndCompositionTest,
+    testing::Combine(testing::Values(ComponentType::kFloat,
+                                     ComponentType::kSInt,
+                                     ComponentType::kUInt),
+                     testing::Values(CompositionType::kScalar,
+                                     CompositionType::kVec2,
+                                     CompositionType::kVec3,
+                                     CompositionType::kVec4)));
+
+TEST_F(InspectorGetEntryPointTest, MultipleInOutVariables) {
+  auto* in_var0 = Param("in_var0", ty.u32(), {Location(0u), Flat()});
+  auto* in_var1 = Param("in_var1", ty.u32(), {Location(1u), Flat()});
+  auto* in_var4 = Param("in_var4", ty.u32(), {Location(4u), Flat()});
+  Func("foo", {in_var0, in_var1, in_var4}, ty.u32(), {Return("in_var0")},
+       {Stage(ast::PipelineStage::kFragment)}, {Location(0u)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  ASSERT_EQ(3u, result[0].input_variables.size());
+  EXPECT_EQ("in_var0", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(InterpolationType::kFlat,
+            result[0].input_variables[0].interpolation_type);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[0].component_type);
+  EXPECT_EQ("in_var1", result[0].input_variables[1].name);
+  EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+  EXPECT_EQ(InterpolationType::kFlat,
+            result[0].input_variables[1].interpolation_type);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[1].component_type);
+  EXPECT_EQ("in_var4", result[0].input_variables[2].name);
+  EXPECT_TRUE(result[0].input_variables[2].has_location_attribute);
+  EXPECT_EQ(4u, result[0].input_variables[2].location_attribute);
+  EXPECT_EQ(InterpolationType::kFlat,
+            result[0].input_variables[2].interpolation_type);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[2].component_type);
+
+  ASSERT_EQ(1u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[0].component_type);
+}
+
+TEST_F(InspectorGetEntryPointTest, MultipleEntryPointsInOutVariables) {
+  auto* in_var_foo = Param("in_var_foo", ty.u32(), {Location(0u), Flat()});
+  Func("foo", {in_var_foo}, ty.u32(), {Return("in_var_foo")},
+       {Stage(ast::PipelineStage::kFragment)}, {Location(0u)});
+
+  auto* in_var_bar = Param("in_var_bar", ty.u32(), {Location(0u), Flat()});
+  Func("bar", {in_var_bar}, ty.u32(), {Return("in_var_bar")},
+       {Stage(ast::PipelineStage::kFragment)}, {Location(1u)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(2u, result.size());
+
+  ASSERT_EQ(1u, result[0].input_variables.size());
+  EXPECT_EQ("in_var_foo", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(InterpolationType::kFlat,
+            result[0].input_variables[0].interpolation_type);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[0].component_type);
+
+  ASSERT_EQ(1u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[0].component_type);
+
+  ASSERT_EQ(1u, result[1].input_variables.size());
+  EXPECT_EQ("in_var_bar", result[1].input_variables[0].name);
+  EXPECT_TRUE(result[1].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[1].input_variables[0].location_attribute);
+  EXPECT_EQ(InterpolationType::kFlat,
+            result[1].input_variables[0].interpolation_type);
+  EXPECT_EQ(ComponentType::kUInt, result[1].input_variables[0].component_type);
+
+  ASSERT_EQ(1u, result[1].output_variables.size());
+  EXPECT_EQ("<retval>", result[1].output_variables[0].name);
+  EXPECT_TRUE(result[1].output_variables[0].has_location_attribute);
+  EXPECT_EQ(1u, result[1].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[1].output_variables[0].component_type);
+}
+
+TEST_F(InspectorGetEntryPointTest, BuiltInsNotStageVariables) {
+  auto* in_var0 =
+      Param("in_var0", ty.u32(), {Builtin(ast::Builtin::kSampleIndex)});
+  auto* in_var1 = Param("in_var1", ty.f32(), {Location(0u)});
+  Func("foo", {in_var0, in_var1}, ty.f32(), {Return("in_var1")},
+       {Stage(ast::PipelineStage::kFragment)},
+       {Builtin(ast::Builtin::kFragDepth)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  ASSERT_EQ(1u, result[0].input_variables.size());
+  EXPECT_EQ("in_var1", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kFloat, result[0].input_variables[0].component_type);
+
+  ASSERT_EQ(0u, result[0].output_variables.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, InOutStruct) {
+  auto* interface = MakeInOutStruct("interface", {{"a", 0u}, {"b", 1u}});
+  Func("foo", {Param("param", ty.Of(interface))}, ty.Of(interface),
+       {Return("param")}, {Stage(ast::PipelineStage::kFragment)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  ASSERT_EQ(2u, result[0].input_variables.size());
+  EXPECT_EQ("param.a", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[0].component_type);
+  EXPECT_EQ("param.b", result[0].input_variables[1].name);
+  EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[1].component_type);
+
+  ASSERT_EQ(2u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[0].component_type);
+  EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
+  EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[1].component_type);
+}
+
+TEST_F(InspectorGetEntryPointTest, MultipleEntryPointsInOutSharedStruct) {
+  auto* interface = MakeInOutStruct("interface", {{"a", 0u}, {"b", 1u}});
+  Func("foo", {}, ty.Of(interface), {Return(Construct(ty.Of(interface)))},
+       {Stage(ast::PipelineStage::kFragment)});
+  Func("bar", {Param("param", ty.Of(interface))}, ty.void_(), {},
+       {Stage(ast::PipelineStage::kFragment)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(2u, result.size());
+
+  ASSERT_EQ(0u, result[0].input_variables.size());
+
+  ASSERT_EQ(2u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[0].component_type);
+  EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
+  EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[1].component_type);
+
+  ASSERT_EQ(2u, result[1].input_variables.size());
+  EXPECT_EQ("param.a", result[1].input_variables[0].name);
+  EXPECT_TRUE(result[1].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[1].input_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[1].input_variables[0].component_type);
+  EXPECT_EQ("param.b", result[1].input_variables[1].name);
+  EXPECT_TRUE(result[1].input_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[1].input_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[1].input_variables[1].component_type);
+
+  ASSERT_EQ(0u, result[1].output_variables.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, MixInOutVariablesAndStruct) {
+  auto* struct_a = MakeInOutStruct("struct_a", {{"a", 0u}, {"b", 1u}});
+  auto* struct_b = MakeInOutStruct("struct_b", {{"a", 2u}});
+  Func("foo",
+       {Param("param_a", ty.Of(struct_a)), Param("param_b", ty.Of(struct_b)),
+        Param("param_c", ty.f32(), {Location(3u)}),
+        Param("param_d", ty.f32(), {Location(4u)})},
+       ty.Of(struct_a), {Return("param_a")},
+       {Stage(ast::PipelineStage::kFragment)});
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  ASSERT_EQ(5u, result[0].input_variables.size());
+  EXPECT_EQ("param_a.a", result[0].input_variables[0].name);
+  EXPECT_TRUE(result[0].input_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].input_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[0].component_type);
+  EXPECT_EQ("param_a.b", result[0].input_variables[1].name);
+  EXPECT_TRUE(result[0].input_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].input_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[1].component_type);
+  EXPECT_EQ("param_b.a", result[0].input_variables[2].name);
+  EXPECT_TRUE(result[0].input_variables[2].has_location_attribute);
+  EXPECT_EQ(2u, result[0].input_variables[2].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].input_variables[2].component_type);
+  EXPECT_EQ("param_c", result[0].input_variables[3].name);
+  EXPECT_TRUE(result[0].input_variables[3].has_location_attribute);
+  EXPECT_EQ(3u, result[0].input_variables[3].location_attribute);
+  EXPECT_EQ(ComponentType::kFloat, result[0].input_variables[3].component_type);
+  EXPECT_EQ("param_d", result[0].input_variables[4].name);
+  EXPECT_TRUE(result[0].input_variables[4].has_location_attribute);
+  EXPECT_EQ(4u, result[0].input_variables[4].location_attribute);
+  EXPECT_EQ(ComponentType::kFloat, result[0].input_variables[4].component_type);
+
+  ASSERT_EQ(2u, result[0].output_variables.size());
+  EXPECT_EQ("<retval>.a", result[0].output_variables[0].name);
+  EXPECT_TRUE(result[0].output_variables[0].has_location_attribute);
+  EXPECT_EQ(0u, result[0].output_variables[0].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[0].component_type);
+  EXPECT_EQ("<retval>.b", result[0].output_variables[1].name);
+  EXPECT_TRUE(result[0].output_variables[1].has_location_attribute);
+  EXPECT_EQ(1u, result[0].output_variables[1].location_attribute);
+  EXPECT_EQ(ComponentType::kUInt, result[0].output_variables[1].component_type);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantUnreferenced) {
+  AddOverridableConstantWithoutID("foo", ty.f32(), nullptr);
+  MakeEmptyBodyFunction(
+      "ep_func", {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].overridable_constants.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantReferencedByEntryPoint) {
+  AddOverridableConstantWithoutID("foo", ty.f32(), nullptr);
+  MakePlainGlobalReferenceBodyFunction(
+      "ep_func", "foo", ty.f32(),
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo", result[0].overridable_constants[0].name);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantReferencedByCallee) {
+  AddOverridableConstantWithoutID("foo", ty.f32(), nullptr);
+  MakePlainGlobalReferenceBodyFunction("callee_func", "foo", ty.f32(), {});
+  MakeCallerBodyFunction(
+      "ep_func", {"callee_func"},
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo", result[0].overridable_constants[0].name);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantSomeReferenced) {
+  AddOverridableConstantWithID("foo", 1, ty.f32(), nullptr);
+  AddOverridableConstantWithID("bar", 2, ty.f32(), nullptr);
+  MakePlainGlobalReferenceBodyFunction("callee_func", "foo", ty.f32(), {});
+  MakeCallerBodyFunction(
+      "ep_func", {"callee_func"},
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo", result[0].overridable_constants[0].name);
+  EXPECT_EQ(1, result[0].overridable_constants[0].numeric_id);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantTypes) {
+  AddOverridableConstantWithoutID("bool_var", ty.bool_(), nullptr);
+  AddOverridableConstantWithoutID("float_var", ty.f32(), nullptr);
+  AddOverridableConstantWithoutID("u32_var", ty.u32(), nullptr);
+  AddOverridableConstantWithoutID("i32_var", ty.i32(), nullptr);
+
+  MakePlainGlobalReferenceBodyFunction("bool_func", "bool_var", ty.bool_(), {});
+  MakePlainGlobalReferenceBodyFunction("float_func", "float_var", ty.f32(), {});
+  MakePlainGlobalReferenceBodyFunction("u32_func", "u32_var", ty.u32(), {});
+  MakePlainGlobalReferenceBodyFunction("i32_func", "i32_var", ty.i32(), {});
+
+  MakeCallerBodyFunction(
+      "ep_func", {"bool_func", "float_func", "u32_func", "i32_func"},
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(4u, result[0].overridable_constants.size());
+  EXPECT_EQ("bool_var", result[0].overridable_constants[0].name);
+  EXPECT_EQ(inspector::OverridableConstant::Type::kBool,
+            result[0].overridable_constants[0].type);
+  EXPECT_EQ("float_var", result[0].overridable_constants[1].name);
+  EXPECT_EQ(inspector::OverridableConstant::Type::kFloat32,
+            result[0].overridable_constants[1].type);
+  EXPECT_EQ("u32_var", result[0].overridable_constants[2].name);
+  EXPECT_EQ(inspector::OverridableConstant::Type::kUint32,
+            result[0].overridable_constants[2].type);
+  EXPECT_EQ("i32_var", result[0].overridable_constants[3].name);
+  EXPECT_EQ(inspector::OverridableConstant::Type::kInt32,
+            result[0].overridable_constants[3].type);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantInitialized) {
+  AddOverridableConstantWithoutID("foo", ty.f32(), Expr(0.0f));
+  MakePlainGlobalReferenceBodyFunction(
+      "ep_func", "foo", ty.f32(),
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo", result[0].overridable_constants[0].name);
+  EXPECT_TRUE(result[0].overridable_constants[0].is_initialized);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantUninitialized) {
+  AddOverridableConstantWithoutID("foo", ty.f32(), nullptr);
+  MakePlainGlobalReferenceBodyFunction(
+      "ep_func", "foo", ty.f32(),
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo", result[0].overridable_constants[0].name);
+
+  EXPECT_FALSE(result[0].overridable_constants[0].is_initialized);
+}
+
+TEST_F(InspectorGetEntryPointTest, OverridableConstantNumericIDSpecified) {
+  AddOverridableConstantWithoutID("foo_no_id", ty.f32(), nullptr);
+  AddOverridableConstantWithID("foo_id", 1234, ty.f32(), nullptr);
+
+  MakePlainGlobalReferenceBodyFunction("no_id_func", "foo_no_id", ty.f32(), {});
+  MakePlainGlobalReferenceBodyFunction("id_func", "foo_id", ty.f32(), {});
+
+  MakeCallerBodyFunction(
+      "ep_func", {"no_id_func", "id_func"},
+      {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(2u, result[0].overridable_constants.size());
+  EXPECT_EQ("foo_no_id", result[0].overridable_constants[0].name);
+  EXPECT_EQ("foo_id", result[0].overridable_constants[1].name);
+  EXPECT_EQ(1234, result[0].overridable_constants[1].numeric_id);
+
+  EXPECT_FALSE(result[0].overridable_constants[0].is_numeric_id_specified);
+  EXPECT_TRUE(result[0].overridable_constants[1].is_numeric_id_specified);
+}
+
+TEST_F(InspectorGetEntryPointTest, NonOverridableConstantSkipped) {
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+  MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         {Stage(ast::PipelineStage::kFragment)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].overridable_constants.size());
+}
+
+TEST_F(InspectorGetEntryPointTest, BuiltinNotReferenced) {
+  MakeEmptyBodyFunction("ep_func", {Stage(ast::PipelineStage::kFragment)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_FALSE(result[0].input_sample_mask_used);
+  EXPECT_FALSE(result[0].output_sample_mask_used);
+  EXPECT_FALSE(result[0].input_position_used);
+  EXPECT_FALSE(result[0].front_facing_used);
+  EXPECT_FALSE(result[0].sample_index_used);
+  EXPECT_FALSE(result[0].num_workgroups_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, InputSampleMaskSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.u32(), {Builtin(ast::Builtin::kSampleMask)});
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].input_sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, InputSampleMaskStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(
+      Member("inner_position", ty.u32(), {Builtin(ast::Builtin::kSampleMask)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].input_sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, OutputSampleMaskSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.u32(), {Builtin(ast::Builtin::kSampleMask)});
+  Func("ep_func", {in_var}, ty.u32(), {Return("in_var")},
+       {Stage(ast::PipelineStage::kFragment)},
+       {Builtin(ast::Builtin::kSampleMask)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].output_sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, OutputSampleMaskStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_sample_mask", ty.u32(),
+                           {Builtin(ast::Builtin::kSampleMask)}));
+  Structure("out_struct", members);
+
+  Func("ep_func", {}, ty.type_name("out_struct"),
+       {Decl(Var("out_var", ty.type_name("out_struct"))), Return("out_var")},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].output_sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, InputPositionSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.vec4<f32>(), {Builtin(ast::Builtin::kPosition)});
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].input_position_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, InputPositionStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_position", ty.vec4<f32>(),
+                           {Builtin(ast::Builtin::kPosition)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].input_position_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, FrontFacingSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.bool_(), {Builtin(ast::Builtin::kFrontFacing)});
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].front_facing_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, FrontFacingStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_position", ty.bool_(),
+                           {Builtin(ast::Builtin::kFrontFacing)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].front_facing_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, SampleIndexSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.u32(), {Builtin(ast::Builtin::kSampleIndex)});
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].sample_index_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, SampleIndexStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_position", ty.u32(),
+                           {Builtin(ast::Builtin::kSampleIndex)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].sample_index_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, NumWorkgroupsSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.vec3<u32>(), {Builtin(ast::Builtin::kNumWorkgroups)});
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].num_workgroups_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, NumWorkgroupsStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_position", ty.vec3<u32>(),
+                           {Builtin(ast::Builtin::kNumWorkgroups)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].num_workgroups_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, ImplicitInterpolate) {
+  ast::StructMemberList members;
+  members.push_back(Member("struct_inner", ty.f32(), {Location(0)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].input_variables.size());
+  EXPECT_EQ(InterpolationType::kPerspective,
+            result[0].input_variables[0].interpolation_type);
+  EXPECT_EQ(InterpolationSampling::kCenter,
+            result[0].input_variables[0].interpolation_sampling);
+}
+
+TEST_P(InspectorGetEntryPointInterpolateTest, Test) {
+  auto& params = GetParam();
+  ast::StructMemberList members;
+  members.push_back(
+      Member("struct_inner", ty.f32(),
+             {Interpolate(params.in_type, params.in_sampling), Location(0)}));
+  Structure("in_struct", members);
+  auto* in_var = Param("in_var", ty.type_name("in_struct"), {});
+
+  Func("ep_func", {in_var}, ty.void_(), {Return()},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  ASSERT_EQ(1u, result[0].input_variables.size());
+  EXPECT_EQ(params.out_type, result[0].input_variables[0].interpolation_type);
+  EXPECT_EQ(params.out_sampling,
+            result[0].input_variables[0].interpolation_sampling);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetEntryPointTest,
+    InspectorGetEntryPointInterpolateTest,
+    testing::Values(
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kPerspective,
+            ast::InterpolationSampling::kCenter,
+            InterpolationType::kPerspective, InterpolationSampling::kCenter},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kPerspective,
+            ast::InterpolationSampling::kCentroid,
+            InterpolationType::kPerspective, InterpolationSampling::kCentroid},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kPerspective,
+            ast::InterpolationSampling::kSample,
+            InterpolationType::kPerspective, InterpolationSampling::kSample},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kPerspective,
+            ast::InterpolationSampling::kNone, InterpolationType::kPerspective,
+            InterpolationSampling::kCenter},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kLinear,
+            ast::InterpolationSampling::kCenter, InterpolationType::kLinear,
+            InterpolationSampling::kCenter},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kLinear,
+            ast::InterpolationSampling::kCentroid, InterpolationType::kLinear,
+            InterpolationSampling::kCentroid},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kLinear,
+            ast::InterpolationSampling::kSample, InterpolationType::kLinear,
+            InterpolationSampling::kSample},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kLinear, ast::InterpolationSampling::kNone,
+            InterpolationType::kLinear, InterpolationSampling::kCenter},
+        InspectorGetEntryPointInterpolateTestParams{
+            ast::InterpolationType::kFlat, ast::InterpolationSampling::kNone,
+            InterpolationType::kFlat, InterpolationSampling::kNone}));
+
+TEST_F(InspectorGetConstantIDsTest, Bool) {
+  AddOverridableConstantWithID("foo", 1, ty.bool_(), nullptr);
+  AddOverridableConstantWithID("bar", 20, ty.bool_(), Expr(true));
+  AddOverridableConstantWithID("baz", 300, ty.bool_(), Expr(false));
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetConstantIDs();
+  ASSERT_EQ(3u, result.size());
+
+  ASSERT_TRUE(result.find(1) != result.end());
+  EXPECT_TRUE(result[1].IsNull());
+
+  ASSERT_TRUE(result.find(20) != result.end());
+  EXPECT_TRUE(result[20].IsBool());
+  EXPECT_TRUE(result[20].AsBool());
+
+  ASSERT_TRUE(result.find(300) != result.end());
+  EXPECT_TRUE(result[300].IsBool());
+  EXPECT_FALSE(result[300].AsBool());
+}
+
+TEST_F(InspectorGetConstantIDsTest, U32) {
+  AddOverridableConstantWithID("foo", 1, ty.u32(), nullptr);
+  AddOverridableConstantWithID("bar", 20, ty.u32(), Expr(42u));
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetConstantIDs();
+  ASSERT_EQ(2u, result.size());
+
+  ASSERT_TRUE(result.find(1) != result.end());
+  EXPECT_TRUE(result[1].IsNull());
+
+  ASSERT_TRUE(result.find(20) != result.end());
+  EXPECT_TRUE(result[20].IsU32());
+  EXPECT_EQ(42u, result[20].AsU32());
+}
+
+TEST_F(InspectorGetConstantIDsTest, I32) {
+  AddOverridableConstantWithID("foo", 1, ty.i32(), nullptr);
+  AddOverridableConstantWithID("bar", 20, ty.i32(), Expr(-42));
+  AddOverridableConstantWithID("baz", 300, ty.i32(), Expr(42));
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetConstantIDs();
+  ASSERT_EQ(3u, result.size());
+
+  ASSERT_TRUE(result.find(1) != result.end());
+  EXPECT_TRUE(result[1].IsNull());
+
+  ASSERT_TRUE(result.find(20) != result.end());
+  EXPECT_TRUE(result[20].IsI32());
+  EXPECT_EQ(-42, result[20].AsI32());
+
+  ASSERT_TRUE(result.find(300) != result.end());
+  EXPECT_TRUE(result[300].IsI32());
+  EXPECT_EQ(42, result[300].AsI32());
+}
+
+TEST_F(InspectorGetConstantIDsTest, Float) {
+  AddOverridableConstantWithID("foo", 1, ty.f32(), nullptr);
+  AddOverridableConstantWithID("bar", 20, ty.f32(), Expr(0.0f));
+  AddOverridableConstantWithID("baz", 300, ty.f32(), Expr(-10.0f));
+  AddOverridableConstantWithID("x", 4000, ty.f32(), Expr(15.0f));
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetConstantIDs();
+  ASSERT_EQ(4u, result.size());
+
+  ASSERT_TRUE(result.find(1) != result.end());
+  EXPECT_TRUE(result[1].IsNull());
+
+  ASSERT_TRUE(result.find(20) != result.end());
+  EXPECT_TRUE(result[20].IsFloat());
+  EXPECT_FLOAT_EQ(0.0, result[20].AsFloat());
+
+  ASSERT_TRUE(result.find(300) != result.end());
+  EXPECT_TRUE(result[300].IsFloat());
+  EXPECT_FLOAT_EQ(-10.0, result[300].AsFloat());
+
+  ASSERT_TRUE(result.find(4000) != result.end());
+  EXPECT_TRUE(result[4000].IsFloat());
+  EXPECT_FLOAT_EQ(15.0, result[4000].AsFloat());
+}
+
+TEST_F(InspectorGetConstantNameToIdMapTest, WithAndWithoutIds) {
+  AddOverridableConstantWithID("v1", 1, ty.f32(), nullptr);
+  AddOverridableConstantWithID("v20", 20, ty.f32(), nullptr);
+  AddOverridableConstantWithID("v300", 300, ty.f32(), nullptr);
+  auto* a = AddOverridableConstantWithoutID("a", ty.f32(), nullptr);
+  auto* b = AddOverridableConstantWithoutID("b", ty.f32(), nullptr);
+  auto* c = AddOverridableConstantWithoutID("c", ty.f32(), nullptr);
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetConstantNameToIdMap();
+  ASSERT_EQ(6u, result.size());
+
+  ASSERT_TRUE(result.count("v1"));
+  EXPECT_EQ(result["v1"], 1u);
+
+  ASSERT_TRUE(result.count("v20"));
+  EXPECT_EQ(result["v20"], 20u);
+
+  ASSERT_TRUE(result.count("v300"));
+  EXPECT_EQ(result["v300"], 300u);
+
+  ASSERT_TRUE(result.count("a"));
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(a));
+  EXPECT_EQ(result["a"],
+            program_->Sem().Get<sem::GlobalVariable>(a)->ConstantId());
+
+  ASSERT_TRUE(result.count("b"));
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(b));
+  EXPECT_EQ(result["b"],
+            program_->Sem().Get<sem::GlobalVariable>(b)->ConstantId());
+
+  ASSERT_TRUE(result.count("c"));
+  ASSERT_TRUE(program_->Sem().Get<sem::GlobalVariable>(c));
+  EXPECT_EQ(result["c"],
+            program_->Sem().Get<sem::GlobalVariable>(c)->ConstantId());
+}
+
+TEST_F(InspectorGetStorageSizeTest, Empty) {
+  MakeEmptyBodyFunction("ep_func",
+                        ast::AttributeList{Stage(ast::PipelineStage::kCompute),
+                                           WorkgroupSize(1)});
+  Inspector& inspector = Build();
+  EXPECT_EQ(0u, inspector.GetStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetStorageSizeTest, Simple_NonStruct) {
+  AddUniformBuffer("ub_var", ty.i32(), 0, 0);
+  AddStorageBuffer("sb_var", ty.i32(), ast::Access::kReadWrite, 1, 0);
+  AddStorageBuffer("rosb_var", ty.i32(), ast::Access::kRead, 1, 1);
+  Func("ep_func", {}, ty.void_(),
+       {
+           Decl(Const("ub", nullptr, Expr("ub_var"))),
+           Decl(Const("sb", nullptr, Expr("sb_var"))),
+           Decl(Const("rosb", nullptr, Expr("rosb_var"))),
+       },
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  EXPECT_EQ(12u, inspector.GetStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetStorageSizeTest, Simple_Struct) {
+  auto* ub_struct_type = MakeUniformBufferType("ub_type", {ty.i32(), ty.i32()});
+  AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
+  MakeStructVariableReferenceBodyFunction("ub_func", "ub_var", {{0, ty.i32()}});
+
+  auto sb = MakeStorageBufferTypes("sb_type", {ty.i32()});
+  AddStorageBuffer("sb_var", sb(), ast::Access::kReadWrite, 1, 0);
+  MakeStructVariableReferenceBodyFunction("sb_func", "sb_var", {{0, ty.i32()}});
+
+  auto ro_sb = MakeStorageBufferTypes("rosb_type", {ty.i32()});
+  AddStorageBuffer("rosb_var", ro_sb(), ast::Access::kRead, 1, 1);
+  MakeStructVariableReferenceBodyFunction("rosb_func", "rosb_var",
+                                          {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func", "sb_func", "rosb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kCompute),
+                             WorkgroupSize(1),
+                         });
+
+  Inspector& inspector = Build();
+
+  EXPECT_EQ(16u, inspector.GetStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetStorageSizeTest, NonStructVec3) {
+  AddUniformBuffer("ub_var", ty.vec3<f32>(), 0, 0);
+  Func("ep_func", {}, ty.void_(),
+       {
+           Decl(Const("ub", nullptr, Expr("ub_var"))),
+       },
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  EXPECT_EQ(12u, inspector.GetStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetStorageSizeTest, StructVec3) {
+  auto* ub_struct_type = MakeUniformBufferType("ub_type", {ty.vec3<f32>()});
+  AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
+  Func("ep_func", {}, ty.void_(),
+       {
+           Decl(Const("ub", nullptr, Expr("ub_var"))),
+       },
+       {Stage(ast::PipelineStage::kCompute), WorkgroupSize(1)});
+
+  Inspector& inspector = Build();
+
+  EXPECT_EQ(16u, inspector.GetStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetResourceBindingsTest, Empty) {
+  MakeCallerBodyFunction("ep_func", {},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetResourceBindingsTest, Simple) {
+  auto* ub_struct_type = MakeUniformBufferType("ub_type", {ty.i32()});
+  AddUniformBuffer("ub_var", ty.Of(ub_struct_type), 0, 0);
+  MakeStructVariableReferenceBodyFunction("ub_func", "ub_var", {{0, ty.i32()}});
+
+  auto sb = MakeStorageBufferTypes("sb_type", {ty.i32()});
+  AddStorageBuffer("sb_var", sb(), ast::Access::kReadWrite, 1, 0);
+  MakeStructVariableReferenceBodyFunction("sb_func", "sb_var", {{0, ty.i32()}});
+
+  auto ro_sb = MakeStorageBufferTypes("rosb_type", {ty.i32()});
+  AddStorageBuffer("rosb_var", ro_sb(), ast::Access::kRead, 1, 1);
+  MakeStructVariableReferenceBodyFunction("rosb_func", "rosb_var",
+                                          {{0, ty.i32()}});
+
+  auto* s_texture_type =
+      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
+  AddResource("s_texture", s_texture_type, 2, 0);
+  AddSampler("s_var", 3, 0);
+  AddGlobalVariable("s_coords", ty.f32());
+  MakeSamplerReferenceBodyFunction("s_func", "s_texture", "s_var", "s_coords",
+                                   ty.f32(), {});
+
+  auto* cs_depth_texture_type = ty.depth_texture(ast::TextureDimension::k2d);
+  AddResource("cs_texture", cs_depth_texture_type, 3, 1);
+  AddComparisonSampler("cs_var", 3, 2);
+  AddGlobalVariable("cs_coords", ty.vec2<f32>());
+  AddGlobalVariable("cs_depth", ty.f32());
+  MakeComparisonSamplerReferenceBodyFunction(
+      "cs_func", "cs_texture", "cs_var", "cs_coords", "cs_depth", ty.f32(), {});
+
+  auto* depth_ms_texture_type =
+      ty.depth_multisampled_texture(ast::TextureDimension::k2d);
+  AddResource("depth_ms_texture", depth_ms_texture_type, 3, 3);
+  Func("depth_ms_func", {}, ty.void_(), {Ignore("depth_ms_texture")});
+
+  auto* st_type = MakeStorageTextureTypes(ast::TextureDimension::k2d,
+                                          ast::TexelFormat::kR32Uint);
+  AddStorageTexture("st_var", st_type, 4, 0);
+  MakeStorageTextureBodyFunction("st_func", "st_var", ty.vec2<i32>(), {});
+
+  MakeCallerBodyFunction("ep_func",
+                         {"ub_func", "sb_func", "rosb_func", "s_func",
+                          "cs_func", "depth_ms_func", "st_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(9u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[1].resource_type);
+  EXPECT_EQ(1u, result[1].bind_group);
+  EXPECT_EQ(0u, result[1].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[2].resource_type);
+  EXPECT_EQ(1u, result[2].bind_group);
+  EXPECT_EQ(1u, result[2].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampler, result[3].resource_type);
+  EXPECT_EQ(3u, result[3].bind_group);
+  EXPECT_EQ(0u, result[3].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kComparisonSampler,
+            result[4].resource_type);
+  EXPECT_EQ(3u, result[4].bind_group);
+  EXPECT_EQ(2u, result[4].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampledTexture,
+            result[5].resource_type);
+  EXPECT_EQ(2u, result[5].bind_group);
+  EXPECT_EQ(0u, result[5].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kWriteOnlyStorageTexture,
+            result[6].resource_type);
+  EXPECT_EQ(4u, result[6].bind_group);
+  EXPECT_EQ(0u, result[6].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kDepthTexture,
+            result[7].resource_type);
+  EXPECT_EQ(3u, result[7].bind_group);
+  EXPECT_EQ(1u, result[7].binding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kDepthMultisampledTexture,
+            result[8].resource_type);
+  EXPECT_EQ(3u, result[8].bind_group);
+  EXPECT_EQ(3u, result[8].binding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, MissingEntryPoint) {
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_TRUE(inspector.has_error());
+  std::string error = inspector.error();
+  EXPECT_TRUE(error.find("not found") != std::string::npos);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, NonEntryPointFunc) {
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ub_func");
+  std::string error = inspector.error();
+  EXPECT_TRUE(error.find("not an entry point") != std::string::npos);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, Simple_NonStruct) {
+  AddUniformBuffer("foo_ub", ty.i32(), 0, 0);
+  MakePlainGlobalReferenceBodyFunction("ub_func", "foo_ub", ty.i32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].size);
+  EXPECT_EQ(4u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, Simple_Struct) {
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.i32()});
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].size);
+  EXPECT_EQ(4u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, MultipleMembers) {
+  auto* foo_struct_type =
+      MakeUniformBufferType("foo_type", {ty.i32(), ty.u32(), ty.f32()});
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+
+  MakeStructVariableReferenceBodyFunction(
+      "ub_func", "foo_ub", {{0, ty.i32()}, {1, ty.u32()}, {2, ty.f32()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, ContainingPadding) {
+  auto* foo_struct_type = MakeUniformBufferType("foo_type", {ty.vec3<f32>()});
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub",
+                                          {{0, ty.vec3<f32>()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(16u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, NonStructVec3) {
+  AddUniformBuffer("foo_ub", ty.vec3<f32>(), 0, 0);
+  MakePlainGlobalReferenceBodyFunction("ub_func", "foo_ub", ty.vec3<f32>(), {});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, MultipleUniformBuffers) {
+  auto* ub_struct_type =
+      MakeUniformBufferType("ub_type", {ty.i32(), ty.u32(), ty.f32()});
+  AddUniformBuffer("ub_foo", ty.Of(ub_struct_type), 0, 0);
+  AddUniformBuffer("ub_bar", ty.Of(ub_struct_type), 0, 1);
+  AddUniformBuffer("ub_baz", ty.Of(ub_struct_type), 2, 0);
+
+  auto AddReferenceFunc = [this](const std::string& func_name,
+                                 const std::string& var_name) {
+    MakeStructVariableReferenceBodyFunction(
+        func_name, var_name, {{0, ty.i32()}, {1, ty.u32()}, {2, ty.f32()}});
+  };
+  AddReferenceFunc("ub_foo_func", "ub_foo");
+  AddReferenceFunc("ub_bar_func", "ub_bar");
+  AddReferenceFunc("ub_baz_func", "ub_baz");
+
+  auto FuncCall = [&](const std::string& callee) {
+    return create<ast::CallStatement>(Call(callee));
+  };
+
+  Func("ep_func", ast::VariableList(), ty.void_(),
+       ast::StatementList{FuncCall("ub_foo_func"), FuncCall("ub_bar_func"),
+                          FuncCall("ub_baz_func"), Return()},
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(3u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[1].resource_type);
+  EXPECT_EQ(0u, result[1].bind_group);
+  EXPECT_EQ(1u, result[1].binding);
+  EXPECT_EQ(12u, result[1].size);
+  EXPECT_EQ(12u, result[1].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[2].resource_type);
+  EXPECT_EQ(2u, result[2].bind_group);
+  EXPECT_EQ(0u, result[2].binding);
+  EXPECT_EQ(12u, result[2].size);
+  EXPECT_EQ(12u, result[2].size_no_padding);
+}
+
+TEST_F(InspectorGetUniformBufferResourceBindingsTest, ContainingArray) {
+  // Manually create uniform buffer to make sure it had a valid layout (array
+  // with elem stride of 16, and that is 16-byte aligned within the struct)
+  auto* foo_struct_type = Structure(
+      "foo_type",
+      {Member("0i32", ty.i32()),
+       Member("b", ty.array(ty.u32(), 4, /*stride*/ 16), {MemberAlign(16)})});
+
+  AddUniformBuffer("foo_ub", ty.Of(foo_struct_type), 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("ub_func", "foo_ub", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetUniformBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kUniformBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(80u, result[0].size);
+  EXPECT_EQ(80u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, Simple_NonStruct) {
+  AddStorageBuffer("foo_sb", ty.i32(), ast::Access::kReadWrite, 0, 0);
+  MakePlainGlobalReferenceBodyFunction("sb_func", "foo_sb", ty.i32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].size);
+  EXPECT_EQ(4u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, Simple_Struct) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {ty.i32()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].size);
+  EXPECT_EQ(4u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, MultipleMembers) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {
+                                                                ty.i32(),
+                                                                ty.u32(),
+                                                                ty.f32(),
+                                                            });
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction(
+      "sb_func", "foo_sb", {{0, ty.i32()}, {1, ty.u32()}, {2, ty.f32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, MultipleStorageBuffers) {
+  auto sb_struct_type = MakeStorageBufferTypes("sb_type", {
+                                                              ty.i32(),
+                                                              ty.u32(),
+                                                              ty.f32(),
+                                                          });
+  AddStorageBuffer("sb_foo", sb_struct_type(), ast::Access::kReadWrite, 0, 0);
+  AddStorageBuffer("sb_bar", sb_struct_type(), ast::Access::kReadWrite, 0, 1);
+  AddStorageBuffer("sb_baz", sb_struct_type(), ast::Access::kReadWrite, 2, 0);
+
+  auto AddReferenceFunc = [this](const std::string& func_name,
+                                 const std::string& var_name) {
+    MakeStructVariableReferenceBodyFunction(
+        func_name, var_name, {{0, ty.i32()}, {1, ty.u32()}, {2, ty.f32()}});
+  };
+  AddReferenceFunc("sb_foo_func", "sb_foo");
+  AddReferenceFunc("sb_bar_func", "sb_bar");
+  AddReferenceFunc("sb_baz_func", "sb_baz");
+
+  auto FuncCall = [&](const std::string& callee) {
+    return create<ast::CallStatement>(Call(callee));
+  };
+
+  Func("ep_func", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           FuncCall("sb_foo_func"),
+           FuncCall("sb_bar_func"),
+           FuncCall("sb_baz_func"),
+           Return(),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(3u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[1].resource_type);
+  EXPECT_EQ(0u, result[1].bind_group);
+  EXPECT_EQ(1u, result[1].binding);
+  EXPECT_EQ(12u, result[1].size);
+  EXPECT_EQ(12u, result[1].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[2].resource_type);
+  EXPECT_EQ(2u, result[2].bind_group);
+  EXPECT_EQ(0u, result[2].binding);
+  EXPECT_EQ(12u, result[2].size);
+  EXPECT_EQ(12u, result[2].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, ContainingArray) {
+  auto foo_struct_type =
+      MakeStorageBufferTypes("foo_type", {ty.i32(), ty.array<u32, 4>()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(20u, result[0].size);
+  EXPECT_EQ(20u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, ContainingRuntimeArray) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {
+                                                                ty.i32(),
+                                                                ty.array<u32>(),
+                                                            });
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(8u, result[0].size);
+  EXPECT_EQ(8u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, ContainingPadding) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {ty.vec3<f32>()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb",
+                                          {{0, ty.vec3<f32>()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(16u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, NonStructVec3) {
+  AddStorageBuffer("foo_ub", ty.vec3<f32>(), ast::Access::kReadWrite, 0, 0);
+  MakePlainGlobalReferenceBodyFunction("ub_func", "foo_ub", ty.vec3<f32>(), {});
+
+  MakeCallerBodyFunction("ep_func", {"ub_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetStorageBufferResourceBindingsTest, SkipReadOnly) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {ty.i32()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kRead, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetReadOnlyStorageBufferResourceBindingsTest, Simple) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {ty.i32()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kRead, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetReadOnlyStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(4u, result[0].size);
+  EXPECT_EQ(4u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetReadOnlyStorageBufferResourceBindingsTest,
+       MultipleStorageBuffers) {
+  auto sb_struct_type = MakeStorageBufferTypes("sb_type", {
+                                                              ty.i32(),
+                                                              ty.u32(),
+                                                              ty.f32(),
+                                                          });
+  AddStorageBuffer("sb_foo", sb_struct_type(), ast::Access::kRead, 0, 0);
+  AddStorageBuffer("sb_bar", sb_struct_type(), ast::Access::kRead, 0, 1);
+  AddStorageBuffer("sb_baz", sb_struct_type(), ast::Access::kRead, 2, 0);
+
+  auto AddReferenceFunc = [this](const std::string& func_name,
+                                 const std::string& var_name) {
+    MakeStructVariableReferenceBodyFunction(
+        func_name, var_name, {{0, ty.i32()}, {1, ty.u32()}, {2, ty.f32()}});
+  };
+  AddReferenceFunc("sb_foo_func", "sb_foo");
+  AddReferenceFunc("sb_bar_func", "sb_bar");
+  AddReferenceFunc("sb_baz_func", "sb_baz");
+
+  auto FuncCall = [&](const std::string& callee) {
+    return create<ast::CallStatement>(Call(callee));
+  };
+
+  Func("ep_func", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           FuncCall("sb_foo_func"),
+           FuncCall("sb_bar_func"),
+           FuncCall("sb_baz_func"),
+           Return(),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetReadOnlyStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(3u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(12u, result[0].size);
+  EXPECT_EQ(12u, result[0].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[1].resource_type);
+  EXPECT_EQ(0u, result[1].bind_group);
+  EXPECT_EQ(1u, result[1].binding);
+  EXPECT_EQ(12u, result[1].size);
+  EXPECT_EQ(12u, result[1].size_no_padding);
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[2].resource_type);
+  EXPECT_EQ(2u, result[2].bind_group);
+  EXPECT_EQ(0u, result[2].binding);
+  EXPECT_EQ(12u, result[2].size);
+  EXPECT_EQ(12u, result[2].size_no_padding);
+}
+
+TEST_F(InspectorGetReadOnlyStorageBufferResourceBindingsTest, ContainingArray) {
+  auto foo_struct_type =
+      MakeStorageBufferTypes("foo_type", {
+                                             ty.i32(),
+                                             ty.array<u32, 4>(),
+                                         });
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kRead, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetReadOnlyStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(20u, result[0].size);
+  EXPECT_EQ(20u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetReadOnlyStorageBufferResourceBindingsTest,
+       ContainingRuntimeArray) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {
+                                                                ty.i32(),
+                                                                ty.array<u32>(),
+                                                            });
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kRead, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetReadOnlyStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kReadOnlyStorageBuffer,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(8u, result[0].size);
+  EXPECT_EQ(8u, result[0].size_no_padding);
+}
+
+TEST_F(InspectorGetReadOnlyStorageBufferResourceBindingsTest, SkipNonReadOnly) {
+  auto foo_struct_type = MakeStorageBufferTypes("foo_type", {ty.i32()});
+  AddStorageBuffer("foo_sb", foo_struct_type(), ast::Access::kReadWrite, 0, 0);
+
+  MakeStructVariableReferenceBodyFunction("sb_func", "foo_sb", {{0, ty.i32()}});
+
+  MakeCallerBodyFunction("ep_func", {"sb_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetReadOnlyStorageBufferResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetSamplerResourceBindingsTest, Simple) {
+  auto* sampled_texture_type =
+      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.f32());
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords", ty.f32(),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampler, result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(1u, result[0].binding);
+}
+
+TEST_F(InspectorGetSamplerResourceBindingsTest, NoSampler) {
+  MakeEmptyBodyFunction("ep_func", ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetSamplerResourceBindingsTest, InFunction) {
+  auto* sampled_texture_type =
+      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.f32());
+
+  MakeSamplerReferenceBodyFunction("foo_func", "foo_texture", "foo_sampler",
+                                   "foo_coords", ty.f32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"foo_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampler, result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(1u, result[0].binding);
+}
+
+TEST_F(InspectorGetSamplerResourceBindingsTest, UnknownEntryPoint) {
+  auto* sampled_texture_type =
+      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.f32());
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords", ty.f32(),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("foo");
+  ASSERT_TRUE(inspector.has_error()) << inspector.error();
+}
+
+TEST_F(InspectorGetSamplerResourceBindingsTest, SkipsComparisonSamplers) {
+  auto* depth_texture_type = ty.depth_texture(ast::TextureDimension::k2d);
+  AddResource("foo_texture", depth_texture_type, 0, 0);
+  AddComparisonSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.vec2<f32>());
+  AddGlobalVariable("foo_depth", ty.f32());
+
+  MakeComparisonSamplerReferenceBodyFunction(
+      "ep", "foo_texture", "foo_sampler", "foo_coords", "foo_depth", ty.f32(),
+      ast::AttributeList{
+          Stage(ast::PipelineStage::kFragment),
+      });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetComparisonSamplerResourceBindingsTest, Simple) {
+  auto* depth_texture_type = ty.depth_texture(ast::TextureDimension::k2d);
+  AddResource("foo_texture", depth_texture_type, 0, 0);
+  AddComparisonSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.vec2<f32>());
+  AddGlobalVariable("foo_depth", ty.f32());
+
+  MakeComparisonSamplerReferenceBodyFunction(
+      "ep", "foo_texture", "foo_sampler", "foo_coords", "foo_depth", ty.f32(),
+      ast::AttributeList{
+          Stage(ast::PipelineStage::kFragment),
+      });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetComparisonSamplerResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kComparisonSampler,
+            result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(1u, result[0].binding);
+}
+
+TEST_F(InspectorGetComparisonSamplerResourceBindingsTest, NoSampler) {
+  MakeEmptyBodyFunction("ep_func", ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetComparisonSamplerResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetComparisonSamplerResourceBindingsTest, InFunction) {
+  auto* depth_texture_type = ty.depth_texture(ast::TextureDimension::k2d);
+  AddResource("foo_texture", depth_texture_type, 0, 0);
+  AddComparisonSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.vec2<f32>());
+  AddGlobalVariable("foo_depth", ty.f32());
+
+  MakeComparisonSamplerReferenceBodyFunction("foo_func", "foo_texture",
+                                             "foo_sampler", "foo_coords",
+                                             "foo_depth", ty.f32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"foo_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kFragment),
+                         });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetComparisonSamplerResourceBindings("ep_func");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kComparisonSampler,
+            result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(1u, result[0].binding);
+}
+
+TEST_F(InspectorGetComparisonSamplerResourceBindingsTest, UnknownEntryPoint) {
+  auto* depth_texture_type = ty.depth_texture(ast::TextureDimension::k2d);
+  AddResource("foo_texture", depth_texture_type, 0, 0);
+  AddComparisonSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.vec2<f32>());
+  AddGlobalVariable("foo_depth", ty.f32());
+
+  MakeComparisonSamplerReferenceBodyFunction(
+      "ep", "foo_texture", "foo_sampler", "foo_coords", "foo_depth", ty.f32(),
+      ast::AttributeList{
+          Stage(ast::PipelineStage::kFragment),
+      });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSamplerResourceBindings("foo");
+  ASSERT_TRUE(inspector.has_error()) << inspector.error();
+}
+
+TEST_F(InspectorGetComparisonSamplerResourceBindingsTest, SkipsSamplers) {
+  auto* sampled_texture_type =
+      ty.sampled_texture(ast::TextureDimension::k1d, ty.f32());
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  AddGlobalVariable("foo_coords", ty.f32());
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords", ty.f32(),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetComparisonSamplerResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetSampledTextureResourceBindingsTest, Empty) {
+  MakeEmptyBodyFunction("foo", ast::AttributeList{
+                                   Stage(ast::PipelineStage::kFragment),
+                               });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSampledTextureResourceBindings("foo");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_P(InspectorGetSampledTextureResourceBindingsTestWithParam, textureSample) {
+  auto* sampled_texture_type = ty.sampled_texture(
+      GetParam().type_dim, GetBaseType(GetParam().sampled_kind));
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  auto* coord_type = GetCoordsType(GetParam().type_dim, ty.f32());
+  AddGlobalVariable("foo_coords", coord_type);
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords",
+                                   GetBaseType(GetParam().sampled_kind),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampledTexture,
+            result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(GetParam().inspector_dim, result[0].dim);
+  EXPECT_EQ(GetParam().sampled_kind, result[0].sampled_kind);
+
+  // Prove that sampled and multi-sampled bindings are accounted
+  // for separately.
+  auto multisampled_result =
+      inspector.GetMultisampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_TRUE(multisampled_result.empty());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetSampledTextureResourceBindingsTest,
+    InspectorGetSampledTextureResourceBindingsTestWithParam,
+    testing::Values(
+        GetSampledTextureTestParams{
+            ast::TextureDimension::k1d,
+            inspector::ResourceBinding::TextureDimension::k1d,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetSampledTextureTestParams{
+            ast::TextureDimension::k2d,
+            inspector::ResourceBinding::TextureDimension::k2d,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetSampledTextureTestParams{
+            ast::TextureDimension::k3d,
+            inspector::ResourceBinding::TextureDimension::k3d,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetSampledTextureTestParams{
+            ast::TextureDimension::kCube,
+            inspector::ResourceBinding::TextureDimension::kCube,
+            inspector::ResourceBinding::SampledKind::kFloat}));
+
+TEST_P(InspectorGetSampledArrayTextureResourceBindingsTestWithParam,
+       textureSample) {
+  auto* sampled_texture_type = ty.sampled_texture(
+      GetParam().type_dim, GetBaseType(GetParam().sampled_kind));
+  AddResource("foo_texture", sampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  auto* coord_type = GetCoordsType(GetParam().type_dim, ty.f32());
+  AddGlobalVariable("foo_coords", coord_type);
+  AddGlobalVariable("foo_array_index", ty.i32());
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords", "foo_array_index",
+                                   GetBaseType(GetParam().sampled_kind),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kSampledTexture,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(GetParam().inspector_dim, result[0].dim);
+  EXPECT_EQ(GetParam().sampled_kind, result[0].sampled_kind);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetSampledArrayTextureResourceBindingsTest,
+    InspectorGetSampledArrayTextureResourceBindingsTestWithParam,
+    testing::Values(
+        GetSampledTextureTestParams{
+            ast::TextureDimension::k2dArray,
+            inspector::ResourceBinding::TextureDimension::k2dArray,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetSampledTextureTestParams{
+            ast::TextureDimension::kCubeArray,
+            inspector::ResourceBinding::TextureDimension::kCubeArray,
+            inspector::ResourceBinding::SampledKind::kFloat}));
+
+TEST_P(InspectorGetMultisampledTextureResourceBindingsTestWithParam,
+       textureLoad) {
+  auto* multisampled_texture_type = ty.multisampled_texture(
+      GetParam().type_dim, GetBaseType(GetParam().sampled_kind));
+  AddResource("foo_texture", multisampled_texture_type, 0, 0);
+  auto* coord_type = GetCoordsType(GetParam().type_dim, ty.i32());
+  AddGlobalVariable("foo_coords", coord_type);
+  AddGlobalVariable("foo_sample_index", ty.i32());
+
+  Func("ep", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           CallStmt(Call("textureLoad", "foo_texture", "foo_coords",
+                         "foo_sample_index")),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetMultisampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(ResourceBinding::ResourceType::kMultisampledTexture,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(GetParam().inspector_dim, result[0].dim);
+  EXPECT_EQ(GetParam().sampled_kind, result[0].sampled_kind);
+
+  // Prove that sampled and multi-sampled bindings are accounted
+  // for separately.
+  auto single_sampled_result =
+      inspector.GetSampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_TRUE(single_sampled_result.empty());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetMultisampledTextureResourceBindingsTest,
+    InspectorGetMultisampledTextureResourceBindingsTestWithParam,
+    testing::Values(
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2d,
+            inspector::ResourceBinding::TextureDimension::k2d,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2d,
+            inspector::ResourceBinding::TextureDimension::k2d,
+            inspector::ResourceBinding::SampledKind::kSInt},
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2d,
+            inspector::ResourceBinding::TextureDimension::k2d,
+            inspector::ResourceBinding::SampledKind::kUInt}));
+
+TEST_F(InspectorGetMultisampledArrayTextureResourceBindingsTest, Empty) {
+  MakeEmptyBodyFunction("foo", ast::AttributeList{
+                                   Stage(ast::PipelineStage::kFragment),
+                               });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetSampledTextureResourceBindings("foo");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_P(InspectorGetMultisampledArrayTextureResourceBindingsTestWithParam,
+       DISABLED_textureSample) {
+  auto* multisampled_texture_type = ty.multisampled_texture(
+      GetParam().type_dim, GetBaseType(GetParam().sampled_kind));
+  AddResource("foo_texture", multisampled_texture_type, 0, 0);
+  AddSampler("foo_sampler", 0, 1);
+  auto* coord_type = GetCoordsType(GetParam().type_dim, ty.f32());
+  AddGlobalVariable("foo_coords", coord_type);
+  AddGlobalVariable("foo_array_index", ty.i32());
+
+  MakeSamplerReferenceBodyFunction("ep", "foo_texture", "foo_sampler",
+                                   "foo_coords", "foo_array_index",
+                                   GetBaseType(GetParam().sampled_kind),
+                                   ast::AttributeList{
+                                       Stage(ast::PipelineStage::kFragment),
+                                   });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetMultisampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kMultisampledTexture,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(GetParam().inspector_dim, result[0].dim);
+  EXPECT_EQ(GetParam().sampled_kind, result[0].sampled_kind);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetMultisampledArrayTextureResourceBindingsTest,
+    InspectorGetMultisampledArrayTextureResourceBindingsTestWithParam,
+    testing::Values(
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2dArray,
+            inspector::ResourceBinding::TextureDimension::k2dArray,
+            inspector::ResourceBinding::SampledKind::kFloat},
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2dArray,
+            inspector::ResourceBinding::TextureDimension::k2dArray,
+            inspector::ResourceBinding::SampledKind::kSInt},
+        GetMultisampledTextureTestParams{
+            ast::TextureDimension::k2dArray,
+            inspector::ResourceBinding::TextureDimension::k2dArray,
+            inspector::ResourceBinding::SampledKind::kUInt}));
+
+TEST_F(InspectorGetStorageTextureResourceBindingsTest, Empty) {
+  MakeEmptyBodyFunction("ep", ast::AttributeList{
+                                  Stage(ast::PipelineStage::kFragment),
+                              });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetWriteOnlyStorageTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  EXPECT_EQ(0u, result.size());
+}
+
+TEST_P(InspectorGetStorageTextureResourceBindingsTestWithParam, Simple) {
+  DimensionParams dim_params;
+  TexelFormatParams format_params;
+  std::tie(dim_params, format_params) = GetParam();
+
+  ast::TextureDimension dim;
+  ResourceBinding::TextureDimension expected_dim;
+  std::tie(dim, expected_dim) = dim_params;
+
+  ast::TexelFormat format;
+  ResourceBinding::TexelFormat expected_format;
+  ResourceBinding::SampledKind expected_kind;
+  std::tie(format, expected_format, expected_kind) = format_params;
+
+  auto* st_type = MakeStorageTextureTypes(dim, format);
+  AddStorageTexture("st_var", st_type, 0, 0);
+
+  const ast::Type* dim_type = nullptr;
+  switch (dim) {
+    case ast::TextureDimension::k1d:
+      dim_type = ty.i32();
+      break;
+    case ast::TextureDimension::k2d:
+    case ast::TextureDimension::k2dArray:
+      dim_type = ty.vec2<i32>();
+      break;
+    case ast::TextureDimension::k3d:
+      dim_type = ty.vec3<i32>();
+      break;
+    default:
+      break;
+  }
+
+  ASSERT_FALSE(dim_type == nullptr);
+
+  MakeStorageTextureBodyFunction(
+      "ep", "st_var", dim_type,
+      ast::AttributeList{Stage(ast::PipelineStage::kFragment)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetWriteOnlyStorageTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kWriteOnlyStorageTexture,
+            result[0].resource_type);
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(expected_dim, result[0].dim);
+  EXPECT_EQ(expected_format, result[0].image_format);
+  EXPECT_EQ(expected_kind, result[0].sampled_kind);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetStorageTextureResourceBindingsTest,
+    InspectorGetStorageTextureResourceBindingsTestWithParam,
+    testing::Combine(
+        testing::Values(
+            std::make_tuple(ast::TextureDimension::k1d,
+                            ResourceBinding::TextureDimension::k1d),
+            std::make_tuple(ast::TextureDimension::k2d,
+                            ResourceBinding::TextureDimension::k2d),
+            std::make_tuple(ast::TextureDimension::k2dArray,
+                            ResourceBinding::TextureDimension::k2dArray),
+            std::make_tuple(ast::TextureDimension::k3d,
+                            ResourceBinding::TextureDimension::k3d)),
+        testing::Values(
+            std::make_tuple(ast::TexelFormat::kR32Float,
+                            ResourceBinding::TexelFormat::kR32Float,
+                            ResourceBinding::SampledKind::kFloat),
+            std::make_tuple(ast::TexelFormat::kR32Sint,
+                            ResourceBinding::TexelFormat::kR32Sint,
+                            ResourceBinding::SampledKind::kSInt),
+            std::make_tuple(ast::TexelFormat::kR32Uint,
+                            ResourceBinding::TexelFormat::kR32Uint,
+                            ResourceBinding::SampledKind::kUInt),
+            std::make_tuple(ast::TexelFormat::kRg32Float,
+                            ResourceBinding::TexelFormat::kRg32Float,
+                            ResourceBinding::SampledKind::kFloat),
+            std::make_tuple(ast::TexelFormat::kRg32Sint,
+                            ResourceBinding::TexelFormat::kRg32Sint,
+                            ResourceBinding::SampledKind::kSInt),
+            std::make_tuple(ast::TexelFormat::kRg32Uint,
+                            ResourceBinding::TexelFormat::kRg32Uint,
+                            ResourceBinding::SampledKind::kUInt),
+            std::make_tuple(ast::TexelFormat::kRgba16Float,
+                            ResourceBinding::TexelFormat::kRgba16Float,
+                            ResourceBinding::SampledKind::kFloat),
+            std::make_tuple(ast::TexelFormat::kRgba16Sint,
+                            ResourceBinding::TexelFormat::kRgba16Sint,
+                            ResourceBinding::SampledKind::kSInt),
+            std::make_tuple(ast::TexelFormat::kRgba16Uint,
+                            ResourceBinding::TexelFormat::kRgba16Uint,
+                            ResourceBinding::SampledKind::kUInt),
+            std::make_tuple(ast::TexelFormat::kRgba32Float,
+                            ResourceBinding::TexelFormat::kRgba32Float,
+                            ResourceBinding::SampledKind::kFloat),
+            std::make_tuple(ast::TexelFormat::kRgba32Sint,
+                            ResourceBinding::TexelFormat::kRgba32Sint,
+                            ResourceBinding::SampledKind::kSInt),
+            std::make_tuple(ast::TexelFormat::kRgba32Uint,
+                            ResourceBinding::TexelFormat::kRgba32Uint,
+                            ResourceBinding::SampledKind::kUInt),
+            std::make_tuple(ast::TexelFormat::kRgba8Sint,
+                            ResourceBinding::TexelFormat::kRgba8Sint,
+                            ResourceBinding::SampledKind::kSInt),
+            std::make_tuple(ast::TexelFormat::kRgba8Snorm,
+                            ResourceBinding::TexelFormat::kRgba8Snorm,
+                            ResourceBinding::SampledKind::kFloat),
+            std::make_tuple(ast::TexelFormat::kRgba8Uint,
+                            ResourceBinding::TexelFormat::kRgba8Uint,
+                            ResourceBinding::SampledKind::kUInt),
+            std::make_tuple(ast::TexelFormat::kRgba8Unorm,
+                            ResourceBinding::TexelFormat::kRgba8Unorm,
+                            ResourceBinding::SampledKind::kFloat))));
+
+TEST_P(InspectorGetDepthTextureResourceBindingsTestWithParam,
+       textureDimensions) {
+  auto* depth_texture_type = ty.depth_texture(GetParam().type_dim);
+  AddResource("dt", depth_texture_type, 0, 0);
+
+  Func("ep", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           CallStmt(Call("textureDimensions", "dt")),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetDepthTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kDepthTexture,
+            result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(GetParam().inspector_dim, result[0].dim);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InspectorGetDepthTextureResourceBindingsTest,
+    InspectorGetDepthTextureResourceBindingsTestWithParam,
+    testing::Values(
+        GetDepthTextureTestParams{
+            ast::TextureDimension::k2d,
+            inspector::ResourceBinding::TextureDimension::k2d},
+        GetDepthTextureTestParams{
+            ast::TextureDimension::k2dArray,
+            inspector::ResourceBinding::TextureDimension::k2dArray},
+        GetDepthTextureTestParams{
+            ast::TextureDimension::kCube,
+            inspector::ResourceBinding::TextureDimension::kCube},
+        GetDepthTextureTestParams{
+            ast::TextureDimension::kCubeArray,
+            inspector::ResourceBinding::TextureDimension::kCubeArray}));
+
+TEST_F(InspectorGetDepthMultisampledTextureResourceBindingsTest,
+       textureDimensions) {
+  auto* depth_ms_texture_type =
+      ty.depth_multisampled_texture(ast::TextureDimension::k2d);
+  AddResource("tex", depth_ms_texture_type, 0, 0);
+
+  Func("ep", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           CallStmt(Call("textureDimensions", "tex")),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetDepthMultisampledTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(ResourceBinding::ResourceType::kDepthMultisampledTexture,
+            result[0].resource_type);
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+  EXPECT_EQ(ResourceBinding::TextureDimension::k2d, result[0].dim);
+}
+
+TEST_F(InspectorGetExternalTextureResourceBindingsTest, Simple) {
+  auto* external_texture_type = ty.external_texture();
+  AddResource("et", external_texture_type, 0, 0);
+
+  Func("ep", ast::VariableList(), ty.void_(),
+       ast::StatementList{
+           CallStmt(Call("textureDimensions", "et")),
+       },
+       ast::AttributeList{
+           Stage(ast::PipelineStage::kFragment),
+       });
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetExternalTextureResourceBindings("ep");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+  EXPECT_EQ(ResourceBinding::ResourceType::kExternalTexture,
+            result[0].resource_type);
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_EQ(0u, result[0].bind_group);
+  EXPECT_EQ(0u, result[0].binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, None) {
+  std::string shader = R"(
+@stage(fragment)
+fn main() {
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(0u, result.size());
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, Simple) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, UnknownEntryPoint) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("foo");
+  ASSERT_TRUE(inspector.has_error()) << inspector.error();
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, MultipleCalls) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result_0 = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  auto result_1 = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  EXPECT_EQ(result_0, result_1);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, BothIndirect) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return doSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, SamplerIndirect) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+fn doSample(s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(myTexture, s, uv);
+}
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return doSample(mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, TextureIndirect) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+fn doSample(t: texture_2d<f32>, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, mySampler, uv);
+}
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return doSample(myTexture, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, NeitherIndirect) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+fn doSample(uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(myTexture, mySampler, uv);
+}
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return doSample(fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+  ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+  ASSERT_EQ(1u, result.size());
+
+  EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+  EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+  EXPECT_EQ(0u, result[0].texture_binding_point.group);
+  EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+}
+
+TEST_F(InspectorGetSamplerTextureUsesTest, Complex) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
+
+fn X(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return doSample(t, s, uv);
+}
+
+fn Y(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return doSample(t, s, uv);
+}
+
+fn Z(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return X(t, s, uv) + Y(t, s, uv);
+}
+
+@stage(fragment)
+fn via_call(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return Z(myTexture, mySampler, fragUV) * fragPosition;
+}
+
+@stage(fragment)
+fn via_ptr(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) + fragPosition;
+}
+
+@stage(fragment)
+fn direct(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return textureSample(myTexture, mySampler, fragUV) + fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+
+  {
+    auto result = inspector.GetSamplerTextureUses("via_call");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
+
+  {
+    auto result = inspector.GetSamplerTextureUses("via_ptr");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
+
+  {
+    auto result = inspector.GetSamplerTextureUses("direct");
+    ASSERT_FALSE(inspector.has_error()) << inspector.error();
+
+    ASSERT_EQ(1u, result.size());
+
+    EXPECT_EQ(0u, result[0].sampler_binding_point.group);
+    EXPECT_EQ(1u, result[0].sampler_binding_point.binding);
+    EXPECT_EQ(0u, result[0].texture_binding_point.group);
+    EXPECT_EQ(2u, result[0].texture_binding_point.binding);
+  }
+}
+
+TEST_F(InspectorGetWorkgroupStorageSizeTest, Empty) {
+  MakeEmptyBodyFunction("ep_func",
+                        ast::AttributeList{Stage(ast::PipelineStage::kCompute),
+                                           WorkgroupSize(1)});
+  Inspector& inspector = Build();
+  EXPECT_EQ(0u, inspector.GetWorkgroupStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetWorkgroupStorageSizeTest, Simple) {
+  AddWorkgroupStorage("wg_f32", ty.f32());
+  MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"f32_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kCompute),
+                             WorkgroupSize(1),
+                         });
+
+  Inspector& inspector = Build();
+  EXPECT_EQ(4u, inspector.GetWorkgroupStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetWorkgroupStorageSizeTest, CompoundTypes) {
+  // This struct should occupy 68 bytes. 4 from the i32 field, and another 64
+  // from the 4-element array with 16-byte stride.
+  auto* wg_struct_type = MakeStructType(
+      "WgStruct", {ty.i32(), ty.array(ty.i32(), 4, /*stride=*/16)});
+  AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
+  MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
+                                          {{0, ty.i32()}});
+
+  // Plus another 4 bytes from this other workgroup-class f32.
+  AddWorkgroupStorage("wg_f32", ty.f32());
+  MakePlainGlobalReferenceBodyFunction("f32_func", "wg_f32", ty.f32(), {});
+
+  MakeCallerBodyFunction("ep_func", {"wg_struct_func", "f32_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kCompute),
+                             WorkgroupSize(1),
+                         });
+
+  Inspector& inspector = Build();
+  EXPECT_EQ(72u, inspector.GetWorkgroupStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetWorkgroupStorageSizeTest, AlignmentPadding) {
+  // vec3<f32> has an alignment of 16 but a size of 12. We leverage this to test
+  // that our padded size calculation for workgroup storage is accurate.
+  AddWorkgroupStorage("wg_vec3", ty.vec3<f32>());
+  MakePlainGlobalReferenceBodyFunction("wg_func", "wg_vec3", ty.vec3<f32>(),
+                                       {});
+
+  MakeCallerBodyFunction("ep_func", {"wg_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kCompute),
+                             WorkgroupSize(1),
+                         });
+
+  Inspector& inspector = Build();
+  EXPECT_EQ(16u, inspector.GetWorkgroupStorageSize("ep_func"));
+}
+
+TEST_F(InspectorGetWorkgroupStorageSizeTest, StructAlignment) {
+  // Per WGSL spec, a struct's size is the offset its last member plus the size
+  // of its last member, rounded up to the alignment of its largest member. So
+  // here the struct is expected to occupy 1024 bytes of workgroup storage.
+  const auto* wg_struct_type = MakeStructTypeFromMembers(
+      "WgStruct",
+      {MakeStructMember(0, ty.f32(),
+                        {create<ast::StructMemberAlignAttribute>(1024)})});
+
+  AddWorkgroupStorage("wg_struct_var", ty.Of(wg_struct_type));
+  MakeStructVariableReferenceBodyFunction("wg_struct_func", "wg_struct_var",
+                                          {{0, ty.f32()}});
+
+  MakeCallerBodyFunction("ep_func", {"wg_struct_func"},
+                         ast::AttributeList{
+                             Stage(ast::PipelineStage::kCompute),
+                             WorkgroupSize(1),
+                         });
+
+  Inspector& inspector = Build();
+  EXPECT_EQ(1024u, inspector.GetWorkgroupStorageSize("ep_func"));
+}
+
+// Crash was occuring in ::GenerateSamplerTargets, when
+// ::GetSamplerTextureUses was called.
+TEST_F(InspectorRegressionTest, tint967) {
+  std::string shader = R"(
+@group(0) @binding(1) var mySampler: sampler;
+@group(0) @binding(2) var myTexture: texture_2d<f32>;
+
+fn doSample(t: texture_2d<f32>, s: sampler, uv: vec2<f32>) -> vec4<f32> {
+  return textureSample(t, s, uv);
+}
+
+@stage(fragment)
+fn main(@location(0) fragUV: vec2<f32>,
+        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
+  return doSample(myTexture, mySampler, fragUV) * fragPosition;
+})";
+
+  Inspector& inspector = Initialize(shader);
+  auto result = inspector.GetSamplerTextureUses("main");
+}
+
+}  // namespace
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/resource_binding.cc b/src/tint/inspector/resource_binding.cc
new file mode 100644
index 0000000..a4a0793
--- /dev/null
+++ b/src/tint/inspector/resource_binding.cc
@@ -0,0 +1,116 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/resource_binding.h"
+
+#include "src/tint/sem/array.h"
+#include "src/tint/sem/f32_type.h"
+#include "src/tint/sem/i32_type.h"
+#include "src/tint/sem/matrix_type.h"
+#include "src/tint/sem/type.h"
+#include "src/tint/sem/u32_type.h"
+#include "src/tint/sem/vector_type.h"
+
+namespace tint {
+namespace inspector {
+
+ResourceBinding::TextureDimension
+TypeTextureDimensionToResourceBindingTextureDimension(
+    const ast::TextureDimension& type_dim) {
+  switch (type_dim) {
+    case ast::TextureDimension::k1d:
+      return ResourceBinding::TextureDimension::k1d;
+    case ast::TextureDimension::k2d:
+      return ResourceBinding::TextureDimension::k2d;
+    case ast::TextureDimension::k2dArray:
+      return ResourceBinding::TextureDimension::k2dArray;
+    case ast::TextureDimension::k3d:
+      return ResourceBinding::TextureDimension::k3d;
+    case ast::TextureDimension::kCube:
+      return ResourceBinding::TextureDimension::kCube;
+    case ast::TextureDimension::kCubeArray:
+      return ResourceBinding::TextureDimension::kCubeArray;
+    case ast::TextureDimension::kNone:
+      return ResourceBinding::TextureDimension::kNone;
+  }
+  return ResourceBinding::TextureDimension::kNone;
+}
+
+ResourceBinding::SampledKind BaseTypeToSampledKind(const sem::Type* base_type) {
+  if (!base_type) {
+    return ResourceBinding::SampledKind::kUnknown;
+  }
+
+  if (auto* at = base_type->As<sem::Array>()) {
+    base_type = at->ElemType();
+  } else if (auto* mt = base_type->As<sem::Matrix>()) {
+    base_type = mt->type();
+  } else if (auto* vt = base_type->As<sem::Vector>()) {
+    base_type = vt->type();
+  }
+
+  if (base_type->Is<sem::F32>()) {
+    return ResourceBinding::SampledKind::kFloat;
+  } else if (base_type->Is<sem::U32>()) {
+    return ResourceBinding::SampledKind::kUInt;
+  } else if (base_type->Is<sem::I32>()) {
+    return ResourceBinding::SampledKind::kSInt;
+  } else {
+    return ResourceBinding::SampledKind::kUnknown;
+  }
+}
+
+ResourceBinding::TexelFormat TypeTexelFormatToResourceBindingTexelFormat(
+    const ast::TexelFormat& image_format) {
+  switch (image_format) {
+    case ast::TexelFormat::kR32Uint:
+      return ResourceBinding::TexelFormat::kR32Uint;
+    case ast::TexelFormat::kR32Sint:
+      return ResourceBinding::TexelFormat::kR32Sint;
+    case ast::TexelFormat::kR32Float:
+      return ResourceBinding::TexelFormat::kR32Float;
+    case ast::TexelFormat::kRgba8Unorm:
+      return ResourceBinding::TexelFormat::kRgba8Unorm;
+    case ast::TexelFormat::kRgba8Snorm:
+      return ResourceBinding::TexelFormat::kRgba8Snorm;
+    case ast::TexelFormat::kRgba8Uint:
+      return ResourceBinding::TexelFormat::kRgba8Uint;
+    case ast::TexelFormat::kRgba8Sint:
+      return ResourceBinding::TexelFormat::kRgba8Sint;
+    case ast::TexelFormat::kRg32Uint:
+      return ResourceBinding::TexelFormat::kRg32Uint;
+    case ast::TexelFormat::kRg32Sint:
+      return ResourceBinding::TexelFormat::kRg32Sint;
+    case ast::TexelFormat::kRg32Float:
+      return ResourceBinding::TexelFormat::kRg32Float;
+    case ast::TexelFormat::kRgba16Uint:
+      return ResourceBinding::TexelFormat::kRgba16Uint;
+    case ast::TexelFormat::kRgba16Sint:
+      return ResourceBinding::TexelFormat::kRgba16Sint;
+    case ast::TexelFormat::kRgba16Float:
+      return ResourceBinding::TexelFormat::kRgba16Float;
+    case ast::TexelFormat::kRgba32Uint:
+      return ResourceBinding::TexelFormat::kRgba32Uint;
+    case ast::TexelFormat::kRgba32Sint:
+      return ResourceBinding::TexelFormat::kRgba32Sint;
+    case ast::TexelFormat::kRgba32Float:
+      return ResourceBinding::TexelFormat::kRgba32Float;
+    case ast::TexelFormat::kNone:
+      return ResourceBinding::TexelFormat::kNone;
+  }
+  return ResourceBinding::TexelFormat::kNone;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/resource_binding.h b/src/tint/inspector/resource_binding.h
new file mode 100644
index 0000000..f2c74d7
--- /dev/null
+++ b/src/tint/inspector/resource_binding.h
@@ -0,0 +1,129 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_INSPECTOR_RESOURCE_BINDING_H_
+#define SRC_TINT_INSPECTOR_RESOURCE_BINDING_H_
+
+#include <cstdint>
+
+#include "src/tint/ast/storage_texture.h"
+#include "src/tint/ast/texture.h"
+
+namespace tint {
+namespace inspector {
+
+/// Container for information about how a resource is bound
+struct ResourceBinding {
+  /// The dimensionality of a texture
+  enum class TextureDimension {
+    /// Invalid texture
+    kNone = -1,
+    /// 1 dimensional texture
+    k1d,
+    /// 2 dimensional texture
+    k2d,
+    /// 2 dimensional array texture
+    k2dArray,
+    /// 3 dimensional texture
+    k3d,
+    /// cube texture
+    kCube,
+    /// cube array texture
+    kCubeArray,
+  };
+
+  /// Component type of the texture's data. Same as the Sampled Type parameter
+  /// in SPIR-V OpTypeImage.
+  enum class SampledKind { kUnknown = -1, kFloat, kUInt, kSInt };
+
+  /// Enumerator of texel image formats
+  enum class TexelFormat {
+    kNone = -1,
+
+    kRgba8Unorm,
+    kRgba8Snorm,
+    kRgba8Uint,
+    kRgba8Sint,
+    kRgba16Uint,
+    kRgba16Sint,
+    kRgba16Float,
+    kR32Uint,
+    kR32Sint,
+    kR32Float,
+    kRg32Uint,
+    kRg32Sint,
+    kRg32Float,
+    kRgba32Uint,
+    kRgba32Sint,
+    kRgba32Float,
+  };
+
+  /// kXXX maps to entries returned by GetXXXResourceBindings call.
+  enum class ResourceType {
+    kUniformBuffer,
+    kStorageBuffer,
+    kReadOnlyStorageBuffer,
+    kSampler,
+    kComparisonSampler,
+    kSampledTexture,
+    kMultisampledTexture,
+    kWriteOnlyStorageTexture,
+    kDepthTexture,
+    kDepthMultisampledTexture,
+    kExternalTexture
+  };
+
+  /// Type of resource that is bound.
+  ResourceType resource_type;
+  /// Bind group the binding belongs
+  uint32_t bind_group;
+  /// Identifier to identify this binding within the bind group
+  uint32_t binding;
+  /// Size for this binding, in bytes, if defined.
+  uint64_t size;
+  /// Size for this binding without trailing structure padding, in bytes, if
+  /// defined.
+  uint64_t size_no_padding;
+  /// Dimensionality of this binding, if defined.
+  TextureDimension dim;
+  /// Kind of data being sampled, if defined.
+  SampledKind sampled_kind;
+  /// Format of data, if defined.
+  TexelFormat image_format;
+};
+
+/// Convert from internal ast::TextureDimension to public
+/// ResourceBinding::TextureDimension
+/// @param type_dim internal value to convert from
+/// @returns the publicly visible equivalent
+ResourceBinding::TextureDimension
+TypeTextureDimensionToResourceBindingTextureDimension(
+    const ast::TextureDimension& type_dim);
+
+/// Infer ResourceBinding::SampledKind for a given sem::Type
+/// @param base_type internal type to infer from
+/// @returns the publicly visible equivalent
+ResourceBinding::SampledKind BaseTypeToSampledKind(const sem::Type* base_type);
+
+/// Convert from internal ast::TexelFormat to public
+/// ResourceBinding::TexelFormat
+/// @param image_format internal value to convert from
+/// @returns the publicly visible equivalent
+ResourceBinding::TexelFormat TypeTexelFormatToResourceBindingTexelFormat(
+    const ast::TexelFormat& image_format);
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_RESOURCE_BINDING_H_
diff --git a/src/tint/inspector/scalar.cc b/src/tint/inspector/scalar.cc
new file mode 100644
index 0000000..fa276f3
--- /dev/null
+++ b/src/tint/inspector/scalar.cc
@@ -0,0 +1,75 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/scalar.h"
+
+namespace tint {
+namespace inspector {
+
+Scalar::Scalar() : type_(kNull) {}
+
+Scalar::Scalar(bool val) : type_(kBool) {
+  value_.b = val;
+}
+
+Scalar::Scalar(uint32_t val) : type_(kU32) {
+  value_.u = val;
+}
+
+Scalar::Scalar(int32_t val) : type_(kI32) {
+  value_.i = val;
+}
+
+Scalar::Scalar(float val) : type_(kFloat) {
+  value_.f = val;
+}
+
+bool Scalar::IsNull() const {
+  return type_ == kNull;
+}
+
+bool Scalar::IsBool() const {
+  return type_ == kBool;
+}
+
+bool Scalar::IsU32() const {
+  return type_ == kU32;
+}
+
+bool Scalar::IsI32() const {
+  return type_ == kI32;
+}
+
+bool Scalar::IsFloat() const {
+  return type_ == kFloat;
+}
+
+bool Scalar::AsBool() const {
+  return value_.b;
+}
+
+uint32_t Scalar::AsU32() const {
+  return value_.u;
+}
+
+int32_t Scalar::AsI32() const {
+  return value_.i;
+}
+
+float Scalar::AsFloat() const {
+  return value_.f;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/scalar.h b/src/tint/inspector/scalar.h
new file mode 100644
index 0000000..d4d61a0
--- /dev/null
+++ b/src/tint/inspector/scalar.h
@@ -0,0 +1,80 @@
+// Copyright 2020 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_INSPECTOR_SCALAR_H_
+#define SRC_TINT_INSPECTOR_SCALAR_H_
+
+#include <cstdint>
+
+namespace tint {
+namespace inspector {
+
+/// Contains a literal scalar value
+class Scalar {
+ public:
+  /// Null Constructor
+  Scalar();
+  /// @param val literal scalar value to contain
+  explicit Scalar(bool val);
+  /// @param val literal scalar value to contain
+  explicit Scalar(uint32_t val);
+  /// @param val literal scalar value to contain
+  explicit Scalar(int32_t val);
+  /// @param val literal scalar value to contain
+  explicit Scalar(float val);
+
+  /// @returns true if this is a null
+  bool IsNull() const;
+  /// @returns true if this is a bool
+  bool IsBool() const;
+  /// @returns true if this is a unsigned integer.
+  bool IsU32() const;
+  /// @returns true if this is a signed integer.
+  bool IsI32() const;
+  /// @returns true if this is a float.
+  bool IsFloat() const;
+
+  /// @returns scalar value if bool, otherwise undefined behaviour.
+  bool AsBool() const;
+  /// @returns scalar value if unsigned integer, otherwise undefined behaviour.
+  uint32_t AsU32() const;
+  /// @returns scalar value if signed integer, otherwise undefined behaviour.
+  int32_t AsI32() const;
+  /// @returns scalar value if float, otherwise undefined behaviour.
+  float AsFloat() const;
+
+ private:
+  typedef enum {
+    kNull,
+    kBool,
+    kU32,
+    kI32,
+    kFloat,
+  } Type;
+
+  typedef union {
+    bool b;
+    uint32_t u;
+    int32_t i;
+    float f;
+  } Value;
+
+  Type type_;
+  Value value_;
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_SCALAR_H_
diff --git a/src/tint/inspector/test_inspector_builder.cc b/src/tint/inspector/test_inspector_builder.cc
new file mode 100644
index 0000000..183dbeb
--- /dev/null
+++ b/src/tint/inspector/test_inspector_builder.cc
@@ -0,0 +1,399 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/test_inspector_builder.h"
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace inspector {
+
+InspectorBuilder::InspectorBuilder() = default;
+InspectorBuilder::~InspectorBuilder() = default;
+
+void InspectorBuilder::MakeEmptyBodyFunction(std::string name,
+                                             ast::AttributeList attributes) {
+  Func(name, ast::VariableList(), ty.void_(), ast::StatementList{Return()},
+       attributes);
+}
+
+void InspectorBuilder::MakeCallerBodyFunction(std::string caller,
+                                              std::vector<std::string> callees,
+                                              ast::AttributeList attributes) {
+  ast::StatementList body;
+  body.reserve(callees.size() + 1);
+  for (auto callee : callees) {
+    body.push_back(CallStmt(Call(callee)));
+  }
+  body.push_back(Return());
+
+  Func(caller, ast::VariableList(), ty.void_(), body, attributes);
+}
+
+const ast::Struct* InspectorBuilder::MakeInOutStruct(
+    std::string name,
+    std::vector<std::tuple<std::string, uint32_t>> inout_vars) {
+  ast::StructMemberList members;
+  for (auto var : inout_vars) {
+    std::string member_name;
+    uint32_t location;
+    std::tie(member_name, location) = var;
+    members.push_back(
+        Member(member_name, ty.u32(), {Location(location), Flat()}));
+  }
+  return Structure(name, members);
+}
+
+const ast::Function* InspectorBuilder::MakePlainGlobalReferenceBodyFunction(
+    std::string func,
+    std::string var,
+    const ast::Type* type,
+    ast::AttributeList attributes) {
+  ast::StatementList stmts;
+  stmts.emplace_back(Decl(Var("local_" + var, type)));
+  stmts.emplace_back(Assign("local_" + var, var));
+  stmts.emplace_back(Return());
+
+  return Func(func, ast::VariableList(), ty.void_(), stmts, attributes);
+}
+
+bool InspectorBuilder::ContainsName(const std::vector<StageVariable>& vec,
+                                    const std::string& name) {
+  for (auto& s : vec) {
+    if (s.name == name) {
+      return true;
+    }
+  }
+  return false;
+}
+
+std::string InspectorBuilder::StructMemberName(size_t idx,
+                                               const ast::Type* type) {
+  return std::to_string(idx) + type->FriendlyName(Symbols());
+}
+
+const ast::Struct* InspectorBuilder::MakeStructType(
+    const std::string& name,
+    std::vector<const ast::Type*> member_types) {
+  ast::StructMemberList members;
+  for (auto* type : member_types) {
+    members.push_back(MakeStructMember(members.size(), type, {}));
+  }
+  return MakeStructTypeFromMembers(name, std::move(members));
+}
+
+const ast::Struct* InspectorBuilder::MakeStructTypeFromMembers(
+    const std::string& name,
+    ast::StructMemberList members) {
+  return Structure(name, std::move(members));
+}
+
+const ast::StructMember* InspectorBuilder::MakeStructMember(
+    size_t index,
+    const ast::Type* type,
+    ast::AttributeList attributes) {
+  return Member(StructMemberName(index, type), type, std::move(attributes));
+}
+
+const ast::Struct* InspectorBuilder::MakeUniformBufferType(
+    const std::string& name,
+    std::vector<const ast::Type*> member_types) {
+  return MakeStructType(name, member_types);
+}
+
+std::function<const ast::TypeName*()> InspectorBuilder::MakeStorageBufferTypes(
+    const std::string& name,
+    std::vector<const ast::Type*> member_types) {
+  MakeStructType(name, member_types);
+  return [this, name] { return ty.type_name(name); };
+}
+
+void InspectorBuilder::AddUniformBuffer(const std::string& name,
+                                        const ast::Type* type,
+                                        uint32_t group,
+                                        uint32_t binding) {
+  Global(name, type, ast::StorageClass::kUniform,
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+void InspectorBuilder::AddWorkgroupStorage(const std::string& name,
+                                           const ast::Type* type) {
+  Global(name, type, ast::StorageClass::kWorkgroup);
+}
+
+void InspectorBuilder::AddStorageBuffer(const std::string& name,
+                                        const ast::Type* type,
+                                        ast::Access access,
+                                        uint32_t group,
+                                        uint32_t binding) {
+  Global(name, type, ast::StorageClass::kStorage, access,
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+void InspectorBuilder::MakeStructVariableReferenceBodyFunction(
+    std::string func_name,
+    std::string struct_name,
+    std::vector<std::tuple<size_t, const ast::Type*>> members) {
+  ast::StatementList stmts;
+  for (auto member : members) {
+    size_t member_idx;
+    const ast::Type* member_type;
+    std::tie(member_idx, member_type) = member;
+    std::string member_name = StructMemberName(member_idx, member_type);
+
+    stmts.emplace_back(Decl(Var("local" + member_name, member_type)));
+  }
+
+  for (auto member : members) {
+    size_t member_idx;
+    const ast::Type* member_type;
+    std::tie(member_idx, member_type) = member;
+    std::string member_name = StructMemberName(member_idx, member_type);
+
+    stmts.emplace_back(Assign("local" + member_name,
+                              MemberAccessor(struct_name, member_name)));
+  }
+
+  stmts.emplace_back(Return());
+
+  Func(func_name, ast::VariableList(), ty.void_(), stmts, ast::AttributeList{});
+}
+
+void InspectorBuilder::AddSampler(const std::string& name,
+                                  uint32_t group,
+                                  uint32_t binding) {
+  Global(name, sampler_type(),
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+void InspectorBuilder::AddComparisonSampler(const std::string& name,
+                                            uint32_t group,
+                                            uint32_t binding) {
+  Global(name, comparison_sampler_type(),
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+void InspectorBuilder::AddResource(const std::string& name,
+                                   const ast::Type* type,
+                                   uint32_t group,
+                                   uint32_t binding) {
+  Global(name, type,
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+void InspectorBuilder::AddGlobalVariable(const std::string& name,
+                                         const ast::Type* type) {
+  Global(name, type, ast::StorageClass::kPrivate);
+}
+
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    const ast::Type* base_type,
+    ast::AttributeList attributes) {
+  std::string result_name = "sampler_result";
+
+  ast::StatementList stmts;
+  stmts.emplace_back(Decl(Var(result_name, ty.vec(base_type, 4))));
+
+  stmts.emplace_back(Assign(result_name, Call("textureSample", texture_name,
+                                              sampler_name, coords_name)));
+  stmts.emplace_back(Return());
+
+  return Func(func_name, ast::VariableList(), ty.void_(), stmts, attributes);
+}
+
+const ast::Function* InspectorBuilder::MakeSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    const std::string& array_index,
+    const ast::Type* base_type,
+    ast::AttributeList attributes) {
+  std::string result_name = "sampler_result";
+
+  ast::StatementList stmts;
+
+  stmts.emplace_back(Decl(Var("sampler_result", ty.vec(base_type, 4))));
+
+  stmts.emplace_back(
+      Assign("sampler_result", Call("textureSample", texture_name, sampler_name,
+                                    coords_name, array_index)));
+  stmts.emplace_back(Return());
+
+  return Func(func_name, ast::VariableList(), ty.void_(), stmts, attributes);
+}
+
+const ast::Function*
+InspectorBuilder::MakeComparisonSamplerReferenceBodyFunction(
+    const std::string& func_name,
+    const std::string& texture_name,
+    const std::string& sampler_name,
+    const std::string& coords_name,
+    const std::string& depth_name,
+    const ast::Type* base_type,
+    ast::AttributeList attributes) {
+  std::string result_name = "sampler_result";
+
+  ast::StatementList stmts;
+
+  stmts.emplace_back(Decl(Var("sampler_result", base_type)));
+  stmts.emplace_back(
+      Assign("sampler_result", Call("textureSampleCompare", texture_name,
+                                    sampler_name, coords_name, depth_name)));
+  stmts.emplace_back(Return());
+
+  return Func(func_name, ast::VariableList(), ty.void_(), stmts, attributes);
+}
+
+const ast::Type* InspectorBuilder::GetBaseType(
+    ResourceBinding::SampledKind sampled_kind) {
+  switch (sampled_kind) {
+    case ResourceBinding::SampledKind::kFloat:
+      return ty.f32();
+    case ResourceBinding::SampledKind::kSInt:
+      return ty.i32();
+    case ResourceBinding::SampledKind::kUInt:
+      return ty.u32();
+    default:
+      return nullptr;
+  }
+}
+
+const ast::Type* InspectorBuilder::GetCoordsType(ast::TextureDimension dim,
+                                                 const ast::Type* scalar) {
+  switch (dim) {
+    case ast::TextureDimension::k1d:
+      return scalar;
+    case ast::TextureDimension::k2d:
+    case ast::TextureDimension::k2dArray:
+      return create<ast::Vector>(scalar, 2);
+    case ast::TextureDimension::k3d:
+    case ast::TextureDimension::kCube:
+    case ast::TextureDimension::kCubeArray:
+      return create<ast::Vector>(scalar, 3);
+    default:
+      [=]() { FAIL() << "Unsupported texture dimension: " << dim; }();
+  }
+  return nullptr;
+}
+
+const ast::Type* InspectorBuilder::MakeStorageTextureTypes(
+    ast::TextureDimension dim,
+    ast::TexelFormat format) {
+  return ty.storage_texture(dim, format, ast::Access::kWrite);
+}
+
+void InspectorBuilder::AddStorageTexture(const std::string& name,
+                                         const ast::Type* type,
+                                         uint32_t group,
+                                         uint32_t binding) {
+  Global(name, type,
+         ast::AttributeList{
+             create<ast::BindingAttribute>(binding),
+             create<ast::GroupAttribute>(group),
+         });
+}
+
+const ast::Function* InspectorBuilder::MakeStorageTextureBodyFunction(
+    const std::string& func_name,
+    const std::string& st_name,
+    const ast::Type* dim_type,
+    ast::AttributeList attributes) {
+  ast::StatementList stmts;
+
+  stmts.emplace_back(Decl(Var("dim", dim_type)));
+  stmts.emplace_back(Assign("dim", Call("textureDimensions", st_name)));
+  stmts.emplace_back(Return());
+
+  return Func(func_name, ast::VariableList(), ty.void_(), stmts, attributes);
+}
+
+std::function<const ast::Type*()> InspectorBuilder::GetTypeFunction(
+    ComponentType component,
+    CompositionType composition) {
+  std::function<const ast::Type*()> func;
+  switch (component) {
+    case ComponentType::kFloat:
+      func = [this]() -> const ast::Type* { return ty.f32(); };
+      break;
+    case ComponentType::kSInt:
+      func = [this]() -> const ast::Type* { return ty.i32(); };
+      break;
+    case ComponentType::kUInt:
+      func = [this]() -> const ast::Type* { return ty.u32(); };
+      break;
+    case ComponentType::kUnknown:
+      return []() -> const ast::Type* { return nullptr; };
+  }
+
+  uint32_t n;
+  switch (composition) {
+    case CompositionType::kScalar:
+      return func;
+    case CompositionType::kVec2:
+      n = 2;
+      break;
+    case CompositionType::kVec3:
+      n = 3;
+      break;
+    case CompositionType::kVec4:
+      n = 4;
+      break;
+    default:
+      return []() -> ast::Type* { return nullptr; };
+  }
+
+  return [this, func, n]() -> const ast::Type* { return ty.vec(func(), n); };
+}
+
+Inspector& InspectorBuilder::Build() {
+  if (inspector_) {
+    return *inspector_;
+  }
+  program_ = std::make_unique<Program>(std::move(*this));
+  [&]() {
+    ASSERT_TRUE(program_->IsValid())
+        << diag::Formatter().format(program_->Diagnostics());
+  }();
+  inspector_ = std::make_unique<Inspector>(program_.get());
+  return *inspector_;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/test_inspector_builder.h b/src/tint/inspector/test_inspector_builder.h
new file mode 100644
index 0000000..b19066d
--- /dev/null
+++ b/src/tint/inspector/test_inspector_builder.h
@@ -0,0 +1,386 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_INSPECTOR_TEST_INSPECTOR_BUILDER_H_
+#define SRC_TINT_INSPECTOR_TEST_INSPECTOR_BUILDER_H_
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/disable_validation_attribute.h"
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/workgroup_attribute.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/external_texture_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/variable.h"
+#include "tint/tint.h"
+
+namespace tint {
+namespace inspector {
+
+/// Utility class for building programs in inspector tests
+class InspectorBuilder : public ProgramBuilder {
+ public:
+  InspectorBuilder();
+  ~InspectorBuilder() override;
+
+  /// Generates an empty function
+  /// @param name name of the function created
+  /// @param attributes the function attributes
+  void MakeEmptyBodyFunction(std::string name, ast::AttributeList attributes);
+
+  /// Generates a function that calls other functions
+  /// @param caller name of the function created
+  /// @param callees names of the functions to be called
+  /// @param attributes the function attributes
+  void MakeCallerBodyFunction(std::string caller,
+                              std::vector<std::string> callees,
+                              ast::AttributeList attributes);
+
+  /// Generates a struct that contains user-defined IO members
+  /// @param name the name of the generated struct
+  /// @param inout_vars tuples of {name, loc} that will be the struct members
+  /// @returns a structure object
+  const ast::Struct* MakeInOutStruct(
+      std::string name,
+      std::vector<std::tuple<std::string, uint32_t>> inout_vars);
+
+  // TODO(crbug.com/tint/697): Remove this.
+  /// Add In/Out variables to the global variables
+  /// @param inout_vars tuples of {in, out} that will be added as entries to the
+  ///                   global variables
+  void AddInOutVariables(
+      std::vector<std::tuple<std::string, std::string>> inout_vars);
+
+  // TODO(crbug.com/tint/697): Remove this.
+  /// Generates a function that references in/out variables
+  /// @param name name of the function created
+  /// @param inout_vars tuples of {in, out} that will be converted into out = in
+  ///                   calls in the function body
+  /// @param attributes the function attributes
+  void MakeInOutVariableBodyFunction(
+      std::string name,
+      std::vector<std::tuple<std::string, std::string>> inout_vars,
+      ast::AttributeList attributes);
+
+  // TODO(crbug.com/tint/697): Remove this.
+  /// Generates a function that references in/out variables and calls another
+  /// function.
+  /// @param caller name of the function created
+  /// @param callee name of the function to be called
+  /// @param inout_vars tuples of {in, out} that will be converted into out = in
+  ///                   calls in the function body
+  /// @param attributes the function attributes
+  /// @returns a function object
+  const ast::Function* MakeInOutVariableCallerBodyFunction(
+      std::string caller,
+      std::string callee,
+      std::vector<std::tuple<std::string, std::string>> inout_vars,
+      ast::AttributeList attributes);
+
+  /// Add a pipeline constant to the global variables, with a specific ID.
+  /// @param name name of the variable to add
+  /// @param id id number for the constant id
+  /// @param type type of the variable
+  /// @param constructor val to initialize the constant with, if NULL no
+  ///             constructor will be added.
+  /// @returns the constant that was created
+  const ast::Variable* AddOverridableConstantWithID(
+      std::string name,
+      uint32_t id,
+      const ast::Type* type,
+      const ast::Expression* constructor) {
+    return Override(name, type, constructor, {Id(id)});
+  }
+
+  /// Add a pipeline constant to the global variables, without a specific ID.
+  /// @param name name of the variable to add
+  /// @param type type of the variable
+  /// @param constructor val to initialize the constant with, if NULL no
+  ///             constructor will be added.
+  /// @returns the constant that was created
+  const ast::Variable* AddOverridableConstantWithoutID(
+      std::string name,
+      const ast::Type* type,
+      const ast::Expression* constructor) {
+    return Override(name, type, constructor);
+  }
+
+  /// Generates a function that references module-scoped, plain-typed constant
+  /// or variable.
+  /// @param func name of the function created
+  /// @param var name of the constant to be reference
+  /// @param type type of the const being referenced
+  /// @param attributes the function attributes
+  /// @returns a function object
+  const ast::Function* MakePlainGlobalReferenceBodyFunction(
+      std::string func,
+      std::string var,
+      const ast::Type* type,
+      ast::AttributeList attributes);
+
+  /// @param vec Vector of StageVariable to be searched
+  /// @param name Name to be searching for
+  /// @returns true if name is in vec, otherwise false
+  bool ContainsName(const std::vector<StageVariable>& vec,
+                    const std::string& name);
+
+  /// Builds a string for accessing a member in a generated struct
+  /// @param idx index of member
+  /// @param type type of member
+  /// @returns a string for the member
+  std::string StructMemberName(size_t idx, const ast::Type* type);
+
+  /// Generates a struct type
+  /// @param name name for the type
+  /// @param member_types a vector of member types
+  /// @returns a struct type
+  const ast::Struct* MakeStructType(const std::string& name,
+                                    std::vector<const ast::Type*> member_types);
+
+  /// Generates a struct type from a list of member nodes.
+  /// @param name name for the struct type
+  /// @param members a vector of members
+  /// @returns a struct type
+  const ast::Struct* MakeStructTypeFromMembers(const std::string& name,
+                                               ast::StructMemberList members);
+
+  /// Generates a struct member with a specified index and type.
+  /// @param index index of the field within the struct
+  /// @param type the type of the member field
+  /// @param attributes a list of attributes to apply to the member field
+  /// @returns a struct member
+  const ast::StructMember* MakeStructMember(size_t index,
+                                            const ast::Type* type,
+                                            ast::AttributeList attributes);
+
+  /// Generates types appropriate for using in an uniform buffer
+  /// @param name name for the type
+  /// @param member_types a vector of member types
+  /// @returns a struct type that has the layout for an uniform buffer.
+  const ast::Struct* MakeUniformBufferType(
+      const std::string& name,
+      std::vector<const ast::Type*> member_types);
+
+  /// Generates types appropriate for using in a storage buffer
+  /// @param name name for the type
+  /// @param member_types a vector of member types
+  /// @returns a function that returns the created structure.
+  std::function<const ast::TypeName*()> MakeStorageBufferTypes(
+      const std::string& name,
+      std::vector<const ast::Type*> member_types);
+
+  /// Adds an uniform buffer variable to the program
+  /// @param name the name of the variable
+  /// @param type the type to use
+  /// @param group the binding/group/ to use for the uniform buffer
+  /// @param binding the binding number to use for the uniform buffer
+  void AddUniformBuffer(const std::string& name,
+                        const ast::Type* type,
+                        uint32_t group,
+                        uint32_t binding);
+
+  /// Adds a workgroup storage variable to the program
+  /// @param name the name of the variable
+  /// @param type the type of the variable
+  void AddWorkgroupStorage(const std::string& name, const ast::Type* type);
+
+  /// Adds a storage buffer variable to the program
+  /// @param name the name of the variable
+  /// @param type the type to use
+  /// @param access the storage buffer access control
+  /// @param group the binding/group to use for the storage buffer
+  /// @param binding the binding number to use for the storage buffer
+  void AddStorageBuffer(const std::string& name,
+                        const ast::Type* type,
+                        ast::Access access,
+                        uint32_t group,
+                        uint32_t binding);
+
+  /// Generates a function that references a specific struct variable
+  /// @param func_name name of the function created
+  /// @param struct_name name of the struct variabler to be accessed
+  /// @param members list of members to access, by index and type
+  void MakeStructVariableReferenceBodyFunction(
+      std::string func_name,
+      std::string struct_name,
+      std::vector<std::tuple<size_t, const ast::Type*>> members);
+
+  /// Adds a regular sampler variable to the program
+  /// @param name the name of the variable
+  /// @param group the binding/group to use for the storage buffer
+  /// @param binding the binding number to use for the storage buffer
+  void AddSampler(const std::string& name, uint32_t group, uint32_t binding);
+
+  /// Adds a comparison sampler variable to the program
+  /// @param name the name of the variable
+  /// @param group the binding/group to use for the storage buffer
+  /// @param binding the binding number to use for the storage buffer
+  void AddComparisonSampler(const std::string& name,
+                            uint32_t group,
+                            uint32_t binding);
+
+  /// Adds a sampler or texture variable to the program
+  /// @param name the name of the variable
+  /// @param type the type to use
+  /// @param group the binding/group to use for the resource
+  /// @param binding the binding number to use for the resource
+  void AddResource(const std::string& name,
+                   const ast::Type* type,
+                   uint32_t group,
+                   uint32_t binding);
+
+  /// Add a module scope private variable to the progames
+  /// @param name the name of the variable
+  /// @param type the type to use
+  void AddGlobalVariable(const std::string& name, const ast::Type* type);
+
+  /// Generates a function that references a specific sampler variable
+  /// @param func_name name of the function created
+  /// @param texture_name name of the texture to be sampled
+  /// @param sampler_name name of the sampler to use
+  /// @param coords_name name of the coords variable to use
+  /// @param base_type sampler base type
+  /// @param attributes the function attributes
+  /// @returns a function that references all of the values specified
+  const ast::Function* MakeSamplerReferenceBodyFunction(
+      const std::string& func_name,
+      const std::string& texture_name,
+      const std::string& sampler_name,
+      const std::string& coords_name,
+      const ast::Type* base_type,
+      ast::AttributeList attributes);
+
+  /// Generates a function that references a specific sampler variable
+  /// @param func_name name of the function created
+  /// @param texture_name name of the texture to be sampled
+  /// @param sampler_name name of the sampler to use
+  /// @param coords_name name of the coords variable to use
+  /// @param array_index name of the array index variable to use
+  /// @param base_type sampler base type
+  /// @param attributes the function attributes
+  /// @returns a function that references all of the values specified
+  const ast::Function* MakeSamplerReferenceBodyFunction(
+      const std::string& func_name,
+      const std::string& texture_name,
+      const std::string& sampler_name,
+      const std::string& coords_name,
+      const std::string& array_index,
+      const ast::Type* base_type,
+      ast::AttributeList attributes);
+
+  /// Generates a function that references a specific comparison sampler
+  /// variable.
+  /// @param func_name name of the function created
+  /// @param texture_name name of the depth texture to  use
+  /// @param sampler_name name of the sampler to use
+  /// @param coords_name name of the coords variable to use
+  /// @param depth_name name of the depth reference to use
+  /// @param base_type sampler base type
+  /// @param attributes the function attributes
+  /// @returns a function that references all of the values specified
+  const ast::Function* MakeComparisonSamplerReferenceBodyFunction(
+      const std::string& func_name,
+      const std::string& texture_name,
+      const std::string& sampler_name,
+      const std::string& coords_name,
+      const std::string& depth_name,
+      const ast::Type* base_type,
+      ast::AttributeList attributes);
+
+  /// Gets an appropriate type for the data in a given texture type.
+  /// @param sampled_kind type of in the texture
+  /// @returns a pointer to a type appropriate for the coord param
+  const ast::Type* GetBaseType(ResourceBinding::SampledKind sampled_kind);
+
+  /// Gets an appropriate type for the coords parameter depending the the
+  /// dimensionality of the texture being sampled.
+  /// @param dim dimensionality of the texture being sampled
+  /// @param scalar the scalar type
+  /// @returns a pointer to a type appropriate for the coord param
+  const ast::Type* GetCoordsType(ast::TextureDimension dim,
+                                 const ast::Type* scalar);
+
+  /// Generates appropriate types for a Read-Only StorageTexture
+  /// @param dim the texture dimension of the storage texture
+  /// @param format the texel format of the storage texture
+  /// @returns the storage texture type
+  const ast::Type* MakeStorageTextureTypes(ast::TextureDimension dim,
+                                           ast::TexelFormat format);
+
+  /// Adds a storage texture variable to the program
+  /// @param name the name of the variable
+  /// @param type the type to use
+  /// @param group the binding/group to use for the sampled texture
+  /// @param binding the binding57 number to use for the sampled texture
+  void AddStorageTexture(const std::string& name,
+                         const ast::Type* type,
+                         uint32_t group,
+                         uint32_t binding);
+
+  /// Generates a function that references a storage texture variable.
+  /// @param func_name name of the function created
+  /// @param st_name name of the storage texture to use
+  /// @param dim_type type expected by textureDimensons to return
+  /// @param attributes the function attributes
+  /// @returns a function that references all of the values specified
+  const ast::Function* MakeStorageTextureBodyFunction(
+      const std::string& func_name,
+      const std::string& st_name,
+      const ast::Type* dim_type,
+      ast::AttributeList attributes);
+
+  /// Get a generator function that returns a type appropriate for a stage
+  /// variable with the given combination of component and composition type.
+  /// @param component component type of the stage variable
+  /// @param composition composition type of the stage variable
+  /// @returns a generator function for the stage variable's type.
+  std::function<const ast::Type*()> GetTypeFunction(
+      ComponentType component,
+      CompositionType composition);
+
+  /// Build the Program given all of the previous methods called and return an
+  /// Inspector for it.
+  /// Should only be called once per test.
+  /// @returns a reference to the Inspector for the built Program.
+  Inspector& Build();
+
+  /// @returns the type for a SamplerKind::kSampler
+  const ast::Sampler* sampler_type() {
+    return ty.sampler(ast::SamplerKind::kSampler);
+  }
+
+  /// @returns the type for a SamplerKind::kComparison
+  const ast::Sampler* comparison_sampler_type() {
+    return ty.sampler(ast::SamplerKind::kComparisonSampler);
+  }
+
+ protected:
+  /// Program built by this builder.
+  std::unique_ptr<Program> program_;
+  /// Inspector for |program_|
+  std::unique_ptr<Inspector> inspector_;
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_TEST_INSPECTOR_BUILDER_H_
diff --git a/src/tint/inspector/test_inspector_runner.cc b/src/tint/inspector/test_inspector_runner.cc
new file mode 100644
index 0000000..5b937eb
--- /dev/null
+++ b/src/tint/inspector/test_inspector_runner.cc
@@ -0,0 +1,39 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/inspector/test_inspector_runner.h"
+
+namespace tint {
+namespace inspector {
+
+InspectorRunner::InspectorRunner() = default;
+InspectorRunner::~InspectorRunner() = default;
+
+Inspector& InspectorRunner::Initialize(std::string shader) {
+  if (inspector_) {
+    return *inspector_;
+  }
+
+  file_ = std::make_unique<Source::File>("test", shader);
+  program_ = std::make_unique<Program>(reader::wgsl::Parse(file_.get()));
+  [&]() {
+    ASSERT_TRUE(program_->IsValid())
+        << diag::Formatter().format(program_->Diagnostics());
+  }();
+  inspector_ = std::make_unique<Inspector>(program_.get());
+  return *inspector_;
+}
+
+}  // namespace inspector
+}  // namespace tint
diff --git a/src/tint/inspector/test_inspector_runner.h b/src/tint/inspector/test_inspector_runner.h
new file mode 100644
index 0000000..0d435d1
--- /dev/null
+++ b/src/tint/inspector/test_inspector_runner.h
@@ -0,0 +1,51 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
+#define SRC_TINT_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
+
+#include <memory>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "tint/tint.h"
+
+namespace tint {
+namespace inspector {
+
+/// Utility class for running shaders in inspector tests
+class InspectorRunner {
+ public:
+  InspectorRunner();
+  virtual ~InspectorRunner();
+
+  /// Create a Program with Inspector from the provided WGSL shader.
+  /// Should only be called once per test.
+  /// @param shader a WGSL shader
+  /// @returns a reference to the Inspector for the built Program.
+  Inspector& Initialize(std::string shader);
+
+ protected:
+  /// File created from input shader and used to create Program.
+  std::unique_ptr<Source::File> file_;
+  /// Program created by this runner.
+  std::unique_ptr<Program> program_;
+  /// Inspector for |program_|
+  std::unique_ptr<Inspector> inspector_;
+};
+
+}  // namespace inspector
+}  // namespace tint
+
+#endif  // SRC_TINT_INSPECTOR_TEST_INSPECTOR_RUNNER_H_
diff --git a/src/tint/program.cc b/src/tint/program.cc
new file mode 100644
index 0000000..a6a6ab7
--- /dev/null
+++ b/src/tint/program.cc
@@ -0,0 +1,131 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/program.h"
+
+#include <utility>
+
+#include "src/tint/demangler.h"
+#include "src/tint/resolver/resolver.h"
+#include "src/tint/sem/expression.h"
+
+namespace tint {
+namespace {
+
+std::string DefaultPrinter(const Program*) {
+  return "<no program printer assigned>";
+}
+
+}  // namespace
+
+Program::Printer Program::printer = DefaultPrinter;
+
+Program::Program() = default;
+
+Program::Program(Program&& program)
+    : id_(std::move(program.id_)),
+      types_(std::move(program.types_)),
+      ast_nodes_(std::move(program.ast_nodes_)),
+      sem_nodes_(std::move(program.sem_nodes_)),
+      ast_(std::move(program.ast_)),
+      sem_(std::move(program.sem_)),
+      symbols_(std::move(program.symbols_)),
+      diagnostics_(std::move(program.diagnostics_)),
+      is_valid_(program.is_valid_) {
+  program.AssertNotMoved();
+  program.moved_ = true;
+}
+
+Program::Program(ProgramBuilder&& builder) {
+  id_ = builder.ID();
+
+  is_valid_ = builder.IsValid();
+  if (builder.ResolveOnBuild() && builder.IsValid()) {
+    resolver::Resolver resolver(&builder);
+    if (!resolver.Resolve()) {
+      is_valid_ = false;
+    }
+  }
+
+  // The above must be called *before* the calls to std::move() below
+  types_ = std::move(builder.Types());
+  ast_nodes_ = std::move(builder.ASTNodes());
+  sem_nodes_ = std::move(builder.SemNodes());
+  ast_ = &builder.AST();  // ast::Module is actually a heap allocation.
+  sem_ = std::move(builder.Sem());
+  symbols_ = std::move(builder.Symbols());
+  diagnostics_.add(std::move(builder.Diagnostics()));
+  builder.MarkAsMoved();
+
+  if (!is_valid_ && !diagnostics_.contains_errors()) {
+    // If the builder claims to be invalid, then we really should have an error
+    // message generated. If we find a situation where the program is not valid
+    // and there are no errors reported, add one here.
+    diagnostics_.add_error(diag::System::Program, "invalid program generated");
+  }
+}
+
+Program::~Program() = default;
+
+Program& Program::operator=(Program&& program) {
+  program.AssertNotMoved();
+  program.moved_ = true;
+  moved_ = false;
+  id_ = std::move(program.id_);
+  types_ = std::move(program.types_);
+  ast_nodes_ = std::move(program.ast_nodes_);
+  sem_nodes_ = std::move(program.sem_nodes_);
+  ast_ = std::move(program.ast_);
+  sem_ = std::move(program.sem_);
+  symbols_ = std::move(program.symbols_);
+  diagnostics_ = std::move(program.diagnostics_);
+  is_valid_ = program.is_valid_;
+  return *this;
+}
+
+Program Program::Clone() const {
+  AssertNotMoved();
+  return Program(CloneAsBuilder());
+}
+
+ProgramBuilder Program::CloneAsBuilder() const {
+  AssertNotMoved();
+  ProgramBuilder out;
+  CloneContext(&out, this).Clone();
+  return out;
+}
+
+bool Program::IsValid() const {
+  AssertNotMoved();
+  return is_valid_;
+}
+
+const sem::Type* Program::TypeOf(const ast::Expression* expr) const {
+  auto* sem = Sem().Get(expr);
+  return sem ? sem->Type() : nullptr;
+}
+
+const sem::Type* Program::TypeOf(const ast::Type* type) const {
+  return Sem().Get(type);
+}
+
+const sem::Type* Program::TypeOf(const ast::TypeDecl* type_decl) const {
+  return Sem().Get(type_decl);
+}
+
+void Program::AssertNotMoved() const {
+  TINT_ASSERT(Program, !moved_);
+}
+
+}  // namespace tint
diff --git a/src/tint/program.h b/src/tint/program.h
new file mode 100644
index 0000000..23e0945
--- /dev/null
+++ b/src/tint/program.h
@@ -0,0 +1,180 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_PROGRAM_H_
+#define SRC_TINT_PROGRAM_H_
+
+#include <string>
+#include <unordered_set>
+
+#include "src/tint/ast/function.h"
+#include "src/tint/program_id.h"
+#include "src/tint/sem/info.h"
+#include "src/tint/sem/type_manager.h"
+#include "src/tint/symbol_table.h"
+
+namespace tint {
+
+// Forward declarations
+class CloneContext;
+
+namespace ast {
+
+class Module;
+
+}  // namespace ast
+
+/// Program holds the AST, Type information and SymbolTable for a tint program.
+class Program {
+ public:
+  /// ASTNodeAllocator is an alias to BlockAllocator<ast::Node>
+  using ASTNodeAllocator = utils::BlockAllocator<ast::Node>;
+
+  /// SemNodeAllocator is an alias to BlockAllocator<sem::Node>
+  using SemNodeAllocator = utils::BlockAllocator<sem::Node>;
+
+  /// Constructor
+  Program();
+
+  /// Move constructor
+  /// @param rhs the Program to move
+  Program(Program&& rhs);
+
+  /// Move constructor from builder
+  /// @param builder the builder used to construct the program
+  explicit Program(ProgramBuilder&& builder);
+
+  /// Destructor
+  ~Program();
+
+  /// Move assignment operator
+  /// @param rhs the Program to move
+  /// @return this Program
+  Program& operator=(Program&& rhs);
+
+  /// @returns the unique identifier for this program
+  ProgramID ID() const { return id_; }
+
+  /// @returns a reference to the program's types
+  const sem::Manager& Types() const {
+    AssertNotMoved();
+    return types_;
+  }
+
+  /// @returns a reference to the program's AST nodes storage
+  const ASTNodeAllocator& ASTNodes() const {
+    AssertNotMoved();
+    return ast_nodes_;
+  }
+
+  /// @returns a reference to the program's semantic nodes storage
+  const SemNodeAllocator& SemNodes() const {
+    AssertNotMoved();
+    return sem_nodes_;
+  }
+
+  /// @returns a reference to the program's AST root Module
+  const ast::Module& AST() const {
+    AssertNotMoved();
+    return *ast_;
+  }
+
+  /// @returns a reference to the program's semantic info
+  const sem::Info& Sem() const {
+    AssertNotMoved();
+    return sem_;
+  }
+
+  /// @returns a reference to the program's SymbolTable
+  const SymbolTable& Symbols() const {
+    AssertNotMoved();
+    return symbols_;
+  }
+
+  /// @returns a reference to the program's diagnostics
+  const diag::List& Diagnostics() const {
+    AssertNotMoved();
+    return diagnostics_;
+  }
+
+  /// Performs a deep clone of this program.
+  /// The returned Program will contain no pointers to objects owned by this
+  /// Program, and so after calling, this Program can be safely destructed.
+  /// @return a new Program copied from this Program
+  Program Clone() const;
+
+  /// Performs a deep clone of this Program's AST nodes, types and symbols into
+  /// a new ProgramBuilder. Semantic nodes are not cloned, as these will be
+  /// rebuilt when the ProgramBuilder builds its Program.
+  /// The returned ProgramBuilder will contain no pointers to objects owned by
+  /// this Program, and so after calling, this Program can be safely destructed.
+  /// @return a new ProgramBuilder copied from this Program
+  ProgramBuilder CloneAsBuilder() const;
+
+  /// @returns true if the program has no error diagnostics and is not missing
+  /// information
+  bool IsValid() const;
+
+  /// Helper for returning the resolved semantic type of the expression `expr`.
+  /// @param expr the AST expression
+  /// @return the resolved semantic type for the expression, or nullptr if the
+  /// expression has no resolved type.
+  const sem::Type* TypeOf(const ast::Expression* expr) const;
+
+  /// Helper for returning the resolved semantic type of the AST type `type`.
+  /// @param type the AST type
+  /// @return the resolved semantic type for the type, or nullptr if the type
+  /// has no resolved type.
+  const sem::Type* TypeOf(const ast::Type* type) const;
+
+  /// Helper for returning the resolved semantic type of the AST type
+  /// declaration `type_decl`.
+  /// @param type_decl the AST type declaration
+  /// @return the resolved semantic type for the type declaration, or nullptr if
+  /// the type declaration has no resolved type.
+  const sem::Type* TypeOf(const ast::TypeDecl* type_decl) const;
+
+  /// A function that can be used to print a program
+  using Printer = std::string (*)(const Program*);
+
+  /// The Program printer used for testing and debugging.
+  static Printer printer;
+
+ private:
+  Program(const Program&) = delete;
+
+  /// Asserts that the program has not been moved.
+  void AssertNotMoved() const;
+
+  ProgramID id_;
+  sem::Manager types_;
+  ASTNodeAllocator ast_nodes_;
+  SemNodeAllocator sem_nodes_;
+  ast::Module* ast_ = nullptr;
+  sem::Info sem_;
+  SymbolTable symbols_{id_};
+  diag::List diagnostics_;
+  bool is_valid_ = false;  // Not valid until it is built
+  bool moved_ = false;
+};
+
+/// @param program the Program
+/// @returns the ProgramID of the Program
+inline ProgramID ProgramIDOf(const Program* program) {
+  return program->ID();
+}
+
+}  // namespace tint
+
+#endif  // SRC_TINT_PROGRAM_H_
diff --git a/src/tint/program_builder.cc b/src/tint/program_builder.cc
new file mode 100644
index 0000000..c2f58ec
--- /dev/null
+++ b/src/tint/program_builder.cc
@@ -0,0 +1,138 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/program_builder.h"
+
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/debug.h"
+#include "src/tint/demangler.h"
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+
+ProgramBuilder::VarOptionals::~VarOptionals() = default;
+
+ProgramBuilder::ProgramBuilder()
+    : id_(ProgramID::New()),
+      ast_(ast_nodes_.Create<ast::Module>(id_, Source{})) {}
+
+ProgramBuilder::ProgramBuilder(ProgramBuilder&& rhs)
+    : id_(std::move(rhs.id_)),
+      types_(std::move(rhs.types_)),
+      ast_nodes_(std::move(rhs.ast_nodes_)),
+      sem_nodes_(std::move(rhs.sem_nodes_)),
+      ast_(rhs.ast_),
+      sem_(std::move(rhs.sem_)),
+      symbols_(std::move(rhs.symbols_)),
+      diagnostics_(std::move(rhs.diagnostics_)) {
+  rhs.MarkAsMoved();
+}
+
+ProgramBuilder::~ProgramBuilder() = default;
+
+ProgramBuilder& ProgramBuilder::operator=(ProgramBuilder&& rhs) {
+  rhs.MarkAsMoved();
+  AssertNotMoved();
+  id_ = std::move(rhs.id_);
+  types_ = std::move(rhs.types_);
+  ast_nodes_ = std::move(rhs.ast_nodes_);
+  sem_nodes_ = std::move(rhs.sem_nodes_);
+  ast_ = rhs.ast_;
+  sem_ = std::move(rhs.sem_);
+  symbols_ = std::move(rhs.symbols_);
+  diagnostics_ = std::move(rhs.diagnostics_);
+
+  return *this;
+}
+
+ProgramBuilder ProgramBuilder::Wrap(const Program* program) {
+  ProgramBuilder builder;
+  builder.id_ = program->ID();
+  builder.types_ = sem::Manager::Wrap(program->Types());
+  builder.ast_ = builder.create<ast::Module>(
+      program->AST().source, program->AST().GlobalDeclarations());
+  builder.sem_ = sem::Info::Wrap(program->Sem());
+  builder.symbols_ = program->Symbols();
+  builder.diagnostics_ = program->Diagnostics();
+  return builder;
+}
+
+bool ProgramBuilder::IsValid() const {
+  return !diagnostics_.contains_errors();
+}
+
+void ProgramBuilder::MarkAsMoved() {
+  AssertNotMoved();
+  moved_ = true;
+}
+
+void ProgramBuilder::AssertNotMoved() const {
+  if (moved_) {
+    TINT_ICE(ProgramBuilder, const_cast<ProgramBuilder*>(this)->diagnostics_)
+        << "Attempting to use ProgramBuilder after it has been moved";
+  }
+}
+
+const sem::Type* ProgramBuilder::TypeOf(const ast::Expression* expr) const {
+  auto* sem = Sem().Get(expr);
+  return sem ? sem->Type() : nullptr;
+}
+
+const sem::Type* ProgramBuilder::TypeOf(const ast::Variable* var) const {
+  auto* sem = Sem().Get(var);
+  return sem ? sem->Type() : nullptr;
+}
+
+const sem::Type* ProgramBuilder::TypeOf(const ast::Type* type) const {
+  return Sem().Get(type);
+}
+
+const sem::Type* ProgramBuilder::TypeOf(const ast::TypeDecl* type_decl) const {
+  return Sem().Get(type_decl);
+}
+
+const ast::TypeName* ProgramBuilder::TypesBuilder::Of(
+    const ast::TypeDecl* decl) const {
+  return type_name(decl->name);
+}
+
+ProgramBuilder::TypesBuilder::TypesBuilder(ProgramBuilder* pb) : builder(pb) {}
+
+const ast::Statement* ProgramBuilder::WrapInStatement(
+    const ast::Expression* expr) {
+  // Create a temporary variable of inferred type from expr.
+  return Decl(Const(symbols_.New(), nullptr, expr));
+}
+
+const ast::VariableDeclStatement* ProgramBuilder::WrapInStatement(
+    const ast::Variable* v) {
+  return create<ast::VariableDeclStatement>(v);
+}
+
+const ast::Statement* ProgramBuilder::WrapInStatement(
+    const ast::Statement* stmt) {
+  return stmt;
+}
+
+const ast::Function* ProgramBuilder::WrapInFunction(
+    const ast::StatementList stmts) {
+  return Func("test_function", {}, ty.void_(), std::move(stmts),
+              {create<ast::StageAttribute>(ast::PipelineStage::kCompute),
+               WorkgroupSize(1, 1, 1)});
+}
+
+}  // namespace tint
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
new file mode 100644
index 0000000..eb87d3e
--- /dev/null
+++ b/src/tint/program_builder.h
@@ -0,0 +1,2779 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_PROGRAM_BUILDER_H_
+#define SRC_TINT_PROGRAM_BUILDER_H_
+
+#include <string>
+#include <unordered_set>
+#include <utility>
+
+#include "src/tint/ast/alias.h"
+#include "src/tint/ast/array.h"
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/atomic.h"
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/ast/binding_attribute.h"
+#include "src/tint/ast/bitcast_expression.h"
+#include "src/tint/ast/bool.h"
+#include "src/tint/ast/bool_literal_expression.h"
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/case_statement.h"
+#include "src/tint/ast/compound_assignment_statement.h"
+#include "src/tint/ast/continue_statement.h"
+#include "src/tint/ast/depth_multisampled_texture.h"
+#include "src/tint/ast/depth_texture.h"
+#include "src/tint/ast/disable_validation_attribute.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/external_texture.h"
+#include "src/tint/ast/f32.h"
+#include "src/tint/ast/fallthrough_statement.h"
+#include "src/tint/ast/float_literal_expression.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/i32.h"
+#include "src/tint/ast/id_attribute.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/index_accessor_expression.h"
+#include "src/tint/ast/interpolate_attribute.h"
+#include "src/tint/ast/invariant_attribute.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/matrix.h"
+#include "src/tint/ast/member_accessor_expression.h"
+#include "src/tint/ast/module.h"
+#include "src/tint/ast/multisampled_texture.h"
+#include "src/tint/ast/phony_expression.h"
+#include "src/tint/ast/pointer.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/sampled_texture.h"
+#include "src/tint/ast/sampler.h"
+#include "src/tint/ast/sint_literal_expression.h"
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/storage_texture.h"
+#include "src/tint/ast/stride_attribute.h"
+#include "src/tint/ast/struct_member_align_attribute.h"
+#include "src/tint/ast/struct_member_offset_attribute.h"
+#include "src/tint/ast/struct_member_size_attribute.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/type_name.h"
+#include "src/tint/ast/u32.h"
+#include "src/tint/ast/uint_literal_expression.h"
+#include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/ast/vector.h"
+#include "src/tint/ast/void.h"
+#include "src/tint/ast/workgroup_attribute.h"
+#include "src/tint/program.h"
+#include "src/tint/program_id.h"
+#include "src/tint/sem/array.h"
+#include "src/tint/sem/bool_type.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/external_texture_type.h"
+#include "src/tint/sem/f32_type.h"
+#include "src/tint/sem/i32_type.h"
+#include "src/tint/sem/matrix_type.h"
+#include "src/tint/sem/multisampled_texture_type.h"
+#include "src/tint/sem/pointer_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+#include "src/tint/sem/storage_texture_type.h"
+#include "src/tint/sem/struct.h"
+#include "src/tint/sem/u32_type.h"
+#include "src/tint/sem/vector_type.h"
+#include "src/tint/sem/void_type.h"
+
+#ifdef INCLUDE_TINT_TINT_H_
+#error "internal tint header being #included from tint.h"
+#endif
+
+// Forward declarations
+namespace tint {
+namespace ast {
+class VariableDeclStatement;
+}  // namespace ast
+}  // namespace tint
+
+namespace tint {
+class CloneContext;
+
+/// ProgramBuilder is a mutable builder for a Program.
+/// To construct a Program, populate the builder and then `std::move` it to a
+/// Program.
+class ProgramBuilder {
+  /// A helper used to disable overloads if the first type in `TYPES` is a
+  /// Source. Used to avoid ambiguities in overloads that take a Source as the
+  /// first parameter and those that perfectly-forward the first argument.
+  template <typename... TYPES>
+  using DisableIfSource = traits::EnableIfIsNotType<
+      traits::Decay<traits::NthTypeOf<0, TYPES..., void>>,
+      Source>;
+
+  /// VarOptionals is a helper for accepting a number of optional, extra
+  /// arguments for Var() and Global().
+  struct VarOptionals {
+    template <typename... ARGS>
+    explicit VarOptionals(ARGS&&... args) {
+      Apply(std::forward<ARGS>(args)...);
+    }
+    ~VarOptionals();
+
+    ast::StorageClass storage = ast::StorageClass::kNone;
+    ast::Access access = ast::Access::kUndefined;
+    const ast::Expression* constructor = nullptr;
+    ast::AttributeList attributes = {};
+
+   private:
+    void Set(ast::StorageClass sc) { storage = sc; }
+    void Set(ast::Access ac) { access = ac; }
+    void Set(const ast::Expression* c) { constructor = c; }
+    void Set(const ast::AttributeList& l) { attributes = l; }
+
+    template <typename FIRST, typename... ARGS>
+    void Apply(FIRST&& first, ARGS&&... args) {
+      Set(std::forward<FIRST>(first));
+      Apply(std::forward<ARGS>(args)...);
+    }
+    void Apply() {}
+  };
+
+ public:
+  /// ASTNodeAllocator is an alias to BlockAllocator<ast::Node>
+  using ASTNodeAllocator = utils::BlockAllocator<ast::Node>;
+
+  /// SemNodeAllocator is an alias to BlockAllocator<sem::Node>
+  using SemNodeAllocator = utils::BlockAllocator<sem::Node>;
+
+  /// `i32` is a type alias to `int`.
+  /// Useful for passing to template methods such as `vec2<i32>()` to imitate
+  /// WGSL syntax.
+  /// Note: this is intentionally not aliased to uint32_t as we want integer
+  /// literals passed to the builder to match WGSL's integer literal types.
+  using i32 = decltype(1);
+  /// `u32` is a type alias to `unsigned int`.
+  /// Useful for passing to template methods such as `vec2<u32>()` to imitate
+  /// WGSL syntax.
+  /// Note: this is intentionally not aliased to uint32_t as we want integer
+  /// literals passed to the builder to match WGSL's integer literal types.
+  using u32 = decltype(1u);
+  /// `f32` is a type alias to `float`
+  /// Useful for passing to template methods such as `vec2<f32>()` to imitate
+  /// WGSL syntax.
+  using f32 = float;
+
+  /// Constructor
+  ProgramBuilder();
+
+  /// Move constructor
+  /// @param rhs the builder to move
+  ProgramBuilder(ProgramBuilder&& rhs);
+
+  /// Destructor
+  virtual ~ProgramBuilder();
+
+  /// Move assignment operator
+  /// @param rhs the builder to move
+  /// @return this builder
+  ProgramBuilder& operator=(ProgramBuilder&& rhs);
+
+  /// Wrap returns a new ProgramBuilder wrapping the Program `program` without
+  /// making a deep clone of the Program contents.
+  /// ProgramBuilder returned by Wrap() is intended to temporarily extend an
+  /// existing immutable program.
+  /// As the returned ProgramBuilder wraps `program`, `program` must not be
+  /// destructed or assigned while using the returned ProgramBuilder.
+  /// TODO(bclayton) - Evaluate whether there are safer alternatives to this
+  /// function. See crbug.com/tint/460.
+  /// @param program the immutable Program to wrap
+  /// @return the ProgramBuilder that wraps `program`
+  static ProgramBuilder Wrap(const Program* program);
+
+  /// @returns the unique identifier for this program
+  ProgramID ID() const { return id_; }
+
+  /// @returns a reference to the program's types
+  sem::Manager& Types() {
+    AssertNotMoved();
+    return types_;
+  }
+
+  /// @returns a reference to the program's types
+  const sem::Manager& Types() const {
+    AssertNotMoved();
+    return types_;
+  }
+
+  /// @returns a reference to the program's AST nodes storage
+  ASTNodeAllocator& ASTNodes() {
+    AssertNotMoved();
+    return ast_nodes_;
+  }
+
+  /// @returns a reference to the program's AST nodes storage
+  const ASTNodeAllocator& ASTNodes() const {
+    AssertNotMoved();
+    return ast_nodes_;
+  }
+
+  /// @returns a reference to the program's semantic nodes storage
+  SemNodeAllocator& SemNodes() {
+    AssertNotMoved();
+    return sem_nodes_;
+  }
+
+  /// @returns a reference to the program's semantic nodes storage
+  const SemNodeAllocator& SemNodes() const {
+    AssertNotMoved();
+    return sem_nodes_;
+  }
+
+  /// @returns a reference to the program's AST root Module
+  ast::Module& AST() {
+    AssertNotMoved();
+    return *ast_;
+  }
+
+  /// @returns a reference to the program's AST root Module
+  const ast::Module& AST() const {
+    AssertNotMoved();
+    return *ast_;
+  }
+
+  /// @returns a reference to the program's semantic info
+  sem::Info& Sem() {
+    AssertNotMoved();
+    return sem_;
+  }
+
+  /// @returns a reference to the program's semantic info
+  const sem::Info& Sem() const {
+    AssertNotMoved();
+    return sem_;
+  }
+
+  /// @returns a reference to the program's SymbolTable
+  SymbolTable& Symbols() {
+    AssertNotMoved();
+    return symbols_;
+  }
+
+  /// @returns a reference to the program's SymbolTable
+  const SymbolTable& Symbols() const {
+    AssertNotMoved();
+    return symbols_;
+  }
+
+  /// @returns a reference to the program's diagnostics
+  diag::List& Diagnostics() {
+    AssertNotMoved();
+    return diagnostics_;
+  }
+
+  /// @returns a reference to the program's diagnostics
+  const diag::List& Diagnostics() const {
+    AssertNotMoved();
+    return diagnostics_;
+  }
+
+  /// Controls whether the Resolver will be run on the program when it is built.
+  /// @param enable the new flag value (defaults to true)
+  void SetResolveOnBuild(bool enable) { resolve_on_build_ = enable; }
+
+  /// @return true if the Resolver will be run on the program when it is
+  /// built.
+  bool ResolveOnBuild() const { return resolve_on_build_; }
+
+  /// @returns true if the program has no error diagnostics and is not missing
+  /// information
+  bool IsValid() const;
+
+  /// Creates a new ast::Node owned by the ProgramBuilder. When the
+  /// ProgramBuilder is destructed, the ast::Node will also be destructed.
+  /// @param source the Source of the node
+  /// @param args the arguments to pass to the type constructor
+  /// @returns the node pointer
+  template <typename T, typename... ARGS>
+  traits::EnableIfIsType<T, ast::Node>* create(const Source& source,
+                                               ARGS&&... args) {
+    AssertNotMoved();
+    return ast_nodes_.Create<T>(id_, source, std::forward<ARGS>(args)...);
+  }
+
+  /// Creates a new ast::Node owned by the ProgramBuilder, injecting the current
+  /// Source as set by the last call to SetSource() as the only argument to the
+  /// constructor.
+  /// When the ProgramBuilder is destructed, the ast::Node will also be
+  /// destructed.
+  /// @returns the node pointer
+  template <typename T>
+  traits::EnableIfIsType<T, ast::Node>* create() {
+    AssertNotMoved();
+    return ast_nodes_.Create<T>(id_, source_);
+  }
+
+  /// Creates a new ast::Node owned by the ProgramBuilder, injecting the current
+  /// Source as set by the last call to SetSource() as the first argument to the
+  /// constructor.
+  /// When the ProgramBuilder is destructed, the ast::Node will also be
+  /// destructed.
+  /// @param arg0 the first arguments to pass to the type constructor
+  /// @param args the remaining arguments to pass to the type constructor
+  /// @returns the node pointer
+  template <typename T, typename ARG0, typename... ARGS>
+  traits::EnableIf</* T is ast::Node and ARG0 is not Source */
+                   traits::IsTypeOrDerived<T, ast::Node> &&
+                       !traits::IsTypeOrDerived<ARG0, Source>,
+                   T>*
+  create(ARG0&& arg0, ARGS&&... args) {
+    AssertNotMoved();
+    return ast_nodes_.Create<T>(id_, source_, std::forward<ARG0>(arg0),
+                                std::forward<ARGS>(args)...);
+  }
+
+  /// Creates a new sem::Node owned by the ProgramBuilder.
+  /// When the ProgramBuilder is destructed, the sem::Node will also be
+  /// destructed.
+  /// @param args the arguments to pass to the type constructor
+  /// @returns the node pointer
+  template <typename T, typename... ARGS>
+  traits::EnableIf<traits::IsTypeOrDerived<T, sem::Node> &&
+                       !traits::IsTypeOrDerived<T, sem::Type>,
+                   T>*
+  create(ARGS&&... args) {
+    AssertNotMoved();
+    return sem_nodes_.Create<T>(std::forward<ARGS>(args)...);
+  }
+
+  /// Creates a new sem::Type owned by the ProgramBuilder.
+  /// When the ProgramBuilder is destructed, owned ProgramBuilder and the
+  /// returned`Type` will also be destructed.
+  /// Types are unique (de-aliased), and so calling create() for the same `T`
+  /// and arguments will return the same pointer.
+  /// @warning Use this method to acquire a type only if all of its type
+  /// information is provided in the constructor arguments `args`.<br>
+  /// If the type requires additional configuration after construction that
+  /// affect its fundamental type, build the type with `std::make_unique`, make
+  /// any necessary alterations and then call unique_type() instead.
+  /// @param args the arguments to pass to the type constructor
+  /// @returns the de-aliased type pointer
+  template <typename T, typename... ARGS>
+  traits::EnableIfIsType<T, sem::Type>* create(ARGS&&... args) {
+    static_assert(std::is_base_of<sem::Type, T>::value,
+                  "T does not derive from sem::Type");
+    AssertNotMoved();
+    return types_.Get<T>(std::forward<ARGS>(args)...);
+  }
+
+  /// Marks this builder as moved, preventing any further use of the builder.
+  void MarkAsMoved();
+
+  //////////////////////////////////////////////////////////////////////////////
+  // TypesBuilder
+  //////////////////////////////////////////////////////////////////////////////
+
+  /// TypesBuilder holds basic `tint` types and methods for constructing
+  /// complex types.
+  class TypesBuilder {
+   public:
+    /// Constructor
+    /// @param builder the program builder
+    explicit TypesBuilder(ProgramBuilder* builder);
+
+    /// @return the tint AST type for the C type `T`.
+    template <typename T>
+    const ast::Type* Of() const {
+      return CToAST<T>::get(this);
+    }
+
+    /// @returns a boolean type
+    const ast::Bool* bool_() const { return builder->create<ast::Bool>(); }
+
+    /// @param source the Source of the node
+    /// @returns a boolean type
+    const ast::Bool* bool_(const Source& source) const {
+      return builder->create<ast::Bool>(source);
+    }
+
+    /// @returns a f32 type
+    const ast::F32* f32() const { return builder->create<ast::F32>(); }
+
+    /// @param source the Source of the node
+    /// @returns a f32 type
+    const ast::F32* f32(const Source& source) const {
+      return builder->create<ast::F32>(source);
+    }
+
+    /// @returns a i32 type
+    const ast::I32* i32() const { return builder->create<ast::I32>(); }
+
+    /// @param source the Source of the node
+    /// @returns a i32 type
+    const ast::I32* i32(const Source& source) const {
+      return builder->create<ast::I32>(source);
+    }
+
+    /// @returns a u32 type
+    const ast::U32* u32() const { return builder->create<ast::U32>(); }
+
+    /// @param source the Source of the node
+    /// @returns a u32 type
+    const ast::U32* u32(const Source& source) const {
+      return builder->create<ast::U32>(source);
+    }
+
+    /// @returns a void type
+    const ast::Void* void_() const { return builder->create<ast::Void>(); }
+
+    /// @param source the Source of the node
+    /// @returns a void type
+    const ast::Void* void_(const Source& source) const {
+      return builder->create<ast::Void>(source);
+    }
+
+    /// @param type vector subtype
+    /// @param n vector width in elements
+    /// @return the tint AST type for a `n`-element vector of `type`.
+    const ast::Vector* vec(const ast::Type* type, uint32_t n) const {
+      return builder->create<ast::Vector>(type, n);
+    }
+
+    /// @param source the Source of the node
+    /// @param type vector subtype
+    /// @param n vector width in elements
+    /// @return the tint AST type for a `n`-element vector of `type`.
+    const ast::Vector* vec(const Source& source,
+                           const ast::Type* type,
+                           uint32_t n) const {
+      return builder->create<ast::Vector>(source, type, n);
+    }
+
+    /// @param type vector subtype
+    /// @return the tint AST type for a 2-element vector of `type`.
+    const ast::Vector* vec2(const ast::Type* type) const {
+      return vec(type, 2u);
+    }
+
+    /// @param type vector subtype
+    /// @return the tint AST type for a 3-element vector of `type`.
+    const ast::Vector* vec3(const ast::Type* type) const {
+      return vec(type, 3u);
+    }
+
+    /// @param type vector subtype
+    /// @return the tint AST type for a 4-element vector of `type`.
+    const ast::Vector* vec4(const ast::Type* type) const {
+      return vec(type, 4u);
+    }
+
+    /// @param n vector width in elements
+    /// @return the tint AST type for a `n`-element vector of `type`.
+    template <typename T>
+    const ast::Vector* vec(uint32_t n) const {
+      return vec(Of<T>(), n);
+    }
+
+    /// @return the tint AST type for a 2-element vector of the C type `T`.
+    template <typename T>
+    const ast::Vector* vec2() const {
+      return vec2(Of<T>());
+    }
+
+    /// @return the tint AST type for a 3-element vector of the C type `T`.
+    template <typename T>
+    const ast::Vector* vec3() const {
+      return vec3(Of<T>());
+    }
+
+    /// @return the tint AST type for a 4-element vector of the C type `T`.
+    template <typename T>
+    const ast::Vector* vec4() const {
+      return vec4(Of<T>());
+    }
+
+    /// @param type matrix subtype
+    /// @param columns number of columns for the matrix
+    /// @param rows number of rows for the matrix
+    /// @return the tint AST type for a matrix of `type`
+    const ast::Matrix* mat(const ast::Type* type,
+                           uint32_t columns,
+                           uint32_t rows) const {
+      return builder->create<ast::Matrix>(type, rows, columns);
+    }
+
+    /// @param source the Source of the node
+    /// @param type matrix subtype
+    /// @param columns number of columns for the matrix
+    /// @param rows number of rows for the matrix
+    /// @return the tint AST type for a matrix of `type`
+    const ast::Matrix* mat(const Source& source,
+                           const ast::Type* type,
+                           uint32_t columns,
+                           uint32_t rows) const {
+      return builder->create<ast::Matrix>(source, type, rows, columns);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 2x3 matrix of `type`.
+    const ast::Matrix* mat2x2(const ast::Type* type) const {
+      return mat(type, 2u, 2u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 2x3 matrix of `type`.
+    const ast::Matrix* mat2x3(const ast::Type* type) const {
+      return mat(type, 2u, 3u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 2x4 matrix of `type`.
+    const ast::Matrix* mat2x4(const ast::Type* type) const {
+      return mat(type, 2u, 4u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 3x2 matrix of `type`.
+    const ast::Matrix* mat3x2(const ast::Type* type) const {
+      return mat(type, 3u, 2u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 3x3 matrix of `type`.
+    const ast::Matrix* mat3x3(const ast::Type* type) const {
+      return mat(type, 3u, 3u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 3x4 matrix of `type`.
+    const ast::Matrix* mat3x4(const ast::Type* type) const {
+      return mat(type, 3u, 4u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 4x2 matrix of `type`.
+    const ast::Matrix* mat4x2(const ast::Type* type) const {
+      return mat(type, 4u, 2u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 4x3 matrix of `type`.
+    const ast::Matrix* mat4x3(const ast::Type* type) const {
+      return mat(type, 4u, 3u);
+    }
+
+    /// @param type matrix subtype
+    /// @return the tint AST type for a 4x4 matrix of `type`.
+    const ast::Matrix* mat4x4(const ast::Type* type) const {
+      return mat(type, 4u, 4u);
+    }
+
+    /// @param columns number of columns for the matrix
+    /// @param rows number of rows for the matrix
+    /// @return the tint AST type for a matrix of `type`
+    template <typename T>
+    const ast::Matrix* mat(uint32_t columns, uint32_t rows) const {
+      return mat(Of<T>(), columns, rows);
+    }
+
+    /// @return the tint AST type for a 2x3 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat2x2() const {
+      return mat2x2(Of<T>());
+    }
+
+    /// @return the tint AST type for a 2x3 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat2x3() const {
+      return mat2x3(Of<T>());
+    }
+
+    /// @return the tint AST type for a 2x4 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat2x4() const {
+      return mat2x4(Of<T>());
+    }
+
+    /// @return the tint AST type for a 3x2 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat3x2() const {
+      return mat3x2(Of<T>());
+    }
+
+    /// @return the tint AST type for a 3x3 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat3x3() const {
+      return mat3x3(Of<T>());
+    }
+
+    /// @return the tint AST type for a 3x4 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat3x4() const {
+      return mat3x4(Of<T>());
+    }
+
+    /// @return the tint AST type for a 4x2 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat4x2() const {
+      return mat4x2(Of<T>());
+    }
+
+    /// @return the tint AST type for a 4x3 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat4x3() const {
+      return mat4x3(Of<T>());
+    }
+
+    /// @return the tint AST type for a 4x4 matrix of the C type `T`.
+    template <typename T>
+    const ast::Matrix* mat4x4() const {
+      return mat4x4(Of<T>());
+    }
+
+    /// @param subtype the array element type
+    /// @param n the array size. nullptr represents a runtime-array
+    /// @param attrs the optional attributes for the array
+    /// @return the tint AST type for a array of size `n` of type `T`
+    template <typename EXPR = ast::Expression*>
+    const ast::Array* array(const ast::Type* subtype,
+                            EXPR&& n = nullptr,
+                            ast::AttributeList attrs = {}) const {
+      return builder->create<ast::Array>(
+          subtype, builder->Expr(std::forward<EXPR>(n)), attrs);
+    }
+
+    /// @param source the Source of the node
+    /// @param subtype the array element type
+    /// @param n the array size. nullptr represents a runtime-array
+    /// @param attrs the optional attributes for the array
+    /// @return the tint AST type for a array of size `n` of type `T`
+    template <typename EXPR = ast::Expression*>
+    const ast::Array* array(const Source& source,
+                            const ast::Type* subtype,
+                            EXPR&& n = nullptr,
+                            ast::AttributeList attrs = {}) const {
+      return builder->create<ast::Array>(
+          source, subtype, builder->Expr(std::forward<EXPR>(n)), attrs);
+    }
+
+    /// @param subtype the array element type
+    /// @param n the array size. nullptr represents a runtime-array
+    /// @param stride the array stride. 0 represents implicit stride
+    /// @return the tint AST type for a array of size `n` of type `T`
+    template <typename EXPR>
+    const ast::Array* array(const ast::Type* subtype,
+                            EXPR&& n,
+                            uint32_t stride) const {
+      ast::AttributeList attrs;
+      if (stride) {
+        attrs.emplace_back(builder->create<ast::StrideAttribute>(stride));
+      }
+      return array(subtype, std::forward<EXPR>(n), std::move(attrs));
+    }
+
+    /// @param source the Source of the node
+    /// @param subtype the array element type
+    /// @param n the array size. nullptr represents a runtime-array
+    /// @param stride the array stride. 0 represents implicit stride
+    /// @return the tint AST type for a array of size `n` of type `T`
+    template <typename EXPR>
+    const ast::Array* array(const Source& source,
+                            const ast::Type* subtype,
+                            EXPR&& n,
+                            uint32_t stride) const {
+      ast::AttributeList attrs;
+      if (stride) {
+        attrs.emplace_back(builder->create<ast::StrideAttribute>(stride));
+      }
+      return array(source, subtype, std::forward<EXPR>(n), std::move(attrs));
+    }
+
+    /// @return the tint AST type for a runtime-sized array of type `T`
+    template <typename T>
+    const ast::Array* array() const {
+      return array(Of<T>(), nullptr);
+    }
+
+    /// @return the tint AST type for an array of size `N` of type `T`
+    template <typename T, int N>
+    const ast::Array* array() const {
+      return array(Of<T>(), builder->Expr(N));
+    }
+
+    /// @param stride the array stride
+    /// @return the tint AST type for a runtime-sized array of type `T`
+    template <typename T>
+    const ast::Array* array(uint32_t stride) const {
+      return array(Of<T>(), nullptr, stride);
+    }
+
+    /// @param stride the array stride
+    /// @return the tint AST type for an array of size `N` of type `T`
+    template <typename T, int N>
+    const ast::Array* array(uint32_t stride) const {
+      return array(Of<T>(), builder->Expr(N), stride);
+    }
+
+    /// Creates a type name
+    /// @param name the name
+    /// @returns the type name
+    template <typename NAME>
+    const ast::TypeName* type_name(NAME&& name) const {
+      return builder->create<ast::TypeName>(
+          builder->Sym(std::forward<NAME>(name)));
+    }
+
+    /// Creates a type name
+    /// @param source the Source of the node
+    /// @param name the name
+    /// @returns the type name
+    template <typename NAME>
+    const ast::TypeName* type_name(const Source& source, NAME&& name) const {
+      return builder->create<ast::TypeName>(
+          source, builder->Sym(std::forward<NAME>(name)));
+    }
+
+    /// Creates an alias type
+    /// @param name the alias name
+    /// @param type the alias type
+    /// @returns the alias pointer
+    template <typename NAME>
+    const ast::Alias* alias(NAME&& name, const ast::Type* type) const {
+      auto sym = builder->Sym(std::forward<NAME>(name));
+      return builder->create<ast::Alias>(sym, type);
+    }
+
+    /// Creates an alias type
+    /// @param source the Source of the node
+    /// @param name the alias name
+    /// @param type the alias type
+    /// @returns the alias pointer
+    template <typename NAME>
+    const ast::Alias* alias(const Source& source,
+                            NAME&& name,
+                            const ast::Type* type) const {
+      auto sym = builder->Sym(std::forward<NAME>(name));
+      return builder->create<ast::Alias>(source, sym, type);
+    }
+
+    /// @param type the type of the pointer
+    /// @param storage_class the storage class of the pointer
+    /// @param access the optional access control of the pointer
+    /// @return the pointer to `type` with the given ast::StorageClass
+    const ast::Pointer* pointer(
+        const ast::Type* type,
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
+      return builder->create<ast::Pointer>(type, storage_class, access);
+    }
+
+    /// @param source the Source of the node
+    /// @param type the type of the pointer
+    /// @param storage_class the storage class of the pointer
+    /// @param access the optional access control of the pointer
+    /// @return the pointer to `type` with the given ast::StorageClass
+    const ast::Pointer* pointer(
+        const Source& source,
+        const ast::Type* type,
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
+      return builder->create<ast::Pointer>(source, type, storage_class, access);
+    }
+
+    /// @param storage_class the storage class of the pointer
+    /// @param access the optional access control of the pointer
+    /// @return the pointer to type `T` with the given ast::StorageClass.
+    template <typename T>
+    const ast::Pointer* pointer(
+        ast::StorageClass storage_class,
+        ast::Access access = ast::Access::kUndefined) const {
+      return pointer(Of<T>(), storage_class, access);
+    }
+
+    /// @param source the Source of the node
+    /// @param type the type of the atomic
+    /// @return the atomic to `type`
+    const ast::Atomic* atomic(const Source& source,
+                              const ast::Type* type) const {
+      return builder->create<ast::Atomic>(source, type);
+    }
+
+    /// @param type the type of the atomic
+    /// @return the atomic to `type`
+    const ast::Atomic* atomic(const ast::Type* type) const {
+      return builder->create<ast::Atomic>(type);
+    }
+
+    /// @return the atomic to type `T`
+    template <typename T>
+    const ast::Atomic* atomic() const {
+      return atomic(Of<T>());
+    }
+
+    /// @param kind the kind of sampler
+    /// @returns the sampler
+    const ast::Sampler* sampler(ast::SamplerKind kind) const {
+      return builder->create<ast::Sampler>(kind);
+    }
+
+    /// @param source the Source of the node
+    /// @param kind the kind of sampler
+    /// @returns the sampler
+    const ast::Sampler* sampler(const Source& source,
+                                ast::SamplerKind kind) const {
+      return builder->create<ast::Sampler>(source, kind);
+    }
+
+    /// @param dims the dimensionality of the texture
+    /// @returns the depth texture
+    const ast::DepthTexture* depth_texture(ast::TextureDimension dims) const {
+      return builder->create<ast::DepthTexture>(dims);
+    }
+
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @returns the depth texture
+    const ast::DepthTexture* depth_texture(const Source& source,
+                                           ast::TextureDimension dims) const {
+      return builder->create<ast::DepthTexture>(source, dims);
+    }
+
+    /// @param dims the dimensionality of the texture
+    /// @returns the multisampled depth texture
+    const ast::DepthMultisampledTexture* depth_multisampled_texture(
+        ast::TextureDimension dims) const {
+      return builder->create<ast::DepthMultisampledTexture>(dims);
+    }
+
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @returns the multisampled depth texture
+    const ast::DepthMultisampledTexture* depth_multisampled_texture(
+        const Source& source,
+        ast::TextureDimension dims) const {
+      return builder->create<ast::DepthMultisampledTexture>(source, dims);
+    }
+
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the sampled texture
+    const ast::SampledTexture* sampled_texture(ast::TextureDimension dims,
+                                               const ast::Type* subtype) const {
+      return builder->create<ast::SampledTexture>(dims, subtype);
+    }
+
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the sampled texture
+    const ast::SampledTexture* sampled_texture(const Source& source,
+                                               ast::TextureDimension dims,
+                                               const ast::Type* subtype) const {
+      return builder->create<ast::SampledTexture>(source, dims, subtype);
+    }
+
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the multisampled texture
+    const ast::MultisampledTexture* multisampled_texture(
+        ast::TextureDimension dims,
+        const ast::Type* subtype) const {
+      return builder->create<ast::MultisampledTexture>(dims, subtype);
+    }
+
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param subtype the texture subtype.
+    /// @returns the multisampled texture
+    const ast::MultisampledTexture* multisampled_texture(
+        const Source& source,
+        ast::TextureDimension dims,
+        const ast::Type* subtype) const {
+      return builder->create<ast::MultisampledTexture>(source, dims, subtype);
+    }
+
+    /// @param dims the dimensionality of the texture
+    /// @param format the texel format of the texture
+    /// @param access the access control of the texture
+    /// @returns the storage texture
+    const ast::StorageTexture* storage_texture(ast::TextureDimension dims,
+                                               ast::TexelFormat format,
+                                               ast::Access access) const {
+      auto* subtype = ast::StorageTexture::SubtypeFor(format, *builder);
+      return builder->create<ast::StorageTexture>(dims, format, subtype,
+                                                  access);
+    }
+
+    /// @param source the Source of the node
+    /// @param dims the dimensionality of the texture
+    /// @param format the texel format of the texture
+    /// @param access the access control of the texture
+    /// @returns the storage texture
+    const ast::StorageTexture* storage_texture(const Source& source,
+                                               ast::TextureDimension dims,
+                                               ast::TexelFormat format,
+                                               ast::Access access) const {
+      auto* subtype = ast::StorageTexture::SubtypeFor(format, *builder);
+      return builder->create<ast::StorageTexture>(source, dims, format, subtype,
+                                                  access);
+    }
+
+    /// @returns the external texture
+    const ast::ExternalTexture* external_texture() const {
+      return builder->create<ast::ExternalTexture>();
+    }
+
+    /// @param source the Source of the node
+    /// @returns the external texture
+    const ast::ExternalTexture* external_texture(const Source& source) const {
+      return builder->create<ast::ExternalTexture>(source);
+    }
+
+    /// Constructs a TypeName for the type declaration.
+    /// @param type the type
+    /// @return either type or a pointer to a new ast::TypeName
+    const ast::TypeName* Of(const ast::TypeDecl* type) const;
+
+    /// The ProgramBuilder
+    ProgramBuilder* const builder;
+
+   private:
+    /// CToAST<T> is specialized for various `T` types and each specialization
+    /// contains a single static `get()` method for obtaining the corresponding
+    /// AST type for the C type `T`.
+    /// `get()` has the signature:
+    ///    `static const ast::Type* get(Types* t)`
+    template <typename T>
+    struct CToAST {};
+  };
+
+  //////////////////////////////////////////////////////////////////////////////
+  // AST helper methods
+  //////////////////////////////////////////////////////////////////////////////
+
+  /// @return a new unnamed symbol
+  Symbol Sym() { return Symbols().New(); }
+
+  /// @param name the symbol string
+  /// @return a Symbol with the given name
+  Symbol Sym(const std::string& name) { return Symbols().Register(name); }
+
+  /// @param sym the symbol
+  /// @return `sym`
+  Symbol Sym(Symbol sym) { return sym; }
+
+  /// @param expr the expression
+  /// @return expr
+  template <typename T>
+  traits::EnableIfIsType<T, ast::Expression>* Expr(T* expr) {
+    return expr;
+  }
+
+  /// Passthrough for nullptr
+  /// @return nullptr
+  const ast::IdentifierExpression* Expr(std::nullptr_t) { return nullptr; }
+
+  /// @param source the source information
+  /// @param symbol the identifier symbol
+  /// @return an ast::IdentifierExpression with the given symbol
+  const ast::IdentifierExpression* Expr(const Source& source, Symbol symbol) {
+    return create<ast::IdentifierExpression>(source, symbol);
+  }
+
+  /// @param symbol the identifier symbol
+  /// @return an ast::IdentifierExpression with the given symbol
+  const ast::IdentifierExpression* Expr(Symbol symbol) {
+    return create<ast::IdentifierExpression>(symbol);
+  }
+
+  /// @param source the source information
+  /// @param variable the AST variable
+  /// @return an ast::IdentifierExpression with the variable's symbol
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const ast::Variable* variable) {
+    return create<ast::IdentifierExpression>(source, variable->symbol);
+  }
+
+  /// @param variable the AST variable
+  /// @return an ast::IdentifierExpression with the variable's symbol
+  const ast::IdentifierExpression* Expr(const ast::Variable* variable) {
+    return create<ast::IdentifierExpression>(variable->symbol);
+  }
+
+  /// @param source the source information
+  /// @param name the identifier name
+  /// @return an ast::IdentifierExpression with the given name
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const char* name) {
+    return create<ast::IdentifierExpression>(source, Symbols().Register(name));
+  }
+
+  /// @param name the identifier name
+  /// @return an ast::IdentifierExpression with the given name
+  const ast::IdentifierExpression* Expr(const char* name) {
+    return create<ast::IdentifierExpression>(Symbols().Register(name));
+  }
+
+  /// @param source the source information
+  /// @param name the identifier name
+  /// @return an ast::IdentifierExpression with the given name
+  const ast::IdentifierExpression* Expr(const Source& source,
+                                        const std::string& name) {
+    return create<ast::IdentifierExpression>(source, Symbols().Register(name));
+  }
+
+  /// @param name the identifier name
+  /// @return an ast::IdentifierExpression with the given name
+  const ast::IdentifierExpression* Expr(const std::string& name) {
+    return create<ast::IdentifierExpression>(Symbols().Register(name));
+  }
+
+  /// @param source the source information
+  /// @param value the boolean value
+  /// @return a Scalar constructor for the given value
+  const ast::BoolLiteralExpression* Expr(const Source& source, bool value) {
+    return create<ast::BoolLiteralExpression>(source, value);
+  }
+
+  /// @param value the boolean value
+  /// @return a Scalar constructor for the given value
+  const ast::BoolLiteralExpression* Expr(bool value) {
+    return create<ast::BoolLiteralExpression>(value);
+  }
+
+  /// @param source the source information
+  /// @param value the float value
+  /// @return a Scalar constructor for the given value
+  const ast::FloatLiteralExpression* Expr(const Source& source, f32 value) {
+    return create<ast::FloatLiteralExpression>(source, value);
+  }
+
+  /// @param value the float value
+  /// @return a Scalar constructor for the given value
+  const ast::FloatLiteralExpression* Expr(f32 value) {
+    return create<ast::FloatLiteralExpression>(value);
+  }
+
+  /// @param source the source information
+  /// @param value the integer value
+  /// @return a Scalar constructor for the given value
+  const ast::SintLiteralExpression* Expr(const Source& source, i32 value) {
+    return create<ast::SintLiteralExpression>(source, value);
+  }
+
+  /// @param value the integer value
+  /// @return a Scalar constructor for the given value
+  const ast::SintLiteralExpression* Expr(i32 value) {
+    return create<ast::SintLiteralExpression>(value);
+  }
+
+  /// @param source the source information
+  /// @param value the unsigned int value
+  /// @return a Scalar constructor for the given value
+  const ast::UintLiteralExpression* Expr(const Source& source, u32 value) {
+    return create<ast::UintLiteralExpression>(source, value);
+  }
+
+  /// @param value the unsigned int value
+  /// @return a Scalar constructor for the given value
+  const ast::UintLiteralExpression* Expr(u32 value) {
+    return create<ast::UintLiteralExpression>(value);
+  }
+
+  /// Converts `arg` to an `ast::Expression` using `Expr()`, then appends it to
+  /// `list`.
+  /// @param list the list to append too
+  /// @param arg the arg to create
+  template <typename ARG>
+  void Append(ast::ExpressionList& list, ARG&& arg) {
+    list.emplace_back(Expr(std::forward<ARG>(arg)));
+  }
+
+  /// Converts `arg0` and `args` to `ast::Expression`s using `Expr()`,
+  /// then appends them to `list`.
+  /// @param list the list to append too
+  /// @param arg0 the first argument
+  /// @param args the rest of the arguments
+  template <typename ARG0, typename... ARGS>
+  void Append(ast::ExpressionList& list, ARG0&& arg0, ARGS&&... args) {
+    Append(list, std::forward<ARG0>(arg0));
+    Append(list, std::forward<ARGS>(args)...);
+  }
+
+  /// @return an empty list of expressions
+  ast::ExpressionList ExprList() { return {}; }
+
+  /// @param args the list of expressions
+  /// @return the list of expressions converted to `ast::Expression`s using
+  /// `Expr()`,
+  template <typename... ARGS>
+  ast::ExpressionList ExprList(ARGS&&... args) {
+    ast::ExpressionList list;
+    list.reserve(sizeof...(args));
+    Append(list, std::forward<ARGS>(args)...);
+    return list;
+  }
+
+  /// @param list the list of expressions
+  /// @return `list`
+  ast::ExpressionList ExprList(ast::ExpressionList list) { return list; }
+
+  /// @param args the arguments for the type constructor
+  /// @return an `ast::CallExpression` of type `ty`, with the values
+  /// of `args` converted to `ast::Expression`s using `Expr()`
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* Construct(ARGS&&... args) {
+    return Construct(ty.Of<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param type the type to construct
+  /// @param args the arguments for the constructor
+  /// @return an `ast::CallExpression` of `type` constructed with the
+  /// values `args`.
+  template <typename... ARGS>
+  const ast::CallExpression* Construct(const ast::Type* type, ARGS&&... args) {
+    return Construct(source_, type, std::forward<ARGS>(args)...);
+  }
+
+  /// @param source the source information
+  /// @param type the type to construct
+  /// @param args the arguments for the constructor
+  /// @return an `ast::CallExpression` of `type` constructed with the
+  /// values `args`.
+  template <typename... ARGS>
+  const ast::CallExpression* Construct(const Source& source,
+                                       const ast::Type* type,
+                                       ARGS&&... args) {
+    return create<ast::CallExpression>(source, type,
+                                       ExprList(std::forward<ARGS>(args)...));
+  }
+
+  /// @param expr the expression for the bitcast
+  /// @return an `ast::BitcastExpression` of type `ty`, with the values of
+  /// `expr` converted to `ast::Expression`s using `Expr()`
+  template <typename T, typename EXPR>
+  const ast::BitcastExpression* Bitcast(EXPR&& expr) {
+    return Bitcast(ty.Of<T>(), std::forward<EXPR>(expr));
+  }
+
+  /// @param type the type to cast to
+  /// @param expr the expression for the bitcast
+  /// @return an `ast::BitcastExpression` of `type` constructed with the values
+  /// `expr`.
+  template <typename EXPR>
+  const ast::BitcastExpression* Bitcast(const ast::Type* type, EXPR&& expr) {
+    return create<ast::BitcastExpression>(type, Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param source the source information
+  /// @param type the type to cast to
+  /// @param expr the expression for the bitcast
+  /// @return an `ast::BitcastExpression` of `type` constructed with the values
+  /// `expr`.
+  template <typename EXPR>
+  const ast::BitcastExpression* Bitcast(const Source& source,
+                                        const ast::Type* type,
+                                        EXPR&& expr) {
+    return create<ast::BitcastExpression>(source, type,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param args the arguments for the vector constructor
+  /// @param type the vector type
+  /// @param size the vector size
+  /// @return an `ast::CallExpression` of a `size`-element vector of
+  /// type `type`, constructed with the values `args`.
+  template <typename... ARGS>
+  const ast::CallExpression* vec(const ast::Type* type,
+                                 uint32_t size,
+                                 ARGS&&... args) {
+    return Construct(ty.vec(type, size), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the vector constructor
+  /// @return an `ast::CallExpression` of a 2-element vector of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* vec2(ARGS&&... args) {
+    return Construct(ty.vec2<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the vector constructor
+  /// @return an `ast::CallExpression` of a 3-element vector of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* vec3(ARGS&&... args) {
+    return Construct(ty.vec3<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the vector constructor
+  /// @return an `ast::CallExpression` of a 4-element vector of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* vec4(ARGS&&... args) {
+    return Construct(ty.vec4<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 2x2 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat2x2(ARGS&&... args) {
+    return Construct(ty.mat2x2<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 2x3 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat2x3(ARGS&&... args) {
+    return Construct(ty.mat2x3<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 2x4 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat2x4(ARGS&&... args) {
+    return Construct(ty.mat2x4<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 3x2 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat3x2(ARGS&&... args) {
+    return Construct(ty.mat3x2<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 3x3 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat3x3(ARGS&&... args) {
+    return Construct(ty.mat3x3<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 3x4 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat3x4(ARGS&&... args) {
+    return Construct(ty.mat3x4<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 4x2 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat4x2(ARGS&&... args) {
+    return Construct(ty.mat4x2<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 4x3 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat4x3(ARGS&&... args) {
+    return Construct(ty.mat4x3<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the matrix constructor
+  /// @return an `ast::CallExpression` of a 4x4 matrix of type
+  /// `T`, constructed with the values `args`.
+  template <typename T, typename... ARGS>
+  const ast::CallExpression* mat4x4(ARGS&&... args) {
+    return Construct(ty.mat4x4<T>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param args the arguments for the array constructor
+  /// @return an `ast::CallExpression` of an array with element type
+  /// `T` and size `N`, constructed with the values `args`.
+  template <typename T, int N, typename... ARGS>
+  const ast::CallExpression* array(ARGS&&... args) {
+    return Construct(ty.array<T, N>(), std::forward<ARGS>(args)...);
+  }
+
+  /// @param subtype the array element type
+  /// @param n the array size. nullptr represents a runtime-array.
+  /// @param args the arguments for the array constructor
+  /// @return an `ast::CallExpression` of an array with element type
+  /// `subtype`, constructed with the values `args`.
+  template <typename EXPR, typename... ARGS>
+  const ast::CallExpression* array(const ast::Type* subtype,
+                                   EXPR&& n,
+                                   ARGS&&... args) {
+    return Construct(ty.array(subtype, std::forward<EXPR>(n)),
+                     std::forward<ARGS>(args)...);
+  }
+
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param optional the optional variable settings.
+  /// Can be any of the following, in any order:
+  ///   * ast::StorageClass   - specifies the variable storage class
+  ///   * ast::Access         - specifies the variable's access control
+  ///   * ast::Expression*    - specifies the variable's initializer expression
+  ///   * ast::AttributeList - specifies the variable's attributes
+  /// Note that repeated arguments of the same type will use the last argument's
+  /// value.
+  /// @returns a `ast::Variable` with the given name, type and additional
+  /// options
+  template <typename NAME, typename... OPTIONAL>
+  const ast::Variable* Var(NAME&& name,
+                           const ast::Type* type,
+                           OPTIONAL&&... optional) {
+    VarOptionals opts(std::forward<OPTIONAL>(optional)...);
+    return create<ast::Variable>(Sym(std::forward<NAME>(name)), opts.storage,
+                                 opts.access, type, false /* is_const */,
+                                 false /* is_overridable */, opts.constructor,
+                                 std::move(opts.attributes));
+  }
+
+  /// @param source the variable source
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param optional the optional variable settings.
+  /// Can be any of the following, in any order:
+  ///   * ast::StorageClass   - specifies the variable storage class
+  ///   * ast::Access         - specifies the variable's access control
+  ///   * ast::Expression*    - specifies the variable's initializer expression
+  ///   * ast::AttributeList - specifies the variable's attributes
+  /// Note that repeated arguments of the same type will use the last argument's
+  /// value.
+  /// @returns a `ast::Variable` with the given name, storage and type
+  template <typename NAME, typename... OPTIONAL>
+  const ast::Variable* Var(const Source& source,
+                           NAME&& name,
+                           const ast::Type* type,
+                           OPTIONAL&&... optional) {
+    VarOptionals opts(std::forward<OPTIONAL>(optional)...);
+    return create<ast::Variable>(
+        source, Sym(std::forward<NAME>(name)), opts.storage, opts.access, type,
+        false /* is_const */, false /* is_overridable */, opts.constructor,
+        std::move(opts.attributes));
+  }
+
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns a constant `ast::Variable` with the given name and type
+  template <typename NAME>
+  const ast::Variable* Const(NAME&& name,
+                             const ast::Type* type,
+                             const ast::Expression* constructor,
+                             ast::AttributeList attributes = {}) {
+    return create<ast::Variable>(
+        Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        false /* is_overridable */, constructor, attributes);
+  }
+
+  /// @param source the variable source
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns a constant `ast::Variable` with the given name and type
+  template <typename NAME>
+  const ast::Variable* Const(const Source& source,
+                             NAME&& name,
+                             const ast::Type* type,
+                             const ast::Expression* constructor,
+                             ast::AttributeList attributes = {}) {
+    return create<ast::Variable>(
+        source, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        false /* is_overridable */, constructor, attributes);
+  }
+
+  /// @param name the parameter name
+  /// @param type the parameter type
+  /// @param attributes optional parameter attributes
+  /// @returns a constant `ast::Variable` with the given name and type
+  template <typename NAME>
+  const ast::Variable* Param(NAME&& name,
+                             const ast::Type* type,
+                             ast::AttributeList attributes = {}) {
+    return create<ast::Variable>(
+        Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        false /* is_overridable */, nullptr, attributes);
+  }
+
+  /// @param source the parameter source
+  /// @param name the parameter name
+  /// @param type the parameter type
+  /// @param attributes optional parameter attributes
+  /// @returns a constant `ast::Variable` with the given name and type
+  template <typename NAME>
+  const ast::Variable* Param(const Source& source,
+                             NAME&& name,
+                             const ast::Type* type,
+                             ast::AttributeList attributes = {}) {
+    return create<ast::Variable>(
+        source, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        false /* is_overridable */, nullptr, attributes);
+  }
+
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param optional the optional variable settings.
+  /// Can be any of the following, in any order:
+  ///   * ast::StorageClass   - specifies the variable storage class
+  ///   * ast::Access         - specifies the variable's access control
+  ///   * ast::Expression*    - specifies the variable's initializer expression
+  ///   * ast::AttributeList - specifies the variable's attributes
+  /// Note that repeated arguments of the same type will use the last argument's
+  /// value.
+  /// @returns a new `ast::Variable`, which is automatically registered as a
+  /// global variable with the ast::Module.
+  template <typename NAME,
+            typename... OPTIONAL,
+            typename = DisableIfSource<NAME>>
+  const ast::Variable* Global(NAME&& name,
+                              const ast::Type* type,
+                              OPTIONAL&&... optional) {
+    auto* var = Var(std::forward<NAME>(name), type,
+                    std::forward<OPTIONAL>(optional)...);
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param source the variable source
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param optional the optional variable settings.
+  /// Can be any of the following, in any order:
+  ///   * ast::StorageClass   - specifies the variable storage class
+  ///   * ast::Access         - specifies the variable's access control
+  ///   * ast::Expression*    - specifies the variable's initializer expression
+  ///   * ast::AttributeList - specifies the variable's attributes
+  /// Note that repeated arguments of the same type will use the last argument's
+  /// value.
+  /// @returns a new `ast::Variable`, which is automatically registered as a
+  /// global variable with the ast::Module.
+  template <typename NAME, typename... OPTIONAL>
+  const ast::Variable* Global(const Source& source,
+                              NAME&& name,
+                              const ast::Type* type,
+                              OPTIONAL&&... optional) {
+    auto* var = Var(source, std::forward<NAME>(name), type,
+                    std::forward<OPTIONAL>(optional)...);
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns a const `ast::Variable` constructed by calling Var() with the
+  /// arguments of `args`, which is automatically registered as a global
+  /// variable with the ast::Module.
+  template <typename NAME>
+  const ast::Variable* GlobalConst(NAME&& name,
+                                   const ast::Type* type,
+                                   const ast::Expression* constructor,
+                                   ast::AttributeList attributes = {}) {
+    auto* var = Const(std::forward<NAME>(name), type, constructor,
+                      std::move(attributes));
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param source the variable source
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns a const `ast::Variable` constructed by calling Var() with the
+  /// arguments of `args`, which is automatically registered as a global
+  /// variable with the ast::Module.
+  template <typename NAME>
+  const ast::Variable* GlobalConst(const Source& source,
+                                   NAME&& name,
+                                   const ast::Type* type,
+                                   const ast::Expression* constructor,
+                                   ast::AttributeList attributes = {}) {
+    auto* var = Const(source, std::forward<NAME>(name), type, constructor,
+                      std::move(attributes));
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor optional constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns an overridable const `ast::Variable` which is automatically
+  /// registered as a global variable with the ast::Module.
+  template <typename NAME>
+  const ast::Variable* Override(NAME&& name,
+                                const ast::Type* type,
+                                const ast::Expression* constructor,
+                                ast::AttributeList attributes = {}) {
+    auto* var = create<ast::Variable>(
+        source_, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        true /* is_overridable */, constructor, std::move(attributes));
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param source the variable source
+  /// @param name the variable name
+  /// @param type the variable type
+  /// @param constructor constructor expression
+  /// @param attributes optional variable attributes
+  /// @returns a const `ast::Variable` constructed by calling Var() with the
+  /// arguments of `args`, which is automatically registered as a global
+  /// variable with the ast::Module.
+  template <typename NAME>
+  const ast::Variable* Override(const Source& source,
+                                NAME&& name,
+                                const ast::Type* type,
+                                const ast::Expression* constructor,
+                                ast::AttributeList attributes = {}) {
+    auto* var = create<ast::Variable>(
+        source, Sym(std::forward<NAME>(name)), ast::StorageClass::kNone,
+        ast::Access::kUndefined, type, true /* is_const */,
+        true /* is_overridable */, constructor, std::move(attributes));
+    AST().AddGlobalVariable(var);
+    return var;
+  }
+
+  /// @param source the source information
+  /// @param expr the expression to take the address of
+  /// @return an ast::UnaryOpExpression that takes the address of `expr`
+  template <typename EXPR>
+  const ast::UnaryOpExpression* AddressOf(const Source& source, EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(source, ast::UnaryOp::kAddressOf,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param expr the expression to take the address of
+  /// @return an ast::UnaryOpExpression that takes the address of `expr`
+  template <typename EXPR>
+  const ast::UnaryOpExpression* AddressOf(EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(ast::UnaryOp::kAddressOf,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param source the source information
+  /// @param expr the expression to perform an indirection on
+  /// @return an ast::UnaryOpExpression that dereferences the pointer `expr`
+  template <typename EXPR>
+  const ast::UnaryOpExpression* Deref(const Source& source, EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(source, ast::UnaryOp::kIndirection,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param expr the expression to perform an indirection on
+  /// @return an ast::UnaryOpExpression that dereferences the pointer `expr`
+  template <typename EXPR>
+  const ast::UnaryOpExpression* Deref(EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(ast::UnaryOp::kIndirection,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param expr the expression to perform a unary not on
+  /// @return an ast::UnaryOpExpression that is the unary not of the input
+  /// expression
+  template <typename EXPR>
+  const ast::UnaryOpExpression* Not(EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(ast::UnaryOp::kNot,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param expr the expression to perform a unary complement on
+  /// @return an ast::UnaryOpExpression that is the unary complement of the
+  /// input expression
+  template <typename EXPR>
+  const ast::UnaryOpExpression* Complement(EXPR&& expr) {
+    return create<ast::UnaryOpExpression>(ast::UnaryOp::kComplement,
+                                          Expr(std::forward<EXPR>(expr)));
+  }
+
+  /// @param source the source information
+  /// @param func the function name
+  /// @param args the function call arguments
+  /// @returns a `ast::CallExpression` to the function `func`, with the
+  /// arguments of `args` converted to `ast::Expression`s using `Expr()`.
+  template <typename NAME, typename... ARGS>
+  const ast::CallExpression* Call(const Source& source,
+                                  NAME&& func,
+                                  ARGS&&... args) {
+    return create<ast::CallExpression>(source, Expr(func),
+                                       ExprList(std::forward<ARGS>(args)...));
+  }
+
+  /// @param func the function name
+  /// @param args the function call arguments
+  /// @returns a `ast::CallExpression` to the function `func`, with the
+  /// arguments of `args` converted to `ast::Expression`s using `Expr()`.
+  template <typename NAME, typename... ARGS, typename = DisableIfSource<NAME>>
+  const ast::CallExpression* Call(NAME&& func, ARGS&&... args) {
+    return create<ast::CallExpression>(Expr(func),
+                                       ExprList(std::forward<ARGS>(args)...));
+  }
+
+  /// @param source the source information
+  /// @param call the call expression to wrap in a call statement
+  /// @returns a `ast::CallStatement` for the given call expression
+  const ast::CallStatement* CallStmt(const Source& source,
+                                     const ast::CallExpression* call) {
+    return create<ast::CallStatement>(source, call);
+  }
+
+  /// @param call the call expression to wrap in a call statement
+  /// @returns a `ast::CallStatement` for the given call expression
+  const ast::CallStatement* CallStmt(const ast::CallExpression* call) {
+    return create<ast::CallStatement>(call);
+  }
+
+  /// @param source the source information
+  /// @returns a `ast::PhonyExpression`
+  const ast::PhonyExpression* Phony(const Source& source) {
+    return create<ast::PhonyExpression>(source);
+  }
+
+  /// @returns a `ast::PhonyExpression`
+  const ast::PhonyExpression* Phony() { return create<ast::PhonyExpression>(); }
+
+  /// @param expr the expression to ignore
+  /// @returns a `ast::AssignmentStatement` that assigns 'expr' to the phony
+  /// (underscore) variable.
+  template <typename EXPR>
+  const ast::AssignmentStatement* Ignore(EXPR&& expr) {
+    return create<ast::AssignmentStatement>(Phony(), Expr(expr));
+  }
+
+  /// @param lhs the left hand argument to the addition operation
+  /// @param rhs the right hand argument to the addition operation
+  /// @returns a `ast::BinaryExpression` summing the arguments `lhs` and `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Add(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kAdd,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the and operation
+  /// @param rhs the right hand argument to the and operation
+  /// @returns a `ast::BinaryExpression` bitwise anding `lhs` and `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* And(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kAnd,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the or operation
+  /// @param rhs the right hand argument to the or operation
+  /// @returns a `ast::BinaryExpression` bitwise or-ing `lhs` and `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Or(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kOr,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the subtraction operation
+  /// @param rhs the right hand argument to the subtraction operation
+  /// @returns a `ast::BinaryExpression` subtracting `rhs` from `lhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Sub(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kSubtract,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the multiplication operation
+  /// @param rhs the right hand argument to the multiplication operation
+  /// @returns a `ast::BinaryExpression` multiplying `rhs` from `lhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Mul(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kMultiply,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param source the source information
+  /// @param lhs the left hand argument to the multiplication operation
+  /// @param rhs the right hand argument to the multiplication operation
+  /// @returns a `ast::BinaryExpression` multiplying `rhs` from `lhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Mul(const Source& source, LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(source, ast::BinaryOp::kMultiply,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the division operation
+  /// @param rhs the right hand argument to the division operation
+  /// @returns a `ast::BinaryExpression` dividing `lhs` by `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Div(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kDivide,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the modulo operation
+  /// @param rhs the right hand argument to the modulo operation
+  /// @returns a `ast::BinaryExpression` applying modulo of `lhs` by `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Mod(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kModulo,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the bit shift right operation
+  /// @param rhs the right hand argument to the bit shift right operation
+  /// @returns a `ast::BinaryExpression` bit shifting right `lhs` by `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Shr(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kShiftRight,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the bit shift left operation
+  /// @param rhs the right hand argument to the bit shift left operation
+  /// @returns a `ast::BinaryExpression` bit shifting left `lhs` by `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Shl(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kShiftLeft,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the xor operation
+  /// @param rhs the right hand argument to the xor operation
+  /// @returns a `ast::BinaryExpression` bitwise xor-ing `lhs` and `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Xor(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kXor,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the logical and operation
+  /// @param rhs the right hand argument to the logical and operation
+  /// @returns a `ast::BinaryExpression` of `lhs` && `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LogicalAnd(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLogicalAnd,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the logical or operation
+  /// @param rhs the right hand argument to the logical or operation
+  /// @returns a `ast::BinaryExpression` of `lhs` || `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LogicalOr(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLogicalOr,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the greater than operation
+  /// @param rhs the right hand argument to the greater than operation
+  /// @returns a `ast::BinaryExpression` of `lhs` > `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* GreaterThan(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kGreaterThan,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the greater than or equal operation
+  /// @param rhs the right hand argument to the greater than or equal operation
+  /// @returns a `ast::BinaryExpression` of `lhs` >= `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* GreaterThanEqual(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kGreaterThanEqual,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the less than operation
+  /// @param rhs the right hand argument to the less than operation
+  /// @returns a `ast::BinaryExpression` of `lhs` < `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LessThan(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLessThan,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the less than or equal operation
+  /// @param rhs the right hand argument to the less than or equal operation
+  /// @returns a `ast::BinaryExpression` of `lhs` <= `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LessThanEqual(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLessThanEqual,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the equal expression
+  /// @param rhs the right hand argument to the equal expression
+  /// @returns a `ast::BinaryExpression` comparing `lhs` equal to `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* Equal(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kEqual,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the not-equal expression
+  /// @param rhs the right hand argument to the not-equal expression
+  /// @returns a `ast::BinaryExpression` comparing `lhs` equal to `rhs` for
+  ///          disequality
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* NotEqual(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kNotEqual,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param source the source information
+  /// @param obj the object for the index accessor expression
+  /// @param idx the index argument for the index accessor expression
+  /// @returns a `ast::IndexAccessorExpression` that indexes `arr` with `idx`
+  template <typename OBJ, typename IDX>
+  const ast::IndexAccessorExpression* IndexAccessor(const Source& source,
+                                                    OBJ&& obj,
+                                                    IDX&& idx) {
+    return create<ast::IndexAccessorExpression>(
+        source, Expr(std::forward<OBJ>(obj)), Expr(std::forward<IDX>(idx)));
+  }
+
+  /// @param obj the object for the index accessor expression
+  /// @param idx the index argument for the index accessor expression
+  /// @returns a `ast::IndexAccessorExpression` that indexes `arr` with `idx`
+  template <typename OBJ, typename IDX>
+  const ast::IndexAccessorExpression* IndexAccessor(OBJ&& obj, IDX&& idx) {
+    return create<ast::IndexAccessorExpression>(Expr(std::forward<OBJ>(obj)),
+                                                Expr(std::forward<IDX>(idx)));
+  }
+
+  /// @param source the source information
+  /// @param obj the object for the member accessor expression
+  /// @param idx the index argument for the member accessor expression
+  /// @returns a `ast::MemberAccessorExpression` that indexes `obj` with `idx`
+  template <typename OBJ, typename IDX>
+  const ast::MemberAccessorExpression* MemberAccessor(const Source& source,
+                                                      OBJ&& obj,
+                                                      IDX&& idx) {
+    return create<ast::MemberAccessorExpression>(
+        source, Expr(std::forward<OBJ>(obj)), Expr(std::forward<IDX>(idx)));
+  }
+
+  /// @param obj the object for the member accessor expression
+  /// @param idx the index argument for the member accessor expression
+  /// @returns a `ast::MemberAccessorExpression` that indexes `obj` with `idx`
+  template <typename OBJ, typename IDX>
+  const ast::MemberAccessorExpression* MemberAccessor(OBJ&& obj, IDX&& idx) {
+    return create<ast::MemberAccessorExpression>(Expr(std::forward<OBJ>(obj)),
+                                                 Expr(std::forward<IDX>(idx)));
+  }
+
+  /// Creates a ast::StructMemberOffsetAttribute
+  /// @param val the offset value
+  /// @returns the offset attribute pointer
+  const ast::StructMemberOffsetAttribute* MemberOffset(uint32_t val) {
+    return create<ast::StructMemberOffsetAttribute>(source_, val);
+  }
+
+  /// Creates a ast::StructMemberSizeAttribute
+  /// @param source the source information
+  /// @param val the size value
+  /// @returns the size attribute pointer
+  const ast::StructMemberSizeAttribute* MemberSize(const Source& source,
+                                                   uint32_t val) {
+    return create<ast::StructMemberSizeAttribute>(source, val);
+  }
+
+  /// Creates a ast::StructMemberSizeAttribute
+  /// @param val the size value
+  /// @returns the size attribute pointer
+  const ast::StructMemberSizeAttribute* MemberSize(uint32_t val) {
+    return create<ast::StructMemberSizeAttribute>(source_, val);
+  }
+
+  /// Creates a ast::StructMemberAlignAttribute
+  /// @param source the source information
+  /// @param val the align value
+  /// @returns the align attribute pointer
+  const ast::StructMemberAlignAttribute* MemberAlign(const Source& source,
+                                                     uint32_t val) {
+    return create<ast::StructMemberAlignAttribute>(source, val);
+  }
+
+  /// Creates a ast::StructMemberAlignAttribute
+  /// @param val the align value
+  /// @returns the align attribute pointer
+  const ast::StructMemberAlignAttribute* MemberAlign(uint32_t val) {
+    return create<ast::StructMemberAlignAttribute>(source_, val);
+  }
+
+  /// Creates the ast::GroupAttribute
+  /// @param value group attribute index
+  /// @returns the group attribute pointer
+  const ast::GroupAttribute* Group(uint32_t value) {
+    return create<ast::GroupAttribute>(value);
+  }
+
+  /// Creates the ast::BindingAttribute
+  /// @param value the binding index
+  /// @returns the binding deocration pointer
+  const ast::BindingAttribute* Binding(uint32_t value) {
+    return create<ast::BindingAttribute>(value);
+  }
+
+  /// Convenience function to create both a ast::GroupAttribute and
+  /// ast::BindingAttribute
+  /// @param group the group index
+  /// @param binding the binding index
+  /// @returns a attribute list with both the group and binding attributes
+  ast::AttributeList GroupAndBinding(uint32_t group, uint32_t binding) {
+    return {Group(group), Binding(binding)};
+  }
+
+  /// Creates an ast::Function and registers it with the ast::Module.
+  /// @param source the source information
+  /// @param name the function name
+  /// @param params the function parameters
+  /// @param type the function return type
+  /// @param body the function body
+  /// @param attributes the optional function attributes
+  /// @param return_type_attributes the optional function return type
+  /// attributes
+  /// @returns the function pointer
+  template <typename NAME>
+  const ast::Function* Func(const Source& source,
+                            NAME&& name,
+                            ast::VariableList params,
+                            const ast::Type* type,
+                            ast::StatementList body,
+                            ast::AttributeList attributes = {},
+                            ast::AttributeList return_type_attributes = {}) {
+    auto* func = create<ast::Function>(
+        source, Sym(std::forward<NAME>(name)), params, type,
+        create<ast::BlockStatement>(body), attributes, return_type_attributes);
+    AST().AddFunction(func);
+    return func;
+  }
+
+  /// Creates an ast::Function and registers it with the ast::Module.
+  /// @param name the function name
+  /// @param params the function parameters
+  /// @param type the function return type
+  /// @param body the function body
+  /// @param attributes the optional function attributes
+  /// @param return_type_attributes the optional function return type
+  /// attributes
+  /// @returns the function pointer
+  template <typename NAME>
+  const ast::Function* Func(NAME&& name,
+                            ast::VariableList params,
+                            const ast::Type* type,
+                            ast::StatementList body,
+                            ast::AttributeList attributes = {},
+                            ast::AttributeList return_type_attributes = {}) {
+    auto* func = create<ast::Function>(Sym(std::forward<NAME>(name)), params,
+                                       type, create<ast::BlockStatement>(body),
+                                       attributes, return_type_attributes);
+    AST().AddFunction(func);
+    return func;
+  }
+
+  /// Creates an ast::BreakStatement
+  /// @param source the source information
+  /// @returns the break statement pointer
+  const ast::BreakStatement* Break(const Source& source) {
+    return create<ast::BreakStatement>(source);
+  }
+
+  /// Creates an ast::BreakStatement
+  /// @returns the break statement pointer
+  const ast::BreakStatement* Break() { return create<ast::BreakStatement>(); }
+
+  /// Creates an ast::ContinueStatement
+  /// @param source the source information
+  /// @returns the continue statement pointer
+  const ast::ContinueStatement* Continue(const Source& source) {
+    return create<ast::ContinueStatement>(source);
+  }
+
+  /// Creates an ast::ContinueStatement
+  /// @returns the continue statement pointer
+  const ast::ContinueStatement* Continue() {
+    return create<ast::ContinueStatement>();
+  }
+
+  /// Creates an ast::ReturnStatement with no return value
+  /// @param source the source information
+  /// @returns the return statement pointer
+  const ast::ReturnStatement* Return(const Source& source) {
+    return create<ast::ReturnStatement>(source);
+  }
+
+  /// Creates an ast::ReturnStatement with no return value
+  /// @returns the return statement pointer
+  const ast::ReturnStatement* Return() {
+    return create<ast::ReturnStatement>();
+  }
+
+  /// Creates an ast::ReturnStatement with the given return value
+  /// @param source the source information
+  /// @param val the return value
+  /// @returns the return statement pointer
+  template <typename EXPR>
+  const ast::ReturnStatement* Return(const Source& source, EXPR&& val) {
+    return create<ast::ReturnStatement>(source, Expr(std::forward<EXPR>(val)));
+  }
+
+  /// Creates an ast::ReturnStatement with the given return value
+  /// @param val the return value
+  /// @returns the return statement pointer
+  template <typename EXPR, typename = DisableIfSource<EXPR>>
+  const ast::ReturnStatement* Return(EXPR&& val) {
+    return create<ast::ReturnStatement>(Expr(std::forward<EXPR>(val)));
+  }
+
+  /// Creates an ast::DiscardStatement
+  /// @param source the source information
+  /// @returns the discard statement pointer
+  const ast::DiscardStatement* Discard(const Source& source) {
+    return create<ast::DiscardStatement>(source);
+  }
+
+  /// Creates an ast::DiscardStatement
+  /// @returns the discard statement pointer
+  const ast::DiscardStatement* Discard() {
+    return create<ast::DiscardStatement>();
+  }
+
+  /// Creates a ast::Alias registering it with the AST().TypeDecls().
+  /// @param source the source information
+  /// @param name the alias name
+  /// @param type the alias target type
+  /// @returns the alias type
+  template <typename NAME>
+  const ast::Alias* Alias(const Source& source,
+                          NAME&& name,
+                          const ast::Type* type) {
+    auto* out = ty.alias(source, std::forward<NAME>(name), type);
+    AST().AddTypeDecl(out);
+    return out;
+  }
+
+  /// Creates a ast::Alias registering it with the AST().TypeDecls().
+  /// @param name the alias name
+  /// @param type the alias target type
+  /// @returns the alias type
+  template <typename NAME>
+  const ast::Alias* Alias(NAME&& name, const ast::Type* type) {
+    auto* out = ty.alias(std::forward<NAME>(name), type);
+    AST().AddTypeDecl(out);
+    return out;
+  }
+
+  /// Creates a ast::Struct registering it with the AST().TypeDecls().
+  /// @param source the source information
+  /// @param name the struct name
+  /// @param members the struct members
+  /// @returns the struct type
+  template <typename NAME>
+  const ast::Struct* Structure(const Source& source,
+                               NAME&& name,
+                               ast::StructMemberList members) {
+    auto sym = Sym(std::forward<NAME>(name));
+    auto* type = create<ast::Struct>(source, sym, std::move(members),
+                                     ast::AttributeList{});
+    AST().AddTypeDecl(type);
+    return type;
+  }
+
+  /// Creates a ast::Struct registering it with the AST().TypeDecls().
+  /// @param name the struct name
+  /// @param members the struct members
+  /// @returns the struct type
+  template <typename NAME>
+  const ast::Struct* Structure(NAME&& name, ast::StructMemberList members) {
+    auto sym = Sym(std::forward<NAME>(name));
+    auto* type =
+        create<ast::Struct>(sym, std::move(members), ast::AttributeList{});
+    AST().AddTypeDecl(type);
+    return type;
+  }
+
+  /// Creates a ast::StructMember
+  /// @param source the source information
+  /// @param name the struct member name
+  /// @param type the struct member type
+  /// @param attributes the optional struct member attributes
+  /// @returns the struct member pointer
+  template <typename NAME>
+  const ast::StructMember* Member(const Source& source,
+                                  NAME&& name,
+                                  const ast::Type* type,
+                                  ast::AttributeList attributes = {}) {
+    return create<ast::StructMember>(source, Sym(std::forward<NAME>(name)),
+                                     type, std::move(attributes));
+  }
+
+  /// Creates a ast::StructMember
+  /// @param name the struct member name
+  /// @param type the struct member type
+  /// @param attributes the optional struct member attributes
+  /// @returns the struct member pointer
+  template <typename NAME>
+  const ast::StructMember* Member(NAME&& name,
+                                  const ast::Type* type,
+                                  ast::AttributeList attributes = {}) {
+    return create<ast::StructMember>(source_, Sym(std::forward<NAME>(name)),
+                                     type, std::move(attributes));
+  }
+
+  /// Creates a ast::StructMember with the given byte offset
+  /// @param offset the offset to use in the StructMemberOffsetattribute
+  /// @param name the struct member name
+  /// @param type the struct member type
+  /// @returns the struct member pointer
+  template <typename NAME>
+  const ast::StructMember* Member(uint32_t offset,
+                                  NAME&& name,
+                                  const ast::Type* type) {
+    return create<ast::StructMember>(
+        source_, Sym(std::forward<NAME>(name)), type,
+        ast::AttributeList{
+            create<ast::StructMemberOffsetAttribute>(offset),
+        });
+  }
+
+  /// Creates a ast::BlockStatement with input statements
+  /// @param source the source information for the block
+  /// @param statements statements of block
+  /// @returns the block statement pointer
+  template <typename... Statements>
+  const ast::BlockStatement* Block(const Source& source,
+                                   Statements&&... statements) {
+    return create<ast::BlockStatement>(
+        source, ast::StatementList{std::forward<Statements>(statements)...});
+  }
+
+  /// Creates a ast::BlockStatement with input statements
+  /// @param statements statements of block
+  /// @returns the block statement pointer
+  template <typename... STATEMENTS, typename = DisableIfSource<STATEMENTS...>>
+  const ast::BlockStatement* Block(STATEMENTS&&... statements) {
+    return create<ast::BlockStatement>(
+        ast::StatementList{std::forward<STATEMENTS>(statements)...});
+  }
+
+  /// Creates a ast::ElseStatement with input condition and body
+  /// @param condition the else condition expression
+  /// @param body the else body
+  /// @returns the else statement pointer
+  template <typename CONDITION>
+  const ast::ElseStatement* Else(CONDITION&& condition,
+                                 const ast::BlockStatement* body) {
+    return create<ast::ElseStatement>(Expr(std::forward<CONDITION>(condition)),
+                                      body);
+  }
+
+  /// Creates a ast::ElseStatement with no condition and body
+  /// @param body the else body
+  /// @returns the else statement pointer
+  const ast::ElseStatement* Else(const ast::BlockStatement* body) {
+    return create<ast::ElseStatement>(nullptr, body);
+  }
+
+  /// Creates a ast::IfStatement with input condition, body, and optional
+  /// variadic else statements
+  /// @param source the source information for the if statement
+  /// @param condition the if statement condition expression
+  /// @param body the if statement body
+  /// @param elseStatements optional variadic else statements
+  /// @returns the if statement pointer
+  template <typename CONDITION, typename... ELSE_STATEMENTS>
+  const ast::IfStatement* If(const Source& source,
+                             CONDITION&& condition,
+                             const ast::BlockStatement* body,
+                             ELSE_STATEMENTS&&... elseStatements) {
+    return create<ast::IfStatement>(
+        source, Expr(std::forward<CONDITION>(condition)), body,
+        ast::ElseStatementList{
+            std::forward<ELSE_STATEMENTS>(elseStatements)...});
+  }
+
+  /// Creates a ast::IfStatement with input condition, body, and optional
+  /// variadic else statements
+  /// @param condition the if statement condition expression
+  /// @param body the if statement body
+  /// @param elseStatements optional variadic else statements
+  /// @returns the if statement pointer
+  template <typename CONDITION, typename... ELSE_STATEMENTS>
+  const ast::IfStatement* If(CONDITION&& condition,
+                             const ast::BlockStatement* body,
+                             ELSE_STATEMENTS&&... elseStatements) {
+    return create<ast::IfStatement>(
+        Expr(std::forward<CONDITION>(condition)), body,
+        ast::ElseStatementList{
+            std::forward<ELSE_STATEMENTS>(elseStatements)...});
+  }
+
+  /// Creates a ast::AssignmentStatement with input lhs and rhs expressions
+  /// @param source the source information
+  /// @param lhs the left hand side expression initializer
+  /// @param rhs the right hand side expression initializer
+  /// @returns the assignment statement pointer
+  template <typename LhsExpressionInit, typename RhsExpressionInit>
+  const ast::AssignmentStatement* Assign(const Source& source,
+                                         LhsExpressionInit&& lhs,
+                                         RhsExpressionInit&& rhs) {
+    return create<ast::AssignmentStatement>(
+        source, Expr(std::forward<LhsExpressionInit>(lhs)),
+        Expr(std::forward<RhsExpressionInit>(rhs)));
+  }
+
+  /// Creates a ast::AssignmentStatement with input lhs and rhs expressions
+  /// @param lhs the left hand side expression initializer
+  /// @param rhs the right hand side expression initializer
+  /// @returns the assignment statement pointer
+  template <typename LhsExpressionInit, typename RhsExpressionInit>
+  const ast::AssignmentStatement* Assign(LhsExpressionInit&& lhs,
+                                         RhsExpressionInit&& rhs) {
+    return create<ast::AssignmentStatement>(
+        Expr(std::forward<LhsExpressionInit>(lhs)),
+        Expr(std::forward<RhsExpressionInit>(rhs)));
+  }
+
+  /// Creates a ast::CompoundAssignmentStatement with input lhs and rhs
+  /// expressions, and a binary operator.
+  /// @param source the source information
+  /// @param lhs the left hand side expression initializer
+  /// @param rhs the right hand side expression initializer
+  /// @param op the binary operator
+  /// @returns the compound assignment statement pointer
+  template <typename LhsExpressionInit, typename RhsExpressionInit>
+  const ast::CompoundAssignmentStatement* CompoundAssign(
+      const Source& source,
+      LhsExpressionInit&& lhs,
+      RhsExpressionInit&& rhs,
+      ast::BinaryOp op) {
+    return create<ast::CompoundAssignmentStatement>(
+        source, Expr(std::forward<LhsExpressionInit>(lhs)),
+        Expr(std::forward<RhsExpressionInit>(rhs)), op);
+  }
+
+  /// Creates a ast::CompoundAssignmentStatement with input lhs and rhs
+  /// expressions, and a binary operator.
+  /// @param lhs the left hand side expression initializer
+  /// @param rhs the right hand side expression initializer
+  /// @param op the binary operator
+  /// @returns the compound assignment statement pointer
+  template <typename LhsExpressionInit, typename RhsExpressionInit>
+  const ast::CompoundAssignmentStatement* CompoundAssign(
+      LhsExpressionInit&& lhs,
+      RhsExpressionInit&& rhs,
+      ast::BinaryOp op) {
+    return create<ast::CompoundAssignmentStatement>(
+        Expr(std::forward<LhsExpressionInit>(lhs)),
+        Expr(std::forward<RhsExpressionInit>(rhs)), op);
+  }
+
+  /// Creates a ast::LoopStatement with input body and optional continuing
+  /// @param source the source information
+  /// @param body the loop body
+  /// @param continuing the optional continuing block
+  /// @returns the loop statement pointer
+  const ast::LoopStatement* Loop(
+      const Source& source,
+      const ast::BlockStatement* body,
+      const ast::BlockStatement* continuing = nullptr) {
+    return create<ast::LoopStatement>(source, body, continuing);
+  }
+
+  /// Creates a ast::LoopStatement with input body and optional continuing
+  /// @param body the loop body
+  /// @param continuing the optional continuing block
+  /// @returns the loop statement pointer
+  const ast::LoopStatement* Loop(
+      const ast::BlockStatement* body,
+      const ast::BlockStatement* continuing = nullptr) {
+    return create<ast::LoopStatement>(body, continuing);
+  }
+
+  /// Creates a ast::ForLoopStatement with input body and optional initializer,
+  /// condition and continuing.
+  /// @param source the source information
+  /// @param init the optional loop initializer
+  /// @param cond the optional loop condition
+  /// @param cont the optional loop continuing
+  /// @param body the loop body
+  /// @returns the for loop statement pointer
+  template <typename COND>
+  const ast::ForLoopStatement* For(const Source& source,
+                                   const ast::Statement* init,
+                                   COND&& cond,
+                                   const ast::Statement* cont,
+                                   const ast::BlockStatement* body) {
+    return create<ast::ForLoopStatement>(
+        source, init, Expr(std::forward<COND>(cond)), cont, body);
+  }
+
+  /// Creates a ast::ForLoopStatement with input body and optional initializer,
+  /// condition and continuing.
+  /// @param init the optional loop initializer
+  /// @param cond the optional loop condition
+  /// @param cont the optional loop continuing
+  /// @param body the loop body
+  /// @returns the for loop statement pointer
+  template <typename COND>
+  const ast::ForLoopStatement* For(const ast::Statement* init,
+                                   COND&& cond,
+                                   const ast::Statement* cont,
+                                   const ast::BlockStatement* body) {
+    return create<ast::ForLoopStatement>(init, Expr(std::forward<COND>(cond)),
+                                         cont, body);
+  }
+
+  /// Creates a ast::VariableDeclStatement for the input variable
+  /// @param source the source information
+  /// @param var the variable to wrap in a decl statement
+  /// @returns the variable decl statement pointer
+  const ast::VariableDeclStatement* Decl(const Source& source,
+                                         const ast::Variable* var) {
+    return create<ast::VariableDeclStatement>(source, var);
+  }
+
+  /// Creates a ast::VariableDeclStatement for the input variable
+  /// @param var the variable to wrap in a decl statement
+  /// @returns the variable decl statement pointer
+  const ast::VariableDeclStatement* Decl(const ast::Variable* var) {
+    return create<ast::VariableDeclStatement>(var);
+  }
+
+  /// Creates a ast::SwitchStatement with input expression and cases
+  /// @param source the source information
+  /// @param condition the condition expression initializer
+  /// @param cases case statements
+  /// @returns the switch statement pointer
+  template <typename ExpressionInit, typename... Cases>
+  const ast::SwitchStatement* Switch(const Source& source,
+                                     ExpressionInit&& condition,
+                                     Cases&&... cases) {
+    return create<ast::SwitchStatement>(
+        source, Expr(std::forward<ExpressionInit>(condition)),
+        ast::CaseStatementList{std::forward<Cases>(cases)...});
+  }
+
+  /// Creates a ast::SwitchStatement with input expression and cases
+  /// @param condition the condition expression initializer
+  /// @param cases case statements
+  /// @returns the switch statement pointer
+  template <typename ExpressionInit,
+            typename... Cases,
+            typename = DisableIfSource<ExpressionInit>>
+  const ast::SwitchStatement* Switch(ExpressionInit&& condition,
+                                     Cases&&... cases) {
+    return create<ast::SwitchStatement>(
+        Expr(std::forward<ExpressionInit>(condition)),
+        ast::CaseStatementList{std::forward<Cases>(cases)...});
+  }
+
+  /// Creates a ast::CaseStatement with input list of selectors, and body
+  /// @param source the source information
+  /// @param selectors list of selectors
+  /// @param body the case body
+  /// @returns the case statement pointer
+  const ast::CaseStatement* Case(const Source& source,
+                                 ast::CaseSelectorList selectors,
+                                 const ast::BlockStatement* body = nullptr) {
+    return create<ast::CaseStatement>(source, std::move(selectors),
+                                      body ? body : Block());
+  }
+
+  /// Creates a ast::CaseStatement with input list of selectors, and body
+  /// @param selectors list of selectors
+  /// @param body the case body
+  /// @returns the case statement pointer
+  const ast::CaseStatement* Case(ast::CaseSelectorList selectors,
+                                 const ast::BlockStatement* body = nullptr) {
+    return create<ast::CaseStatement>(std::move(selectors),
+                                      body ? body : Block());
+  }
+
+  /// Convenient overload that takes a single selector
+  /// @param selector a single case selector
+  /// @param body the case body
+  /// @returns the case statement pointer
+  const ast::CaseStatement* Case(const ast::IntLiteralExpression* selector,
+                                 const ast::BlockStatement* body = nullptr) {
+    return Case(ast::CaseSelectorList{selector}, body);
+  }
+
+  /// Convenience function that creates a 'default' ast::CaseStatement
+  /// @param source the source information
+  /// @param body the case body
+  /// @returns the case statement pointer
+  const ast::CaseStatement* DefaultCase(
+      const Source& source,
+      const ast::BlockStatement* body = nullptr) {
+    return Case(source, ast::CaseSelectorList{}, body);
+  }
+
+  /// Convenience function that creates a 'default' ast::CaseStatement
+  /// @param body the case body
+  /// @returns the case statement pointer
+  const ast::CaseStatement* DefaultCase(
+      const ast::BlockStatement* body = nullptr) {
+    return Case(ast::CaseSelectorList{}, body);
+  }
+
+  /// Creates an ast::FallthroughStatement
+  /// @param source the source information
+  /// @returns the fallthrough statement pointer
+  const ast::FallthroughStatement* Fallthrough(const Source& source) {
+    return create<ast::FallthroughStatement>(source);
+  }
+
+  /// Creates an ast::FallthroughStatement
+  /// @returns the fallthrough statement pointer
+  const ast::FallthroughStatement* Fallthrough() {
+    return create<ast::FallthroughStatement>();
+  }
+
+  /// Creates an ast::BuiltinAttribute
+  /// @param source the source information
+  /// @param builtin the builtin value
+  /// @returns the builtin attribute pointer
+  const ast::BuiltinAttribute* Builtin(const Source& source,
+                                       ast::Builtin builtin) {
+    return create<ast::BuiltinAttribute>(source, builtin);
+  }
+
+  /// Creates an ast::BuiltinAttribute
+  /// @param builtin the builtin value
+  /// @returns the builtin attribute pointer
+  const ast::BuiltinAttribute* Builtin(ast::Builtin builtin) {
+    return create<ast::BuiltinAttribute>(source_, builtin);
+  }
+
+  /// Creates an ast::InterpolateAttribute
+  /// @param source the source information
+  /// @param type the interpolation type
+  /// @param sampling the interpolation sampling
+  /// @returns the interpolate attribute pointer
+  const ast::InterpolateAttribute* Interpolate(
+      const Source& source,
+      ast::InterpolationType type,
+      ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone) {
+    return create<ast::InterpolateAttribute>(source, type, sampling);
+  }
+
+  /// Creates an ast::InterpolateAttribute
+  /// @param type the interpolation type
+  /// @param sampling the interpolation sampling
+  /// @returns the interpolate attribute pointer
+  const ast::InterpolateAttribute* Interpolate(
+      ast::InterpolationType type,
+      ast::InterpolationSampling sampling = ast::InterpolationSampling::kNone) {
+    return create<ast::InterpolateAttribute>(source_, type, sampling);
+  }
+
+  /// Creates an ast::InterpolateAttribute using flat interpolation
+  /// @param source the source information
+  /// @returns the interpolate attribute pointer
+  const ast::InterpolateAttribute* Flat(const Source& source) {
+    return Interpolate(source, ast::InterpolationType::kFlat);
+  }
+
+  /// Creates an ast::InterpolateAttribute using flat interpolation
+  /// @returns the interpolate attribute pointer
+  const ast::InterpolateAttribute* Flat() {
+    return Interpolate(ast::InterpolationType::kFlat);
+  }
+
+  /// Creates an ast::InvariantAttribute
+  /// @param source the source information
+  /// @returns the invariant attribute pointer
+  const ast::InvariantAttribute* Invariant(const Source& source) {
+    return create<ast::InvariantAttribute>(source);
+  }
+
+  /// Creates an ast::InvariantAttribute
+  /// @returns the invariant attribute pointer
+  const ast::InvariantAttribute* Invariant() {
+    return create<ast::InvariantAttribute>(source_);
+  }
+
+  /// Creates an ast::LocationAttribute
+  /// @param source the source information
+  /// @param location the location value
+  /// @returns the location attribute pointer
+  const ast::LocationAttribute* Location(const Source& source,
+                                         uint32_t location) {
+    return create<ast::LocationAttribute>(source, location);
+  }
+
+  /// Creates an ast::LocationAttribute
+  /// @param location the location value
+  /// @returns the location attribute pointer
+  const ast::LocationAttribute* Location(uint32_t location) {
+    return create<ast::LocationAttribute>(source_, location);
+  }
+
+  /// Creates an ast::IdAttribute
+  /// @param source the source information
+  /// @param id the id value
+  /// @returns the override attribute pointer
+  const ast::IdAttribute* Id(const Source& source, uint32_t id) {
+    return create<ast::IdAttribute>(source, id);
+  }
+
+  /// Creates an ast::IdAttribute with a constant ID
+  /// @param id the optional id value
+  /// @returns the override attribute pointer
+  const ast::IdAttribute* Id(uint32_t id) { return Id(source_, id); }
+
+  /// Creates an ast::StageAttribute
+  /// @param source the source information
+  /// @param stage the pipeline stage
+  /// @returns the stage attribute pointer
+  const ast::StageAttribute* Stage(const Source& source,
+                                   ast::PipelineStage stage) {
+    return create<ast::StageAttribute>(source, stage);
+  }
+
+  /// Creates an ast::StageAttribute
+  /// @param stage the pipeline stage
+  /// @returns the stage attribute pointer
+  const ast::StageAttribute* Stage(ast::PipelineStage stage) {
+    return create<ast::StageAttribute>(source_, stage);
+  }
+
+  /// Creates an ast::WorkgroupAttribute
+  /// @param x the x dimension expression
+  /// @returns the workgroup attribute pointer
+  template <typename EXPR_X>
+  const ast::WorkgroupAttribute* WorkgroupSize(EXPR_X&& x) {
+    return WorkgroupSize(std::forward<EXPR_X>(x), nullptr, nullptr);
+  }
+
+  /// Creates an ast::WorkgroupAttribute
+  /// @param x the x dimension expression
+  /// @param y the y dimension expression
+  /// @returns the workgroup attribute pointer
+  template <typename EXPR_X, typename EXPR_Y>
+  const ast::WorkgroupAttribute* WorkgroupSize(EXPR_X&& x, EXPR_Y&& y) {
+    return WorkgroupSize(std::forward<EXPR_X>(x), std::forward<EXPR_Y>(y),
+                         nullptr);
+  }
+
+  /// Creates an ast::WorkgroupAttribute
+  /// @param source the source information
+  /// @param x the x dimension expression
+  /// @param y the y dimension expression
+  /// @param z the z dimension expression
+  /// @returns the workgroup attribute pointer
+  template <typename EXPR_X, typename EXPR_Y, typename EXPR_Z>
+  const ast::WorkgroupAttribute* WorkgroupSize(const Source& source,
+                                               EXPR_X&& x,
+                                               EXPR_Y&& y,
+                                               EXPR_Z&& z) {
+    return create<ast::WorkgroupAttribute>(
+        source, Expr(std::forward<EXPR_X>(x)), Expr(std::forward<EXPR_Y>(y)),
+        Expr(std::forward<EXPR_Z>(z)));
+  }
+
+  /// Creates an ast::WorkgroupAttribute
+  /// @param x the x dimension expression
+  /// @param y the y dimension expression
+  /// @param z the z dimension expression
+  /// @returns the workgroup attribute pointer
+  template <typename EXPR_X, typename EXPR_Y, typename EXPR_Z>
+  const ast::WorkgroupAttribute* WorkgroupSize(EXPR_X&& x,
+                                               EXPR_Y&& y,
+                                               EXPR_Z&& z) {
+    return create<ast::WorkgroupAttribute>(
+        source_, Expr(std::forward<EXPR_X>(x)), Expr(std::forward<EXPR_Y>(y)),
+        Expr(std::forward<EXPR_Z>(z)));
+  }
+
+  /// Creates an ast::DisableValidationAttribute
+  /// @param validation the validation to disable
+  /// @returns the disable validation attribute pointer
+  const ast::DisableValidationAttribute* Disable(
+      ast::DisabledValidation validation) {
+    return ASTNodes().Create<ast::DisableValidationAttribute>(ID(), validation);
+  }
+
+  /// Sets the current builder source to `src`
+  /// @param src the Source used for future create() calls
+  void SetSource(const Source& src) {
+    AssertNotMoved();
+    source_ = src;
+  }
+
+  /// Sets the current builder source to `loc`
+  /// @param loc the Source used for future create() calls
+  void SetSource(const Source::Location& loc) {
+    AssertNotMoved();
+    source_ = Source(loc);
+  }
+
+  /// Helper for returning the resolved semantic type of the expression `expr`.
+  /// @note As the Resolver is run when the Program is built, this will only be
+  /// useful for the Resolver itself and tests that use their own Resolver.
+  /// @param expr the AST expression
+  /// @return the resolved semantic type for the expression, or nullptr if the
+  /// expression has no resolved type.
+  const sem::Type* TypeOf(const ast::Expression* expr) const;
+
+  /// Helper for returning the resolved semantic type of the variable `var`.
+  /// @note As the Resolver is run when the Program is built, this will only be
+  /// useful for the Resolver itself and tests that use their own Resolver.
+  /// @param var the AST variable
+  /// @return the resolved semantic type for the variable, or nullptr if the
+  /// variable has no resolved type.
+  const sem::Type* TypeOf(const ast::Variable* var) const;
+
+  /// Helper for returning the resolved semantic type of the AST type `type`.
+  /// @note As the Resolver is run when the Program is built, this will only be
+  /// useful for the Resolver itself and tests that use their own Resolver.
+  /// @param type the AST type
+  /// @return the resolved semantic type for the type, or nullptr if the type
+  /// has no resolved type.
+  const sem::Type* TypeOf(const ast::Type* type) const;
+
+  /// Helper for returning the resolved semantic type of the AST type
+  /// declaration `type_decl`.
+  /// @note As the Resolver is run when the Program is built, this will only be
+  /// useful for the Resolver itself and tests that use their own Resolver.
+  /// @param type_decl the AST type declaration
+  /// @return the resolved semantic type for the type declaration, or nullptr if
+  /// the type declaration has no resolved type.
+  const sem::Type* TypeOf(const ast::TypeDecl* type_decl) const;
+
+  /// Wraps the ast::Expression in a statement. This is used by tests that
+  /// construct a partial AST and require the Resolver to reach these
+  /// nodes.
+  /// @param expr the ast::Expression to be wrapped by an ast::Statement
+  /// @return the ast::Statement that wraps the ast::Expression
+  const ast::Statement* WrapInStatement(const ast::Expression* expr);
+  /// Wraps the ast::Variable in a ast::VariableDeclStatement. This is used by
+  /// tests that construct a partial AST and require the Resolver to reach
+  /// these nodes.
+  /// @param v the ast::Variable to be wrapped by an ast::VariableDeclStatement
+  /// @return the ast::VariableDeclStatement that wraps the ast::Variable
+  const ast::VariableDeclStatement* WrapInStatement(const ast::Variable* v);
+  /// Returns the statement argument. Used as a passthrough-overload by
+  /// WrapInFunction().
+  /// @param stmt the ast::Statement
+  /// @return `stmt`
+  const ast::Statement* WrapInStatement(const ast::Statement* stmt);
+  /// Wraps the list of arguments in a simple function so that each is reachable
+  /// by the Resolver.
+  /// @param args a mix of ast::Expression, ast::Statement, ast::Variables.
+  /// @returns the function
+  template <typename... ARGS>
+  const ast::Function* WrapInFunction(ARGS&&... args) {
+    ast::StatementList stmts{WrapInStatement(std::forward<ARGS>(args))...};
+    return WrapInFunction(std::move(stmts));
+  }
+  /// @param stmts a list of ast::Statement that will be wrapped by a function,
+  /// so that each statement is reachable by the Resolver.
+  /// @returns the function
+  const ast::Function* WrapInFunction(ast::StatementList stmts);
+
+  /// The builder types
+  TypesBuilder const ty{this};
+
+ protected:
+  /// Asserts that the builder has not been moved.
+  void AssertNotMoved() const;
+
+ private:
+  ProgramID id_;
+  sem::Manager types_;
+  ASTNodeAllocator ast_nodes_;
+  SemNodeAllocator sem_nodes_;
+  ast::Module* ast_;
+  sem::Info sem_;
+  SymbolTable symbols_{id_};
+  diag::List diagnostics_;
+
+  /// The source to use when creating AST nodes without providing a Source as
+  /// the first argument.
+  Source source_;
+
+  /// Set by SetResolveOnBuild(). If set, the Resolver will be run on the
+  /// program when built.
+  bool resolve_on_build_ = true;
+
+  /// Set by MarkAsMoved(). Once set, no methods may be called on this builder.
+  bool moved_ = false;
+};
+
+//! @cond Doxygen_Suppress
+// Various template specializations for ProgramBuilder::TypesBuilder::CToAST.
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::i32> {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+    return t->i32();
+  }
+};
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::u32> {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+    return t->u32();
+  }
+};
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<ProgramBuilder::f32> {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+    return t->f32();
+  }
+};
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<bool> {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+    return t->bool_();
+  }
+};
+template <>
+struct ProgramBuilder::TypesBuilder::CToAST<void> {
+  static const ast::Type* get(const ProgramBuilder::TypesBuilder* t) {
+    return t->void_();
+  }
+};
+//! @endcond
+
+/// @param builder the ProgramBuilder
+/// @returns the ProgramID of the ProgramBuilder
+inline ProgramID ProgramIDOf(const ProgramBuilder* builder) {
+  return builder->ID();
+}
+
+}  // namespace tint
+
+#endif  // SRC_TINT_PROGRAM_BUILDER_H_
diff --git a/src/tint/program_builder_test.cc b/src/tint/program_builder_test.cc
new file mode 100644
index 0000000..f18aa10
--- /dev/null
+++ b/src/tint/program_builder_test.cc
@@ -0,0 +1,72 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/program_builder.h"
+
+#include "gtest/gtest.h"
+
+namespace tint {
+namespace {
+
+using ProgramBuilderTest = testing::Test;
+
+TEST_F(ProgramBuilderTest, IDsAreUnique) {
+  Program program_a(ProgramBuilder{});
+  Program program_b(ProgramBuilder{});
+  Program program_c(ProgramBuilder{});
+  EXPECT_NE(program_a.ID(), program_b.ID());
+  EXPECT_NE(program_b.ID(), program_c.ID());
+  EXPECT_NE(program_c.ID(), program_a.ID());
+}
+
+TEST_F(ProgramBuilderTest, WrapDoesntAffectInner) {
+  Program inner([] {
+    ProgramBuilder builder;
+    auto* ty = builder.ty.f32();
+    builder.Func("a", {}, ty, {}, {});
+    return builder;
+  }());
+
+  ASSERT_EQ(inner.AST().Functions().size(), 1u);
+  ASSERT_TRUE(inner.Symbols().Get("a").IsValid());
+  ASSERT_FALSE(inner.Symbols().Get("b").IsValid());
+
+  ProgramBuilder outer = ProgramBuilder::Wrap(&inner);
+
+  ASSERT_EQ(inner.AST().Functions().size(), 1u);
+  ASSERT_EQ(outer.AST().Functions().size(), 1u);
+  EXPECT_EQ(inner.AST().Functions()[0], outer.AST().Functions()[0]);
+  EXPECT_TRUE(inner.Symbols().Get("a").IsValid());
+  EXPECT_EQ(inner.Symbols().Get("a"), outer.Symbols().Get("a"));
+  EXPECT_TRUE(inner.Symbols().Get("a").IsValid());
+  EXPECT_TRUE(outer.Symbols().Get("a").IsValid());
+  EXPECT_FALSE(inner.Symbols().Get("b").IsValid());
+  EXPECT_FALSE(outer.Symbols().Get("b").IsValid());
+
+  auto* ty = outer.ty.f32();
+  outer.Func("b", {}, ty, {}, {});
+
+  ASSERT_EQ(inner.AST().Functions().size(), 1u);
+  ASSERT_EQ(outer.AST().Functions().size(), 2u);
+  EXPECT_EQ(inner.AST().Functions()[0], outer.AST().Functions()[0]);
+  EXPECT_EQ(outer.AST().Functions()[1]->symbol, outer.Symbols().Get("b"));
+  EXPECT_EQ(inner.Symbols().Get("a"), outer.Symbols().Get("a"));
+  EXPECT_TRUE(inner.Symbols().Get("a").IsValid());
+  EXPECT_TRUE(outer.Symbols().Get("a").IsValid());
+  EXPECT_FALSE(inner.Symbols().Get("b").IsValid());
+  EXPECT_TRUE(outer.Symbols().Get("b").IsValid());
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/program_id.cc b/src/tint/program_id.cc
new file mode 100644
index 0000000..5350de7
--- /dev/null
+++ b/src/tint/program_id.cc
@@ -0,0 +1,58 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/program_id.h"
+
+#include <atomic>
+
+namespace tint {
+
+namespace {
+
+std::atomic<uint32_t> next_program_id{1};
+
+}  // namespace
+
+ProgramID::ProgramID() = default;
+
+ProgramID::ProgramID(uint32_t id) : val(id) {}
+
+ProgramID ProgramID::New() {
+  return ProgramID(next_program_id++);
+}
+
+namespace detail {
+
+/// AssertProgramIDsEqual is called by TINT_ASSERT_PROGRAM_IDS_EQUAL() and
+/// TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID() to assert that the ProgramIDs
+/// `a` and `b` are equal.
+void AssertProgramIDsEqual(ProgramID a,
+                           ProgramID b,
+                           bool if_valid,
+                           diag::System system,
+                           const char* msg,
+                           const char* file,
+                           size_t line) {
+  if (a == b) {
+    return;  // matched
+  }
+  if (if_valid && (!a || !b)) {
+    return;  //  a or b were not valid
+  }
+  diag::List diagnostics;
+  tint::InternalCompilerError(file, line, system, diagnostics) << msg;
+}
+
+}  // namespace detail
+}  // namespace tint
diff --git a/src/tint/program_id.h b/src/tint/program_id.h
new file mode 100644
index 0000000..09e232f
--- /dev/null
+++ b/src/tint/program_id.h
@@ -0,0 +1,126 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_PROGRAM_ID_H_
+#define SRC_TINT_PROGRAM_ID_H_
+
+#include <stdint.h>
+#include <iostream>
+#include <utility>
+
+#include "src/tint/debug.h"
+
+namespace tint {
+
+/// If 1 then checks are enabled that AST nodes are not leaked from one program
+/// to another.
+/// TODO(bclayton): We'll want to disable this in production builds. For now we
+/// always check.
+#define TINT_CHECK_FOR_CROSS_PROGRAM_LEAKS 1
+
+/// A ProgramID is a unique identifier of a Program.
+/// ProgramID can be used to ensure that objects referenced by the Program are
+/// owned exclusively by that Program and have accidentally not leaked from
+/// another Program.
+class ProgramID {
+ public:
+  /// Constructor
+  ProgramID();
+
+  /// @returns a new. globally unique ProgramID
+  static ProgramID New();
+
+  /// Equality operator
+  /// @param rhs the other ProgramID
+  /// @returns true if the ProgramIDs are equal
+  bool operator==(const ProgramID& rhs) const { return val == rhs.val; }
+
+  /// Inequality operator
+  /// @param rhs the other ProgramID
+  /// @returns true if the ProgramIDs are not equal
+  bool operator!=(const ProgramID& rhs) const { return val != rhs.val; }
+
+  /// @returns the numerical identifier value
+  uint32_t Value() const { return val; }
+
+  /// @returns true if this ProgramID is valid
+  operator bool() const { return val != 0; }
+
+ private:
+  explicit ProgramID(uint32_t);
+
+  uint32_t val = 0;
+};
+
+/// A simple pass-through function for ProgramID. Intended to be overloaded for
+/// other types.
+/// @param id a ProgramID
+/// @returns id. Simple pass-through function
+inline ProgramID ProgramIDOf(ProgramID id) {
+  return id;
+}
+
+/// Writes the ProgramID to the std::ostream.
+/// @param out the std::ostream to write to
+/// @param id the program identifier to write
+/// @returns out so calls can be chained
+inline std::ostream& operator<<(std::ostream& out, ProgramID id) {
+  out << "Program<" << id.Value() << ">";
+  return out;
+}
+
+namespace detail {
+
+/// AssertProgramIDsEqual is called by TINT_ASSERT_PROGRAM_IDS_EQUAL() and
+/// TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID() to assert that the ProgramIDs
+/// `a` and `b` are equal.
+void AssertProgramIDsEqual(ProgramID a,
+                           ProgramID b,
+                           bool if_valid,
+                           diag::System system,
+                           const char* msg,
+                           const char* file,
+                           size_t line);
+
+}  // namespace detail
+
+/// TINT_ASSERT_PROGRAM_IDS_EQUAL(SYSTEM, A, B) is a macro that asserts that the
+/// program identifiers for A and B are equal.
+///
+/// TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(SYSTEM, A, B) is a macro that asserts
+/// that the program identifiers for A and B are equal, if both A and B have
+/// valid program identifiers.
+#if TINT_CHECK_FOR_CROSS_PROGRAM_LEAKS
+#define TINT_ASSERT_PROGRAM_IDS_EQUAL(system, a, b)                          \
+  detail::AssertProgramIDsEqual(                                             \
+      ProgramIDOf(a), ProgramIDOf(b), false, tint::diag::System::system,     \
+      "TINT_ASSERT_PROGRAM_IDS_EQUAL(" #system "," #a ", " #b ")", __FILE__, \
+      __LINE__)
+#define TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(system, a, b)                 \
+  detail::AssertProgramIDsEqual(                                             \
+      ProgramIDOf(a), ProgramIDOf(b), true, tint::diag::System::system,      \
+      "TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(" #system ", " #a ", " #b ")", \
+      __FILE__, __LINE__)
+#else
+#define TINT_ASSERT_PROGRAM_IDS_EQUAL(a, b) \
+  do {                                      \
+  } while (false)
+#define TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(a, b) \
+  do {                                               \
+  } while (false)
+#endif
+
+}  // namespace tint
+
+#endif  // SRC_TINT_PROGRAM_ID_H_
diff --git a/src/tint/program_test.cc b/src/tint/program_test.cc
new file mode 100644
index 0000000..a161ecb
--- /dev/null
+++ b/src/tint/program_test.cc
@@ -0,0 +1,110 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint {
+namespace {
+
+using ProgramTest = ast::TestHelper;
+
+TEST_F(ProgramTest, Unbuilt) {
+  Program program;
+  EXPECT_FALSE(program.IsValid());
+}
+
+TEST_F(ProgramTest, Creation) {
+  Program program(std::move(*this));
+  EXPECT_EQ(program.AST().Functions().size(), 0u);
+}
+
+TEST_F(ProgramTest, EmptyIsValid) {
+  Program program(std::move(*this));
+  EXPECT_TRUE(program.IsValid());
+}
+
+TEST_F(ProgramTest, IDsAreUnique) {
+  Program program_a(ProgramBuilder{});
+  Program program_b(ProgramBuilder{});
+  Program program_c(ProgramBuilder{});
+  EXPECT_NE(program_a.ID(), program_b.ID());
+  EXPECT_NE(program_b.ID(), program_c.ID());
+  EXPECT_NE(program_c.ID(), program_a.ID());
+}
+
+TEST_F(ProgramTest, Assert_GlobalVariable) {
+  Global("var", ty.f32(), ast::StorageClass::kPrivate);
+
+  Program program(std::move(*this));
+  EXPECT_TRUE(program.IsValid());
+}
+
+TEST_F(ProgramTest, Assert_NullGlobalVariable) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.AST().AddGlobalVariable(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ProgramTest, Assert_NullTypeDecl) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.AST().AddTypeDecl(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ProgramTest, Assert_Null_Function) {
+  EXPECT_FATAL_FAILURE(
+      {
+        ProgramBuilder b;
+        b.AST().AddFunction(nullptr);
+      },
+      "internal compiler error");
+}
+
+TEST_F(ProgramTest, DiagnosticsMove) {
+  Diagnostics().add_error(diag::System::Program, "an error message");
+
+  Program program_a(std::move(*this));
+  EXPECT_FALSE(program_a.IsValid());
+  EXPECT_EQ(program_a.Diagnostics().count(), 1u);
+  EXPECT_EQ(program_a.Diagnostics().error_count(), 1u);
+  EXPECT_EQ(program_a.Diagnostics().begin()->message, "an error message");
+
+  Program program_b(std::move(program_a));
+  EXPECT_FALSE(program_b.IsValid());
+  EXPECT_EQ(program_b.Diagnostics().count(), 1u);
+  EXPECT_EQ(program_b.Diagnostics().error_count(), 1u);
+  EXPECT_EQ(program_b.Diagnostics().begin()->message, "an error message");
+}
+
+TEST_F(ProgramTest, ReuseMovedFromVariable) {
+  Program a(std::move(*this));
+  EXPECT_TRUE(a.IsValid());
+
+  Program b = std::move(a);
+  EXPECT_TRUE(b.IsValid());
+
+  a = std::move(b);
+  EXPECT_TRUE(a.IsValid());
+}
+
+}  // namespace
+}  // namespace tint
diff --git a/src/tint/reader/reader.cc b/src/tint/reader/reader.cc
new file mode 100644
index 0000000..2937ff8
--- /dev/null
+++ b/src/tint/reader/reader.cc
@@ -0,0 +1,25 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/reader.h"
+
+namespace tint {
+namespace reader {
+
+Reader::Reader() = default;
+
+Reader::~Reader() = default;
+
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/reader.h b/src/tint/reader/reader.h
new file mode 100644
index 0000000..6c9c52a
--- /dev/null
+++ b/src/tint/reader/reader.h
@@ -0,0 +1,65 @@
+// Copyright 2020 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_READER_READER_H_
+#define SRC_TINT_READER_READER_H_
+
+#include <string>
+
+#include "src/tint/program.h"
+
+namespace tint {
+namespace reader {
+
+/// Base class for input readers
+class Reader {
+ public:
+  virtual ~Reader();
+
+  /// Parses the input data
+  /// @returns true if the parse was successful
+  virtual bool Parse() = 0;
+
+  /// @returns true if an error was encountered.
+  bool has_error() const { return diags_.contains_errors(); }
+
+  /// @returns the parser error string
+  std::string error() const {
+    diag::Formatter formatter{{false, false, false, false}};
+    return formatter.format(diags_);
+  }
+
+  /// @returns the full list of diagnostic messages.
+  const diag::List& diagnostics() const { return diags_; }
+
+  /// @returns the program. The program builder in the parser will be reset
+  /// after this.
+  virtual Program program() = 0;
+
+ protected:
+  /// Constructor
+  Reader();
+
+  /// Sets the diagnostic messages
+  /// @param diags the list of diagnostic messages
+  void set_diagnostics(const diag::List& diags) { diags_ = diags; }
+
+  /// All diagnostic messages from the reader.
+  diag::List diags_;
+};
+
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_TINT_READER_READER_H_
diff --git a/src/tint/reader/spirv/README.md b/src/tint/reader/spirv/README.md
new file mode 100644
index 0000000..f782e21
--- /dev/null
+++ b/src/tint/reader/spirv/README.md
@@ -0,0 +1,34 @@
+# SPIR-V Reader
+
+This component translates SPIR-V written for Vulkan into the Tint AST.
+
+The SPIR-V reader entry point is `tint::reader::spirv::Parser`, which
+implements the Reader interface in `tint::reader::Reader`.
+
+It's usable from the Tint command line:
+
+    # Translate SPIR-V into WGSL.
+    tint --format wgsl a.spv
+
+## Supported dialects
+
+The SPIR-V module must pass validation for the Vulkan 1.1 environment in SPIRV-Tools.
+In particular, SPIR-V 1.4 and later are not supported.
+
+For example, the equivalent of the following must pass:
+
+    spirv-val --target-env vulkan1.1 a.spv
+
+Additionally, the reader imposes additional constraints based on:
+
+* The features supported by WGSL. Some Vulkan features might not be supportable because
+   WebGPU must be portable to other graphics APIs.
+* Limitations of the reader itself. These might be relaxed in the future with extra
+   engineering work.
+
+## Feedback
+
+Please file issues at https://crbug.com/tint, and apply label `SpirvReader`.
+
+Outstanding issues can be found by using the `SpirvReader` label in the Chromium project's
+bug tracker: https://bugs.chromium.org/p/tint/issues/list?q=label:SpirvReader
diff --git a/src/tint/reader/spirv/construct.cc b/src/tint/reader/spirv/construct.cc
new file mode 100644
index 0000000..23808e1
--- /dev/null
+++ b/src/tint/reader/spirv/construct.cc
@@ -0,0 +1,70 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/construct.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+Construct::Construct(const Construct* the_parent,
+                     int the_depth,
+                     Kind the_kind,
+                     uint32_t the_begin_id,
+                     uint32_t the_end_id,
+                     uint32_t the_begin_pos,
+                     uint32_t the_end_pos,
+                     uint32_t the_scope_end_pos)
+    : parent(the_parent),
+      enclosing_loop(
+          // Compute the enclosing loop construct. Doing this in the
+          // constructor member list lets us make the member const.
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          the_kind == kLoop
+              ? this
+              : ((parent && parent->depth < the_depth) ? parent->enclosing_loop
+                                                       : nullptr)),
+      enclosing_continue(
+          // Compute the enclosing continue construct. Doing this in the
+          // constructor member list lets us make the member const.
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          the_kind == kContinue ? this
+                                : ((parent && parent->depth < the_depth)
+                                       ? parent->enclosing_continue
+                                       : nullptr)),
+      enclosing_loop_or_continue_or_switch(
+          // Compute the enclosing loop or continue or switch construct.
+          // Doing this in the constructor member list lets us make the
+          // member const.
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          (the_kind == kLoop || the_kind == kContinue ||
+           the_kind == kSwitchSelection)
+              ? this
+              : ((parent && parent->depth < the_depth)
+                     ? parent->enclosing_loop_or_continue_or_switch
+                     : nullptr)),
+      depth(the_depth),
+      kind(the_kind),
+      begin_id(the_begin_id),
+      end_id(the_end_id),
+      begin_pos(the_begin_pos),
+      end_pos(the_end_pos),
+      scope_end_pos(the_scope_end_pos) {}
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/spirv/construct.h b/src/tint/reader/spirv/construct.h
new file mode 100644
index 0000000..898b682
--- /dev/null
+++ b/src/tint/reader/spirv/construct.h
@@ -0,0 +1,278 @@
+// Copyright 2020 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_READER_SPIRV_CONSTRUCT_H_
+#define SRC_TINT_READER_SPIRV_CONSTRUCT_H_
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+/// A structured control flow construct, consisting of a set of basic blocks.
+/// A construct is a span of blocks in the computed block order,
+/// and will appear contiguously in the WGSL source.
+///
+/// SPIR-V (2.11 Structured Control Flow) defines:
+///   - loop construct
+///   - continue construct
+///   - selection construct
+/// We also define a "function construct" consisting of all the basic blocks in
+/// the function.
+///
+/// The first block in a construct (by computed block order) is called a
+/// "header". For the constructs defined by SPIR-V, the header block is the
+/// basic block containing the merge instruction.  The header for the function
+/// construct is the entry block of the function.
+///
+/// Given two constructs A and B, we say "A encloses B" if B is a subset of A,
+/// i.e. if every basic block in B is also in A.  Note that a construct encloses
+/// itself.
+///
+/// In a valid SPIR-V module, constructs will nest, meaning given
+/// constructs A and B, either A encloses B, or B encloses A, or
+/// or they are disjoint (have no basic blocks in commont).
+///
+/// A loop in a high level language translates into either:
+//
+///  - a single-block loop, where the loop header branches back to itself.
+///     In this case this single-block loop consists only of the *continue
+///     construct*.  There is no "loop construct" for this case.
+//
+///  - a multi-block loop, where the loop back-edge is different from the loop
+///     header.
+///     This case has both a non-empty loop construct containing at least the
+///     loop header, and a non-empty continue construct, containing at least the
+///     back-edge block.
+///
+/// We care about two kinds of selection constructs:
+///
+///  - if-selection: where the header block ends in OpBranchConditional
+///
+///  - switch-selection: where the header block ends in OpSwitch
+///
+struct Construct {
+  /// Enumeration for the kinds of structured constructs.
+  enum Kind {
+    /// The whole function.
+    kFunction,
+    /// A SPIR-V selection construct, header basic block ending in
+    /// OpBrancConditional.
+    kIfSelection,
+    /// A SPIR-V selection construct, header basic block ending in OpSwitch.
+    kSwitchSelection,
+    /// A SPIR-V loop construct.
+    kLoop,
+    /// A SPIR-V continue construct.
+    kContinue,
+  };
+
+  /// Constructor
+  /// @param the_parent parent construct
+  /// @param the_depth construct nesting depth
+  /// @param the_kind construct kind
+  /// @param the_begin_id block id of the first block in the construct
+  /// @param the_end_id block id of the first block after the construct, or 0
+  /// @param the_begin_pos block order position of the_begin_id
+  /// @param the_end_pos block order position of the_end_id or a too-large value
+  /// @param the_scope_end_pos block position of the first block past the end of
+  /// the WGSL scope
+  Construct(const Construct* the_parent,
+            int the_depth,
+            Kind the_kind,
+            uint32_t the_begin_id,
+            uint32_t the_end_id,
+            uint32_t the_begin_pos,
+            uint32_t the_end_pos,
+            uint32_t the_scope_end_pos);
+
+  /// @param pos a block position
+  /// @returns true if the given block position is inside this construct.
+  bool ContainsPos(uint32_t pos) const {
+    return begin_pos <= pos && pos < end_pos;
+  }
+  /// Returns true if the given block position is inside the WGSL scope
+  /// corresponding to this construct. A loop construct's WGSL scope encloses
+  /// the associated continue construct. Otherwise the WGSL scope extent is the
+  /// same as the block extent.
+  /// @param pos a block position
+  /// @returns true if the given block position is inside the WGSL scope.
+  bool ScopeContainsPos(uint32_t pos) const {
+    return begin_pos <= pos && pos < scope_end_pos;
+  }
+
+  /// The nearest enclosing construct other than itself, or nullptr if
+  /// this construct represents the entire function.
+  const Construct* const parent = nullptr;
+  /// The nearest enclosing loop construct, if one exists.  Points to `this`
+  /// when this is a loop construct.
+  const Construct* const enclosing_loop = nullptr;
+  /// The nearest enclosing continue construct, if one exists.  Points to
+  /// `this` when this is a contnue construct.
+  const Construct* const enclosing_continue = nullptr;
+  /// The nearest enclosing loop construct or continue construct or
+  /// switch-selection construct, if one exists. The signficance is
+  /// that a high level language "break" will branch to the merge block
+  /// of such an enclosing construct. Points to `this` when this is
+  /// a loop construct, a continue construct, or a switch-selection construct.
+  const Construct* const enclosing_loop_or_continue_or_switch = nullptr;
+
+  /// Control flow nesting depth. The entry block is at nesting depth 0.
+  const int depth = 0;
+  /// The construct kind
+  const Kind kind = kFunction;
+  /// The id of the first block in this structure.
+  const uint32_t begin_id = 0;
+  /// 0 for kFunction, or the id of the block immediately after this construct
+  /// in the computed block order.
+  const uint32_t end_id = 0;
+  /// The position of block #begin_id in the computed block order.
+  const uint32_t begin_pos = 0;
+  /// The position of block #end_id in the block order, or the number of
+  /// block order elements if #end_id is 0.
+  const uint32_t end_pos = 0;
+  /// The position of the first block after the WGSL scope corresponding to
+  /// this construct.
+  const uint32_t scope_end_pos = 0;
+};
+
+using ConstructList = std::vector<std::unique_ptr<Construct>>;
+
+/// Converts a construct kind to a string.
+/// @param kind the construct kind to convert
+/// @returns the string representation
+inline std::string ToString(Construct::Kind kind) {
+  switch (kind) {
+    case Construct::kFunction:
+      return "Function";
+    case Construct::kIfSelection:
+      return "IfSelection";
+    case Construct::kSwitchSelection:
+      return "SwitchSelection";
+    case Construct::kLoop:
+      return "Loop";
+    case Construct::kContinue:
+      return "Continue";
+  }
+  return "NONE";
+}
+
+/// Converts a construct into a short summary string.
+/// @param c the construct, which can be null
+/// @returns a short summary string
+inline std::string ToStringBrief(const Construct* c) {
+  if (c) {
+    std::stringstream ss;
+    ss << ToString(c->kind) << "@" << c->begin_id;
+    return ss.str();
+  }
+  return "null";
+}
+
+/// Emits a construct to a stream.
+/// @param o the stream
+/// @param c the structured construct
+/// @returns the stream
+inline std::ostream& operator<<(std::ostream& o, const Construct& c) {
+  o << "Construct{ " << ToString(c.kind) << " [" << c.begin_pos << ","
+    << c.end_pos << ")"
+    << " begin_id:" << c.begin_id << " end_id:" << c.end_id
+    << " depth:" << c.depth;
+
+  o << " parent:" << ToStringBrief(c.parent);
+
+  if (c.scope_end_pos != c.end_pos) {
+    o << " scope:[" << c.begin_pos << "," << c.scope_end_pos << ")";
+  }
+
+  if (c.enclosing_loop) {
+    o << " in-l:" << ToStringBrief(c.enclosing_loop);
+  }
+
+  if (c.enclosing_continue) {
+    o << " in-c:" << ToStringBrief(c.enclosing_continue);
+  }
+
+  if ((c.enclosing_loop_or_continue_or_switch != c.enclosing_loop) &&
+      (c.enclosing_loop_or_continue_or_switch != c.enclosing_continue)) {
+    o << " in-c-l-s:" << ToStringBrief(c.enclosing_loop_or_continue_or_switch);
+  }
+
+  o << " }";
+  return o;
+}
+
+/// Emits a construct to a stream.
+/// @param o the stream
+/// @param c the structured construct
+/// @returns the stream
+inline std::ostream& operator<<(std::ostream& o,
+                                const std::unique_ptr<Construct>& c) {
+  return o << *(c.get());
+}
+
+/// Converts a construct to a string.
+/// @param c the construct
+/// @returns the string representation
+inline std::string ToString(const Construct& c) {
+  std::stringstream ss;
+  ss << c;
+  return ss.str();
+}
+
+/// Converts a construct to a string.
+/// @param c the construct
+/// @returns the string representation
+inline std::string ToString(const Construct* c) {
+  return c ? ToString(*c) : ToStringBrief(c);
+}
+
+/// Converts a unique pointer to a construct to a string.
+/// @param c the construct
+/// @returns the string representation
+inline std::string ToString(const std::unique_ptr<Construct>& c) {
+  return ToString(*(c.get()));
+}
+
+/// Emits a construct list to a stream.
+/// @param o the stream
+/// @param cl the construct list
+/// @returns the stream
+inline std::ostream& operator<<(std::ostream& o, const ConstructList& cl) {
+  o << "ConstructList{\n";
+  for (const auto& c : cl) {
+    o << "  " << c << "\n";
+  }
+  o << "}";
+  return o;
+}
+
+/// Converts a construct list to a string.
+/// @param cl the construct list
+/// @returns the string representation
+inline std::string ToString(const ConstructList& cl) {
+  std::stringstream ss;
+  ss << cl;
+  return ss.str();
+}
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_TINT_READER_SPIRV_CONSTRUCT_H_
diff --git a/src/tint/reader/spirv/entry_point_info.cc b/src/tint/reader/spirv/entry_point_info.cc
new file mode 100644
index 0000000..5258606
--- /dev/null
+++ b/src/tint/reader/spirv/entry_point_info.cc
@@ -0,0 +1,44 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/entry_point_info.h"
+
+#include <utility>
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+EntryPointInfo::EntryPointInfo(std::string the_name,
+                               ast::PipelineStage the_stage,
+                               bool the_owns_inner_implementation,
+                               std::string the_inner_name,
+                               std::vector<uint32_t>&& the_inputs,
+                               std::vector<uint32_t>&& the_outputs,
+                               GridSize the_wg_size)
+    : name(the_name),
+      stage(the_stage),
+      owns_inner_implementation(the_owns_inner_implementation),
+      inner_name(std::move(the_inner_name)),
+      inputs(std::move(the_inputs)),
+      outputs(std::move(the_outputs)),
+      workgroup_size(the_wg_size) {}
+
+EntryPointInfo::EntryPointInfo(const EntryPointInfo&) = default;
+
+EntryPointInfo::~EntryPointInfo() = default;
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/spirv/entry_point_info.h b/src/tint/reader/spirv/entry_point_info.h
new file mode 100644
index 0000000..e65b79d
--- /dev/null
+++ b/src/tint/reader/spirv/entry_point_info.h
@@ -0,0 +1,95 @@
+// Copyright 2020 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_READER_SPIRV_ENTRY_POINT_INFO_H_
+#define SRC_TINT_READER_SPIRV_ENTRY_POINT_INFO_H_
+
+#include <string>
+#include <vector>
+
+#include "src/tint/ast/pipeline_stage.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+/// The size of an integer-coordinate grid, in the x, y, and z dimensions.
+struct GridSize {
+  /// x value
+  uint32_t x = 0;
+  /// y value
+  uint32_t y = 0;
+  /// z value
+  uint32_t z = 0;
+};
+
+/// Entry point information for a function
+struct EntryPointInfo {
+  /// Constructor.
+  /// @param the_name the name of the entry point
+  /// @param the_stage the pipeline stage
+  /// @param the_owns_inner_implementation if true, this entry point is
+  /// responsible for generating the inner implementation function.
+  /// @param the_inner_name the name of the inner implementation function of the
+  /// entry point
+  /// @param the_inputs list of IDs for Input variables used by the shader
+  /// @param the_outputs list of IDs for Output variables used by the shader
+  /// @param the_wg_size the workgroup_size, for a compute shader
+  EntryPointInfo(std::string the_name,
+                 ast::PipelineStage the_stage,
+                 bool the_owns_inner_implementation,
+                 std::string the_inner_name,
+                 std::vector<uint32_t>&& the_inputs,
+                 std::vector<uint32_t>&& the_outputs,
+                 GridSize the_wg_size);
+  /// Copy constructor
+  /// @param other the other entry point info to be built from
+  EntryPointInfo(const EntryPointInfo& other);
+  /// Destructor
+  ~EntryPointInfo();
+
+  /// The entry point name.
+  /// In the WGSL output, this function will have pipeline inputs and outputs
+  /// as parameters. This function will store them into Private variables,
+  /// and then call the "inner" function, named by the next memeber.
+  /// Then outputs are copied from the private variables to the return value.
+  std::string name;
+  /// The entry point stage
+  ast::PipelineStage stage = ast::PipelineStage::kNone;
+
+  /// True when this entry point is responsible for generating the
+  /// inner implementation function.  False when this is the second entry
+  /// point encountered for the same function in SPIR-V. It's unusual, but
+  /// possible for the same function to be the implementation for multiple
+  /// entry points.
+  bool owns_inner_implementation;
+  /// The name of the inner implementation function of the entry point.
+  std::string inner_name;
+  /// IDs of pipeline input variables, sorted and without duplicates.
+  std::vector<uint32_t> inputs;
+  /// IDs of pipeline output variables, sorted and without duplicates.
+  std::vector<uint32_t> outputs;
+
+  /// If this is a compute shader, this is the workgroup size in the x, y,
+  /// and z dimensions set via LocalSize, or via the composite value
+  /// decorated as the WorkgroupSize BuiltIn.  The WorkgroupSize builtin
+  /// takes priority.
+  GridSize workgroup_size;
+};
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_TINT_READER_SPIRV_ENTRY_POINT_INFO_H_
diff --git a/src/tint/reader/spirv/enum_converter.cc b/src/tint/reader/spirv/enum_converter.cc
new file mode 100644
index 0000000..a197bac
--- /dev/null
+++ b/src/tint/reader/spirv/enum_converter.cc
@@ -0,0 +1,182 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/enum_converter.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+EnumConverter::EnumConverter(const FailStream& fs) : fail_stream_(fs) {}
+
+EnumConverter::~EnumConverter() = default;
+
+ast::PipelineStage EnumConverter::ToPipelineStage(SpvExecutionModel model) {
+  switch (model) {
+    case SpvExecutionModelVertex:
+      return ast::PipelineStage::kVertex;
+    case SpvExecutionModelFragment:
+      return ast::PipelineStage::kFragment;
+    case SpvExecutionModelGLCompute:
+      return ast::PipelineStage::kCompute;
+    default:
+      break;
+  }
+
+  Fail() << "unknown SPIR-V execution model: " << uint32_t(model);
+  return ast::PipelineStage::kNone;
+}
+
+ast::StorageClass EnumConverter::ToStorageClass(const SpvStorageClass sc) {
+  switch (sc) {
+    case SpvStorageClassInput:
+      return ast::StorageClass::kInput;
+    case SpvStorageClassOutput:
+      return ast::StorageClass::kOutput;
+    case SpvStorageClassUniform:
+      return ast::StorageClass::kUniform;
+    case SpvStorageClassWorkgroup:
+      return ast::StorageClass::kWorkgroup;
+    case SpvStorageClassUniformConstant:
+      return ast::StorageClass::kNone;
+    case SpvStorageClassStorageBuffer:
+      return ast::StorageClass::kStorage;
+    case SpvStorageClassPrivate:
+      return ast::StorageClass::kPrivate;
+    case SpvStorageClassFunction:
+      return ast::StorageClass::kFunction;
+    default:
+      break;
+  }
+
+  Fail() << "unknown SPIR-V storage class: " << uint32_t(sc);
+  return ast::StorageClass::kInvalid;
+}
+
+ast::Builtin EnumConverter::ToBuiltin(SpvBuiltIn b) {
+  switch (b) {
+    case SpvBuiltInPosition:
+      return ast::Builtin::kPosition;
+    case SpvBuiltInVertexIndex:
+      return ast::Builtin::kVertexIndex;
+    case SpvBuiltInInstanceIndex:
+      return ast::Builtin::kInstanceIndex;
+    case SpvBuiltInFrontFacing:
+      return ast::Builtin::kFrontFacing;
+    case SpvBuiltInFragCoord:
+      return ast::Builtin::kPosition;
+    case SpvBuiltInFragDepth:
+      return ast::Builtin::kFragDepth;
+    case SpvBuiltInLocalInvocationId:
+      return ast::Builtin::kLocalInvocationId;
+    case SpvBuiltInLocalInvocationIndex:
+      return ast::Builtin::kLocalInvocationIndex;
+    case SpvBuiltInGlobalInvocationId:
+      return ast::Builtin::kGlobalInvocationId;
+    case SpvBuiltInWorkgroupId:
+      return ast::Builtin::kWorkgroupId;
+    case SpvBuiltInSampleId:
+      return ast::Builtin::kSampleIndex;
+    case SpvBuiltInSampleMask:
+      return ast::Builtin::kSampleMask;
+    default:
+      break;
+  }
+
+  Fail() << "unknown SPIR-V builtin: " << uint32_t(b);
+  return ast::Builtin::kNone;
+}
+
+ast::TextureDimension EnumConverter::ToDim(SpvDim dim, bool arrayed) {
+  if (arrayed) {
+    switch (dim) {
+      case SpvDim2D:
+        return ast::TextureDimension::k2dArray;
+      case SpvDimCube:
+        return ast::TextureDimension::kCubeArray;
+      default:
+        break;
+    }
+    Fail() << "arrayed dimension must be 2D or Cube. Got " << int(dim);
+    return ast::TextureDimension::kNone;
+  }
+  // Assume non-arrayed
+  switch (dim) {
+    case SpvDim1D:
+      return ast::TextureDimension::k1d;
+    case SpvDim2D:
+      return ast::TextureDimension::k2d;
+    case SpvDim3D:
+      return ast::TextureDimension::k3d;
+    case SpvDimCube:
+      return ast::TextureDimension::kCube;
+    default:
+      break;
+  }
+  Fail() << "invalid dimension: " << int(dim);
+  return ast::TextureDimension::kNone;
+}
+
+ast::TexelFormat EnumConverter::ToTexelFormat(SpvImageFormat fmt) {
+  switch (fmt) {
+    case SpvImageFormatUnknown:
+      return ast::TexelFormat::kNone;
+
+    // 8 bit channels
+    case SpvImageFormatRgba8:
+      return ast::TexelFormat::kRgba8Unorm;
+    case SpvImageFormatRgba8Snorm:
+      return ast::TexelFormat::kRgba8Snorm;
+    case SpvImageFormatRgba8ui:
+      return ast::TexelFormat::kRgba8Uint;
+    case SpvImageFormatRgba8i:
+      return ast::TexelFormat::kRgba8Sint;
+
+    // 16 bit channels
+    case SpvImageFormatRgba16ui:
+      return ast::TexelFormat::kRgba16Uint;
+    case SpvImageFormatRgba16i:
+      return ast::TexelFormat::kRgba16Sint;
+    case SpvImageFormatRgba16f:
+      return ast::TexelFormat::kRgba16Float;
+
+    // 32 bit channels
+    case SpvImageFormatR32ui:
+      return ast::TexelFormat::kR32Uint;
+    case SpvImageFormatR32i:
+      return ast::TexelFormat::kR32Sint;
+    case SpvImageFormatR32f:
+      return ast::TexelFormat::kR32Float;
+    case SpvImageFormatRg32ui:
+      return ast::TexelFormat::kRg32Uint;
+    case SpvImageFormatRg32i:
+      return ast::TexelFormat::kRg32Sint;
+    case SpvImageFormatRg32f:
+      return ast::TexelFormat::kRg32Float;
+    case SpvImageFormatRgba32ui:
+      return ast::TexelFormat::kRgba32Uint;
+    case SpvImageFormatRgba32i:
+      return ast::TexelFormat::kRgba32Sint;
+    case SpvImageFormatRgba32f:
+      return ast::TexelFormat::kRgba32Float;
+    default:
+      break;
+  }
+  Fail() << "invalid image format: " << int(fmt);
+  return ast::TexelFormat::kNone;
+}
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/spirv/enum_converter.h b/src/tint/reader/spirv/enum_converter.h
new file mode 100644
index 0000000..d2caac3
--- /dev/null
+++ b/src/tint/reader/spirv/enum_converter.h
@@ -0,0 +1,81 @@
+// Copyright 2020 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_READER_SPIRV_ENUM_CONVERTER_H_
+#define SRC_TINT_READER_SPIRV_ENUM_CONVERTER_H_
+
+#include "spirv/unified1/spirv.h"
+#include "src/tint/ast/builtin.h"
+#include "src/tint/ast/pipeline_stage.h"
+#include "src/tint/ast/storage_class.h"
+#include "src/tint/reader/spirv/fail_stream.h"
+#include "src/tint/sem/storage_texture_type.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+/// A converter from SPIR-V enums to Tint AST enums.
+class EnumConverter {
+ public:
+  /// Creates a new enum converter.
+  /// @param fail_stream the error reporting stream.
+  explicit EnumConverter(const FailStream& fail_stream);
+  /// Destructor
+  ~EnumConverter();
+
+  /// Converts a SPIR-V execution model to a Tint pipeline stage.
+  /// On failure, logs an error and returns kNone
+  /// @param model the SPIR-V entry point execution model
+  /// @returns a Tint AST pipeline stage
+  ast::PipelineStage ToPipelineStage(SpvExecutionModel model);
+
+  /// Converts a SPIR-V storage class to a Tint storage class.
+  /// On failure, logs an error and returns kNone
+  /// @param sc the SPIR-V storage class
+  /// @returns a Tint AST storage class
+  ast::StorageClass ToStorageClass(const SpvStorageClass sc);
+
+  /// Converts a SPIR-V Builtin value a Tint Builtin.
+  /// On failure, logs an error and returns kNone
+  /// @param b the SPIR-V builtin
+  /// @returns a Tint AST builtin
+  ast::Builtin ToBuiltin(SpvBuiltIn b);
+
+  /// Converts a possibly arrayed SPIR-V Dim to a Tint texture dimension.
+  /// On failure, logs an error and returns kNone
+  /// @param dim the SPIR-V Dim value
+  /// @param arrayed true if the texture is arrayed
+  /// @returns a Tint AST texture dimension
+  ast::TextureDimension ToDim(SpvDim dim, bool arrayed);
+
+  /// Converts a SPIR-V Image Format to a TexelFormat
+  /// On failure, logs an error and returns kNone
+  /// @param fmt the SPIR-V format
+  /// @returns a Tint AST format
+  ast::TexelFormat ToTexelFormat(SpvImageFormat fmt);
+
+ private:
+  /// Registers a failure and returns a stream for log diagnostics.
+  /// @returns a failure stream
+  FailStream Fail() { return fail_stream_.Fail(); }
+
+  FailStream fail_stream_;
+};
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_TINT_READER_SPIRV_ENUM_CONVERTER_H_
diff --git a/src/tint/reader/spirv/enum_converter_test.cc b/src/tint/reader/spirv/enum_converter_test.cc
new file mode 100644
index 0000000..eec14f3
--- /dev/null
+++ b/src/tint/reader/spirv/enum_converter_test.cc
@@ -0,0 +1,429 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/enum_converter.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+namespace {
+
+// Pipeline stage
+
+struct PipelineStageCase {
+  SpvExecutionModel model;
+  bool expect_success;
+  ast::PipelineStage expected;
+};
+inline std::ostream& operator<<(std::ostream& out, PipelineStageCase psc) {
+  out << "PipelineStageCase{ SpvExecutionModel:" << int(psc.model)
+      << " expect_success?:" << int(psc.expect_success)
+      << " expected:" << int(psc.expected) << "}";
+  return out;
+}
+
+class SpvPipelineStageTest : public testing::TestWithParam<PipelineStageCase> {
+ public:
+  SpvPipelineStageTest()
+      : success_(true),
+        fail_stream_(&success_, &errors_),
+        converter_(fail_stream_) {}
+
+  std::string error() const { return errors_.str(); }
+
+ protected:
+  bool success_ = true;
+  std::stringstream errors_;
+  FailStream fail_stream_;
+  EnumConverter converter_;
+};
+
+TEST_P(SpvPipelineStageTest, Samples) {
+  const auto params = GetParam();
+
+  const auto result = converter_.ToPipelineStage(params.model);
+  EXPECT_EQ(success_, params.expect_success);
+  if (params.expect_success) {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_TRUE(error().empty());
+  } else {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_THAT(error(),
+                ::testing::StartsWith("unknown SPIR-V execution model:"));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood,
+    SpvPipelineStageTest,
+    testing::Values(PipelineStageCase{SpvExecutionModelVertex, true,
+                                      ast::PipelineStage::kVertex},
+                    PipelineStageCase{SpvExecutionModelFragment, true,
+                                      ast::PipelineStage::kFragment},
+                    PipelineStageCase{SpvExecutionModelGLCompute, true,
+                                      ast::PipelineStage::kCompute}));
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterBad,
+    SpvPipelineStageTest,
+    testing::Values(PipelineStageCase{static_cast<SpvExecutionModel>(9999),
+                                      false, ast::PipelineStage::kNone},
+                    PipelineStageCase{SpvExecutionModelTessellationControl,
+                                      false, ast::PipelineStage::kNone}));
+
+// Storage class
+
+struct StorageClassCase {
+  SpvStorageClass sc;
+  bool expect_success;
+  ast::StorageClass expected;
+};
+inline std::ostream& operator<<(std::ostream& out, StorageClassCase scc) {
+  out << "StorageClassCase{ SpvStorageClass:" << int(scc.sc)
+      << " expect_success?:" << int(scc.expect_success)
+      << " expected:" << int(scc.expected) << "}";
+  return out;
+}
+
+class SpvStorageClassTest : public testing::TestWithParam<StorageClassCase> {
+ public:
+  SpvStorageClassTest()
+      : success_(true),
+        fail_stream_(&success_, &errors_),
+        converter_(fail_stream_) {}
+
+  std::string error() const { return errors_.str(); }
+
+ protected:
+  bool success_ = true;
+  std::stringstream errors_;
+  FailStream fail_stream_;
+  EnumConverter converter_;
+};
+
+TEST_P(SpvStorageClassTest, Samples) {
+  const auto params = GetParam();
+
+  const auto result = converter_.ToStorageClass(params.sc);
+  EXPECT_EQ(success_, params.expect_success);
+  if (params.expect_success) {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_TRUE(error().empty());
+  } else {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_THAT(error(),
+                ::testing::StartsWith("unknown SPIR-V storage class: "));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood,
+    SpvStorageClassTest,
+    testing::Values(StorageClassCase{SpvStorageClassInput, true,
+                                     ast::StorageClass::kInput},
+                    StorageClassCase{SpvStorageClassOutput, true,
+                                     ast::StorageClass::kOutput},
+                    StorageClassCase{SpvStorageClassUniform, true,
+                                     ast::StorageClass::kUniform},
+                    StorageClassCase{SpvStorageClassWorkgroup, true,
+                                     ast::StorageClass::kWorkgroup},
+                    StorageClassCase{SpvStorageClassUniformConstant, true,
+                                     ast::StorageClass::kNone},
+                    StorageClassCase{SpvStorageClassStorageBuffer, true,
+                                     ast::StorageClass::kStorage},
+                    StorageClassCase{SpvStorageClassPrivate, true,
+                                     ast::StorageClass::kPrivate},
+                    StorageClassCase{SpvStorageClassFunction, true,
+                                     ast::StorageClass::kFunction}));
+
+INSTANTIATE_TEST_SUITE_P(EnumConverterBad,
+                         SpvStorageClassTest,
+                         testing::Values(StorageClassCase{
+                             static_cast<SpvStorageClass>(9999), false,
+                             ast::StorageClass::kInvalid}));
+
+// Builtin
+
+struct BuiltinCase {
+  SpvBuiltIn builtin;
+  bool expect_success;
+  ast::Builtin expected;
+};
+inline std::ostream& operator<<(std::ostream& out, BuiltinCase bc) {
+  out << "BuiltinCase{ SpvBuiltIn:" << int(bc.builtin)
+      << " expect_success?:" << int(bc.expect_success)
+      << " expected:" << int(bc.expected) << "}";
+  return out;
+}
+
+class SpvBuiltinTest : public testing::TestWithParam<BuiltinCase> {
+ public:
+  SpvBuiltinTest()
+      : success_(true),
+        fail_stream_(&success_, &errors_),
+        converter_(fail_stream_) {}
+
+  std::string error() const { return errors_.str(); }
+
+ protected:
+  bool success_ = true;
+  std::stringstream errors_;
+  FailStream fail_stream_;
+  EnumConverter converter_;
+};
+
+TEST_P(SpvBuiltinTest, Samples) {
+  const auto params = GetParam();
+
+  const auto result = converter_.ToBuiltin(params.builtin);
+  EXPECT_EQ(success_, params.expect_success);
+  if (params.expect_success) {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_TRUE(error().empty());
+  } else {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_THAT(error(), ::testing::StartsWith("unknown SPIR-V builtin: "));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood_Input,
+    SpvBuiltinTest,
+    testing::Values(
+        BuiltinCase{SpvBuiltInPosition, true, ast::Builtin::kPosition},
+        BuiltinCase{SpvBuiltInInstanceIndex, true,
+                    ast::Builtin::kInstanceIndex},
+        BuiltinCase{SpvBuiltInFrontFacing, true, ast::Builtin::kFrontFacing},
+        BuiltinCase{SpvBuiltInFragCoord, true, ast::Builtin::kPosition},
+        BuiltinCase{SpvBuiltInLocalInvocationId, true,
+                    ast::Builtin::kLocalInvocationId},
+        BuiltinCase{SpvBuiltInLocalInvocationIndex, true,
+                    ast::Builtin::kLocalInvocationIndex},
+        BuiltinCase{SpvBuiltInGlobalInvocationId, true,
+                    ast::Builtin::kGlobalInvocationId},
+        BuiltinCase{SpvBuiltInWorkgroupId, true, ast::Builtin::kWorkgroupId},
+        BuiltinCase{SpvBuiltInSampleId, true, ast::Builtin::kSampleIndex},
+        BuiltinCase{SpvBuiltInSampleMask, true, ast::Builtin::kSampleMask}));
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood_Output,
+    SpvBuiltinTest,
+    testing::Values(
+        BuiltinCase{SpvBuiltInPosition, true, ast::Builtin::kPosition},
+        BuiltinCase{SpvBuiltInFragDepth, true, ast::Builtin::kFragDepth},
+        BuiltinCase{SpvBuiltInSampleMask, true, ast::Builtin::kSampleMask}));
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterBad,
+    SpvBuiltinTest,
+    testing::Values(
+        BuiltinCase{static_cast<SpvBuiltIn>(9999), false, ast::Builtin::kNone},
+        BuiltinCase{static_cast<SpvBuiltIn>(9999), false, ast::Builtin::kNone},
+        BuiltinCase{SpvBuiltInNumWorkgroups, false, ast::Builtin::kNone}));
+
+// Dim
+
+struct DimCase {
+  SpvDim dim;
+  bool arrayed;
+  bool expect_success;
+  ast::TextureDimension expected;
+};
+inline std::ostream& operator<<(std::ostream& out, DimCase dc) {
+  out << "DimCase{ SpvDim:" << int(dc.dim) << " arrayed?:" << int(dc.arrayed)
+      << " expect_success?:" << int(dc.expect_success)
+      << " expected:" << int(dc.expected) << "}";
+  return out;
+}
+
+class SpvDimTest : public testing::TestWithParam<DimCase> {
+ public:
+  SpvDimTest()
+      : success_(true),
+        fail_stream_(&success_, &errors_),
+        converter_(fail_stream_) {}
+
+  std::string error() const { return errors_.str(); }
+
+ protected:
+  bool success_ = true;
+  std::stringstream errors_;
+  FailStream fail_stream_;
+  EnumConverter converter_;
+};
+
+TEST_P(SpvDimTest, Samples) {
+  const auto params = GetParam();
+
+  const auto result = converter_.ToDim(params.dim, params.arrayed);
+  EXPECT_EQ(success_, params.expect_success);
+  if (params.expect_success) {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_TRUE(error().empty());
+  } else {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_THAT(error(), ::testing::HasSubstr("dimension"));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood,
+    SpvDimTest,
+    testing::Values(
+        // Non-arrayed
+        DimCase{SpvDim1D, false, true, ast::TextureDimension::k1d},
+        DimCase{SpvDim2D, false, true, ast::TextureDimension::k2d},
+        DimCase{SpvDim3D, false, true, ast::TextureDimension::k3d},
+        DimCase{SpvDimCube, false, true, ast::TextureDimension::kCube},
+        // Arrayed
+        DimCase{SpvDim2D, true, true, ast::TextureDimension::k2dArray},
+        DimCase{SpvDimCube, true, true, ast::TextureDimension::kCubeArray}));
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterBad,
+    SpvDimTest,
+    testing::Values(
+        // Invalid SPIR-V dimensionality.
+        DimCase{SpvDimMax, false, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimMax, true, false, ast::TextureDimension::kNone},
+        // Vulkan non-arrayed dimensionalities not supported by WGSL.
+        DimCase{SpvDimRect, false, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimBuffer, false, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimSubpassData, false, false, ast::TextureDimension::kNone},
+        // Arrayed dimensionalities not supported by WGSL
+        DimCase{SpvDim3D, true, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimRect, true, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimBuffer, true, false, ast::TextureDimension::kNone},
+        DimCase{SpvDimSubpassData, true, false, ast::TextureDimension::kNone}));
+
+// TexelFormat
+
+struct TexelFormatCase {
+  SpvImageFormat format;
+  bool expect_success;
+  ast::TexelFormat expected;
+};
+inline std::ostream& operator<<(std::ostream& out, TexelFormatCase ifc) {
+  out << "TexelFormatCase{ SpvImageFormat:" << int(ifc.format)
+      << " expect_success?:" << int(ifc.expect_success)
+      << " expected:" << int(ifc.expected) << "}";
+  return out;
+}
+
+class SpvImageFormatTest : public testing::TestWithParam<TexelFormatCase> {
+ public:
+  SpvImageFormatTest()
+      : success_(true),
+        fail_stream_(&success_, &errors_),
+        converter_(fail_stream_) {}
+
+  std::string error() const { return errors_.str(); }
+
+ protected:
+  bool success_ = true;
+  std::stringstream errors_;
+  FailStream fail_stream_;
+  EnumConverter converter_;
+};
+
+TEST_P(SpvImageFormatTest, Samples) {
+  const auto params = GetParam();
+
+  const auto result = converter_.ToTexelFormat(params.format);
+  EXPECT_EQ(success_, params.expect_success) << params;
+  if (params.expect_success) {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_TRUE(error().empty());
+  } else {
+    EXPECT_EQ(result, params.expected);
+    EXPECT_THAT(error(), ::testing::StartsWith("invalid image format: "));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterGood,
+    SpvImageFormatTest,
+    testing::Values(
+        // Unknown.  This is used for sampled images.
+        TexelFormatCase{SpvImageFormatUnknown, true, ast::TexelFormat::kNone},
+        // 8 bit channels
+        TexelFormatCase{SpvImageFormatRgba8, true,
+                        ast::TexelFormat::kRgba8Unorm},
+        TexelFormatCase{SpvImageFormatRgba8Snorm, true,
+                        ast::TexelFormat::kRgba8Snorm},
+        TexelFormatCase{SpvImageFormatRgba8ui, true,
+                        ast::TexelFormat::kRgba8Uint},
+        TexelFormatCase{SpvImageFormatRgba8i, true,
+                        ast::TexelFormat::kRgba8Sint},
+        // 16 bit channels
+        TexelFormatCase{SpvImageFormatRgba16ui, true,
+                        ast::TexelFormat::kRgba16Uint},
+        TexelFormatCase{SpvImageFormatRgba16i, true,
+                        ast::TexelFormat::kRgba16Sint},
+        TexelFormatCase{SpvImageFormatRgba16f, true,
+                        ast::TexelFormat::kRgba16Float},
+        // 32 bit channels
+        // ... 1 channel
+        TexelFormatCase{SpvImageFormatR32ui, true, ast::TexelFormat::kR32Uint},
+        TexelFormatCase{SpvImageFormatR32i, true, ast::TexelFormat::kR32Sint},
+        TexelFormatCase{SpvImageFormatR32f, true, ast::TexelFormat::kR32Float},
+        // ... 2 channels
+        TexelFormatCase{SpvImageFormatRg32ui, true,
+                        ast::TexelFormat::kRg32Uint},
+        TexelFormatCase{SpvImageFormatRg32i, true, ast::TexelFormat::kRg32Sint},
+        TexelFormatCase{SpvImageFormatRg32f, true,
+                        ast::TexelFormat::kRg32Float},
+        // ... 4 channels
+        TexelFormatCase{SpvImageFormatRgba32ui, true,
+                        ast::TexelFormat::kRgba32Uint},
+        TexelFormatCase{SpvImageFormatRgba32i, true,
+                        ast::TexelFormat::kRgba32Sint},
+        TexelFormatCase{SpvImageFormatRgba32f, true,
+                        ast::TexelFormat::kRgba32Float}));
+
+INSTANTIATE_TEST_SUITE_P(
+    EnumConverterBad,
+    SpvImageFormatTest,
+    testing::Values(
+        // Scanning in order from the SPIR-V spec.
+        TexelFormatCase{SpvImageFormatRg16f, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatR11fG11fB10f, false,
+                        ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatR16f, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRgb10A2, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg16, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg8, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatR16, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatR8, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRgba16Snorm, false,
+                        ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg16Snorm, false,
+                        ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg8Snorm, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg16i, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg8i, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatR8i, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRgb10a2ui, false,
+                        ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg16ui, false, ast::TexelFormat::kNone},
+        TexelFormatCase{SpvImageFormatRg8ui, false, ast::TexelFormat::kNone}));
+
+}  // namespace
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/spirv/fail_stream.h b/src/tint/reader/spirv/fail_stream.h
new file mode 100644
index 0000000..a7530ca
--- /dev/null
+++ b/src/tint/reader/spirv/fail_stream.h
@@ -0,0 +1,74 @@
+// Copyright 2020 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_READER_SPIRV_FAIL_STREAM_H_
+#define SRC_TINT_READER_SPIRV_FAIL_STREAM_H_
+
+#include <ostream>
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+/// A FailStream object accumulates values onto a given std::ostream,
+/// and can be used to record failure by writing the false value
+/// to given a pointer-to-bool.
+class FailStream {
+ public:
+  /// Creates a new fail stream
+  /// @param status_ptr where we will write false to indicate failure. Assumed
+  /// to be a valid pointer to bool.
+  /// @param out output stream where a message should be written to explain
+  /// the failure
+  FailStream(bool* status_ptr, std::ostream* out)
+      : status_ptr_(status_ptr), out_(out) {}
+  /// Copy constructor
+  /// @param other the fail stream to clone
+  FailStream(const FailStream& other) = default;
+
+  /// Converts to a boolean status. A true result indicates success,
+  /// and a false result indicates failure.
+  /// @returns the status
+  operator bool() const { return *status_ptr_; }
+  /// Returns the current status value.  This can be more readable
+  /// the conversion operator.
+  /// @returns the status
+  bool status() const { return *status_ptr_; }
+
+  /// Records failure.
+  /// @returns a FailStream
+  FailStream& Fail() {
+    *status_ptr_ = false;
+    return *this;
+  }
+
+  /// Appends the given value to the message output stream.
+  /// @param val the value to write to the output stream.
+  /// @returns this object
+  template <typename T>
+  FailStream& operator<<(const T& val) {
+    *out_ << val;
+    return *this;
+  }
+
+ private:
+  bool* status_ptr_;
+  std::ostream* out_;
+};
+
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
+
+#endif  // SRC_TINT_READER_SPIRV_FAIL_STREAM_H_
diff --git a/src/tint/reader/spirv/fail_stream_test.cc b/src/tint/reader/spirv/fail_stream_test.cc
new file mode 100644
index 0000000..4c6e9bf
--- /dev/null
+++ b/src/tint/reader/spirv/fail_stream_test.cc
@@ -0,0 +1,73 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/fail_stream.h"
+
+#include "gmock/gmock.h"
+
+namespace tint {
+namespace reader {
+namespace spirv {
+namespace {
+
+using ::testing::Eq;
+
+using FailStreamTest = ::testing::Test;
+
+TEST_F(FailStreamTest, ConversionToBoolIsSameAsStatusMethod) {
+  bool flag = true;
+  FailStream fs(&flag, nullptr);
+
+  EXPECT_TRUE(fs.status());
+  EXPECT_TRUE(bool(fs));  // NOLINT
+  flag = false;
+  EXPECT_FALSE(fs.status());
+  EXPECT_FALSE(bool(fs));  // NOLINT
+  flag = true;
+  EXPECT_TRUE(fs.status());
+  EXPECT_TRUE(bool(fs));  // NOLINT
+}
+
+TEST_F(FailStreamTest, FailMethodChangesStatusToFalse) {
+  bool flag = true;
+  FailStream fs(&flag, nullptr);
+  EXPECT_TRUE(flag);
+  EXPECT_TRUE(bool(fs));  // NOLINT
+  fs.Fail();
+  EXPECT_FALSE(flag);
+  EXPECT_FALSE(bool(fs));  // NOLINT
+}
+
+TEST_F(FailStreamTest, FailMethodReturnsSelf) {
+  bool flag = true;
+  FailStream fs(&flag, nullptr);
+  FailStream& result = fs.Fail();
+  EXPECT_THAT(&result, Eq(&fs));
+}
+
+TEST_F(FailStreamTest, ShiftOperatorAccumulatesValues) {
+  bool flag = true;
+  std::stringstream ss;
+  FailStream fs(&flag, &ss);
+
+  ss << "prefix ";
+  fs << "cat " << 42;
+
+  EXPECT_THAT(ss.str(), Eq("prefix cat 42"));
+}
+
+}  // namespace
+}  // namespace spirv
+}  // namespace reader
+}  // namespace tint
diff --git a/src/tint/reader/spirv/function.cc b/src/tint/reader/spirv/function.cc
new file mode 100644
index 0000000..c130e85
--- /dev/null
+++ b/src/tint/reader/spirv/function.cc
@@ -0,0 +1,6153 @@
+// Copyright 2020 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/reader/spirv/function.h"
+
+#include <algorithm>
+#include <array>
+
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/bitcast_expression.h"
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/builtin.h"
+#include "src/tint/ast/builtin_attribute.h"
+#include "src/tint/ast/call_statement.h"
+#include "src/tint/ast/continue_statement.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/fallthrough_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/stage_attribute.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/sem/builtin_type.h"
+#include "src/tint/sem/depth_texture_type.h"
+#include "src/tint/sem/sampled_texture_type.h"
+
+// Terms:
+//    CFG: the control flow graph of the function, where basic blocks are the
+//    nodes, and branches form the directed arcs.  The function entry block is
+//    the root of the CFG.
+//
+//    Suppose H is a header block (i.e. has an OpSelectionMerge or OpLoopMerge).
+//    Then:
+//    - Let M(H) be the merge block named by the merge instruction in H.
+//    - If H is a loop header, i.e. has an OpLoopMerge instruction, then let
+//      CT(H) be the continue target block named by the OpLoopMerge
+//      instruction.
+//    - If H is a selection construct whose header ends in
+//      OpBranchConditional with true target %then and false target %else,
+//      then  TT(H) = %then and FT(H) = %else
+//
+// Determining output block order:
+//    The "structured post-order traversal" of the CFG is a post-order traversal
+//    of the basic blocks in the CFG, where:
+//      We visit the entry node of the function first.
+//      When visiting a header block:
+//        We next visit its merge block
+//        Then if it's a loop header, we next visit the continue target,
+//      Then we visit the block's successors (whether it's a header or not)
+//        If the block ends in an OpBranchConditional, we visit the false target
+//        before the true target.
+//
+//    The "reverse structured post-order traversal" of the CFG is the reverse
+//    of the structured post-order traversal.
+//    This is the order of basic blocks as they should be emitted to the WGSL
+//    function. It is the order computed by ComputeBlockOrder, and stored in
+//    the |FunctionEmiter::block_order_|.
+//    Blocks not in this ordering are ignored by the rest of the algorithm.
+//
+//    Note:
+//     - A block D in the function might not appear in this order because
+//       no block in the order branches to D.
+//     - An unreachable block D might still be in the order because some header
+//       block in the order names D as its continue target, or merge block,
+//       or D is reachable from one of those otherwise-unreachable continue
+//       targets or merge blocks.
+//
+// Terms:
+//    Let Pos(B) be the index position of a block B in the computed block order.
+//
+// CFG intervals and valid nesting:
+//
+//    A correctly structured CFG satisfies nesting rules that we can check by
+//    comparing positions of related blocks.
+//
+//    If header block H is in the block order, then the following holds:
+//
+//      Pos(H) < Pos(M(H))
+//
+//      If CT(H) exists, then:
+//
+//         Pos(H) <= Pos(CT(H))
+//         Pos(CT(H)) < Pos(M)
+//
+//    This gives us the fundamental ordering of blocks in relation to a
+//    structured construct:
+//      The blocks before H in the block order, are not in the construct
+//      The blocks at M(H) or later in the block order, are not in the construct
+//      The blocks in a selection headed at H are in positions [ Pos(H),
+//      Pos(M(H)) ) The blocks in a loop construct headed at H are in positions
+//      [ Pos(H), Pos(CT(H)) ) The blocks in the continue construct for loop
+//      headed at H are in
+//        positions [ Pos(CT(H)), Pos(M(H)) )
+//
+//      Schematically, for a selection construct headed by H, the blocks are in
+//      order from left to right:
+//
+//                 ...a-b-c H d-e-f M(H) n-o-p...
+//
+//           where ...a-b-c: blocks before the selection construct
+//           where H and d-e-f: blocks in the selection construct
+//           where M(H) and n-o-p...: blocks after the selection construct
+//
+//      Schematically, for a loop construct headed by H that is its own
+//      continue construct, the blocks in order from left to right:
+//
+//                 ...a-b-c H=CT(H) d-e-f M(H) n-o-p...
+//
+//           where ...a-b-c: blocks before the loop
+//           where H is the continue construct; CT(H)=H, and the loop construct
+//           is *empty*
+//           where d-e-f... are other blocks in the continue construct
+//           where M(H) and n-o-p...: blocks after the continue construct
+//
+//      Schematically, for a multi-block loop construct headed by H, there are
+//      blocks in order from left to right:
+//
+//                 ...a-b-c H d-e-f CT(H) j-k-l M(H) n-o-p...
+//
+//           where ...a-b-c: blocks before the loop
+//           where H and d-e-f: blocks in the loop construct
+//           where CT(H) and j-k-l: blocks in the continue construct
+//           where M(H) and n-o-p...: blocks after the loop and continue
+//           constructs
+//
+
+namespace tint {
+namespace reader {
+namespace spirv {
+
+namespace {
+
+constexpr uint32_t kMaxVectorLen = 4;
+
+// Gets the AST unary opcode for the given SPIR-V opcode, if any
+// @param opcode SPIR-V opcode
+// @param ast_unary_op return parameter
+// @returns true if it was a unary operation
+bool GetUnaryOp(SpvOp opcode, ast::UnaryOp* ast_unary_op) {
+  switch (opcode) {
+    case SpvOpSNegate:
+    case SpvOpFNegate:
+      *ast_unary_op = ast::UnaryOp::kNegation;
+      return true;
+    case SpvOpLogicalNot:
+      *ast_unary_op = ast::UnaryOp::kNot;
+      return true;
+    case SpvOpNot:
+      *ast_unary_op = ast::UnaryOp::kComplement;
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+/// Converts a SPIR-V opcode for a WGSL builtin function, if there is a
+/// direct translation. Returns nullptr otherwise.
+/// @returns the WGSL builtin function name for the given opcode, or nullptr.
+const char* GetUnaryBuiltInFunctionName(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpAny:
+      return "any";
+    case SpvOpAll:
+      return "all";
+    case SpvOpIsNan:
+      return "isNan";
+    case SpvOpIsInf:
+      return "isInf";
+    case SpvOpTranspose:
+      return "transpose";
+    default:
+      break;
+  }
+  return nullptr;
+}
+
+// Converts a SPIR-V opcode to its corresponding AST binary opcode, if any
+// @param opcode SPIR-V opcode
+// @returns the AST binary op for the given opcode, or kNone
+ast::BinaryOp ConvertBinaryOp(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpIAdd:
+    case SpvOpFAdd:
+      return ast::BinaryOp::kAdd;
+    case SpvOpISub:
+    case SpvOpFSub:
+      return ast::BinaryOp::kSubtract;
+    case SpvOpIMul:
+    case SpvOpFMul:
+    case SpvOpVectorTimesScalar:
+    case SpvOpMatrixTimesScalar:
+    case SpvOpVectorTimesMatrix:
+    case SpvOpMatrixTimesVector:
+    case SpvOpMatrixTimesMatrix:
+      return ast::BinaryOp::kMultiply;
+    case SpvOpUDiv:
+    case SpvOpSDiv:
+    case SpvOpFDiv:
+      return ast::BinaryOp::kDivide;
+    case SpvOpUMod:
+    case SpvOpSMod:
+    case SpvOpFRem:
+      return ast::BinaryOp::kModulo;
+    case SpvOpLogicalEqual:
+    case SpvOpIEqual:
+    case SpvOpFOrdEqual:
+      return ast::BinaryOp::kEqual;
+    case SpvOpLogicalNotEqual:
+    case SpvOpINotEqual:
+    case SpvOpFOrdNotEqual:
+      return ast::BinaryOp::kNotEqual;
+    case SpvOpBitwiseAnd:
+      return ast::BinaryOp::kAnd;
+    case SpvOpBitwiseOr:
+      return ast::BinaryOp::kOr;
+    case SpvOpBitwiseXor:
+      return ast::BinaryOp::kXor;
+    case SpvOpLogicalAnd:
+      return ast::BinaryOp::kAnd;
+    case SpvOpLogicalOr:
+      return ast::BinaryOp::kOr;
+    case SpvOpUGreaterThan:
+    case SpvOpSGreaterThan:
+    case SpvOpFOrdGreaterThan:
+      return ast::BinaryOp::kGreaterThan;
+    case SpvOpUGreaterThanEqual:
+    case SpvOpSGreaterThanEqual:
+    case SpvOpFOrdGreaterThanEqual:
+      return ast::BinaryOp::kGreaterThanEqual;
+    case SpvOpULessThan:
+    case SpvOpSLessThan:
+    case SpvOpFOrdLessThan:
+      return ast::BinaryOp::kLessThan;
+    case SpvOpULessThanEqual:
+    case SpvOpSLessThanEqual:
+    case SpvOpFOrdLessThanEqual:
+      return ast::BinaryOp::kLessThanEqual;
+    default:
+      break;
+  }
+  // It's not clear what OpSMod should map to.
+  // https://bugs.chromium.org/p/tint/issues/detail?id=52
+  return ast::BinaryOp::kNone;
+}
+
+// If the given SPIR-V opcode is a floating point unordered comparison,
+// then returns the binary float comparison for which it is the negation.
+// Othewrise returns BinaryOp::kNone.
+// @param opcode SPIR-V opcode
+// @returns operation corresponding to negated version of the SPIR-V opcode
+ast::BinaryOp NegatedFloatCompare(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpFUnordEqual:
+      return ast::BinaryOp::kNotEqual;
+    case SpvOpFUnordNotEqual:
+      return ast::BinaryOp::kEqual;
+    case SpvOpFUnordLessThan:
+      return ast::BinaryOp::kGreaterThanEqual;
+    case SpvOpFUnordLessThanEqual:
+      return ast::BinaryOp::kGreaterThan;
+    case SpvOpFUnordGreaterThan:
+      return ast::BinaryOp::kLessThanEqual;
+    case SpvOpFUnordGreaterThanEqual:
+      return ast::BinaryOp::kLessThan;
+    default:
+      break;
+  }
+  return ast::BinaryOp::kNone;
+}
+
+// Returns the WGSL standard library function for the given
+// GLSL.std.450 extended instruction operation code.  Unknown
+// and invalid opcodes map to the empty string.
+// @returns the WGSL standard function name, or an empty string.
+std::string GetGlslStd450FuncName(uint32_t ext_opcode) {
+  switch (ext_opcode) {
+    case GLSLstd450FAbs:
+    case GLSLstd450SAbs:
+      return "abs";
+    case GLSLstd450Acos:
+      return "acos";
+    case GLSLstd450Asin:
+      return "asin";
+    case GLSLstd450Atan:
+      return "atan";
+    case GLSLstd450Atan2:
+      return "atan2";
+    case GLSLstd450Ceil:
+      return "ceil";
+    case GLSLstd450UClamp:
+    case GLSLstd450SClamp:
+    case GLSLstd450NClamp:
+    case GLSLstd450FClamp:  // FClamp is less prescriptive about NaN operands
+      return "clamp";
+    case GLSLstd450Cos:
+      return "cos";
+    case GLSLstd450Cosh:
+      return "cosh";
+    case GLSLstd450Cross:
+      return "cross";
+    case GLSLstd450Degrees:
+      return "degrees";
+    case GLSLstd450Distance:
+      return "distance";
+    case GLSLstd450Exp:
+      return "exp";
+    case GLSLstd450Exp2:
+      return "exp2";
+    case GLSLstd450FaceForward:
+      return "faceForward";
+    case GLSLstd450Floor:
+      return "floor";
+    case GLSLstd450Fma:
+      return "fma";
+    case GLSLstd450Fract:
+      return "fract";
+    case GLSLstd450InverseSqrt:
+      return "inverseSqrt";
+    case GLSLstd450Ldexp:
+      return "ldexp";
+    case GLSLstd450Length:
+      return "length";
+    case GLSLstd450Log:
+      return "log";
+    case GLSLstd450Log2:
+      return "log2";
+    case GLSLstd450NMax:
+    case GLSLstd450FMax:  // FMax is less prescriptive about NaN operands
+    case GLSLstd450UMax:
+    case GLSLstd450SMax:
+      return "max";
+    case GLSLstd450NMin:
+    case GLSLstd450FMin:  // FMin is less prescriptive about NaN operands
+    case GLSLstd450UMin:
+    case GLSLstd450SMin:
+      return "min";
+    case GLSLstd450FMix:
+      return "mix";
+    case GLSLstd450Normalize:
+      return "normalize";
+    case GLSLstd450PackSnorm4x8:
+      return "pack4x8snorm";
+    case GLSLstd450PackUnorm4x8:
+      return "pack4x8unorm";
+    case GLSLstd450PackSnorm2x16:
+      return "pack2x16snorm";
+    case GLSLstd450PackUnorm2x16:
+      return "pack2x16unorm";
+    case GLSLstd450PackHalf2x16:
+      return "pack2x16float";
+    case GLSLstd450Pow:
+      return "pow";
+    case GLSLstd450FSign:
+      return "sign";
+    case GLSLstd450Radians:
+      return "radians";
+    case GLSLstd450Reflect:
+      return "reflect";
+    case GLSLstd450Refract:
+      return "refract";
+    case GLSLstd450Round:
+    case GLSLstd450RoundEven:
+      return "round";
+    case GLSLstd450Sin:
+      return "sin";
+    case GLSLstd450Sinh:
+      return "sinh";
+    case GLSLstd450SmoothStep:
+      return "smoothStep";
+    case GLSLstd450Sqrt:
+      return "sqrt";
+    case GLSLstd450Step:
+      return "step";
+    case GLSLstd450Tan:
+      return "tan";
+    case GLSLstd450Tanh:
+      return "tanh";
+    case GLSLstd450Trunc:
+      return "trunc";
+    case GLSLstd450UnpackSnorm4x8:
+      return "unpack4x8snorm";
+    case GLSLstd450UnpackUnorm4x8:
+      return "unpack4x8unorm";
+    case GLSLstd450UnpackSnorm2x16:
+      return "unpack2x16snorm";
+    case GLSLstd450UnpackUnorm2x16:
+      return "unpack2x16unorm";
+    case GLSLstd450UnpackHalf2x16:
+      return "unpack2x16float";
+
+    default:
+      // TODO(dneto) - The following are not implemented.
+      // They are grouped semantically, as in GLSL.std.450.h.
+
+    case GLSLstd450SSign:
+
+    case GLSLstd450Asinh:
+    case GLSLstd450Acosh:
+    case GLSLstd450Atanh:
+
+    case GLSLstd450Determinant:
+    case GLSLstd450MatrixInverse:
+
+    case GLSLstd450Modf:
+    case GLSLstd450ModfStruct:
+    case GLSLstd450IMix:
+
+    case GLSLstd450Frexp:
+    case GLSLstd450FrexpStruct:
+
+    case GLSLstd450PackDouble2x32:
+    case GLSLstd450UnpackDouble2x32:
+
+    case GLSLstd450FindILsb:
+    case GLSLstd450FindSMsb:
+    case GLSLstd450FindUMsb:
+
+    case GLSLstd450InterpolateAtCentroid:
+    case GLSLstd450InterpolateAtSample:
+    case GLSLstd450InterpolateAtOffset:
+      break;
+  }
+  return "";
+}
+
+// Returns the WGSL standard library function builtin for the
+// given instruction, or sem::BuiltinType::kNone
+sem::BuiltinType GetBuiltin(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpBitCount:
+      return sem::BuiltinType::kCountOneBits;
+    case SpvOpBitFieldInsert:
+      return sem::BuiltinType::kInsertBits;
+    case SpvOpBitFieldSExtract:
+    case SpvOpBitFieldUExtract:
+      return sem::BuiltinType::kExtractBits;
+    case SpvOpBitReverse:
+      return sem::BuiltinType::kReverseBits;
+    case SpvOpDot:
+      return sem::BuiltinType::kDot;
+    case SpvOpDPdx:
+      return sem::BuiltinType::kDpdx;
+    case SpvOpDPdy:
+      return sem::BuiltinType::kDpdy;
+    case SpvOpFwidth:
+      return sem::BuiltinType::kFwidth;
+    case SpvOpDPdxFine:
+      return sem::BuiltinType::kDpdxFine;
+    case SpvOpDPdyFine:
+      return sem::BuiltinType::kDpdyFine;
+    case SpvOpFwidthFine:
+      return sem::BuiltinType::kFwidthFine;
+    case SpvOpDPdxCoarse:
+      return sem::BuiltinType::kDpdxCoarse;
+    case SpvOpDPdyCoarse:
+      return sem::BuiltinType::kDpdyCoarse;
+    case SpvOpFwidthCoarse:
+      return sem::BuiltinType::kFwidthCoarse;
+    default:
+      break;
+  }
+  return sem::BuiltinType::kNone;
+}
+
+// @param opcode a SPIR-V opcode
+// @returns true if the given instruction is an image access instruction
+// whose first input operand is an OpSampledImage value.
+bool IsSampledImageAccess(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpImageSampleImplicitLod:
+    case SpvOpImageSampleExplicitLod:
+    case SpvOpImageSampleDrefImplicitLod:
+    case SpvOpImageSampleDrefExplicitLod:
+    // WGSL doesn't have *Proj* texturing; spirv reader emulates it.
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
+    case SpvOpImageGather:
+    case SpvOpImageDrefGather:
+    case SpvOpImageQueryLod:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+// @param opcode a SPIR-V opcode
+// @returns true if the given instruction is an image sampling, gather,
+// or gather-compare operation.
+bool IsImageSamplingOrGatherOrDrefGather(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpImageSampleImplicitLod:
+    case SpvOpImageSampleExplicitLod:
+    case SpvOpImageSampleDrefImplicitLod:
+    case SpvOpImageSampleDrefExplicitLod:
+      // WGSL doesn't have *Proj* texturing; spirv reader emulates it.
+    case SpvOpImageSampleProjImplicitLod:
+    case SpvOpImageSampleProjExplicitLod:
+    case SpvOpImageSampleProjDrefImplicitLod:
+    case SpvOpImageSampleProjDrefExplicitLod:
+    case SpvOpImageGather:
+    case SpvOpImageDrefGather:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+// @param opcode a SPIR-V opcode
+// @returns true if the given instruction is an image access instruction
+// whose first input operand is an OpImage value.
+bool IsRawImageAccess(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpImageRead:
+    case SpvOpImageWrite:
+    case SpvOpImageFetch:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+// @param opcode a SPIR-V opcode
+// @returns true if the given instruction is an image query instruction
+bool IsImageQuery(SpvOp opcode) {
+  switch (opcode) {
+    case SpvOpImageQuerySize:
+    case SpvOpImageQuerySizeLod:
+    case SpvOpImageQueryLevels:
+    case SpvOpImageQuerySamples:
+    case SpvOpImageQueryLod:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
+// @returns the merge block ID for the given basic block, or 0 if there is none.
+uint32_t MergeFor(const spvtools::opt::BasicBlock& bb) {
+  // Get the OpSelectionMerge or OpLoopMerge instruction, if any.
+  auto* inst = bb.GetMergeInst();
+  return inst == nullptr ? 0 : inst->GetSingleWordInOperand(0);
+}
+
+// @returns the continue target ID for the given basic block, or 0 if there
+// is none.
+uint32_t ContinueTargetFor(const spvtools::opt::BasicBlock& bb) {
+  // Get the OpLoopMerge instruction, if any.
+  auto* inst = bb.GetLoopMergeInst();
+  return inst == nullptr ? 0 : inst->GetSingleWordInOperand(1);
+}
+
+// A structured traverser produces the reverse structured post-order of the
+// CFG of a function.  The blocks traversed are the transitive closure (minimum
+// fixed point) of:
+//  - the entry block
+//  - a block reached by a branch from another block in the set
+//  - a block mentioned as a merge block or continue target for a block in the
+//  set
+class StructuredTraverser {
+ public:
+  explicit StructuredTraverser(const spvtools::opt::Function& function)
+      : function_(function) {
+    for (auto& block : function_) {
+      id_to_block_[block.id()] = &block;
+    }
+  }
+
+  // Returns the reverse postorder traversal of the CFG, where:
+  //  - a merge block always follows its associated constructs
+  //  - a continue target always follows the associated loop construct, if any
+  // @returns the IDs of blocks in reverse structured post order
+  std::vector<uint32_t> ReverseStructuredPostOrder() {
+    visit_order_.clear();
+    visited_.clear();
+    VisitBackward(function_.entry()->id());
+
+    std::vector<uint32_t> order(visit_order_.rbegin(), visit_order_.rend());
+    return order;
+  }
+
+ private:
+  // Executes a depth first search of the CFG, where right after we visit a
+  // header, we will visit its merge block, then its continue target (if any).
+  // Also records the post order ordering.
+  void VisitBackward(uint32_t id) {
+    if (id == 0)
+      return;
+    if (visited_.count(id))
+      return;
+    visited_.insert(id);
+
+    const spvtools::opt::BasicBlock* bb =
+        id_to_block_[id];  // non-null for valid modules
+    VisitBackward(MergeFor(*bb));
+    VisitBackward(ContinueTargetFor(*bb));
+
+    // Visit successors. We will naturally skip the continue target and merge
+    // blocks.
+    auto* terminator = bb->terminator();
+    auto opcode = terminator->opcode();
+    if (opcode == SpvOpBranchConditional) {
+      // Visit the false branch, then the true branch, to make them come
+      // out in the natural order for an "if".
+      VisitBackward(terminator->GetSingleWordInOperand(2));
+      VisitBackward(terminator->GetSingleWordInOperand(1));
+    } else if (opcode == SpvOpBranch) {
+      VisitBackward(terminator->GetSingleWordInOperand(0));
+    } else if (opcode == SpvOpSwitch) {
+      // TODO(dneto): Consider visiting the labels in literal-value order.
+      std::vector<uint32_t> successors;
+      bb->ForEachSuccessorLabel([&successors](const uint32_t succ_id) {
+        successors.push_back(succ_id);
+      });
+      for (auto succ_id : successors) {
+        VisitBackward(succ_id);
+      }
+    }
+
+    visit_order_.push_back(id);
+  }
+
+  const spvtools::opt::Function& function_;
+  std::unordered_map<uint32_t, const spvtools::opt::BasicBlock*> id_to_block_;
+  std::vector<uint32_t> visit_order_;
+  std::unordered_set<uint32_t> visited_;
+};
+
+/// A StatementBuilder for ast::SwitchStatement
+/// @see StatementBuilder
+struct SwitchStatementBuilder final
+    : public Castable<SwitchStatementBuilder, StatementBuilder> {
+  /// Constructor
+  /// @param cond the switch statement condition
+  explicit SwitchStatementBuilder(const ast::Expression* cond)
+      : condition(cond) {}
+
+  /// @param builder the program builder
+  /// @returns the built ast::SwitchStatement
+  const ast::SwitchStatement* Build(ProgramBuilder* builder) const override {
+    // We've listed cases in reverse order in the switch statement.
+    // Reorder them to match the presentation order in WGSL.
+    auto reversed_cases = cases;
+    std::reverse(reversed_cases.begin(), reversed_cases.end());
+
+    return builder->create<ast::SwitchStatement>(Source{}, condition,
+                                                 reversed_cases);
+  }
+
+  /// Switch statement condition
+  const ast::Expression* const condition;
+  /// Switch statement cases
+  ast::CaseStatementList cases;
+};
+
+/// A StatementBuilder for ast::IfStatement
+/// @see StatementBuilder
+struct IfStatementBuilder final
+    : public Castable<IfStatementBuilder, StatementBuilder> {
+  /// Constructor
+  /// @param c the if-statement condition
+  explicit IfStatementBuilder(const ast::Expression* c) : cond(c) {}
+
+  /// @param builder the program builder
+  /// @returns the built ast::IfStatement
+  const ast::IfStatement* Build(ProgramBuilder* builder) const override {
+    return builder->create<ast::IfStatement>(Source{}, cond, body, else_stmts);
+  }
+
+  /// If-statement condition
+  const ast::Expression* const cond;
+  /// If-statement block body
+  const ast::BlockStatement* body = nullptr;
+  /// Optional if-statement else statements
+  ast::ElseStatementList else_stmts;
+};
+
+/// A StatementBuilder for ast::LoopStatement
+/// @see StatementBuilder
+struct LoopStatementBuilder final
+    : public Castable<LoopStatementBuilder, StatementBuilder> {
+  /// @param builder the program builder
+  /// @returns the built ast::LoopStatement
+  ast::LoopStatement* Build(ProgramBuilder* builder) const override {
+    return builder->create<ast::LoopStatement>(Source{}, body, continuing);
+  }
+
+  /// Loop-statement block body
+  const ast::BlockStatement* body = nullptr;
+  /// Loop-statement continuing body
+  /// @note the mutable keyword here is required as all non-StatementBuilders
+  /// `ast::Node`s are immutable and are referenced with `const` pointers.
+  /// StatementBuilders however exist to provide mutable state while the
+  /// FunctionEmitter is building the function. All StatementBuilders are
+  /// replaced with immutable AST nodes when Finalize() is called.
+  mutable const ast::BlockStatement* continuing = nullptr;
+};
+
+/// @param decos a list of parsed decorations
+/// @returns true if the decorations include a SampleMask builtin
+bool HasBuiltinSampleMask(const ast::AttributeList& decos) {
+  if (auto* builtin = ast::GetAttribute<ast::BuiltinAttribute>(decos)) {
+    return builtin->builtin == ast::Builtin::kSampleMask;
+  }
+  return false;
+}
+
+}  // namespace
+
+BlockInfo::BlockInfo(const spvtools::opt::BasicBlock& bb)
+    : basic_block(&bb), id(bb.id()) {}
+
+BlockInfo::~BlockInfo() = default;
+
+DefInfo::DefInfo(const spvtools::opt::Instruction& def_inst,
+                 uint32_t the_block_pos,
+                 size_t the_index)
+    : inst(def_inst), block_pos(the_block_pos), index(the_index) {}
+
+DefInfo::~DefInfo() = default;
+
+ast::Node* StatementBuilder::Clone(CloneContext*) const {
+  return nullptr;
+}
+
+FunctionEmitter::FunctionEmitter(ParserImpl* pi,
+                                 const spvtools::opt::Function& function,
+                                 const EntryPointInfo* ep_info)
+    : parser_impl_(*pi),
+      ty_(pi->type_manager()),
+      builder_(pi->builder()),
+      ir_context_(*(pi->ir_context())),
+      def_use_mgr_(ir_context_.get_def_use_mgr()),
+      constant_mgr_(ir_context_.get_constant_mgr()),
+      type_mgr_(ir_context_.get_type_mgr()),
+      fail_stream_(pi->fail_stream()),
+      namer_(pi->namer()),
+      function_(function),
+      sample_mask_in_id(0u),
+      sample_mask_out_id(0u),
+      ep_info_(ep_info) {
+  PushNewStatementBlock(nullptr, 0, nullptr);
+}
+
+FunctionEmitter::FunctionEmitter(ParserImpl* pi,
+                                 const spvtools::opt::Function& function)
+    : FunctionEmitter(pi, function, nullptr) {}
+
+FunctionEmitter::FunctionEmitter(FunctionEmitter&& other)
+    : parser_impl_(other.parser_impl_),
+      ty_(other.ty_),
+      builder_(other.builder_),
+      ir_context_(other.ir_context_),
+      def_use_mgr_(ir_context_.get_def_use_mgr()),
+      constant_mgr_(ir_context_.get_constant_mgr()),
+      type_mgr_(ir_context_.get_type_mgr()),
+      fail_stream_(other.fail_stream_),
+      namer_(other.namer_),
+      function_(other.function_),
+      sample_mask_in_id(other.sample_mask_out_id),
+      sample_mask_out_id(other.sample_mask_in_id),
+      ep_info_(other.ep_info_) {
+  other.statements_stack_.clear();
+  PushNewStatementBlock(nullptr, 0, nullptr);
+}
+
+FunctionEmitter::~FunctionEmitter() = default;
+
+FunctionEmitter::StatementBlock::StatementBlock(
+    const Construct* construct,
+    uint32_t end_id,
+    FunctionEmitter::CompletionAction completion_action)
+    : construct_(construct),
+      end_id_(end_id),
+      completion_action_(completion_action) {}
+
+FunctionEmitter::StatementBlock::StatementBlock(StatementBlock&& other) =
+    default;
+
+FunctionEmitter::StatementBlock::~StatementBlock() = default;
+
+void FunctionEmitter::StatementBlock::Finalize(ProgramBuilder* pb) {
+  TINT_ASSERT(Reader, !finalized_ /* Finalize() must only be called once */);
+
+  for (size_t i = 0; i < statements_.size(); i++) {
+    if (auto* sb = statements_[i]->As<StatementBuilder>()) {
+      statements_[i] = sb->Build(pb);
+    }
+  }
+
+  if (completion_action_ != nullptr) {
+    completion_action_(statements_);
+  }
+
+  finalized_ = true;
+}
+
+void FunctionEmitter::StatementBlock::Add(const ast::Statement* statement) {
+  TINT_ASSERT(Reader,
+              !finalized_ /* Add() must not be called after Finalize() */);
+  statements_.emplace_back(statement);
+}
+
+void FunctionEmitter::PushNewStatementBlock(const Construct* construct,
+                                            uint32_t end_id,
+                                            CompletionAction action) {
+  statements_stack_.emplace_back(StatementBlock{construct, end_id, action});
+}
+
+void FunctionEmitter::PushGuard(const std::string& guard_name,
+                                uint32_t end_id) {
+  TINT_ASSERT(Reader, !statements_stack_.empty());
+  TINT_ASSERT(Reader, !guard_name.empty());
+  // Guard control flow by the guard variable.  Introduce a new
+  // if-selection with a then-clause ending at the same block
+  // as the statement block at the top of the stack.
+  const auto& top = statements_stack_.back();
+
+  auto* cond = create<ast::IdentifierExpression>(
+      Source{}, builder_.Symbols().Register(guard_name));
+  auto* builder = AddStatementBuilder<IfStatementBuilder>(cond);
+
+  PushNewStatementBlock(
+      top.GetConstruct(), end_id, [=](const ast::StatementList& stmts) {
+        builder->body = create<ast::BlockStatement>(Source{}, stmts);
+      });
+}
+
+void FunctionEmitter::PushTrueGuard(uint32_t end_id) {
+  TINT_ASSERT(Reader, !statements_stack_.empty());
+  const auto& top = statements_stack_.back();
+
+  auto* cond = MakeTrue(Source{});
+  auto* builder = AddStatementBuilder<IfStatementBuilder>(cond);
+
+  PushNewStatementBlock(
+      top.GetConstruct(), end_id, [=](const ast::StatementList& stmts) {
+        builder->body = create<ast::BlockStatement>(Source{}, stmts);
+      });
+}
+
+const ast::StatementList FunctionEmitter::ast_body() {
+  TINT_ASSERT(Reader, !statements_stack_.empty());
+  auto& entry = statements_stack_[0];
+  entry.Finalize(&builder_);
+  return entry.GetStatements();
+}
+
+const ast::Statement* FunctionEmitter::AddStatement(
+    const ast::Statement* statement) {
+  TINT_ASSERT(Reader, !statements_stack_.empty());
+  if (statement != nullptr) {
+    statements_stack_.back().Add(statement);
+  }
+  return statement;
+}
+
+const ast::Statement* FunctionEmitter::LastStatement() {
+  TINT_ASSERT(Reader, !statements_stack_.empty());
+  auto& statement_list = statements_stack_.back().GetStatements();
+  TINT_ASSERT(Reader, !statement_list.empty());
+  return statement_list.back();
+}
+
+bool FunctionEmitter::Emit() {
+  if (failed()) {
+    return false;
+  }
+  // We only care about functions with bodies.
+  if (function_.cbegin() == function_.cend()) {
+    return true;
+  }
+
+  // The function declaration, corresponding to how it's written in SPIR-V,
+  // and without regard to whether it's an entry point.
+  FunctionDeclaration decl;
+  if (!ParseFunctionDeclaration(&decl)) {
+    return false;
+  }
+
+  bool make_body_function = true;
+  if (ep_info_) {
+    TINT_ASSERT(Reader, !ep_info_->inner_name.empty());
+    if (ep_info_->owns_inner_implementation) {
+      // This is an entry point, and we want to emit it as a wrapper around
+      // an implementation function.
+      decl.name = ep_info_->inner_name;
+    } else {
+      // This is a second entry point that shares an inner implementation
+      // function.
+      make_body_function = false;
+    }
+  }
+
+  if (make_body_function) {
+    auto* body = MakeFunctionBody();
+    if (!body) {
+      return false;
+    }
+
+    builder_.AST().AddFunction(create<ast::Function>(
+        decl.source, builder_.Symbols().Register(decl.name),
+        std::move(decl.params), decl.return_type->Build(builder_), body,
+        std::move(decl.attributes), ast::AttributeList{}));
+  }
+
+  if (ep_info_ && !ep_info_->inner_name.empty()) {
+    return EmitEntryPointAsWrapper();
+  }
+
+  return success();
+}
+
+const ast::BlockStatement* FunctionEmitter::MakeFunctionBody() {
+  TINT_ASSERT(Reader, statements_stack_.size() == 1);
+
+  if (!EmitBody()) {
+    return nullptr;
+  }
+
+  // Set the body of the AST function node.
+  if (statements_stack_.size() != 1) {
+    Fail() << "internal error: statement-list stack should have 1 "
+              "element but has "
+           << statements_stack_.size();
+    return nullptr;
+  }
+
+  statements_stack_[0].Finalize(&builder_);
+  auto& statements = statements_stack_[0].GetStatements();
+  auto* body = create<ast::BlockStatement>(Source{}, statements);
+
+  // Maintain the invariant by repopulating the one and only element.
+  statements_stack_.clear();
+  PushNewStatementBlock(constructs_[0].get(), 0, nullptr);
+
+  return body;
+}
+
+bool FunctionEmitter::EmitPipelineInput(std::string var_name,
+                                        const Type* var_type,
+                                        ast::AttributeList* attrs,
+                                        std::vector<int> index_prefix,
+                                        const Type* tip_type,
+                                        const Type* forced_param_type,
+                                        ast::VariableList* params,
+                                        ast::StatementList* statements) {
+  // TODO(dneto): Handle structs where the locations are annotated on members.
+  tip_type = tip_type->UnwrapAlias();
+  if (auto* ref_type = tip_type->As<Reference>()) {
+    tip_type = ref_type->type;
+  }
+
+  // Recursively flatten matrices, arrays, and structures.
+  return Switch(
+      tip_type,
+      [&](const Matrix* matrix_type) -> bool {
+        index_prefix.push_back(0);
+        const auto num_columns = static_cast<int>(matrix_type->columns);
+        const Type* vec_ty = ty_.Vector(matrix_type->type, matrix_type->rows);
+        for (int col = 0; col < num_columns; col++) {
+          index_prefix.back() = col;
+          if (!EmitPipelineInput(var_name, var_type, attrs, index_prefix,
+                                 vec_ty, forced_param_type, params,
+                                 statements)) {
+            return false;
+          }
+        }
+        return success();
+      },
+      [&](const Array* array_type) -> bool {
+        if (array_type->size == 0) {
+          return Fail() << "runtime-size array not allowed on pipeline IO";
+        }
+        index_prefix.push_back(0);
+        const Type* elem_ty = array_type->type;
+        for (int i = 0; i < static_cast<int>(array_type->size); i++) {
+          index_prefix.back() = i;
+          if (!EmitPipelineInput(var_name, var_type, attrs, index_prefix,
+                                 elem_ty, forced_param_type, params,
+                                 statements)) {
+            return false;
+          }
+        }
+        return success();
+      },
+      [&](const Struct* struct_type) -> bool {
+        const auto& members = struct_type->members;
+        index_prefix.push_back(0);
+        for (int i = 0; i < static_cast<int>(members.size()); ++i) {
+          index_prefix.back() = i;
+          ast::AttributeList member_attrs(*attrs);
+          if (!parser_impl_.ConvertPipelineDecorations(
+                  struct_type,
+                  parser_impl_.GetMemberPipelineDecorations(*struct_type, i),
+                  &member_attrs)) {
+            return false;
+          }
+          if (!EmitPipelineInput(var_name, var_type, &member_attrs,
+                                 index_prefix, members[i], forced_param_type,
+                                 params, statements)) {
+            return false;
+          }
+          // Copy the location as updated by nested expansion of the member.
+          parser_impl_.SetLocation(attrs, GetLocation(member_attrs));
+        }
+        return success();
+      },
+      [&](Default) {
+        const bool is_builtin =
+            ast::HasAttribute<ast::BuiltinAttribute>(*attrs);
+
+        const Type* param_type = is_builtin ? forced_param_type : tip_type;
+
+        const auto param_name = namer_.MakeDerivedName(var_name + "_param");
+        // Create the parameter.
+        // TODO(dneto): Note: If the parameter has non-location decorations,
+        // then those decoration AST nodes will be reused between multiple
+        // elements of a matrix, array, or structure.  Normally that's
+        // disallowed but currently the SPIR-V reader will make duplicates when
+        // the entire AST is cloned at the top level of the SPIR-V reader flow.
+        // Consider rewriting this to avoid this node-sharing.
+        params->push_back(
+            builder_.Param(param_name, param_type->Build(builder_), *attrs));
+
+        // Add a body statement to copy the parameter to the corresponding
+        // private variable.
+        const ast::Expression* param_value = builder_.Expr(param_name);
+        const ast::Expression* store_dest = builder_.Expr(var_name);
+
+        // Index into the LHS as needed.
+        auto* current_type =
+            var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
+        for (auto index : index_prefix) {
+          Switch(
+              current_type,
+              [&](const Matrix* matrix_type) {
+                store_dest =
+                    builder_.IndexAccessor(store_dest, builder_.Expr(index));
+                current_type = ty_.Vector(matrix_type->type, matrix_type->rows);
+              },
+              [&](const Array* array_type) {
+                store_dest =
+                    builder_.IndexAccessor(store_dest, builder_.Expr(index));
+                current_type = array_type->type->UnwrapAlias();
+              },
+              [&](const Struct* struct_type) {
+                store_dest = builder_.MemberAccessor(
+                    store_dest, builder_.Expr(parser_impl_.GetMemberName(
+                                    *struct_type, index)));
+                current_type = struct_type->members[index];
+              });
+        }
+
+        if (is_builtin && (tip_type != forced_param_type)) {
+          // The parameter will have the WGSL type, but we need bitcast to
+          // the variable store type.
+          param_value = create<ast::BitcastExpression>(
+              tip_type->Build(builder_), param_value);
+        }
+
+        statements->push_back(builder_.Assign(store_dest, param_value));
+
+        // Increment the location attribute, in case more parameters will
+        // follow.
+        IncrementLocation(attrs);
+
+        return success();
+      });
+}
+
+void FunctionEmitter::IncrementLocation(ast::AttributeList* attributes) {
+  for (auto*& attr : *attributes) {
+    if (auto* loc_attr = attr->As<ast::LocationAttribute>()) {
+      // Replace this location attribute with a new one with one higher index.
+      // The old one doesn't leak because it's kept in the builder's AST node
+      // list.
+      attr = builder_.Location(loc_attr->source, loc_attr->value + 1);
+    }
+  }
+}
+
+const ast::Attribute* FunctionEmitter::GetLocation(
+    const ast::AttributeList& attributes) {
+  for (auto* const& attr : attributes) {
+    if (attr->Is<ast::LocationAttribute>()) {
+      return attr;
+    }
+  }
+  return nullptr;
+}
+
+bool FunctionEmitter::EmitPipelineOutput(std::string var_name,
+                                         const Type* var_type,
+                                         ast::AttributeList* decos,
+                                         std::vector<int> index_prefix,
+                                         const Type* tip_type,
+                                         const Type* forced_member_type,
+                                         ast::StructMemberList* return_members,
+                                         ast::ExpressionList* return_exprs) {
+  tip_type = tip_type->UnwrapAlias();
+  if (auto* ref_type = tip_type->As<Reference>()) {
+    tip_type = ref_type->type;
+  }
+
+  // Recursively flatten matrices, arrays, and structures.
+  return Switch(
+      tip_type,
+      [&](const Matrix* matrix_type) {
+        index_prefix.push_back(0);
+        const auto num_columns = static_cast<int>(matrix_type->columns);
+        const Type* vec_ty = ty_.Vector(matrix_type->type, matrix_type->rows);
+        for (int col = 0; col < num_columns; col++) {
+          index_prefix.back() = col;
+          if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix,
+                                  vec_ty, forced_member_type, return_members,
+                                  return_exprs)) {
+            return false;
+          }
+        }
+        return success();
+      },
+      [&](const Array* array_type) -> bool {
+        if (array_type->size == 0) {
+          return Fail() << "runtime-size array not allowed on pipeline IO";
+        }
+        index_prefix.push_back(0);
+        const Type* elem_ty = array_type->type;
+        for (int i = 0; i < static_cast<int>(array_type->size); i++) {
+          index_prefix.back() = i;
+          if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix,
+                                  elem_ty, forced_member_type, return_members,
+                                  return_exprs)) {
+            return false;
+          }
+        }
+        return success();
+      },
+      [&](const Struct* struct_type) -> bool {
+        const auto& members = struct_type->members;
+        index_prefix.push_back(0);
+        for (int i = 0; i < static_cast<int>(members.size()); ++i) {
+          index_prefix.back() = i;
+          ast::AttributeList member_attrs(*decos);
+          if (!parser_impl_.ConvertPipelineDecorations(
+                  struct_type,
+                  parser_impl_.GetMemberPipelineDecorations(*struct_type, i),
+                  &member_attrs)) {
+            return false;
+          }
+          if (!EmitPipelineOutput(var_name, var_type, &member_attrs,
+                                  index_prefix, members[i], forced_member_type,
+                                  return_members, return_exprs)) {
+            return false;
+          }
+          // Copy the location as updated by nested expansion of the member.
+          parser_impl_.SetLocation(decos, GetLocation(member_attrs));
+        }
+        return success();
+      },
+      [&](Default) {
+        const bool is_builtin =
+            ast::HasAttribute<ast::BuiltinAttribute>(*decos);
+
+        const Type* member_type = is_builtin ? forced_member_type : tip_type;
+        // Derive the member name directly from the variable name.  They can't
+        // collide.
+        const auto member_name = namer_.MakeDerivedName(var_name);
+        // Create the member.
+        // TODO(dneto): Note: If the parameter has non-location decorations,
+        // then those decoration AST nodes  will be reused between multiple
+        // elements of a matrix, array, or structure.  Normally that's
+        // disallowed but currently the SPIR-V reader will make duplicates when
+        // the entire AST is cloned at the top level of the SPIR-V reader flow.
+        // Consider rewriting this to avoid this node-sharing.
+        return_members->push_back(
+            builder_.Member(member_name, member_type->Build(builder_), *decos));
+
+        // Create an expression to evaluate the part of the variable indexed by
+        // the index_prefix.
+        const ast::Expression* load_source = builder_.Expr(var_name);
+
+        // Index into the variable as needed to pick out the flattened member.
+        auto* current_type =
+            var_type->UnwrapAlias()->UnwrapRef()->UnwrapAlias();
+        for (auto index : index_prefix) {
+          Switch(
+              current_type,
+              [&](const Matrix* matrix_type) {
+                load_source =
+                    builder_.IndexAccessor(load_source, builder_.Expr(index));
+                current_type = ty_.Vector(matrix_type->type, matrix_type->rows);
+              },
+              [&](const Array* array_type) {
+                load_source =
+                    builder_.IndexAccessor(load_source, builder_.Expr(index));
+                current_type = array_type->type->UnwrapAlias();
+              },
+              [&](const Struct* struct_type) {
+                load_source = builder_.MemberAccessor(
+                    load_source, builder_.Expr(parser_impl_.GetMemberName(
+                                     *struct_type, index)));
+                current_type = struct_type->members[index];
+              });
+        }
+
+        if (is_builtin && (tip_type != forced_member_type)) {
+          // The member will have the WGSL type, but we need bitcast to
+          // the variable store type.
+          load_source = create<ast::BitcastExpression>(
+              forced_member_type->Build(builder_), load_source);
+        }
+        return_exprs->push_back(load_source);
+
+        // Increment the location attribute, in case more parameters will
+        // follow.
+        IncrementLocation(decos);
+
+        return success();
+      });
+}
+
+bool FunctionEmitter::EmitEntryPointAsWrapper() {
+  Source source;
+
+  // The statements in the body.
+  ast::StatementList stmts;
+
+  FunctionDeclaration decl;
+  decl.source = source;
+  decl.name = ep_info_->name;
+  const ast::Type* return_type = nullptr;  // Populated below.
+
+  // Pipeline inputs become parameters to the wrapper function, and
+  // their values are saved into the corresponding private variables that
+  // have already been created.
+  for (uint32_t var_id : ep_info_->inputs) {
+    const auto* var = def_use_mgr_->GetDef(var_id);
+    TINT_ASSERT(Reader, var != nullptr);
+    TINT_ASSERT(Reader, var->opcode() == SpvOpVariable);
+    auto* store_type = GetVariableStoreType(*var);
+    auto* forced_param_type = store_type;
+    ast::AttributeList param_decos;
+    if (!parser_impl_.ConvertDecorationsForVariable(var_id, &forced_param_type,
+                                                    &param_decos, true)) {
+      // This occurs, and is not an error, for the PointSize builtin.
+      if (!success()) {
+        // But exit early if an error was logged.
+        return false;
+      }
+      continue;
+    }
+
+    // We don't have to handle initializers because in Vulkan SPIR-V, Input
+    // variables must not have them.
+
+    const auto var_name = namer_.GetName(var_id);
+
+    bool ok = true;
+    if (HasBuiltinSampleMask(param_decos)) {
+      // In Vulkan SPIR-V, the sample mask is an array. In WGSL it's a scalar.
+      // Use the first element only.
+      auto* sample_mask_array_type =
+          store_type->UnwrapRef()->UnwrapAlias()->As<Array>();
+      TINT_ASSERT(Reader, sample_mask_array_type);
+      ok = EmitPipelineInput(var_name, store_type, &param_decos, {0},
+                             sample_mask_array_type->type, forced_param_type,
+                             &(decl.params), &stmts);
+    } else {
+      // The normal path.
+      ok = EmitPipelineInput(var_name, store_type, &param_decos, {}, store_type,
+                             forced_param_type, &(decl.params), &stmts);
+    }
+    if (!ok) {
+      return false;
+    }
+  }
+
+  // Call the inner function.  It has no parameters.
+  stmts.push_back(create<ast::CallStatement>(
+      source,
+      create<ast::CallExpression>(
+          source,
+          create<ast::IdentifierExpression>(
+              source, builder_.Symbols().Register(ep_info_->inner_name)),
+          ast::ExpressionList{})));
+
+  // Pipeline outputs are mapped to the return value.
+  if (ep_info_->outputs.empty()) {
+    // There is nothing to return.
+    return_type = ty_.Void()->Build(builder_);
+  } else {
+    // Pipeline outputs are converted to a structure that is written
+    // to just before returning.
+
+    const auto return_struct_name =
+        namer_.MakeDerivedName(ep_info_->name + "_out");
+    const auto return_struct_sym =
+        builder_.Symbols().Register(return_struct_name);
+
+    // Define the structure.
+    std::vector<const ast::StructMember*> return_members;
+    ast::ExpressionList return_exprs;
+
+    const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
+
+    for (uint32_t var_id : ep_info_->outputs) {
+      if (var_id == builtin_position_info.per_vertex_var_id) {
+        // The SPIR-V gl_PerVertex variable has already been remapped to
+        // a gl_Position variable.  Substitute the type.
+        const Type* param_type = ty_.Vector(ty_.F32(), 4);
+        ast::AttributeList out_decos{
+            create<ast::BuiltinAttribute>(source, ast::Builtin::kPosition)};
+
+        const auto var_name = namer_.GetName(var_id);
+        return_members.push_back(
+            builder_.Member(var_name, param_type->Build(builder_), out_decos));
+        return_exprs.push_back(builder_.Expr(var_name));
+
+      } else {
+        const auto* var = def_use_mgr_->GetDef(var_id);
+        TINT_ASSERT(Reader, var != nullptr);
+        TINT_ASSERT(Reader, var->opcode() == SpvOpVariable);
+        const Type* store_type = GetVariableStoreType(*var);
+        const Type* forced_member_type = store_type;
+        ast::AttributeList out_decos;
+        if (!parser_impl_.ConvertDecorationsForVariable(
+                var_id, &forced_member_type, &out_decos, true)) {
+          // This occurs, and is not an error, for the PointSize builtin.
+          if (!success()) {
+            // But exit early if an error was logged.
+            return false;
+          }
+          continue;
+        }
+
+        const auto var_name = namer_.GetName(var_id);
+        bool ok = true;
+        if (HasBuiltinSampleMask(out_decos)) {
+          // In Vulkan SPIR-V, the sample mask is an array. In WGSL it's a
+          // scalar. Use the first element only.
+          auto* sample_mask_array_type =
+              store_type->UnwrapRef()->UnwrapAlias()->As<Array>();
+          TINT_ASSERT(Reader, sample_mask_array_type);
+          ok = EmitPipelineOutput(var_name, store_type, &out_decos, {0},
+                                  sample_mask_array_type->type,
+                                  forced_member_type, &return_members,
+                                  &return_exprs);
+        } else {
+          // The normal path.
+          ok = EmitPipelineOutput(var_name, store_type, &out_decos, {},
+                                  store_type, forced_member_type,
+                                  &return_members, &return_exprs);
+        }
+        if (!ok) {
+          return false;
+        }
+      }
+    }
+
+    if (return_members.empty()) {
+      // This can occur if only the PointSize member is accessed, because we
+      // never emit it.
+      return_type = ty_.Void()->Build(builder_);
+    } else {
+      // Create and register the result type.
+      auto* str = create<ast::Struct>(Source{}, return_struct_sym,
+                                      return_members, ast::AttributeList{});
+      parser_impl_.AddTypeDecl(return_struct_sym, str);
+      return_type = builder_.ty.Of(str);
+
+      // Add the return-value statement.
+      stmts.push_back(create<ast::ReturnStatement>(
+          source,
+          builder_.Construct(source, return_type, std::move(return_exprs))));
+    }
+  }
+
+  auto* body = create<ast::BlockStatement>(source, stmts);
+  ast::AttributeList fn_attrs;
+  fn_attrs.emplace_back(create<ast::StageAttribute>(source, ep_info_->stage));
+
+  if (ep_info_->stage == ast::PipelineStage::kCompute) {
+    auto& size = ep_info_->workgroup_size;
+    if (size.x != 0 && size.y != 0 && size.z != 0) {
+      const ast::Expression* x = builder_.Expr(static_cast<int>(size.x));
+      const ast::Expression* y =
+          size.y ? builder_.Expr(static_cast<int>(size.y)) : nullptr;
+      const ast::Expression* z =
+          size.z ? builder_.Expr(static_cast<int>(size.z)) : nullptr;
+      fn_attrs.emplace_back(create<ast::WorkgroupAttribute>(Source{}, x, y, z));
+    }
+  }
+
+  builder_.AST().AddFunction(
+      create<ast::Function>(source, builder_.Symbols().Register(ep_info_->name),
+                            std::move(decl.params), return_type, body,
+                            std::move(fn_attrs), ast::AttributeList{}));
+
+  return true;
+}
+
+bool FunctionEmitter::ParseFunctionDeclaration(FunctionDeclaration* decl) {
+  if (failed()) {
+    return false;
+  }
+
+  const std::string name = namer_.Name(function_.result_id());
+
+  // Surprisingly, the "type id" on an OpFunction is the result type of the
+  // function, not the type of the function.  This is the one exceptional case
+  // in SPIR-V where the type ID is not the type of the result ID.
+  auto* ret_ty = parser_impl_.ConvertType(function_.type_id());
+  if (failed()) {
+    return false;
+  }
+  if (ret_ty == nullptr) {
+    return Fail()
+           << "internal error: unregistered return type for function with ID "
+           << function_.result_id();
+  }
+
+  ast::VariableList ast_params;
+  function_.ForEachParam(
+      [this, &ast_params](const spvtools::opt::Instruction* param) {
+        auto* type = parser_impl_.ConvertType(param->type_id());
+        if (type != nullptr) {
+          auto* ast_param = parser_impl_.MakeVariable(
+              param->result_id(), ast::StorageClass::kNone, type, true, false,
+              nullptr, ast::AttributeList{});
+          // Parameters are treated as const declarations.
+          ast_params.emplace_back(ast_param);
+          // The value is accessible by name.
+          identifier_types_.emplace(param->result_id(), type);
+        } else {
+          // We've already logged an error and emitted a diagnostic. Do nothing
+          // here.
+        }
+      });
+  if (failed()) {
+    return false;
+  }
+  decl->name = name;
+  decl->params = std::move(ast_params);
+  decl->return_type = ret_ty;
+  decl->attributes.clear();
+
+  return success();
+}
+
+const Type* FunctionEmitter::GetVariableStoreType(
+    const spvtools::opt::Instruction& var_decl_inst) {
+  const auto type_id = var_decl_inst.type_id();
+  // Normally we use the SPIRV-Tools optimizer to manage types.
+  // But when two struct types have the same member types and decorations,
+  // but differ only in member names, the two struct types will be
+  // represented by a single common internal struct type.
+  // So avoid the optimizer's representation and instead follow the
+  // SPIR-V instructions themselves.
+  const auto* ptr_ty = def_use_mgr_->GetDef(type_id);
+  const auto store_ty_id = ptr_ty->GetSingleWordInOperand(1);
+  const auto* result = parser_impl_.ConvertType(store_ty_id);
+  return result;
+}
+
+bool FunctionEmitter::EmitBody() {
+  RegisterBasicBlocks();
+
+  if (!TerminatorsAreValid()) {
+    return false;
+  }
+  if (!RegisterMerges()) {
+    return false;
+  }
+
+  ComputeBlockOrderAndPositions();
+  if (!VerifyHeaderContinueMergeOrder()) {
+    return false;
+  }
+  if (!LabelControlFlowConstructs()) {
+    return false;
+  }
+  if (!FindSwitchCaseHeaders()) {
+    return false;
+  }
+  if (!ClassifyCFGEdges()) {
+    return false;
+  }
+  if (!FindIfSelectionInternalHeaders()) {
+    return false;
+  }
+
+  if (!RegisterSpecialBuiltInVariables()) {
+    return false;
+  }
+  if (!RegisterLocallyDefinedValues()) {
+    return false;
+  }
+  FindValuesNeedingNamedOrHoistedDefinition();
+
+  if (!EmitFunctionVariables()) {
+    return false;
+  }
+  if (!EmitFunctionBodyStatements()) {
+    return false;
+  }
+  return success();
+}
+
+void FunctionEmitter::RegisterBasicBlocks() {
+  for (auto& block : function_) {
+    block_info_[block.id()] = std::make_unique<BlockInfo>(block);
+  }
+}
+
+bool FunctionEmitter::TerminatorsAreValid() {
+  if (failed()) {
+    return false;
+  }
+
+  const auto entry_id = function_.begin()->id();
+  for (const auto& block : function_) {
+    if (!block.terminator()) {
+      return Fail() << "Block " << block.id() << " has no terminator";
+    }
+  }
+  for (const auto& block : function_) {
+    block.WhileEachSuccessorLabel(
+        [this, &block, entry_id](const uint32_t succ_id) -> bool {
+          if (succ_id == entry_id) {
+            return Fail() << "Block " << block.id()
+                          << " branches to function entry block " << entry_id;
+          }
+          if (!GetBlockInfo(succ_id)) {
+            return Fail() << "Block " << block.id() << " in function "
+                          << function_.DefInst().result_id() << " branches to "
+                          << succ_id << " which is not a block in the function";
+          }
+          return true;
+        });
+  }
+  return success();
+}
+
+bool FunctionEmitter::RegisterMerges() {
+  if (failed()) {
+    return false;
+  }
+
+  const auto entry_id = function_.begin()->id();
+  for (const auto& block : function_) {
+    const auto block_id = block.id();
+    auto* block_info = GetBlockInfo(block_id);
+    if (!block_info) {
+      return Fail() << "internal error: block " << block_id
+                    << " missing; blocks should already "
+                       "have been registered";
+    }
+
+    if (const auto* inst = block.GetMergeInst()) {
+      auto terminator_opcode = block.terminator()->opcode();
+      switch (inst->opcode()) {
+        case SpvOpSelectionMerge:
+          if ((terminator_opcode != SpvOpBranchConditional) &&
+              (terminator_opcode != SpvOpSwitch)) {
+            return Fail() << "Selection header " << block_id
+                          << " does not end in an OpBranchConditional or "
+                             "OpSwitch instruction";
+          }
+          break;
+        case SpvOpLoopMerge:
+          if ((terminator_opcode != SpvOpBranchConditional) &&
+              (terminator_opcode != SpvOpBranch)) {
+            return Fail() << "Loop header " << block_id
+                          << " does not end in an OpBranch or "
+                             "OpBranchConditional instruction";
+          }
+          break;
+        default:
+          break;
+      }
+
+      const uint32_t header = block.id();
+      auto* header_info = block_info;
+      const uint32_t merge = inst->GetSingleWordInOperand(0);
+      auto* merge_info = GetBlockInfo(merge);
+      if (!merge_info) {
+        return Fail() << "Structured header block " << header
+                      << " declares invalid merge block " << merge;
+      }
+      if (merge == header) {
+        return Fail() << "Structured header block " << header
+                      << " cannot be its own merge block";
+      }
+      if (merge_info->header_for_merge) {
+        return Fail() << "Block " << merge
+                      << " declared as merge block for more than one header: "
+                      << merge_info->header_for_merge << ", " << header;
+      }
+      merge_info->header_for_merge = header;
+      header_info->merge_for_header = merge;
+
+      if (inst->opcode() == SpvOpLoopMerge) {
+        if (header == entry_id) {
+          return Fail() << "Function entry block " << entry_id
+                        << " cannot be a loop header";
+        }
+        const uint32_t ct = inst->GetSingleWordInOperand(1);
+        auto* ct_info = GetBlockInfo(ct);
+        if (!ct_info) {
+          return Fail() << "Structured header " << header
+                        << " declares invalid continue target " << ct;
+        }
+        if (ct == merge) {
+          return Fail() << "Invalid structured header block " << header
+                        << ": declares block " << ct
+                        << " as both its merge block and continue target";
+        }
+        if (ct_info->header_for_continue) {
+          return Fail()
+                 << "Block " << ct
+                 << " declared as continue target for more than one header: "
+                 << ct_info->header_for_continue << ", " << header;
+        }
+        ct_info->header_for_continue = header;
+        header_info->continue_for_header = ct;
+      }
+    }
+
+    // Check single-block loop cases.
+    bool is_single_block_loop = false;
+    block_info->basic_block->ForEachSuccessorLabel(
+        [&is_single_block_loop, block_id](const uint32_t succ) {
+          if (block_id == succ)
+            is_single_block_loop = true;
+        });
+    const auto ct = block_info->continue_for_header;
+    block_info->is_continue_entire_loop = ct == block_id;
+    if (is_single_block_loop && !block_info->is_continue_entire_loop) {
+      return Fail() << "Block " << block_id
+                    << " branches to itself but is not its own continue target";
+    }
+    // It's valid for a the header of a multi-block loop header to declare
+    // itself as its own continue target.
+  }
+  return success();
+}
+
+void FunctionEmitter::ComputeBlockOrderAndPositions() {
+  block_order_ = StructuredTraverser(function_).ReverseStructuredPostOrder();
+
+  for (uint32_t i = 0; i < block_order_.size(); ++i) {
+    GetBlockInfo(block_order_[i])->pos = i;
+  }
+  // The invalid block position is not the position of any block that is in the
+  // order.
+  assert(block_order_.size() <= kInvalidBlockPos);
+}
+
+bool FunctionEmitter::VerifyHeaderContinueMergeOrder() {
+  // Verify interval rules for a structured header block:
+  //
+  //    If the CFG satisfies structured control flow rules, then:
+  //    If header H is reachable, then the following "interval rules" hold,
+  //    where M(H) is H's merge block, and CT(H) is H's continue target:
+  //
+  //      Pos(H) < Pos(M(H))
+  //
+  //      If CT(H) exists, then:
+  //         Pos(H) <= Pos(CT(H))
+  //         Pos(CT(H)) < Pos(M)
+  //
+  for (auto block_id : block_order_) {
+    const auto* block_info = GetBlockInfo(block_id);
+    const auto merge = block_info->merge_for_header;
+    if (merge == 0) {
+      continue;
+    }
+    // This is a header.
+    const auto header = block_id;
+    const auto* header_info = block_info;
+    const auto header_pos = header_info->pos;
+    const auto merge_pos = GetBlockInfo(merge)->pos;
+
+    // Pos(H) < Pos(M(H))
+    // Note: When recording merges we made sure H != M(H)
+    if (merge_pos <= header_pos) {
+      return Fail() << "Header " << header
+                    << " does not strictly dominate its merge block " << merge;
+      // TODO(dneto): Report a path from the entry block to the merge block
+      // without going through the header block.
+    }
+
+    const auto ct = block_info->continue_for_header;
+    if (ct == 0) {
+      continue;
+    }
+    // Furthermore, this is a loop header.
+    const auto* ct_info = GetBlockInfo(ct);
+    const auto ct_pos = ct_info->pos;
+    // Pos(H) <= Pos(CT(H))
+    if (ct_pos < header_pos) {
+      Fail() << "Loop header " << header
+             << " does not dominate its continue target " << ct;
+    }
+    // Pos(CT(H)) < Pos(M(H))
+    // Note: When recording merges we made sure CT(H) != M(H)
+    if (merge_pos <= ct_pos) {
+      return Fail() << "Merge block " << merge << " for loop headed at block "
+                    << header
+                    << " appears at or before the loop's continue "
+                       "construct headed by "
+                       "block "
+                    << ct;
+    }
+  }
+  return success();
+}
+
+bool FunctionEmitter::LabelControlFlowConstructs() {
+  // Label each block in the block order with its nearest enclosing structured
+  // control flow construct. Populates the |construct| member of BlockInfo.
+
+  //  Keep a stack of enclosing structured control flow constructs.  Start
+  //  with the synthetic construct representing the entire function.
+  //
+  //  Scan from left to right in the block order, and check conditions
+  //  on each block in the following order:
+  //
+  //        a. When you reach a merge block, the top of the stack should
+  //           be the associated header. Pop it off.
+  //        b. When you reach a header, push it on the stack.
+  //        c. When you reach a continue target, push it on the stack.
+  //           (A block can be both a header and a continue target.)
+  //        c. When you reach a block with an edge branching backward (in the
+  //           structured order) to block T:
+  //            T should be a loop header, and the top of the stack should be a
+  //            continue target associated with T.
+  //            This is the end of the continue construct. Pop the continue
+  //            target off the stack.
+  //
+  //       Note: A loop header can declare itself as its own continue target.
+  //
+  //       Note: For a single-block loop, that block is a header, its own
+  //       continue target, and its own backedge block.
+  //
+  //       Note: We pop the merge off first because a merge block that marks
+  //       the end of one construct can be a single-block loop.  So that block
+  //       is a merge, a header, a continue target, and a backedge block.
+  //       But we want to finish processing of the merge before dealing with
+  //       the loop.
+  //
+  //      In the same scan, mark each basic block with the nearest enclosing
+  //      header: the most recent header for which we haven't reached its merge
+  //      block. Also mark the the most recent continue target for which we
+  //      haven't reached the backedge block.
+
+  TINT_ASSERT(Reader, block_order_.size() > 0);
+  constructs_.clear();
+  const auto entry_id = block_order_[0];
+
+  // The stack of enclosing constructs.
+  std::vector<Construct*> enclosing;
+
+  // Creates a control flow construct and pushes it onto the stack.
+  // Its parent is the top of the stack, or nullptr if the stack is empty.
+  // Returns the newly created construct.
+  auto push_construct = [this, &enclosing](size_t depth, Construct::Kind k,
+                                           uint32_t begin_id,
+                                           uint32_t end_id) -> Construct* {
+    const auto begin_pos = GetBlockInfo(begin_id)->pos;
+    const auto end_pos =
+        end_id == 0 ? uint32_t(block_order_.size()) : GetBlockInfo(end_id)->pos;
+    const auto* parent = enclosing.empty() ? nullptr : enclosing.back();
+    auto scope_end_pos = end_pos;
+    // A loop construct is added right after its associated continue construct.
+    // In that case, adjust the parent up.
+    if (k == Construct::kLoop) {
+      TINT_ASSERT(Reader, parent);
+      TINT_ASSERT(Reader, parent->kind == Construct::kContinue);
+      scope_end_pos = parent->end_pos;
+      parent = parent->parent;
+    }
+    constructs_.push_back(std::make_unique<Construct>(
+        parent, static_cast<int>(depth), k, begin_id, end_id, begin_pos,
+        end_pos, scope_end_pos));
+    Construct* result = constructs_.back().get();
+    enclosing.push_back(result);
+    return result;
+  };
+
+  // Make a synthetic kFunction construct to enclose all blocks in the function.
+  push_construct(0, Construct::kFunction, entry_id, 0);
+  // The entry block can be a selection construct, so be sure to process
+  // it anyway.
+
+  for (uint32_t i = 0; i < block_order_.size(); ++i) {
+    const auto block_id = block_order_[i];
+    TINT_ASSERT(Reader, block_id > 0);
+    auto* block_info = GetBlockInfo(block_id);
+    TINT_ASSERT(Reader, block_info);
+
+    if (enclosing.empty()) {
+      return Fail() << "internal error: too many merge blocks before block "
+                    << block_id;
+    }
+    const Construct* top = enclosing.back();
+
+    while (block_id == top->end_id) {
+      // We've reached a predeclared end of the construct.  Pop it off the
+      // stack.
+      enclosing.pop_back();
+      if (enclosing.empty()) {
+        return Fail() << "internal error: too many merge blocks before block "
+                      << block_id;
+      }
+      top = enclosing.back();
+    }
+
+    const auto merge = block_info->merge_for_header;
+    if (merge != 0) {
+      // The current block is a header.
+      const auto header = block_id;
+      const auto* header_info = block_info;
+      const auto depth = 1 + top->depth;
+      const auto ct = header_info->continue_for_header;
+      if (ct != 0) {
+        // The current block is a loop header.
+        // We should see the continue construct after the loop construct, so
+        // push the loop construct last.
+
+        // From the interval rule, the continue construct consists of blocks
+        // in the block order, starting at the continue target, until just
+        // before the merge block.
+        top = push_construct(depth, Construct::kContinue, ct, merge);
+        // A loop header that is its own continue target will have an
+        // empty loop construct. Only create a loop construct when
+        // the continue target is *not* the same as the loop header.
+        if (header != ct) {
+          // From the interval rule, the loop construct consists of blocks
+          // in the block order, starting at the header, until just
+          // before the continue target.
+          top = push_construct(depth, Construct::kLoop, header, ct);
+
+          // If the loop header branches to two different blocks inside the loop
+          // construct, then the loop body should be modeled as an if-selection
+          // construct
+          std::vector<uint32_t> targets;
+          header_info->basic_block->ForEachSuccessorLabel(
+              [&targets](const uint32_t target) { targets.push_back(target); });
+          if ((targets.size() == 2u) && targets[0] != targets[1]) {
+            const auto target0_pos = GetBlockInfo(targets[0])->pos;
+            const auto target1_pos = GetBlockInfo(targets[1])->pos;
+            if (top->ContainsPos(target0_pos) &&
+                top->ContainsPos(target1_pos)) {
+              // Insert a synthetic if-selection
+              top = push_construct(depth + 1, Construct::kIfSelection, header,
+                                   ct);
+            }
+          }
+        }
+      } else {
+        // From the interval rule, the selection construct consists of blocks
+        // in the block order, starting at the header, until just before the
+        // merge block.
+        const auto branch_opcode =
+            header_info->basic_block->terminator()->opcode();
+        const auto kind = (branch_opcode == SpvOpBranchConditional)
+                              ? Construct::kIfSelection
+                              : Construct::kSwitchSelection;
+        top = push_construct(depth, kind, header, merge);
+      }
+    }
+
+    TINT_ASSERT(Reader, top);
+    block_info->construct = top;
+  }
+
+  // At the end of the block list, we should only have the kFunction construct
+  // left.
+  if (enclosing.size() != 1) {
+    return Fail() << "internal error: unbalanced structured constructs when "
+                     "labeling structured constructs: ended with "
+                  << enclosing.size() - 1 << " unterminated constructs";
+  }
+  const auto* top = enclosing[0];
+  if (top->kind != Construct::kFunction || top->depth != 0) {
+    return Fail() << "internal error: outermost construct is not a function?!";
+  }
+
+  return success();
+}
+
+bool FunctionEmitter::FindSwitchCaseHeaders() {
+  if (failed()) {
+    return false;
+  }
+  for (auto& construct : constructs_) {
+    if (construct->kind != Construct::kSwitchSelection) {
+      continue;
+    }
+    const auto* branch =
+        GetBlockInfo(construct->begin_id)->basic_block->terminator();
+
+    // Mark the default block
+    const auto default_id = branch->GetSingleWordInOperand(1);
+    auto* default_block = GetBlockInfo(default_id);
+    // A default target can't be a backedge.
+    if (construct->begin_pos >= default_block->pos) {
+      // An OpSwitch must dominate its cases.  Also, it can't be a self-loop
+      // as that would be a backedge, and backedges can only target a loop,
+      // and loops use an OpLoopMerge instruction, which can't precede an
+      // OpSwitch.
+      return Fail() << "Switch branch from block " << construct->begin_id
+                    << " to default target block " << default_id
+                    << " can't be a back-edge";
+    }
+    // A default target can be the merge block, but can't go past it.
+    if (construct->end_pos < default_block->pos) {
+      return Fail() << "Switch branch from block " << construct->begin_id
+                    << " to default block " << default_id
+                    << " escapes the selection construct";
+    }
+    if (default_block->default_head_for) {
+      // An OpSwitch must dominate its cases, including the default target.
+      return Fail() << "Block " << default_id
+                    << " is declared as the default target for two OpSwitch "
+                       "instructions, at blocks "
+                    << default_block->default_head_for->begin_id << " and "
+                    << construct->begin_id;
+    }
+    if ((default_block->header_for_merge != 0) &&
+        (default_block->header_for_merge != construct->begin_id)) {
+      // The switch instruction for this default block is an alternate path to
+      // the merge block, and hence the merge block is not dominated by its own
+      // (different) header.
+      return Fail() << "Block " << default_block->id
+                    << " is the default block for switch-selection header "
+                    << construct->begin_id << " and also the merge block for "
+                    << default_block->header_for_merge
+                    << " (violates dominance rule)";
+    }
+
+    default_block->default_head_for = construct.get();
+    default_block->default_is_merge = default_block->pos == construct->end_pos;
+
+    // Map a case target to the list of values selecting that case.
+    std::unordered_map<uint32_t, std::vector<uint64_t>> block_to_values;
+    std::vector<uint32_t> case_targets;
+    std::unordered_set<uint64_t> case_values;
+
+    // Process case targets.
+    for (uint32_t iarg = 2; iarg + 1 < branch->NumInOperands(); iarg += 2) {
+      const auto value = branch->GetInOperand(iarg).AsLiteralUint64();
+      const auto case_target_id = branch->GetSingleWordInOperand(iarg + 1);
+
+      if (case_values.count(value)) {
+        return Fail() << "Duplicate case value " << value
+                      << " in OpSwitch in block " << construct->begin_id;
+      }
+      case_values.insert(value);
+      if (block_to_values.count(case_target_id) == 0) {
+        case_targets.push_back(case_target_id);
+      }
+      block_to_values[case_target_id].push_back(value);
+    }
+
+    for (uint32_t case_target_id : case_targets) {
+      auto* case_block = GetBlockInfo(case_target_id);
+
+      case_block->case_values = std::make_unique<std::vector<uint64_t>>(
+          std::move(block_to_values[case_target_id]));
+
+      // A case target can't be a back-edge.
+      if (construct->begin_pos >= case_block->pos) {
+        // An OpSwitch must dominate its cases.  Also, it can't be a self-loop
+        // as that would be a backedge, and backedges can only target a loop,
+        // and loops use an OpLoopMerge instruction, which can't preceded an
+        // OpSwitch.
+        return Fail() << "Switch branch from block " << construct->begin_id
+                      << " to case target block " << case_target_id
+                      << " can't be a back-edge";
+      }
+      // A case target can be the merge block, but can't go past it.
+      if (construct->end_pos < case_block->pos) {
+        return Fail() << "Switch branch from block " << construct->begin_id
+                      << " to case target block " << case_target_id
+                      << " escapes the selection construct";
+      }
+      if (case_block->header_for_merge != 0 &&
+          case_block->header_for_merge != construct->begin_id) {
+        // The switch instruction for this case block is an alternate path to
+        // the merge block, and hence the merge block is not dominated by its
+        // own (different) header.
+        return Fail() << "Block " << case_block->id
+                      << " is a case block for switch-selection header "
+                      << construct->begin_id << " and also the merge block for "
+                      << case_block->header_for_merge
+                      << " (violates dominance rule)";
+      }
+
+      // Mark the target as a case target.
+      if (case_block->case_head_for) {
+        // An OpSwitch must dominate its cases.
+        return Fail()
+               << "Block " << case_target_id
+               << " is declared as the switch case target for two OpSwitch "
+                  "instructions, at blocks "
+               << case_block->case_head_for->begin_id << " and "
+               << construct->begin_id;
+      }
+      case_block->case_head_for = construct.get();
+    }
+  }
+  return success();
+}
+
+BlockInfo* FunctionEmitter::HeaderIfBreakable(const Construct* c) {
+  if (c == nullptr) {
+    return nullptr;
+  }
+  switch (c->kind) {
+    case Construct::kLoop:
+    case Construct::kSwitchSelection:
+      return GetBlockInfo(c->begin_id);
+    case Construct::kContinue: {
+      const auto* continue_target = GetBlockInfo(c->begin_id);
+      return GetBlockInfo(continue_target->header_for_continue);
+    }
+    default:
+      break;
+  }
+  return nullptr;
+}
+
+const Construct* FunctionEmitter::SiblingLoopConstruct(
+    const Construct* c) const {
+  if (c == nullptr || c->kind != Construct::kContinue) {
+    return nullptr;
+  }
+  const uint32_t continue_target_id = c->begin_id;
+  const auto* continue_target = GetBlockInfo(continue_target_id);
+  const uint32_t header_id = continue_target->header_for_continue;
+  if (continue_target_id == header_id) {
+    // The continue target is the whole loop.
+    return nullptr;
+  }
+  const auto* candidate = GetBlockInfo(header_id)->construct;
+  // Walk up the construct tree until we hit the loop.  In future
+  // we might handle the corner case where the same block is both a
+  // loop header and a selection header. For example, where the
+  // loop header block has a conditional branch going to distinct
+  // targets inside the loop body.
+  while (candidate && candidate->kind != Construct::kLoop) {
+    candidate = candidate->parent;
+  }
+  return candidate;
+}
+
+bool FunctionEmitter::ClassifyCFGEdges() {
+  if (failed()) {
+    return false;
+  }
+
+  // Checks validity of CFG edges leaving each basic block.  This implicitly
+  // checks dominance rules for headers and continue constructs.
+  //
+  // For each branch encountered, classify each edge (S,T) as:
+  //    - a back-edge
+  //    - a structured exit (specific ways of branching to enclosing construct)
+  //    - a normal (forward) edge, either natural control flow or a case
+  //    fallthrough
+  //
+  // If more than one block is targeted by a normal edge, then S must be a
+  // structured header.
+  //
+  // Term: NEC(B) is the nearest enclosing construct for B.
+  //
+  // If edge (S,T) is a normal edge, and NEC(S) != NEC(T), then
+  //    T is the header block of its NEC(T), and
+  //    NEC(S) is the parent of NEC(T).
+
+  for (const auto src : block_order_) {
+    TINT_ASSERT(Reader, src > 0);
+    auto* src_info = GetBlockInfo(src);
+    TINT_ASSERT(Reader, src_info);
+    const auto src_pos = src_info->pos;
+    const auto& src_construct = *(src_info->construct);
+
+    // Compute the ordered list of unique successors.
+    std::vector<uint32_t> successors;
+    {
+      std::unordered_set<uint32_t> visited;
+      src_info->basic_block->ForEachSuccessorLabel(
+          [&successors, &visited](const uint32_t succ) {
+            if (visited.count(succ) == 0) {
+              successors.push_back(succ);
+              visited.insert(succ);
+            }
+          });
+    }
+
+    // There should only be one backedge per backedge block.
+    uint32_t num_backedges = 0;
+
+    // Track destinations for normal forward edges, either kForward
+    // or kCaseFallThrough. These count toward the need
+    // to have a merge instruction.  We also track kIfBreak edges
+    // because when used with normal forward edges, we'll need
+    // to generate a flow guard variable.
+    std::vector<uint32_t> normal_forward_edges;
+    std::vector<uint32_t> if_break_edges;
+
+    if (successors.empty() && src_construct.enclosing_continue) {
+      // Kill and return are not allowed in a continue construct.
+      return Fail() << "Invalid function exit at block " << src
+                    << " from continue construct starting at "
+                    << src_construct.enclosing_continue->begin_id;
+    }
+
+    for (const auto dest : successors) {
+      const auto* dest_info = GetBlockInfo(dest);
+      // We've already checked terminators are valid.
+      TINT_ASSERT(Reader, dest_info);
+      const auto dest_pos = dest_info->pos;
+
+      // Insert the edge kind entry and keep a handle to update
+      // its classification.
+      EdgeKind& edge_kind = src_info->succ_edge[dest];
+
+      if (src_pos >= dest_pos) {
+        // This is a backedge.
+        edge_kind = EdgeKind::kBack;
+        num_backedges++;
+        const auto* continue_construct = src_construct.enclosing_continue;
+        if (!continue_construct) {
+          return Fail() << "Invalid backedge (" << src << "->" << dest
+                        << "): " << src << " is not in a continue construct";
+        }
+        if (src_pos != continue_construct->end_pos - 1) {
+          return Fail() << "Invalid exit (" << src << "->" << dest
+                        << ") from continue construct: " << src
+                        << " is not the last block in the continue construct "
+                           "starting at "
+                        << src_construct.begin_id
+                        << " (violates post-dominance rule)";
+        }
+        const auto* ct_info = GetBlockInfo(continue_construct->begin_id);
+        TINT_ASSERT(Reader, ct_info);
+        if (ct_info->header_for_continue != dest) {
+          return Fail()
+                 << "Invalid backedge (" << src << "->" << dest
+                 << "): does not branch to the corresponding loop header, "
+                    "expected "
+                 << ct_info->header_for_continue;
+        }
+      } else {
+        // This is a forward edge.
+        // For now, classify it that way, but we might update it.
+        edge_kind = EdgeKind::kForward;
+
+        // Exit from a continue construct can only be from the last block.
+        const auto* continue_construct = src_construct.enclosing_continue;
+        if (continue_construct != nullptr) {
+          if (continue_construct->ContainsPos(src_pos) &&
+              !continue_construct->ContainsPos(dest_pos) &&
+              (src_pos != continue_construct->end_pos - 1)) {
+            return Fail() << "Invalid exit (" << src << "->" << dest
+                          << ") from continue construct: " << src
+                          << " is not the last block in the continue construct "
+                             "starting at "
+                          << continue_construct->begin_id
+                          << " (violates post-dominance rule)";
+          }
+        }
+
+        // Check valid structured exit cases.
+
+        if (edge_kind == EdgeKind::kForward) {
+          // Check for a 'break' from a loop or from a switch.
+          const auto* breakable_header = HeaderIfBreakable(
+              src_construct.enclosing_loop_or_continue_or_switch);
+          if (breakable_header != nullptr) {
+            if (dest == breakable_header->merge_for_header) {
+              // It's a break.
+              edge_kind = (breakable_header->construct->kind ==
+                           Construct::kSwitchSelection)
+                              ? EdgeKind::kSwitchBreak
+                              : EdgeKind::kLoopBreak;
+            }
+          }
+        }
+
+        if (edge_kind == EdgeKind::kForward) {
+          // Check for a 'continue' from within a loop.
+          const auto* loop_header =
+              HeaderIfBreakable(src_construct.enclosing_loop);
+          if (loop_header != nullptr) {
+            if (dest == loop_header->continue_for_header) {
+              // It's a continue.
+              edge_kind = EdgeKind::kLoopContinue;
+            }
+          }
+        }
+
+        if (edge_kind == EdgeKind::kForward) {
+          const auto& header_info = *GetBlockInfo(src_construct.begin_id);
+          if (dest == header_info.merge_for_header) {
+            // Branch to construct's merge block.  The loop break and
+            // switch break cases have already been covered.
+            edge_kind = EdgeKind::kIfBreak;
+          }
+        }
+
+        // A forward edge into a case construct that comes from something
+        // other than the OpSwitch is actually a fallthrough.
+        if (edge_kind == EdgeKind::kForward) {
+          const auto* switch_construct =
+              (dest_info->case_head_for ? dest_info->case_head_for
+                                        : dest_info->default_head_for);
+          if (switch_construct != nullptr) {
+            if (src != switch_construct->begin_id) {
+              edge_kind = EdgeKind::kCaseFallThrough;
+            }
+          }
+        }
+
+        // The edge-kind has been finalized.
+
+        if ((edge_kind == EdgeKind::kForward) ||
+            (edge_kind == EdgeKind::kCaseFallThrough)) {
+          normal_forward_edges.push_back(dest);
+        }
+        if (edge_kind == EdgeKind::kIfBreak) {
+          if_break_edges.push_back(dest);
+        }
+
+        if ((edge_kind == EdgeKind::kForward) ||
+            (edge_kind == EdgeKind::kCaseFallThrough)) {
+          // Check for an invalid forward exit out of this construct.
+          if (dest_info->pos > src_construct.end_pos) {
+            // In most cases we're bypassing the merge block for the source
+            // construct.
+            auto end_block = src_construct.end_id;
+            const char* end_block_desc = "merge block";
+            if (src_construct.kind == Construct::kLoop) {
+              // For a loop construct, we have two valid places to go: the
+              // continue target or the merge for the loop header, which is
+              // further down.
+              const auto loop_merge =
+                  GetBlockInfo(src_construct.begin_id)->merge_for_header;
+              if (dest_info->pos >= GetBlockInfo(loop_merge)->pos) {
+                // We're bypassing the loop's merge block.
+                end_block = loop_merge;
+              } else {
+                // We're bypassing the loop's continue target, and going into
+                // the middle of the continue construct.
+                end_block_desc = "continue target";
+              }
+            }
+            return Fail()
+                   << "Branch from block " << src << " to block " << dest
+                   << " is an invalid exit from construct starting at block "
+                   << src_construct.begin_id << "; branch bypasses "
+                   << end_block_desc << " " << end_block;
+          }
+
+          // Check dominance.
+
+          //      Look for edges that violate the dominance condition: a branch
+          //      from X to Y where:
+          //        If Y is in a nearest enclosing continue construct headed by
+          //        CT:
+          //          Y is not CT, and
+          //          In the structured order, X appears before CT order or
+          //          after CT's backedge block.
+          //        Otherwise, if Y is in a nearest enclosing construct
+          //        headed by H:
+          //          Y is not H, and
+          //          In the structured order, X appears before H or after H's
+          //          merge block.
+
+          const auto& dest_construct = *(dest_info->construct);
+          if (dest != dest_construct.begin_id &&
+              !dest_construct.ContainsPos(src_pos)) {
+            return Fail() << "Branch from " << src << " to " << dest
+                          << " bypasses "
+                          << (dest_construct.kind == Construct::kContinue
+                                  ? "continue target "
+                                  : "header ")
+                          << dest_construct.begin_id
+                          << " (dominance rule violated)";
+          }
+        }
+      }  // end forward edge
+    }    // end successor
+
+    if (num_backedges > 1) {
+      return Fail() << "Block " << src
+                    << " has too many backedges: " << num_backedges;
+    }
+    if ((normal_forward_edges.size() > 1) &&
+        (src_info->merge_for_header == 0)) {
+      return Fail() << "Control flow diverges at block " << src << " (to "
+                    << normal_forward_edges[0] << ", "
+                    << normal_forward_edges[1]
+                    << ") but it is not a structured header (it has no merge "
+                       "instruction)";
+    }
+    if ((normal_forward_edges.size() + if_break_edges.size() > 1) &&
+        (src_info->merge_for_header == 0)) {
+      // There is a branch to the merge of an if-selection combined
+      // with an other normal forward