[spirv-reader] Start emitting functions

Build the SPIR-V tools IR context, which gives us a lot of information
about the SPIR-V module.

Add helper functions to emit Tint types from SPIR-V types and result
IDs. Currently only supports the void type.

Emit a function with its return type, and emit the contents of its
entry block. Currently only supports the OpReturn instruction.

Adds a basic test framework for parsing SPIR-V assembly and comparing
the result to an IR disassembly.

Bug: tint:1907
Change-Id: I4eb7441e9459320af11c883ed85aaa3d470b3a64
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/165540
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/tint/cmd/test/BUILD.bazel b/src/tint/cmd/test/BUILD.bazel
index e0e9b73..a44e992 100644
--- a/src/tint/cmd/test/BUILD.bazel
+++ b/src/tint/cmd/test/BUILD.bazel
@@ -121,6 +121,11 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/parser:test",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_spv_reader_and_tint_build_wgsl_reader_and_tint_build_wgsl_writer": [
       "//src/tint/lang/spirv/reader/ast_lower:test",
     ],
diff --git a/src/tint/cmd/test/BUILD.cmake b/src/tint/cmd/test/BUILD.cmake
index 909b3e8..e79c221 100644
--- a/src/tint/cmd/test/BUILD.cmake
+++ b/src/tint/cmd/test/BUILD.cmake
@@ -133,6 +133,12 @@
   )
 endif(TINT_BUILD_MSL_WRITER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
 
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
+    tint_lang_spirv_reader_parser_test
+  )
+endif(TINT_BUILD_SPV_READER)
+
 if(TINT_BUILD_SPV_READER AND TINT_BUILD_WGSL_READER AND TINT_BUILD_WGSL_WRITER)
   tint_target_add_dependencies(tint_cmd_test_test_cmd test_cmd
     tint_lang_spirv_reader_ast_lower_test
diff --git a/src/tint/cmd/test/BUILD.gn b/src/tint/cmd/test/BUILD.gn
index 622cf0f..55c146b 100644
--- a/src/tint/cmd/test/BUILD.gn
+++ b/src/tint/cmd/test/BUILD.gn
@@ -127,6 +127,10 @@
       deps += [ "${tint_src_dir}/lang/msl/writer/ast_raise:unittests" ]
     }
 
+    if (tint_build_spv_reader) {
+      deps += [ "${tint_src_dir}/lang/spirv/reader/parser:unittests" ]
+    }
+
     if (tint_build_spv_reader && tint_build_wgsl_reader &&
         tint_build_wgsl_writer) {
       deps += [ "${tint_src_dir}/lang/spirv/reader/ast_lower:unittests" ]
diff --git a/src/tint/lang/core/ir/builder.cc b/src/tint/lang/core/ir/builder.cc
index 9315f8a4..ba14605 100644
--- a/src/tint/lang/core/ir/builder.cc
+++ b/src/tint/lang/core/ir/builder.cc
@@ -51,17 +51,24 @@
     return ir.blocks.Create<ir::MultiInBlock>();
 }
 
-Function* Builder::Function(std::string_view name,
-                            const core::type::Type* return_type,
+Function* Builder::Function(const core::type::Type* return_type,
                             Function::PipelineStage stage,
                             std::optional<std::array<uint32_t, 3>> wg_size) {
     auto* ir_func = ir.values.Create<ir::Function>(return_type, stage, wg_size);
     ir_func->SetBlock(Block());
-    ir.SetName(ir_func, name);
     ir.functions.Push(ir_func);
     return ir_func;
 }
 
+Function* Builder::Function(std::string_view name,
+                            const core::type::Type* return_type,
+                            Function::PipelineStage stage,
+                            std::optional<std::array<uint32_t, 3>> wg_size) {
+    auto* ir_func = Function(return_type, stage, wg_size);
+    ir.SetName(ir_func, name);
+    return ir_func;
+}
+
 ir::Loop* Builder::Loop() {
     return Append(ir.instructions.Create<ir::Loop>(Block(), MultiInBlock(), MultiInBlock()));
 }
diff --git a/src/tint/lang/core/ir/builder.h b/src/tint/lang/core/ir/builder.h
index ebbd851..fc847c7 100644
--- a/src/tint/lang/core/ir/builder.h
+++ b/src/tint/lang/core/ir/builder.h
@@ -235,12 +235,21 @@
     /// @returns a new multi-in block
     ir::MultiInBlock* MultiInBlock();
 
-    /// Creates a function instruction
+    /// Creates an unnamed function
+    /// @param return_type the function return type
+    /// @param stage the function stage
+    /// @param wg_size the workgroup_size
+    /// @returns the function
+    ir::Function* Function(const core::type::Type* return_type,
+                           Function::PipelineStage stage = Function::PipelineStage::kUndefined,
+                           std::optional<std::array<uint32_t, 3>> wg_size = {});
+
+    /// Creates a function
     /// @param name the function name
     /// @param return_type the function return type
     /// @param stage the function stage
     /// @param wg_size the workgroup_size
-    /// @returns the instruction
+    /// @returns the function
     ir::Function* Function(std::string_view name,
                            const core::type::Type* return_type,
                            Function::PipelineStage stage = Function::PipelineStage::kUndefined,
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.bazel b/src/tint/lang/spirv/reader/parser/BUILD.bazel
index 44f0586..ff01e93 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.bazel
+++ b/src/tint/lang/spirv/reader/parser/BUILD.bazel
@@ -67,6 +67,48 @@
   ] + select({
     ":tint_build_spv_reader_or_tint_build_spv_writer": [
       "//src/tint/lang/spirv/validate",
+      "@spirv_tools//:spirv_tools_opt",
+      "@spirv_tools",
+    ],
+    "//conditions:default": [],
+  }),
+  copts = COPTS,
+  visibility = ["//visibility:public"],
+)
+cc_library(
+  name = "test",
+  alwayslink = True,
+  srcs = [
+    "function_test.cc",
+    "helper_test.h",
+  ],
+  deps = [
+    "//src/tint/api/common",
+    "//src/tint/lang/core",
+    "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/ir",
+    "//src/tint/lang/core/type",
+    "//src/tint/utils/containers",
+    "//src/tint/utils/diagnostic",
+    "//src/tint/utils/ice",
+    "//src/tint/utils/id",
+    "//src/tint/utils/macros",
+    "//src/tint/utils/math",
+    "//src/tint/utils/memory",
+    "//src/tint/utils/reflection",
+    "//src/tint/utils/result",
+    "//src/tint/utils/rtti",
+    "//src/tint/utils/symbol",
+    "//src/tint/utils/text",
+    "//src/tint/utils/traits",
+    "@gtest",
+  ] + select({
+    ":tint_build_spv_reader": [
+      "//src/tint/lang/spirv/reader/parser",
+    ],
+    "//conditions:default": [],
+  }) + select({
+    ":tint_build_spv_reader_or_tint_build_spv_writer": [
       "@spirv_tools",
     ],
     "//conditions:default": [],
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.cmake b/src/tint/lang/spirv/reader/parser/BUILD.cmake
index 0aee0a6..2b6ba04 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.cmake
+++ b/src/tint/lang/spirv/reader/parser/BUILD.cmake
@@ -72,6 +72,56 @@
     tint_lang_spirv_validate
   )
   tint_target_add_external_dependencies(tint_lang_spirv_reader_parser lib
+    "spirv-opt-internal"
+    "spirv-tools"
+  )
+endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+
+endif(TINT_BUILD_SPV_READER)
+if(TINT_BUILD_SPV_READER)
+################################################################################
+# Target:    tint_lang_spirv_reader_parser_test
+# Kind:      test
+# Condition: TINT_BUILD_SPV_READER
+################################################################################
+tint_add_target(tint_lang_spirv_reader_parser_test test
+  lang/spirv/reader/parser/function_test.cc
+  lang/spirv/reader/parser/helper_test.h
+)
+
+tint_target_add_dependencies(tint_lang_spirv_reader_parser_test test
+  tint_api_common
+  tint_lang_core
+  tint_lang_core_constant
+  tint_lang_core_ir
+  tint_lang_core_type
+  tint_utils_containers
+  tint_utils_diagnostic
+  tint_utils_ice
+  tint_utils_id
+  tint_utils_macros
+  tint_utils_math
+  tint_utils_memory
+  tint_utils_reflection
+  tint_utils_result
+  tint_utils_rtti
+  tint_utils_symbol
+  tint_utils_text
+  tint_utils_traits
+)
+
+tint_target_add_external_dependencies(tint_lang_spirv_reader_parser_test test
+  "gtest"
+)
+
+if(TINT_BUILD_SPV_READER)
+  tint_target_add_dependencies(tint_lang_spirv_reader_parser_test test
+    tint_lang_spirv_reader_parser
+  )
+endif(TINT_BUILD_SPV_READER)
+
+if(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
+  tint_target_add_external_dependencies(tint_lang_spirv_reader_parser_test test
     "spirv-tools"
   )
 endif(TINT_BUILD_SPV_READER OR TINT_BUILD_SPV_WRITER)
diff --git a/src/tint/lang/spirv/reader/parser/BUILD.gn b/src/tint/lang/spirv/reader/parser/BUILD.gn
index ccce1da..f2cd647 100644
--- a/src/tint/lang/spirv/reader/parser/BUILD.gn
+++ b/src/tint/lang/spirv/reader/parser/BUILD.gn
@@ -37,6 +37,10 @@
 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) {
   libtint_source_set("parser") {
     sources = [
@@ -67,10 +71,56 @@
 
     if (tint_build_spv_reader || tint_build_spv_writer) {
       deps += [
+        "${tint_spirv_tools_dir}:spvtools",
         "${tint_spirv_tools_dir}:spvtools_headers",
+        "${tint_spirv_tools_dir}:spvtools_opt",
+        "${tint_spirv_tools_dir}:spvtools_val",
         "${tint_spirv_tools_dir}:spvtools_val",
         "${tint_src_dir}/lang/spirv/validate",
       ]
     }
+    public_configs = [ "${tint_spirv_tools_dir}/:spvtools_internal_config" ]
+  }
+}
+if (tint_build_unittests) {
+  if (tint_build_spv_reader) {
+    tint_unittests_source_set("unittests") {
+      sources = [
+        "function_test.cc",
+        "helper_test.h",
+      ]
+      deps = [
+        "${tint_src_dir}:gmock_and_gtest",
+        "${tint_src_dir}/api/common",
+        "${tint_src_dir}/lang/core",
+        "${tint_src_dir}/lang/core/constant",
+        "${tint_src_dir}/lang/core/ir",
+        "${tint_src_dir}/lang/core/type",
+        "${tint_src_dir}/utils/containers",
+        "${tint_src_dir}/utils/diagnostic",
+        "${tint_src_dir}/utils/ice",
+        "${tint_src_dir}/utils/id",
+        "${tint_src_dir}/utils/macros",
+        "${tint_src_dir}/utils/math",
+        "${tint_src_dir}/utils/memory",
+        "${tint_src_dir}/utils/reflection",
+        "${tint_src_dir}/utils/result",
+        "${tint_src_dir}/utils/rtti",
+        "${tint_src_dir}/utils/symbol",
+        "${tint_src_dir}/utils/text",
+        "${tint_src_dir}/utils/traits",
+      ]
+
+      if (tint_build_spv_reader) {
+        deps += [ "${tint_src_dir}/lang/spirv/reader/parser" ]
+      }
+
+      if (tint_build_spv_reader || tint_build_spv_writer) {
+        deps += [
+          "${tint_spirv_tools_dir}:spvtools_headers",
+          "${tint_spirv_tools_dir}:spvtools_val",
+        ]
+      }
+    }
   }
 }
diff --git a/src/tint/lang/spirv/reader/parser/function_test.cc b/src/tint/lang/spirv/reader/parser/function_test.cc
new file mode 100644
index 0000000..be6c28c
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/function_test.cc
@@ -0,0 +1,56 @@
+// 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/reader/parser/helper_test.h"
+
+namespace tint::spirv::reader {
+namespace {
+
+TEST_F(SpirvParserTest, EmptyEntryPoint) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main"
+               OpExecutionMode %main LocalSize 1 1 1
+       %void = OpTypeVoid
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+ %main_start = OpLabel
+               OpReturn
+               OpFunctionEnd
+)",
+              R"(
+%1 = func():void -> %b1 {
+  %b1 = block {
+    ret
+  }
+}
+)");
+}
+
+}  // namespace
+}  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/parser/helper_test.h b/src/tint/lang/spirv/reader/parser/helper_test.h
new file mode 100644
index 0000000..9b03490
--- /dev/null
+++ b/src/tint/lang/spirv/reader/parser/helper_test.h
@@ -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.
+
+#ifndef SRC_TINT_LANG_SPIRV_READER_PARSER_HELPER_TEST_H_
+#define SRC_TINT_LANG_SPIRV_READER_PARSER_HELPER_TEST_H_
+
+#include <string>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "spirv-tools/libspirv.hpp"
+#include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/core/ir/module.h"
+#include "src/tint/lang/core/ir/validator.h"
+#include "src/tint/lang/spirv/reader/parser/parser.h"
+
+namespace tint::spirv::reader {
+
+// Helper macro to run the parser and compare the disassembled IR to a string.
+// Automatically prefixes the IR disassembly with a newline to improve formatting of tests.
+#define EXPECT_IR(asm, ir) ASSERT_EQ("\n" + Run(asm), ir) << ir
+
+/// Base helper class for testing the SPIR-V parser implementation.
+template <typename BASE>
+class SpirvParserTestHelperBase : public BASE {
+  protected:
+    /// Run the parser on a SPIR-V module and return the Tint IR or an error string.
+    /// @param spirv_asm the SPIR-V assembly to parse
+    /// @returns the disassembled Tint IR
+    std::string Run(std::string spirv_asm) {
+        // Assemble the SPIR-V input.
+        StringStream err;
+        std::vector<uint32_t> binary;
+        spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
+        tools.SetMessageConsumer(
+            [&err](spv_message_level_t, const char*, const spv_position_t& pos, const char* msg) {
+                err << "SPIR-V assembly failed:" << pos.line << ":" << pos.column << ": " << msg;
+            });
+        auto assembled =
+            tools.Assemble(spirv_asm, &binary, SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
+        if (!assembled) {
+            return err.str();
+        }
+
+        // Parse the SPIR-V to produce an IR module.
+        auto parsed = Parse(Slice(binary.data(), binary.size()));
+        if (!parsed) {
+            return parsed.Failure().reason.str();
+        }
+
+        // Validate the IR module.
+        auto validated = core::ir::Validate(parsed.Get());
+        if (!validated) {
+            return validated.Failure().reason.str();
+        }
+
+        // Return the disassembled IR module.
+        return core::ir::Disassemble(parsed.Get());
+    }
+};
+
+using SpirvParserTest = SpirvParserTestHelperBase<testing::Test>;
+
+template <typename T>
+using SpirvParserTestWithParam = SpirvParserTestHelperBase<testing::TestWithParam<T>>;
+
+}  // namespace tint::spirv::reader
+
+#endif  // SRC_TINT_LANG_SPIRV_READER_PARSER_HELPER_TEST_H_
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index 11ad5b2..aacba85 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -27,9 +27,20 @@
 
 #include "src/tint/lang/spirv/reader/parser/parser.h"
 
+#include <memory>
 #include <utility>
 #include <vector>
 
+TINT_BEGIN_DISABLE_WARNING(NEWLINE_EOF);
+TINT_BEGIN_DISABLE_WARNING(OLD_STYLE_CAST);
+TINT_BEGIN_DISABLE_WARNING(SIGN_CONVERSION);
+TINT_BEGIN_DISABLE_WARNING(WEAK_VTABLES);
+#include "source/opt/build_module.h"
+TINT_END_DISABLE_WARNING(WEAK_VTABLES);
+TINT_END_DISABLE_WARNING(SIGN_CONVERSION);
+TINT_END_DISABLE_WARNING(OLD_STYLE_CAST);
+TINT_END_DISABLE_WARNING(NEWLINE_EOF);
+
 #include "src/tint/lang/core/ir/builder.h"
 #include "src/tint/lang/core/ir/module.h"
 #include "src/tint/lang/spirv/validate/validate.h"
@@ -42,6 +53,7 @@
 constexpr auto kTargetEnv = SPV_ENV_VULKAN_1_1;
 
 /// PIMPL class for SPIR-V parser.
+/// Validates the SPIR-V module and then parses it to produce a Tint IR module.
 class Parser {
   public:
     /// @param spirv the SPIR-V binary data
@@ -53,14 +65,83 @@
             return result.Failure();
         }
 
-        // TODO(crbug.com/tint/1907): Parse the module.
+        // Build the SPIR-V tools internal representation of the SPIR-V module.
+        spvtools::Context context(kTargetEnv);
+        spirv_context_ =
+            spvtools::BuildModule(kTargetEnv, context.CContext()->consumer, spirv.data, spirv.len);
+        if (!spirv_context_) {
+            return Failure("failed to build the internal representation of the module");
+        }
+
+        // TODO(crbug.com/tint/1907): Emit module-scope variables.
+
+        EmitFunctions();
+
+        // TODO(crbug.com/tint/1907): Handle entry point declarations and execution modes.
+        // TODO(crbug.com/tint/1907): Handle entry point declarations and execution modes.
+        // TODO(crbug.com/tint/1907): Handle annotation instructions.
+        // TODO(crbug.com/tint/1907): Handle names.
 
         return std::move(ir_);
     }
 
+    /// @param type a SPIR-V type object
+    /// @returns a Tint type object
+    const core::type::Type* Type(const spvtools::opt::analysis::Type* type) {
+        switch (type->kind()) {
+            case spvtools::opt::analysis::Type::kVoid:
+                return ty_.void_();
+            default:
+                TINT_UNIMPLEMENTED() << "unhandled SPIR-V type: " << type->str();
+                return ty_.void_();
+        }
+    }
+
+    /// @param id a SPIR-V result ID for a type declaration instruction
+    /// @returns a Tint type object
+    const core::type::Type* Type(uint32_t id) {
+        return Type(spirv_context_->get_type_mgr()->GetType(id));
+    }
+
+    /// Emit the functions.
+    void EmitFunctions() {
+        for (auto& func : *spirv_context_->module()) {
+            // TODO(crbug.com/tint/1907): Emit function parameters as well.
+            current_function_ = b_.Function(
+                Type(func.type_id()), core::ir::Function::PipelineStage::kUndefined, std::nullopt);
+            EmitBlock(current_function_->Block(), *func.entry());
+        }
+    }
+
+    /// Emit the contents of SPIR-V block @p src into Tint IR block @p dst.
+    /// @param dst the Tint IR block to append to
+    /// @param src the SPIR-V block to emit
+    void EmitBlock(core::ir::Block* dst, const spvtools::opt::BasicBlock& src) {
+        for (auto& inst : src) {
+            switch (inst.opcode()) {
+                case spv::Op::OpReturn:
+                    dst->Append(b_.Return(current_function_));
+                    break;
+                default:
+                    TINT_UNIMPLEMENTED()
+                        << "unhandled SPIR-V instruction: " << static_cast<uint32_t>(inst.opcode());
+            }
+        }
+    }
+
   private:
     /// The generated IR module.
     core::ir::Module ir_;
+    /// The Tint IR builder.
+    core::ir::Builder b_{ir_};
+    /// The Tint type manager.
+    core::type::Manager& ty_{ir_.Types()};
+
+    /// The Tint IR function that is currently being emitted.
+    core::ir::Function* current_function_ = nullptr;
+
+    /// The SPIR-V context containing the SPIR-V tools intermediate representation.
+    std::unique_ptr<spvtools::opt::IRContext> spirv_context_;
 };
 
 }  // namespace