[tint][spirv] Add validate library

Matches the HLSL and MSL validation sub-libraries.

Change-Id: I9fc7e3f31a40a807e292459003192513545fa57d
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/162403
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index 6c3f01e..baf7681 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -124,6 +124,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "//src/tint/lang/spirv/validate:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_spv_writer": [
       "//src/tint/lang/spirv/writer/ast_printer:test",
       "//src/tint/lang/spirv/writer/common:test",
@@ -194,6 +199,14 @@
 )
 
 selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
+selects.config_setting_group(
     name = "tint_build_glsl_writer_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer",
     match_all = [
         ":tint_build_glsl_writer",
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 8dac742..519faf7 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -137,6 +137,12 @@
   )
 endif(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_validate_test
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
 if(TINT_BUILD_SPV_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_writer_ast_printer_test
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 9a45716..1700779 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -130,6 +130,10 @@
       deps += [ "${tint_src_dir}/lang/spirv/reader/ast_parser:unittests" ]
     }
 
+    if (tint_build_spv_reader || tint_build_spv_writer) {
+      deps += [ "${tint_src_dir}/lang/spirv/validate:unittests" ]
+    }
+
     if (tint_build_spv_writer) {
       deps += [
         "${tint_src_dir}/lang/spirv/writer:unittests",
diff --git a/src/tint/lang/msl/validate/validate.h b/src/tint/lang/msl/validate/validate.h
index 89581d6..1a303d5 100644
--- a/src/tint/lang/msl/validate/validate.h
+++ b/src/tint/lang/msl/validate/validate.h
@@ -61,7 +61,7 @@
     std::string output;
 };
 
-/// Msl attempts to compile the shader with the Metal Shader Compiler,
+/// Validate attempts to compile the shader with the Metal Shader Compiler,
 /// verifying that the shader compiles successfully.
 /// @param xcrun_path path to xcrun
 /// @param source the generated MSL source
@@ -70,7 +70,7 @@
 Result Validate(const std::string& xcrun_path, const std::string& source, MslVersion version);
 
 #ifdef __APPLE__
-/// Msl attempts to compile the shader with the runtime Metal Shader Compiler
+/// ValidateUsingMetal attempts to compile the shader with the runtime Metal Shader Compiler
 /// API, verifying that the shader compiles successfully.
 /// @param source the generated MSL source
 /// @param version the version of MSL to validate against
diff --git a/src/tint/lang/spirv/BUILD.cmake b/src/tint/lang/spirv/BUILD.cmake
index 30c3163..06f7cfd 100644
--- a/src/tint/lang/spirv/BUILD.cmake
+++ b/src/tint/lang/spirv/BUILD.cmake
@@ -38,6 +38,7 @@
 include(lang/spirv/ir/BUILD.cmake)
 include(lang/spirv/reader/BUILD.cmake)
 include(lang/spirv/type/BUILD.cmake)
+include(lang/spirv/validate/BUILD.cmake)
 include(lang/spirv/writer/BUILD.cmake)
 
 ################################################################################
diff --git a/src/tint/lang/spirv/validate/BUILD.bazel b/src/tint/lang/spirv/validate/BUILD.bazel
new file mode 100644
index 0000000..bc1d952
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.bazel
@@ -0,0 +1,112 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.bazel.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+load("//src/tint:flags.bzl", "COPTS")
+load("@bazel_skylib//lib:selects.bzl", "selects")
+cc_library(
+  name = "validate",
+  srcs = [
+    "validate.cc",
+  ],
+  hdrs = [
+    "validate.h",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "validate_test.cc",
+  ],
+  deps = [
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
+      "//src/tint/lang/spirv/validate",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+
+alias(
+  name = "tint_build_spv_reader",
+  actual = "//src/tint:tint_build_spv_reader_true",
+)
+
+alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+selects.config_setting_group(
+    name = "tint_build_spv_reader_or_tint_build_spv_writer",
+    match_any = [
+        "tint_build_spv_reader",
+        "tint_build_spv_writer",
+    ],
+)
+
diff --git a/src/tint/lang/spirv/validate/BUILD.cfg b/src/tint/lang/spirv/validate/BUILD.cfg
new file mode 100644
index 0000000..3b3caf4
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "condition": "tint_build_spv_reader || tint_build_spv_writer"
+}
diff --git a/src/tint/lang/spirv/validate/BUILD.cmake b/src/tint/lang/spirv/validate/BUILD.cmake
new file mode 100644
index 0000000..e50d0e9
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.cmake
@@ -0,0 +1,101 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_validate
+# Kind:      lib
+# Condition: TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_validate lib
+  lang/spirv/validate/validate.cc
+  lang/spirv/validate/validate.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_validate lib
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_external_dependencies(tint_lang_spirv_validate lib
+    "spirv-tools"
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+################################################################################
+# Target:    tint_lang_spirv_validate_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER
+################################################################################
+tint_add_target(tint_lang_spirv_validate_test test
+  lang/spirv/validate/validate_test.cc
+)
+
+tint_target_add_dependencies(tint_lang_spirv_validate_test test
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_validate_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_lang_spirv_validate_test test
+    tint_lang_spirv_validate
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/lang/spirv/validate/BUILD.gn b/src/tint/lang/spirv/validate/BUILD.gn
new file mode 100644
index 0000000..99890f7
--- /dev/null
+++ b/src/tint/lang/spirv/validate/BUILD.gn
@@ -0,0 +1,94 @@
+# Copyright 2023 The Dawn & Tint Authors
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+#    list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+################################################################################
+# File generated by 'tools/src/cmd/gen' using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# To regenerate run: './tools/run gen'
+#
+#                       Do not modify this file directly
+################################################################################
+
+import("../../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+if (tint_build_unittests || tint_build_benchmarks) {
+  import("//testing/test.gni")
+}
+if (tint_build_spv_reader || tint_build_spv_writer) {
+  libtint_source_set("validate") {
+    sources = [
+      "validate.cc",
+      "validate.h",
+    ]
+    deps = [
+      "${tint_src_dir}/utils/containers",
+      "${tint_src_dir}/utils/diagnostic",
+      "${tint_src_dir}/utils/ice",
+      "${tint_src_dir}/utils/macros",
+      "${tint_src_dir}/utils/math",
+      "${tint_src_dir}/utils/memory",
+      "${tint_src_dir}/utils/result",
+      "${tint_src_dir}/utils/rtti",
+      "${tint_src_dir}/utils/text",
+      "${tint_src_dir}/utils/traits",
+    ]
+
+    if (tint_build_spv_reader || tint_build_spv_writer) {
+      deps += [
+        "${tint_spirv_tools_dir}:spvtools_headers",
+        "${tint_spirv_tools_dir}:spvtools_val",
+      ]
+    }
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_spv_reader || tint_build_spv_writer) {
+    tint_unittests_source_set("unittests") {
+      sources = [ "validate_test.cc" ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_spv_reader || tint_build_spv_writer) {
+        deps += [ "${tint_src_dir}/lang/spirv/validate" ]
+      }
+    }
+  }
+}
diff --git a/src/tint/lang/spirv/validate/validate.cc b/src/tint/lang/spirv/validate/validate.cc
new file mode 100644
index 0000000..13344b7
--- /dev/null
+++ b/src/tint/lang/spirv/validate/validate.cc
@@ -0,0 +1,91 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/spirv/validate/validate.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "spirv-tools/libspirv.hpp"
+
+namespace tint::spirv::validate {
+
+Result<SuccessType> Validate(Slice<const uint32_t> spirv) {
+    Vector<diag::Diagnostic, 4> diags;
+    diags.Push(diag::Diagnostic{});  // Filled in on error
+
+    spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_3);
+    tools.SetMessageConsumer(
+        [&](spv_message_level_t level, const char*, const spv_position_t& pos, const char* msg) {
+            diag::Diagnostic diag;
+            diag.message = msg;
+            diag.source.range.begin.line = pos.line + 1;
+            diag.source.range.begin.column = pos.column + 1;
+            diag.source.range.end = diag.source.range.begin;
+            switch (level) {
+                case SPV_MSG_FATAL:
+                case SPV_MSG_INTERNAL_ERROR:
+                case SPV_MSG_ERROR:
+                    diag.severity = diag::Severity::Error;
+                    break;
+                case SPV_MSG_WARNING:
+                    diag.severity = diag::Severity::Warning;
+                    break;
+                case SPV_MSG_INFO:
+                case SPV_MSG_DEBUG:
+                    diag.severity = diag::Severity::Note;
+                    break;
+            }
+            diags.Push(std::move(diag));
+        });
+
+    if (tools.Validate(spirv.data, spirv.len)) {
+        return Success;
+    }
+
+    std::string disassembly;
+    if (tools.Disassemble(
+            spirv.data, spirv.len, &disassembly,
+            SPV_BINARY_TO_TEXT_OPTION_INDENT | SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) {
+        diag::Diagnostic& err = diags.Front();
+        err.message = "SPIR-V failed validation.\n\nDisassembly:\n" + std::move(disassembly);
+        err.severity = diag::Severity::Error;
+    } else {
+        diag::Diagnostic& err = diags.Front();
+        err.message = "SPIR-V failed validation and disassembly\n";
+        err.severity = diag::Severity::Error;
+    }
+    auto file = std::make_shared<Source::File>("spirv", disassembly);
+    for (auto& diag : diags) {
+        diag.source.file = file.get();
+        diag.owned_file = file;
+    }
+    return Failure{diag::List{std::move(diags)}};
+}
+
+}  // namespace tint::spirv::validate
diff --git a/src/tint/lang/spirv/validate/validate.h b/src/tint/lang/spirv/validate/validate.h
new file mode 100644
index 0000000..840d90f
--- /dev/null
+++ b/src/tint/lang/spirv/validate/validate.h
@@ -0,0 +1,46 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
+#define SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
+
+#include "src/tint/utils/result/result.h"
+
+// Forward declarations
+namespace tint {
+class Program;
+}  // namespace tint
+
+namespace tint::spirv::validate {
+
+/// Validate checks that the provided SPIR-V passes validation.
+/// @return success or failure(s)
+Result<SuccessType> Validate(Slice<const uint32_t> spirv);
+
+}  // namespace tint::spirv::validate
+
+#endif  // SRC_TINT_LANG_SPIRV_VALIDATE_VALIDATE_H_
diff --git a/src/tint/lang/spirv/validate/validate_test.cc b/src/tint/lang/spirv/validate/validate_test.cc
new file mode 100644
index 0000000..96ba63e
--- /dev/null
+++ b/src/tint/lang/spirv/validate/validate_test.cc
@@ -0,0 +1,113 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "gtest/gtest.h"
+
+#include "src/tint/lang/spirv/validate/validate.h"
+
+namespace tint::spirv::validate {
+namespace {
+
+TEST(SpirvValidateTest, Valid) {
+    uint32_t spirv[] = {
+        0x07230203, 0x00010600, 0x00070000, 0x00000011, 0x00000000, 0x00020011, 0x00000001,
+        0x0003000e, 0x00000000, 0x00000001, 0x0005000f, 0x00000005, 0x00000001, 0x6e69616d,
+        0x00000000, 0x00060010, 0x00000001, 0x00000011, 0x00000001, 0x00000001, 0x00000001,
+        0x00040005, 0x00000001, 0x6e69616d, 0x00000000, 0x00030005, 0x00000002, 0x0000006d,
+        0x00020013, 0x00000003, 0x00030021, 0x00000004, 0x00000003, 0x00030016, 0x00000005,
+        0x00000020, 0x00040017, 0x00000006, 0x00000005, 0x00000003, 0x00040018, 0x00000007,
+        0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x0003002e,
+        0x00000007, 0x00000009, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x0004002b,
+        0x0000000a, 0x0000000b, 0x00000001, 0x00040020, 0x0000000c, 0x00000007, 0x00000006,
+        0x00050036, 0x00000003, 0x00000001, 0x00000000, 0x00000004, 0x000200f8, 0x0000000d,
+        0x0005003b, 0x00000008, 0x00000002, 0x00000007, 0x00000009, 0x00050041, 0x0000000c,
+        0x0000000e, 0x00000002, 0x0000000b, 0x0004003d, 0x00000006, 0x0000000f, 0x0000000e,
+        0x00050051, 0x00000005, 0x00000010, 0x0000000f, 0x00000001, 0x000100fd, 0x00010038,
+    };
+    auto res = Validate(spirv);
+    EXPECT_TRUE(res) << res;
+}
+
+TEST(SpirvValidateTest, Invalid) {
+    uint32_t spirv[] = {
+        0x07230203, 0x00010600, 0x00070000, 0x00000011, 0x00000000, 0x00020011, 0x00000001,
+        0x0003000e, 0x00000000, 0x00000001, 0x0005000f, 0x00000005, 0x00000001, 0x6e69616d,
+        0x00000000, 0x00060010, 0x00000001, 0x00000011, 0x00000001, 0x00000001, 0x00000001,
+        0x00040005, 0x00000001, 0x6e69616d, 0x00000000, 0x00030005, 0x00000002, 0x0000006d,
+        0x00020013, 0x00000003, 0x00030021, 0x00000004, 0x00000003, 0x00030016, 0x00000005,
+        0x00000020, 0x00040017, 0x00000006, 0x00000005, 0x00000003, 0x00040018, 0x00000007,
+        0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x0003002e,
+        0x00000006, 0x00000009, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x0004002b,
+        0x0000000a, 0x0000000b, 0x00000001, 0x00040020, 0x0000000c, 0x00000007, 0x00000006,
+        0x00050036, 0x00000003, 0x00000001, 0x00000000, 0x00000004, 0x000200f8, 0x0000000d,
+        0x0005003b, 0x00000008, 0x00000002, 0x00000007, 0x00000009, 0x00050041, 0x0000000c,
+        0x0000000e, 0x00000002, 0x0000000b, 0x0004003d, 0x00000006, 0x0000000f, 0x0000000e,
+        0x00050051, 0x00000005, 0x00000010, 0x0000000f, 0x00000001, 0x000100fd, 0x00010038,
+    };
+    auto res = Validate(spirv);
+    ASSERT_FALSE(res);
+    EXPECT_EQ(res.Failure().reason.str(), R"(spirv error: SPIR-V failed validation.
+
+Disassembly:
+; SPIR-V
+; Version: 1.6
+; Generator: Khronos SPIR-V Tools Assembler; 0
+; Bound: 17
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %main "main"
+               OpName %m "m"
+       %void = OpTypeVoid
+          %4 = OpTypeFunction %void
+      %float = OpTypeFloat 32
+    %v3float = OpTypeVector %float 3
+%mat3v3float = OpTypeMatrix %v3float 3
+%_ptr_Function_mat3v3float = OpTypePointer Function %mat3v3float
+          %9 = OpConstantNull %v3float
+        %int = OpTypeInt 32 1
+      %int_1 = OpConstant %int 1
+%_ptr_Function_v3float = OpTypePointer Function %v3float
+       %main = OpFunction %void None %4
+         %13 = OpLabel
+          %m = OpVariable %_ptr_Function_mat3v3float Function %9
+         %14 = OpAccessChain %_ptr_Function_v3float %m %int_1
+         %15 = OpLoad %v3float %14
+         %16 = OpCompositeExtract %float %15 1
+               OpReturn
+               OpFunctionEnd
+
+spirv:1:1 error: Initializer type must match the type pointed to by the Result Type
+  %m = OpVariable %_ptr_Function_mat3v3float Function %9
+
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::validate