[tint][ir][fuzz] Add SPIR-V output to ir_fuzz_dis

Fixes #352276241

Change-Id: Ic4354f7503f04b0fdd85147894bcbf63183148b6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/197734
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Ryan Harrison <rharrison@chromium.org>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index f167311..fd26dbe 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -345,7 +345,7 @@
     if (tint_build_wgsl_reader) {
       deps += [ "${tint_src_dir}/cmd/fuzz/ir/as" ]
     }
-    if (tint_build_wgsl_writer) {
+    if (tint_build_wgsl_writer && tint_build_spv_writer) {
       deps += [ "${tint_src_dir}/cmd/fuzz/ir/dis" ]
     }
   }
diff --git a/src/tint/cmd/fuzz/ir/dis/BUILD.bazel b/src/tint/cmd/fuzz/ir/dis/BUILD.bazel
index b427ad4..f82eb66 100644
--- a/src/tint/cmd/fuzz/ir/dis/BUILD.bazel
+++ b/src/tint/cmd/fuzz/ir/dis/BUILD.bazel
@@ -88,6 +88,12 @@
     ],
     "//conditions:default": [],
   }) + select({
+    ":tint_build_spv_writer": [
+      "//src/tint/lang/spirv/writer",
+      "//src/tint/lang/spirv/writer/common",
+    ],
+    "//conditions:default": [],
+  }) + select({
     ":tint_build_wgsl_writer": [
       "//src/tint/lang/wgsl/writer",
     ],
@@ -113,6 +119,11 @@
 )
 
 alias(
+  name = "tint_build_spv_writer",
+  actual = "//src/tint:tint_build_spv_writer_true",
+)
+
+alias(
   name = "tint_build_wgsl_writer",
   actual = "//src/tint:tint_build_wgsl_writer_true",
 )
@@ -125,11 +136,12 @@
     ],
 )
 selects.config_setting_group(
-    name = "tint_build_ir_binary_and_tint_build_wgsl_writer_and_tint_build_ir_fuzzer",
+    name = "tint_build_ir_binary_and_tint_build_ir_fuzzer_and_tint_build_wgsl_writer_and_tint_build_spv_writer",
     match_all = [
         ":tint_build_ir_binary",
-        ":tint_build_wgsl_writer",
         ":tint_build_ir_fuzzer",
+        ":tint_build_wgsl_writer",
+        ":tint_build_spv_writer",
     ],
 )
 
diff --git a/src/tint/cmd/fuzz/ir/dis/BUILD.cfg b/src/tint/cmd/fuzz/ir/dis/BUILD.cfg
index 8038547..93c3706 100644
--- a/src/tint/cmd/fuzz/ir/dis/BUILD.cfg
+++ b/src/tint/cmd/fuzz/ir/dis/BUILD.cfg
@@ -1,6 +1,6 @@
 {
     "cmd": {
-        "Condition": "tint_build_ir_binary && tint_build_wgsl_writer && tint_build_ir_fuzzer",
+        "Condition": "tint_build_ir_binary && tint_build_ir_fuzzer && tint_build_wgsl_writer && tint_build_spv_writer",
         "OutputName": "ir_fuzz_dis",
         "Internal": [ "utils/protos/ir_fuzz:proto" ],
     },
diff --git a/src/tint/cmd/fuzz/ir/dis/BUILD.cmake b/src/tint/cmd/fuzz/ir/dis/BUILD.cmake
index e1ab456..169c6c8 100644
--- a/src/tint/cmd/fuzz/ir/dis/BUILD.cmake
+++ b/src/tint/cmd/fuzz/ir/dis/BUILD.cmake
@@ -34,11 +34,11 @@
 #                       Do not modify this file directly
 ################################################################################
 
-if(TINT_BUILD_IR_BINARY AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_IR_FUZZER)
+if(TINT_BUILD_IR_BINARY AND TINT_BUILD_IR_FUZZER AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_SPV_WRITER)
 ################################################################################
 # Target:    tint_cmd_fuzz_ir_dis_cmd
 # Kind:      cmd
-# Condition: TINT_BUILD_IR_BINARY AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_IR_FUZZER
+# Condition: TINT_BUILD_IR_BINARY AND TINT_BUILD_IR_FUZZER AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_SPV_WRITER
 ################################################################################
 tint_add_target(tint_cmd_fuzz_ir_dis_cmd cmd
   cmd/fuzz/ir/dis/main.cc
@@ -95,6 +95,13 @@
   )
 endif(TINT_BUILD_SPV_READER)
 
+if(TINT_BUILD_SPV_WRITER)
+  tint_target_add_dependencies(tint_cmd_fuzz_ir_dis_cmd cmd
+    tint_lang_spirv_writer
+    tint_lang_spirv_writer_common
+  )
+endif(TINT_BUILD_SPV_WRITER)
+
 if(TINT_BUILD_WGSL_WRITER)
   tint_target_add_dependencies(tint_cmd_fuzz_ir_dis_cmd cmd
     tint_lang_wgsl_writer
@@ -103,4 +110,4 @@
 
 tint_target_set_output_name(tint_cmd_fuzz_ir_dis_cmd cmd "ir_fuzz_dis")
 
-endif(TINT_BUILD_IR_BINARY AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_IR_FUZZER)
\ No newline at end of file
+endif(TINT_BUILD_IR_BINARY AND TINT_BUILD_IR_FUZZER AND TINT_BUILD_WGSL_WRITER AND TINT_BUILD_SPV_WRITER)
\ No newline at end of file
diff --git a/src/tint/cmd/fuzz/ir/dis/BUILD.gn b/src/tint/cmd/fuzz/ir/dis/BUILD.gn
index 6b47007..c1c471a 100644
--- a/src/tint/cmd/fuzz/ir/dis/BUILD.gn
+++ b/src/tint/cmd/fuzz/ir/dis/BUILD.gn
@@ -37,7 +37,8 @@
 import("../../../../../../scripts/tint_overrides_with_defaults.gni")
 
 import("${tint_src_dir}/tint.gni")
-if (tint_build_ir_binary && tint_build_wgsl_writer && tint_build_ir_fuzzer) {
+if (tint_build_ir_binary && tint_build_ir_fuzzer && tint_build_wgsl_writer &&
+    tint_build_spv_writer) {
   tint_executable("dis") {
     output_name = "ir_fuzz_dis"
     sources = [ "main.cc" ]
@@ -86,6 +87,13 @@
       deps += [ "${tint_src_dir}/lang/spirv/reader/common" ]
     }
 
+    if (tint_build_spv_writer) {
+      deps += [
+        "${tint_src_dir}/lang/spirv/writer",
+        "${tint_src_dir}/lang/spirv/writer/common",
+      ]
+    }
+
     if (tint_build_wgsl_writer) {
       deps += [ "${tint_src_dir}/lang/wgsl/writer" ]
     }
diff --git a/src/tint/cmd/fuzz/ir/dis/main.cc b/src/tint/cmd/fuzz/ir/dis/main.cc
index f6685cd..40ff405 100644
--- a/src/tint/cmd/fuzz/ir/dis/main.cc
+++ b/src/tint/cmd/fuzz/ir/dis/main.cc
@@ -34,6 +34,7 @@
 #include "src/tint/cmd/common/helper.h"
 #include "src/tint/lang/core/ir/binary/decode.h"
 #include "src/tint/lang/core/ir/disassembler.h"
+#include "src/tint/lang/spirv/writer/writer.h"
 #include "src/tint/lang/wgsl/writer/writer.h"
 #include "src/tint/utils/cli/cli.h"
 #include "src/tint/utils/containers/transform.h"
@@ -43,25 +44,112 @@
 #include "src/tint/utils/text/styled_text.h"
 #include "src/tint/utils/text/styled_text_printer.h"
 
+#include "third_party/spirv-tools/src/include/spirv-tools/libspirv.h"
+#include "third_party/spirv-tools/src/include/spirv-tools/libspirv.hpp"
+
 TINT_BEGIN_DISABLE_PROTOBUF_WARNINGS();
 #include "src/tint/utils/protos/ir_fuzz/ir_fuzz.pb.h"
 TINT_END_DISABLE_PROTOBUF_WARNINGS();
 
 namespace {
 
+/// @param data spriv shader to be converted
+/// @returns human readable text representation of a SPIR-V shader
+tint::Result<std::string> DisassembleSpv(const std::vector<uint32_t>& data) {
+    std::string spv_errors;
+    spv_target_env target_env = SPV_ENV_VULKAN_1_1;
+
+    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)) {
+        return tint::Failure(spv_errors);
+    }
+
+    return result;
+}
+
+enum class Format : uint8_t {
+    kUnknown,
+    kSpirv,
+    kSpvAsm,
+    kWgsl,
+};
+
+/// @param filename the filename to inspect
+/// @returns the inferred format for the filename suffix
+Format InferFormat(const std::string& filename) {
+    if (tint::HasSuffix(filename, ".spv")) {
+        return Format::kSpirv;
+    }
+    if (tint::HasSuffix(filename, ".spvasm")) {
+        return Format::kSpvAsm;
+    }
+
+    if (tint::HasSuffix(filename, ".wgsl")) {
+        return Format::kWgsl;
+    }
+
+    return Format::kUnknown;
+}
+
 struct Options {
     std::unique_ptr<tint::StyledTextPrinter> printer;
 
     std::string input_filename;
     std::string output_filename;
 
+    Format format = Format::kUnknown;
+
     bool dump_wgsl = false;
+    bool dump_spirv = false;
 };
 
 bool ParseArgs(tint::VectorRef<std::string_view> arguments, Options* opts) {
     using namespace tint::cli;  // NOLINT(build/namespaces)
 
     OptionSet options;
+
+    tint::Vector<EnumName<Format>, 3> format_enum_names{
+        EnumName(Format::kSpirv, "spirv"),
+        EnumName(Format::kSpvAsm, "spvasm"),
+        EnumName(Format::kWgsl, "wgsl"),
+    };
+
+    auto& fmt = options.Add<EnumOption<Format>>("format",
+                                                R"(Output format to be written to file.
+If not provided, will be inferred from output filename extension:
+  .spvasm -> spvasm
+  .spv    -> spirv
+  .wgsl   -> wgsl)",
+                                                format_enum_names, ShortName{"f"});
+    TINT_DEFER(opts->format = fmt.value.value_or(Format::kUnknown));
+
     auto& col = options.Add<EnumOption<tint::ColorMode>>(
         "color", "Use colored output",
         tint::Vector{
@@ -73,7 +161,8 @@
     TINT_DEFER(opts->printer = CreatePrinter(*col.value));
 
     auto& output = options.Add<StringOption>(
-        "output-filename", "Output file name, if not specified, IR text output will go to STDOUT",
+        "output-filename",
+        "Output file name for shader output, if not specified nothing will be written to file",
         ShortName{"o"}, Parameter{"name"});
     TINT_DEFER(opts->output_filename = output.value.value_or(""));
 
@@ -82,6 +171,12 @@
         Alias{"emit-wgsl"}, Default{false});
     TINT_DEFER(opts->dump_wgsl = *dump_wgsl.value);
 
+    auto& dump_spirv = options.Add<BoolOption>(
+        "dump-spirv",
+        "Writes the SPIR-V form of input to stdout, may fail due to validation errors",
+        Alias{"emit-spirv"}, Default{false});
+    TINT_DEFER(opts->dump_spirv = *dump_spirv.value);
+
     auto& help = options.Add<BoolOption>("help", "Show usage", ShortName{"h"});
 
     auto show_usage = [&] {
@@ -98,6 +193,7 @@
         show_usage();
         return false;
     }
+
     if (help.value.value_or(false)) {
         show_usage();
         return false;
@@ -109,6 +205,7 @@
                   << tint::Join(Transform(args, tint::Quote), ", ") << "\n";
         return false;
     }
+
     if (args.Length() == 1) {
         opts->input_filename = args[0];
     }
@@ -116,6 +213,28 @@
     return true;
 }
 
+/// Infer any missing option values, then validate that the provide values are
+/// usable
+/// @param options set of values provided by the user to the binary
+/// @returns true if the options are usable, otherwise false. Prints error messages to STDERR
+bool InferAndValidateOptions(Options& options) {
+    if (options.format == Format::kUnknown && !options.output_filename.empty()) {
+        options.format = InferFormat(options.output_filename);
+        if (options.format == Format::kUnknown) {
+            std::cerr << "Unable to determine output format from filename, "
+                      << options.output_filename << "\n";
+            return false;
+        }
+    }
+
+    if (options.format != Format::kUnknown && options.output_filename.empty()) {
+        std::cerr << "Format provided, but no output filename provided\n";
+        return false;
+    }
+
+    return true;
+}
+
 /// @returns a fuzzer test case protobuf for the given file
 /// @param options program options that contains the filename to be read, etc.
 tint::Result<tint::cmd::fuzz::ir::pb::Root> GenerateFuzzCaseProto(const Options& options) {
@@ -129,47 +248,134 @@
     return std::move(fuzz_pb);
 }
 
-bool ProcessFile(const Options& options) {
+/// Prints IR text representation to STDOUT
+/// @param options options passed into the binary
+/// @param module IR module parsed from input protobuf
+void EmitIR(const Options& options, tint::core::ir::Module& module) {
+    const auto ir_text = tint::core::ir::Disassembler(module).Text();
+    if (options.output_filename.empty()) {
+        options.printer->Print(ir_text);
+        options.printer->Print(tint::StyledText{} << "\n");
+    }
+}
+
+/// Prints WGSL shader to STDOUT or to a file as determined by options
+/// @param options options passed into the binary
+/// @param module IR module parsed from input protobuf
+/// @returns true if all operations succeeded, otherwise false. Prints error messages to STDERR
+bool EmitWGSL(const Options& options, tint::core::ir::Module& module) {
+    if (!options.dump_wgsl && options.format != Format::kWgsl) {
+        return true;
+    }
+
+    tint::wgsl::writer::ProgramOptions writer_options;
+    auto output = tint::wgsl::writer::WgslFromIR(module, writer_options);
+    if (output != tint::Success) {
+        std::cerr << "Failed to convert IR to WGSL Program: " << output.Failure() << "\n";
+        return false;
+    }
+
+    if (options.dump_wgsl) {
+        options.printer->Print(tint::StyledText{} << output->wgsl);
+        options.printer->Print(tint::StyledText{} << "\n");
+    }
+
+    if (options.format == Format::kWgsl) {
+        if (!tint::cmd::WriteFile(options.output_filename, "w", output->wgsl)) {
+            std::cerr << "Unable to print WGSL to file, " << options.output_filename << "\n";
+            return false;
+        }
+    }
+
+    return true;
+}
+
+/// Prints SPIR-V shader to STDOUT or to a file as determined by options
+/// @param options options passed into the binary
+/// @param module IR module parsed from input protobuf
+/// @returns true if all operations succeeded, otherwise false. Prints error messages to STDERR
+bool EmitSpv(const Options& options, tint::core::ir::Module& module) {
+    if (!options.dump_spirv && options.format != Format::kSpirv &&
+        options.format != Format::kSpvAsm) {
+        return true;
+    }
+    tint::spirv::writer::Options gen_options;
+    auto spv = tint::spirv::writer::Generate(module, gen_options);
+
+    if (spv != tint::Success) {
+        std::cerr << "Failed to convert IR to SPIR-V: " << spv.Failure() << "\n";
+        return false;
+    }
+
+    if (options.dump_spirv || options.format == Format::kSpvAsm) {
+        auto spv_asm = DisassembleSpv(spv.Get().spirv);
+        if (spv_asm != tint::Success) {
+            std::cerr << "Failed to disassemble SPIR-V: " << spv_asm.Failure() << "\n";
+            return false;
+        }
+
+        if (options.dump_spirv) {
+            options.printer->Print(tint::StyledText{} << spv_asm.Get());
+            options.printer->Print(tint::StyledText{} << "\n");
+        }
+
+        if (options.format == Format::kSpvAsm) {
+            if (!tint::cmd::WriteFile(options.output_filename, "w", spv_asm.Get())) {
+                std::cerr << "Unable to print SPIR-V text to file, " << options.output_filename
+                          << "\n";
+                return false;
+            }
+        }
+    }
+
+    if (options.format == Format::kSpirv) {
+        if (!tint::cmd::WriteFile(options.output_filename, "wb", spv.Get().spirv)) {
+            std::cerr << "Unable to print SPIR-V to file, " << options.output_filename << "\n";
+            return false;
+        }
+    }
+    return true;
+}
+
+/// Converts and displays the given test case file as determined by the options
+///
+/// NB: There is multiple ::Decode calls, because each emission step may modify the passed in Module
+/// and modules are intentionally non-copyable.
+///
+/// @param options options passed into the binary
+/// @returns true if all operations succeeded, otherwise false. Prints error messages to STDERR
+bool Run(const Options& options) {
     auto fuzz_pb = GenerateFuzzCaseProto(options);
     if (fuzz_pb != tint::Success) {
         std::cerr << "Failed to read test case protobuf: " << fuzz_pb.Failure() << "\n";
         return false;
     }
 
-    auto module = tint::core::ir::binary::Decode(fuzz_pb.Get().module());
-    if (module != tint::Success) {
-        std::cerr << "Unable to decode ir protobuf from test case protobuf: " << module.Failure()
-                  << "\n";
-        return false;
-    }
-
-    const auto ir_text = tint::core::ir::Disassembler(module.Get()).Text();
-    if (options.output_filename.empty()) {
-        options.printer->Print(ir_text);
-        options.printer->Print(tint::StyledText{} << "\n");
-    } else {
-        if (!tint::cmd::WriteFile(options.output_filename, "w", ir_text.Plain())) {
-            std::cerr << "Unable to print IR text to file, " << options.output_filename << "\n";
+    {
+        auto module = tint::core::ir::binary::Decode(fuzz_pb.Get().module());
+        if (module != tint::Success) {
+            std::cerr << "Unable to decode ir protobuf from test case protobuf: "
+                      << module.Failure() << "\n";
             return false;
         }
     }
 
-    if (options.dump_wgsl) {
-        tint::wgsl::writer::ProgramOptions writer_options;
-        auto output = tint::wgsl::writer::WgslFromIR(module.Get(), writer_options);
-        if (output != tint::Success) {
-            std::cerr << "Failed to convert IR to Program: " << output.Failure() << "\n";
+    {
+        auto module = tint::core::ir::binary::Decode(fuzz_pb.Get().module());
+        EmitIR(options, module.Get());
+    }
+
+    {
+        auto module = tint::core::ir::binary::Decode(fuzz_pb.Get().module());
+        if (!EmitWGSL(options, module.Get())) {
             return false;
         }
+    }
 
-        if (options.output_filename.empty()) {
-            options.printer->Print(tint::StyledText{} << output->wgsl);
-            options.printer->Print(tint::StyledText{} << "\n");
-        } else {
-            if (!tint::cmd::WriteFile(options.output_filename, "w", output->wgsl)) {
-                std::cerr << "Unable to print WGSL to file, " << options.output_filename << "\n";
-                return false;
-            }
+    {
+        auto module = tint::core::ir::binary::Decode(fuzz_pb.Get().module());
+        if (!EmitSpv(options, module.Get())) {
+            return false;
         }
     }
 
@@ -196,7 +402,11 @@
         return EXIT_FAILURE;
     }
 
-    if (!ProcessFile(options)) {
+    if (!InferAndValidateOptions(options)) {
+        return EXIT_FAILURE;
+    }
+
+    if (!Run(options)) {
         return EXIT_FAILURE;
     }