[IR] Add ability to dump IR graph to dot

This CL adds the ability to dump an IR graph to a dot file. The
`--dump-ir-graph` option is added to the main tint command. The IR code
is moved behind a TINT_BUILD_IR flag in order to allow the GN build to
continue to build the tint program.

Bug: tint:1718
Change-Id: I0953fe2a59a34c21bb6cd288cb90e9d0298af793
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/107860
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/.gitignore b/.gitignore
index 6fb333a..15b0736 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,3 +128,4 @@
 ### Clang-Tidy files
 all_findings.json
 
+tint.dot
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2503583..b918c75 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -155,6 +155,7 @@
 option_if_not_defined(TINT_BUILD_WGSL_WRITER "Build the WGSL output writer" ON)
 
 option_if_not_defined(TINT_BUILD_RUN_GENERATOR "Run the intrinsic generator" OFF)
+option_if_not_defined(TINT_BUILD_IR "Build the IR" 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)
@@ -290,6 +291,7 @@
 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 run generator: ${TINT_BUILD_RUN_GENERATOR}")
+message(STATUS "Tint build IR: ${TINT_BUILD_IR}")
 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}")
@@ -495,6 +497,7 @@
   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}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_IR=$<BOOL:${TINT_BUILD_IR}>)
 
   common_compile_options(${TARGET})
 
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 71f85e8..e2bc7f8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -238,26 +238,6 @@
   inspector/resource_binding.h
   inspector/scalar.cc
   inspector/scalar.h
-  ir/block.cc
-  ir/block.h
-  ir/builder.cc
-  ir/builder.h
-  ir/builder_impl.cc
-  ir/builder_impl.h
-  ir/flow_node.cc
-  ir/flow_node.h
-  ir/function.cc
-  ir/function.h
-  ir/if.cc
-  ir/if.h
-  ir/loop.cc
-  ir/loop.h
-  ir/module.cc
-  ir/module.h
-  ir/switch.cc
-  ir/switch.h
-  ir/terminator.cc
-  ir/terminator.h
   number.cc
   number.h
   program_builder.cc
@@ -701,6 +681,33 @@
   )
 endif()
 
+if(${TINT_BUILD_IR})
+  list(APPEND TINT_LIB_SRCS
+    ir/block.cc
+    ir/block.h
+    ir/builder.cc
+    ir/builder.h
+    ir/builder_impl.cc
+    ir/builder_impl.h
+    ir/debug.cc
+    ir/debug.h
+    ir/flow_node.cc
+    ir/flow_node.h
+    ir/function.cc
+    ir/function.h
+    ir/if.cc
+    ir/if.h
+    ir/loop.cc
+    ir/loop.h
+    ir/module.cc
+    ir/module.h
+    ir/switch.cc
+    ir/switch.h
+    ir/terminator.cc
+    ir/terminator.h
+  )
+endif()
+
 if(MSVC)
   list(APPEND TINT_LIB_SRCS
     tint.natvis
@@ -864,8 +871,6 @@
     diagnostic/diagnostic_test.cc
     diagnostic/formatter_test.cc
     diagnostic/printer_test.cc
-    ir/builder_impl_test.cc
-    ir/test_helper.h
     number_test.cc
     program_builder_test.cc
     program_test.cc
@@ -1368,6 +1373,13 @@
     )
   endif()
 
+  if (${TINT_BUILD_IR})
+    list(APPEND TINT_TEST_SRCS
+      ir/builder_impl_test.cc
+      ir/test_helper.h
+    )
+  endif()
+
   if (${TINT_BUILD_FUZZERS})
     list(APPEND TINT_TEST_SRCS
       fuzzers/mersenne_twister_engine.cc
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index bf5a993..485ca5a 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -26,7 +26,7 @@
 #if TINT_BUILD_GLSL_WRITER
 #include "StandAlone/ResourceLimits.h"
 #include "glslang/Public/ShaderLang.h"
-#endif
+#endif  // TINT_BUILD_GLSL_WRITER
 
 #if TINT_BUILD_SPV_READER
 #include "spirv-tools/libspirv.hpp"
@@ -39,6 +39,11 @@
 #include "src/tint/val/val.h"
 #include "tint/tint.h"
 
+#if TINT_BUILD_IR
+#include "src/tint/ir/debug.h"
+#include "src/tint/ir/module.h"
+#endif  // TINT_BUILD_IR
+
 namespace {
 
 [[noreturn]] void TintInternalCompilerErrorReporter(const tint::diag::List& diagnostics) {
@@ -94,6 +99,10 @@
     std::string xcrun_path;
     std::unordered_map<std::string, double> overrides;
     std::optional<tint::sem::BindingPoint> hlsl_root_constant_binding_point;
+
+#if TINT_BUILD_IR
+    bool dump_ir_graph = false;
+#endif  // TINT_BUILD_IR
 };
 
 const char kUsage[] = R"(Usage: tint [options] <input-file>
@@ -436,6 +445,10 @@
                 return false;
             }
             opts->dxc_path = args[i];
+#if TINT_BUILD_IR
+        } else if (arg == "--dump-ir-graph") {
+            opts->dump_ir_graph = true;
+#endif  // TINT_BUILD_IR
         } else if (arg == "--xcrun") {
             ++i;
             if (i >= args.size()) {
@@ -1135,6 +1148,11 @@
 
     if (options.show_help) {
         std::string usage = tint::utils::ReplaceAll(kUsage, "${transforms}", transform_names());
+#if TINT_BUILD_IR
+        usage +=
+            "  --dump-ir-graph           -- Writes the IR graph to 'tint.dot' as a dot graph\n";
+#endif  // TINT_BUILD_IR
+
         std::cout << usage << std::endl;
         return 0;
     }
@@ -1253,6 +1271,19 @@
         return 1;
     }
 
+#if TINT_BUILD_IR
+    if (options.dump_ir_graph) {
+        auto result = tint::ir::Module::FromProgram(program.get());
+        if (!result) {
+            std::cerr << "Failed to build IR from program: " << result.Failure() << std::endl;
+        } else {
+            auto mod = result.Move();
+            auto graph = tint::ir::Debug::AsDotGraph(&mod);
+            WriteFile("tint.dot", "w", graph);
+        }
+    }
+#endif  // TINT_BUILD_IR
+
     tint::inspector::Inspector inspector(program.get());
 
     if (options.dump_inspector_bindings) {
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 224d1ae..5a35b12 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -138,8 +138,10 @@
                 return true;
             },
             [&](Default) {
-                TINT_ICE(IR, diagnostics_) << "unhandled type: " << decl->TypeInfo().name;
-                return false;
+                diagnostics_.add_warning(tint::diag::System::IR,
+                                         "unknown type: " + std::string(decl->TypeInfo().name),
+                                         decl->source);
+                return true;
             });
         if (!ok) {
             return utils::Failure;
@@ -220,9 +222,10 @@
             return true;  // Not emitted
         },
         [&](Default) {
-            TINT_ICE(IR, diagnostics_)
-                << "unknown statement type: " << std::string(stmt->TypeInfo().name);
-            return false;
+            diagnostics_.add_warning(
+                tint::diag::System::IR,
+                "unknown statement type: " + std::string(stmt->TypeInfo().name), stmt->source);
+            return true;
         });
 }
 
diff --git a/src/tint/ir/debug.cc b/src/tint/ir/debug.cc
new file mode 100644
index 0000000..be83111
--- /dev/null
+++ b/src/tint/ir/debug.cc
@@ -0,0 +1,159 @@
+// 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/ir/debug.h"
+
+#include <sstream>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "src/tint/ir/block.h"
+#include "src/tint/ir/if.h"
+#include "src/tint/ir/loop.h"
+#include "src/tint/ir/switch.h"
+#include "src/tint/ir/terminator.h"
+#include "src/tint/program.h"
+
+namespace tint::ir {
+
+// static
+std::string Debug::AsDotGraph(const Module* mod) {
+    size_t node_count = 0;
+
+    std::unordered_set<const FlowNode*> visited;
+    std::unordered_set<const FlowNode*> merge_nodes;
+    std::unordered_map<const FlowNode*, std::string> node_to_name;
+    std::stringstream out;
+
+    auto name_for = [&](const FlowNode* node) -> std::string {
+        if (node_to_name.count(node) > 0) {
+            return node_to_name[node];
+        }
+
+        std::string name = "node_" + std::to_string(node_count);
+        node_count += 1;
+
+        node_to_name[node] = name;
+        return name;
+    };
+
+    std::function<void(const FlowNode*)> Graph = [&](const FlowNode* node) {
+        if (visited.count(node) > 0) {
+            return;
+        }
+        visited.insert(node);
+
+        tint::Switch(
+            node,
+            [&](const ir::Block* b) {
+                if (node_to_name.count(b) == 0) {
+                    out << name_for(b) << R"( [label="block"])" << std::endl;
+                }
+                out << name_for(b) << " -> " << name_for(b->branch_target);
+
+                // Dashed lines to merge blocks
+                if (merge_nodes.count(b->branch_target) != 0) {
+                    out << " [style=dashed]";
+                }
+
+                out << std::endl;
+                Graph(b->branch_target);
+            },
+            [&](const ir::Switch* s) {
+                out << name_for(s) << R"( [label="switch"])" << std::endl;
+                out << name_for(s->merge_target) << R"( [label="switch merge"])" << std::endl;
+                merge_nodes.insert(s->merge_target);
+
+                size_t i = 0;
+                for (const auto& c : s->cases) {
+                    out << name_for(c.start_target)
+                        << R"( [label="case )" + std::to_string(i++) + R"("])" << std::endl;
+                }
+                out << name_for(s) << " -> {";
+                for (const auto& c : s->cases) {
+                    if (&c != &(s->cases[0])) {
+                        out << ", ";
+                    }
+                    out << name_for(c.start_target);
+                }
+                out << "}" << std::endl;
+
+                for (const auto& c : s->cases) {
+                    Graph(c.start_target);
+                }
+                Graph(s->merge_target);
+            },
+            [&](const ir::If* i) {
+                out << name_for(i) << R"( [label="if"])" << std::endl;
+                out << name_for(i->true_target) << R"( [label="true"])" << std::endl;
+                out << name_for(i->false_target) << R"( [label="false"])" << std::endl;
+                out << name_for(i->merge_target) << R"( [label="if merge"])" << std::endl;
+                merge_nodes.insert(i->merge_target);
+
+                out << name_for(i) << " -> {";
+                out << name_for(i->true_target) << ", " << name_for(i->false_target);
+                out << "}" << std::endl;
+
+                // Subgraph if true/false branches so they draw on the same line
+                out << "subgraph sub_" << name_for(i) << " {" << std::endl;
+                out << R"(rank="same")" << std::endl;
+                out << name_for(i->true_target) << std::endl;
+                out << name_for(i->false_target) << std::endl;
+                out << "}" << std::endl;
+
+                Graph(i->true_target);
+                Graph(i->false_target);
+                Graph(i->merge_target);
+            },
+            [&](const ir::Loop* l) {
+                out << name_for(l) << R"( [label="loop"])" << std::endl;
+                out << name_for(l->start_target) << R"( [label="start"])" << std::endl;
+                out << name_for(l->continuing_target) << R"( [label="continuing"])" << std::endl;
+                out << name_for(l->merge_target) << R"( [label="loop merge"])" << std::endl;
+                merge_nodes.insert(l->merge_target);
+
+                // Subgraph the continuing and merge so they get drawn on the same line
+                out << "subgraph sub_" << name_for(l) << " {" << std::endl;
+                out << R"(rank="same")" << std::endl;
+                out << name_for(l->continuing_target) << std::endl;
+                out << name_for(l->merge_target) << std::endl;
+                out << "}" << std::endl;
+
+                out << name_for(l) << " -> " << name_for(l->start_target) << std::endl;
+
+                Graph(l->start_target);
+                Graph(l->continuing_target);
+                Graph(l->merge_target);
+            },
+            [&](const ir::Terminator*) {
+                // Already done
+            });
+    };
+
+    out << "digraph G {" << std::endl;
+    for (const auto* func : mod->functions) {
+        // Cluster each function to label and draw a box around it.
+        out << "subgraph cluster_" << name_for(func) << " {" << std::endl;
+        out << R"(label=")" << mod->program->Symbols().NameFor(func->source->symbol) << R"(")"
+            << std::endl;
+        out << name_for(func->start_target) << R"( [label="start"])" << std::endl;
+        out << name_for(func->end_target) << R"( [label="end"])" << std::endl;
+        Graph(func->start_target);
+        out << "}" << std::endl;
+    }
+    out << "}";
+    return out.str();
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/debug.h b/src/tint/ir/debug.h
new file mode 100644
index 0000000..bd0570b
--- /dev/null
+++ b/src/tint/ir/debug.h
@@ -0,0 +1,35 @@
+// 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_IR_DEBUG_H_
+#define SRC_TINT_IR_DEBUG_H_
+
+#include <string>
+
+#include "src/tint/ir/module.h"
+
+namespace tint::ir {
+
+/// Helper class to debug IR.
+class Debug {
+  public:
+    /// Returns the module as a dot graph
+    /// @param mod the module to emit
+    /// @returns the dot graph for the given module
+    static std::string AsDotGraph(const Module* mod);
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_DEBUG_H_