Add SPIR-V dump to the SPIR-V generator

This Cl adds utility classes to dump out SPIR-V disassembly of the
builder and instructions.

Bug: tint:5
Change-Id: Ib4c57025ac63cb0be456bd819461c98ffa94367f
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/17560
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index aefe620..39156b5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -402,6 +402,8 @@
     writer/spirv/builder_entry_point_test.cc
     writer/spirv/instruction_test.cc
     writer/spirv/operand_test.cc
+    writer/spirv/spv_dump.cc
+    writer/spirv/spv_dump.h
   )
 endif()
 
diff --git a/src/writer/spirv/binary_writer.cc b/src/writer/spirv/binary_writer.cc
index a5d8a3f..81d8fac 100644
--- a/src/writer/spirv/binary_writer.cc
+++ b/src/writer/spirv/binary_writer.cc
@@ -31,39 +31,48 @@
 
 BinaryWriter::~BinaryWriter() = default;
 
-bool BinaryWriter::Write(const Builder& builder) {
-  out_.resize(builder.total_size(), 0);
+void BinaryWriter::WriteBuilder(const Builder& builder) {
+  out_.reserve(builder.total_size());
+  builder.iterate(
+      [this](const Instruction& inst) { this->process_instruction(inst); });
+}
 
-  out_[idx_++] = spv::MagicNumber;
-  out_[idx_++] = 0x00010300;  // Version 1.3
-  out_[idx_++] = kGeneratorId;
-  out_[idx_++] = builder.id_bound();
-  out_[idx_++] = 0;
+void BinaryWriter::WriteInstruction(const Instruction& inst) {
+  process_instruction(inst);
+}
 
-  builder.iterate([this](const Instruction& inst) {
-    out_[idx_++] =
-        inst.word_length() << 16 | static_cast<uint32_t>(inst.opcode());
+void BinaryWriter::WriteHeader(uint32_t bound) {
+  out_.push_back(spv::MagicNumber);
+  out_.push_back(0x00010300);  // Version 1.3
+  out_.push_back(kGeneratorId);
+  out_.push_back(bound);
+  out_.push_back(0);
+}
 
-    for (const auto& op : inst.operands()) {
-      process_op(op);
-    }
-  });
-  return true;
+void BinaryWriter::process_instruction(const Instruction& inst) {
+  out_.push_back(inst.word_length() << 16 |
+                 static_cast<uint32_t>(inst.opcode()));
+
+  for (const auto& op : inst.operands()) {
+    process_op(op);
+  }
 }
 
 void BinaryWriter::process_op(const Operand& op) {
   if (op.IsFloat()) {
+    // Allocate space for the float
+    out_.push_back(0);
     auto f = op.to_f();
-    memcpy(out_.data() + idx_, &f, 4);
+    uint8_t* ptr = reinterpret_cast<uint8_t*>(out_.data() + (out_.size() - 1));
+    memcpy(ptr, &f, 4);
   } else if (op.IsInt()) {
-    out_[idx_] = op.to_i();
+    out_.push_back(op.to_i());
   } else {
+    auto idx = out_.size();
     const auto& str = op.to_s();
-    // This depends on the vector being initialized to 0 values so the string
-    // is correctly padded.
-    memcpy(out_.data() + idx_, str.c_str(), str.size() + 1);
+    out_.resize(out_.size() + op.length(), 0);
+    memcpy(out_.data() + idx, str.c_str(), str.size() + 1);
   }
-  idx_ += op.length();
 }
 
 }  // namespace spirv
diff --git a/src/writer/spirv/binary_writer.h b/src/writer/spirv/binary_writer.h
index 972f001..84fd53e 100644
--- a/src/writer/spirv/binary_writer.h
+++ b/src/writer/spirv/binary_writer.h
@@ -30,19 +30,27 @@
   BinaryWriter();
   ~BinaryWriter();
 
-  /// Writes the given builder data into a binary
+  /// Writes the SPIR-V header.
+  /// @param bound the bound to output
+  void WriteHeader(uint32_t bound);
+
+  /// Writes the given builder data into a binary. Note, this does not emit
+  /// the SPIR-V header. You |must| call |WriteHeader| before |WriteBuilder|
+  /// if you want the SPIR-V to be emitted.
   /// @param builder the builder to assemble from
-  /// @returns true on success
-  bool Write(const Builder& builder);
+  void WriteBuilder(const Builder& builder);
+
+  /// Writes the given instruction into the binary.
+  /// @param inst the instruction to assemble
+  void WriteInstruction(const Instruction& inst);
 
   /// @returns the assembled SPIR-V
   const std::vector<uint32_t>& result() const { return out_; }
 
  private:
+  void process_instruction(const Instruction& inst);
   void process_op(const Operand& op);
 
-  /// Word index of the next word to fill.
-  size_t idx_ = 0;
   std::vector<uint32_t> out_;
 };
 
diff --git a/src/writer/spirv/binary_writer_test.cc b/src/writer/spirv/binary_writer_test.cc
index 6e6265b..564bb06 100644
--- a/src/writer/spirv/binary_writer_test.cc
+++ b/src/writer/spirv/binary_writer_test.cc
@@ -29,14 +29,14 @@
 TEST_F(BinaryWriterTest, Preamble) {
   Builder b;
   BinaryWriter bw;
-  ASSERT_TRUE(bw.Write(b));
+  bw.WriteHeader(5);
 
   auto res = bw.result();
   ASSERT_EQ(res.size(), 5);
   EXPECT_EQ(res[0], spv::MagicNumber);
   EXPECT_EQ(res[1], 0x00010300);  // SPIR-V 1.3
   EXPECT_EQ(res[2], 0);           // Generator ID
-  EXPECT_EQ(res[3], 1);           // ID Bound
+  EXPECT_EQ(res[3], 5);           // ID Bound
   EXPECT_EQ(res[4], 0);           // Reserved
 }
 
@@ -44,12 +44,12 @@
   Builder b;
   b.push_preamble(spv::Op::OpNop, {Operand::Float(2.4f)});
   BinaryWriter bw;
-  ASSERT_TRUE(bw.Write(b));
+  bw.WriteBuilder(b);
 
   auto res = bw.result();
-  ASSERT_EQ(res.size(), 7);
+  ASSERT_EQ(res.size(), 2);
   float f;
-  memcpy(&f, res.data() + 6, 4);
+  memcpy(&f, res.data() + 1, 4);
   EXPECT_EQ(f, 2.4f);
 }
 
@@ -57,23 +57,23 @@
   Builder b;
   b.push_preamble(spv::Op::OpNop, {Operand::Int(2)});
   BinaryWriter bw;
-  ASSERT_TRUE(bw.Write(b));
+  bw.WriteBuilder(b);
 
   auto res = bw.result();
-  ASSERT_EQ(res.size(), 7);
-  EXPECT_EQ(res[6], 2);
+  ASSERT_EQ(res.size(), 2);
+  EXPECT_EQ(res[1], 2);
 }
 
 TEST_F(BinaryWriterTest, String) {
   Builder b;
   b.push_preamble(spv::Op::OpNop, {Operand::String("my_string")});
   BinaryWriter bw;
-  ASSERT_TRUE(bw.Write(b));
+  bw.WriteBuilder(b);
 
   auto res = bw.result();
-  ASSERT_EQ(res.size(), 9);
+  ASSERT_EQ(res.size(), 4);
 
-  uint8_t* v = reinterpret_cast<uint8_t*>(res.data()) + (6 * 4);
+  uint8_t* v = reinterpret_cast<uint8_t*>(res.data() + 1);
   EXPECT_EQ(v[0], 'm');
   EXPECT_EQ(v[1], 'y');
   EXPECT_EQ(v[2], '_');
@@ -92,12 +92,12 @@
   Builder b;
   b.push_preamble(spv::Op::OpNop, {Operand::String("mystring")});
   BinaryWriter bw;
-  ASSERT_TRUE(bw.Write(b));
+  bw.WriteBuilder(b);
 
   auto res = bw.result();
-  ASSERT_EQ(res.size(), 9);
+  ASSERT_EQ(res.size(), 4);
 
-  uint8_t* v = reinterpret_cast<uint8_t*>(res.data()) + (6 * 4);
+  uint8_t* v = reinterpret_cast<uint8_t*>(res.data() + 1);
   EXPECT_EQ(v[0], 'm');
   EXPECT_EQ(v[1], 'y');
   EXPECT_EQ(v[2], 's');
@@ -112,6 +112,20 @@
   EXPECT_EQ(v[11], '\0');
 }
 
+TEST_F(BinaryWriterTest, TestInstructionWriter) {
+  Instruction i1{spv::Op::OpNop, {Operand::Int(2)}};
+  Instruction i2{spv::Op::OpNop, {Operand::Int(4)}};
+
+  BinaryWriter bw;
+  bw.WriteInstruction(i1);
+  bw.WriteInstruction(i2);
+
+  auto res = bw.result();
+  ASSERT_EQ(res.size(), 4);
+  EXPECT_EQ(res[1], 2);
+  EXPECT_EQ(res[3], 4);
+}
+
 }  // namespace spirv
 }  // namespace writer
 }  // namespace tint
diff --git a/src/writer/spirv/builder_entry_point_test.cc b/src/writer/spirv/builder_entry_point_test.cc
index 454aaae..953fb47 100644
--- a/src/writer/spirv/builder_entry_point_test.cc
+++ b/src/writer/spirv/builder_entry_point_test.cc
@@ -20,6 +20,7 @@
 #include "src/ast/entry_point.h"
 #include "src/ast/pipeline_stage.h"
 #include "src/writer/spirv/builder.h"
+#include "src/writer/spirv/spv_dump.h"
 
 namespace tint {
 namespace writer {
@@ -36,12 +37,8 @@
 
   auto preamble = b.preamble();
   ASSERT_EQ(preamble.size(), 1);
-  EXPECT_EQ(preamble[0].opcode(), spv::Op::OpEntryPoint);
-
-  ASSERT_TRUE(preamble[0].operands().size() >= 3);
-  EXPECT_EQ(preamble[0].operands()[0].to_i(), SpvExecutionModelFragment);
-  EXPECT_EQ(preamble[0].operands()[1].to_i(), 2);
-  EXPECT_EQ(preamble[0].operands()[2].to_s(), "main");
+  EXPECT_EQ(DumpInstruction(preamble[0]), R"(OpEntryPoint Fragment %2 "main"
+)");
 }
 
 TEST_F(BuilderTest, EntryPoint_WithoutName) {
@@ -53,12 +50,9 @@
 
   auto preamble = b.preamble();
   ASSERT_EQ(preamble.size(), 1);
-  EXPECT_EQ(preamble[0].opcode(), spv::Op::OpEntryPoint);
-
-  ASSERT_TRUE(preamble[0].operands().size() >= 3);
-  EXPECT_EQ(preamble[0].operands()[0].to_i(), SpvExecutionModelGLCompute);
-  EXPECT_EQ(preamble[0].operands()[1].to_i(), 3);
-  EXPECT_EQ(preamble[0].operands()[2].to_s(), "compute_main");
+  EXPECT_EQ(DumpInstruction(preamble[0]),
+            R"(OpEntryPoint GLCompute %3 "compute_main"
+)");
 }
 
 TEST_F(BuilderTest, EntryPoint_BadFunction) {
diff --git a/src/writer/spirv/builder_test.cc b/src/writer/spirv/builder_test.cc
index 8d06910..f92f62e 100644
--- a/src/writer/spirv/builder_test.cc
+++ b/src/writer/spirv/builder_test.cc
@@ -21,6 +21,7 @@
 #include "spirv/unified1/spirv.hpp11"
 #include "src/ast/import.h"
 #include "src/ast/module.h"
+#include "src/writer/spirv/spv_dump.h"
 
 namespace tint {
 namespace writer {
@@ -36,28 +37,21 @@
   ASSERT_TRUE(b.Build(m));
   ASSERT_EQ(b.preamble().size(), 4);
 
-  auto pre = b.preamble();
-  EXPECT_EQ(pre[0].opcode(), spv::Op::OpCapability);
-  EXPECT_EQ(pre[0].operands()[0].to_i(), SpvCapabilityShader);
-  EXPECT_EQ(pre[1].opcode(), spv::Op::OpCapability);
-  EXPECT_EQ(pre[1].operands()[0].to_i(), SpvCapabilityVulkanMemoryModel);
-  EXPECT_EQ(pre[2].opcode(), spv::Op::OpExtInstImport);
-  EXPECT_EQ(pre[2].operands()[1].to_s(), "GLSL.std.450");
-  EXPECT_EQ(pre[3].opcode(), spv::Op::OpMemoryModel);
+  EXPECT_EQ(DumpBuilder(b), R"(OpCapability Shader
+OpCapability VulkanMemoryModel
+%1 = OpExtInstImport "GLSL.std.450"
+OpMemoryModel Logical Vulkan
+)");
 }
 
 TEST_F(BuilderTest, InsertsPreambleWithoutImport) {
   ast::Module m;
   Builder b;
   ASSERT_TRUE(b.Build(m));
-  ASSERT_EQ(b.preamble().size(), 3);
-
-  auto pre = b.preamble();
-  EXPECT_EQ(pre[0].opcode(), spv::Op::OpCapability);
-  EXPECT_EQ(pre[0].operands()[0].to_i(), SpvCapabilityShader);
-  EXPECT_EQ(pre[1].opcode(), spv::Op::OpCapability);
-  EXPECT_EQ(pre[1].operands()[0].to_i(), SpvCapabilityVulkanMemoryModel);
-  EXPECT_EQ(pre[2].opcode(), spv::Op::OpMemoryModel);
+  EXPECT_EQ(DumpBuilder(b), R"(OpCapability Shader
+OpCapability VulkanMemoryModel
+OpMemoryModel Logical Vulkan
+)");
 }
 
 TEST_F(BuilderTest, TracksIdBounds) {
diff --git a/src/writer/spirv/generator.cc b/src/writer/spirv/generator.cc
index 2f0747c..a530fd0 100644
--- a/src/writer/spirv/generator.cc
+++ b/src/writer/spirv/generator.cc
@@ -30,7 +30,9 @@
     return false;
   }
 
-  return writer_.Write(builder_);
+  writer_.WriteHeader(builder_.id_bound());
+  writer_.WriteBuilder(builder_);
+  return true;
 }
 
 }  // namespace spirv
diff --git a/src/writer/spirv/spv_dump.cc b/src/writer/spirv/spv_dump.cc
new file mode 100644
index 0000000..e001a3a
--- /dev/null
+++ b/src/writer/spirv/spv_dump.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/writer/spirv/spv_dump.h"
+
+#include "spirv-tools/libspirv.hpp"
+#include "src/writer/spirv/binary_writer.h"
+
+namespace tint {
+namespace writer {
+namespace spirv {
+namespace {
+
+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_FRIENDLY_NAMES |
+                             SPV_BINARY_TO_TEXT_OPTION_NO_HEADER)) {
+    printf("%s\n", spv_errors.c_str());
+  }
+  return result;
+}
+
+}  // namespace
+
+std::string DumpBuilder(const Builder& builder) {
+  BinaryWriter writer;
+  writer.WriteHeader(builder.id_bound());
+  writer.WriteBuilder(builder);
+  return Disassemble(writer.result());
+}
+
+std::string DumpInstruction(const Instruction& inst) {
+  BinaryWriter writer;
+  writer.WriteHeader(kDefaultMaxIdBound);
+  writer.WriteInstruction(inst);
+  return Disassemble(writer.result());
+}
+
+}  // namespace spirv
+}  // namespace writer
+}  // namespace tint
diff --git a/src/writer/spirv/spv_dump.h b/src/writer/spirv/spv_dump.h
new file mode 100644
index 0000000..295861c
--- /dev/null
+++ b/src/writer/spirv/spv_dump.h
@@ -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.
+
+#ifndef SRC_WRITER_SPIRV_SPV_DUMP_H_
+#define SRC_WRITER_SPIRV_SPV_DUMP_H_
+
+#include <string>
+
+#include "src/writer/spirv/builder.h"
+#include "src/writer/spirv/instruction.h"
+
+namespace tint {
+namespace writer {
+namespace spirv {
+
+/// Dumps the given builder to a SPIR-V disassembly string
+/// @param builder the builder to convert
+/// @returns the builder as a SPIR-V disassembly string
+std::string DumpBuilder(const Builder& builder);
+
+/// Dumps the given instruction to a SPIR-V disassembly string
+/// @param inst the instruction to dump
+/// @returns the instruction as a SPIR-V disassembly string
+std::string DumpInstruction(const Instruction& inst);
+
+}  // namespace spirv
+}  // namespace writer
+}  // namespace tint
+
+#endif  // SRC_WRITER_SPIRV_SPV_DUMP_H_