Add a `tint_info` command.

This CL adds a second command `tint_info`. The `tint_info` command can
be used to dump information about a WGSL program to the console. This
includes things like the inputs and outputs to an entrypoint along
with structure information like offsets and alignments.

Bug: 1825
Change-Id: Ia2fb4be08b39c1a592f78a388d34edf9af8b6a0e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/118643
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index a8de1d6..93c8f98 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -66,6 +66,8 @@
   diagnostic/formatter.h
   diagnostic/printer.cc
   diagnostic/printer.h
+  text/unicode.cc
+  text/unicode.h
   utils/debugger.cc
   utils/debugger.h
 )
@@ -374,8 +376,6 @@
   symbol.cc
   symbol.h
   tint.cc
-  text/unicode.cc
-  text/unicode.h
   traits.h
   transform/add_empty_entry_point.cc
   transform/add_empty_entry_point.h
diff --git a/src/tint/cmd/BUILD.gn b/src/tint/cmd/BUILD.gn
index e3c73f3..a44829b 100644
--- a/src/tint/cmd/BUILD.gn
+++ b/src/tint/cmd/BUILD.gn
@@ -15,9 +15,19 @@
 import("//build_overrides/build.gni")
 import("../../../tint_overrides_with_defaults.gni")
 
+source_set("tint_cmd_helper") {
+  sources = [
+    "helper.cc",
+    "helper.h",
+  ]
+
+  deps = [ "${tint_root_dir}/src/tint:libtint" ]
+}
+
 executable("tint") {
   sources = [ "main.cc" ]
   deps = [
+    ":tint_cmd_helper",
     "${tint_root_dir}/src/tint:libtint",
     "${tint_root_dir}/src/tint:libtint_base_src",
     "${tint_root_dir}/src/tint:tint_utils_io",
@@ -44,3 +54,27 @@
     configs += [ "//build/config/compiler:no_chromium_code" ]
   }
 }
+
+executable("tint_info") {
+  sources = [ "info.cc" ]
+  deps = [
+    ":tint_cmd_helper",
+    "${tint_root_dir}/src/tint:libtint",
+    "${tint_root_dir}/src/tint:libtint_base_src",
+    "${tint_root_dir}/src/tint:tint_utils_io",
+    "${tint_root_dir}/src/tint:tint_val",
+    "${tint_spirv_tools_dir}/:spvtools",
+    "${tint_spirv_tools_dir}/:spvtools_opt",
+    "${tint_spirv_tools_dir}/:spvtools_val",
+  ]
+
+  configs += [
+    "${tint_root_dir}/src/tint:tint_common_config",
+    "${tint_root_dir}/src/tint:tint_config",
+  ]
+
+  if (build_with_chromium) {
+    configs -= [ "//build/config/compiler:chromium_code" ]
+    configs += [ "//build/config/compiler:no_chromium_code" ]
+  }
+}
diff --git a/src/tint/cmd/CMakeLists.txt b/src/tint/cmd/CMakeLists.txt
index 5c6c6c9..6da9701 100644
--- a/src/tint/cmd/CMakeLists.txt
+++ b/src/tint/cmd/CMakeLists.txt
@@ -12,23 +12,24 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-set(TINT_SRCS
-  main.cc
-)
-
 ## Tint executable
-add_executable(tint ${TINT_SRCS})
+add_executable(tint "")
+target_sources(tint PRIVATE
+  "helper.cc"
+  "helper.h"
+  "main.cc"
+)
 tint_default_compile_options(tint)
 target_link_libraries(tint libtint tint_val)
 
-if(${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
+if (${TINT_BUILD_SPV_READER} OR ${TINT_BUILD_SPV_WRITER})
   target_link_libraries(tint SPIRV-Tools)
 endif()
 
-if(${TINT_BUILD_GLSL_WRITER})
+if (${TINT_BUILD_GLSL_WRITER})
   target_link_libraries(tint glslang)
   target_link_libraries(tint glslang-default-resource-limits)
-  if(NOT MSVC)
+  if (NOT MSVC)
     target_compile_options(tint PRIVATE
       -Wno-reserved-id-macro
       -Wno-shadow-field-in-constructor
@@ -37,3 +38,16 @@
     )
   endif()
 endif()
+
+add_executable(tint_info "")
+target_sources(tint_info PRIVATE
+  "helper.cc"
+  "helper.h"
+  "info.cc"
+)
+tint_default_compile_options(tint_info)
+target_link_libraries(tint_info libtint tint_val)
+
+if (${TINT_BUILD_SPV_READER})
+    target_link_libraries(tint_info SPIRV-Tools)
+endif()
diff --git a/src/tint/cmd/helper.cc b/src/tint/cmd/helper.cc
new file mode 100644
index 0000000..702050e
--- /dev/null
+++ b/src/tint/cmd/helper.cc
@@ -0,0 +1,435 @@
+// Copyright 2023 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/cmd/helper.h"
+
+#include <utility>
+#include <vector>
+
+namespace tint::cmd {
+namespace {
+
+std::string TextureDimensionToString(tint::inspector::ResourceBinding::TextureDimension dim) {
+    switch (dim) {
+        case tint::inspector::ResourceBinding::TextureDimension::kNone:
+            return "None";
+        case tint::inspector::ResourceBinding::TextureDimension::k1d:
+            return "1d";
+        case tint::inspector::ResourceBinding::TextureDimension::k2d:
+            return "2d";
+        case tint::inspector::ResourceBinding::TextureDimension::k2dArray:
+            return "2dArray";
+        case tint::inspector::ResourceBinding::TextureDimension::k3d:
+            return "3d";
+        case tint::inspector::ResourceBinding::TextureDimension::kCube:
+            return "Cube";
+        case tint::inspector::ResourceBinding::TextureDimension::kCubeArray:
+            return "CubeArray";
+    }
+
+    return "Unknown";
+}
+
+std::string SampledKindToString(tint::inspector::ResourceBinding::SampledKind kind) {
+    switch (kind) {
+        case tint::inspector::ResourceBinding::SampledKind::kFloat:
+            return "Float";
+        case tint::inspector::ResourceBinding::SampledKind::kUInt:
+            return "UInt";
+        case tint::inspector::ResourceBinding::SampledKind::kSInt:
+            return "SInt";
+        case tint::inspector::ResourceBinding::SampledKind::kUnknown:
+            break;
+    }
+
+    return "Unknown";
+}
+
+std::string TexelFormatToString(tint::inspector::ResourceBinding::TexelFormat format) {
+    switch (format) {
+        case tint::inspector::ResourceBinding::TexelFormat::kR32Uint:
+            return "R32Uint";
+        case tint::inspector::ResourceBinding::TexelFormat::kR32Sint:
+            return "R32Sint";
+        case tint::inspector::ResourceBinding::TexelFormat::kR32Float:
+            return "R32Float";
+        case tint::inspector::ResourceBinding::TexelFormat::kBgra8Unorm:
+            return "Bgra8Unorm";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Unorm:
+            return "Rgba8Unorm";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Snorm:
+            return "Rgba8Snorm";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Uint:
+            return "Rgba8Uint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Sint:
+            return "Rgba8Sint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRg32Uint:
+            return "Rg32Uint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRg32Sint:
+            return "Rg32Sint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRg32Float:
+            return "Rg32Float";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Uint:
+            return "Rgba16Uint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Sint:
+            return "Rgba16Sint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Float:
+            return "Rgba16Float";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Uint:
+            return "Rgba32Uint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Sint:
+            return "Rgba32Sint";
+        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Float:
+            return "Rgba32Float";
+        case tint::inspector::ResourceBinding::TexelFormat::kNone:
+            return "None";
+    }
+    return "Unknown";
+}
+
+std::string ResourceTypeToString(tint::inspector::ResourceBinding::ResourceType type) {
+    switch (type) {
+        case tint::inspector::ResourceBinding::ResourceType::kUniformBuffer:
+            return "UniformBuffer";
+        case tint::inspector::ResourceBinding::ResourceType::kStorageBuffer:
+            return "StorageBuffer";
+        case tint::inspector::ResourceBinding::ResourceType::kReadOnlyStorageBuffer:
+            return "ReadOnlyStorageBuffer";
+        case tint::inspector::ResourceBinding::ResourceType::kSampler:
+            return "Sampler";
+        case tint::inspector::ResourceBinding::ResourceType::kComparisonSampler:
+            return "ComparisonSampler";
+        case tint::inspector::ResourceBinding::ResourceType::kSampledTexture:
+            return "SampledTexture";
+        case tint::inspector::ResourceBinding::ResourceType::kMultisampledTexture:
+            return "MultisampledTexture";
+        case tint::inspector::ResourceBinding::ResourceType::kWriteOnlyStorageTexture:
+            return "WriteOnlyStorageTexture";
+        case tint::inspector::ResourceBinding::ResourceType::kDepthTexture:
+            return "DepthTexture";
+        case tint::inspector::ResourceBinding::ResourceType::kDepthMultisampledTexture:
+            return "DepthMultisampledTexture";
+        case tint::inspector::ResourceBinding::ResourceType::kExternalTexture:
+            return "ExternalTexture";
+    }
+
+    return "Unknown";
+}
+
+std::string EntryPointStageToString(tint::inspector::PipelineStage stage) {
+    switch (stage) {
+        case tint::inspector::PipelineStage::kVertex:
+            return "Vertex Shader";
+        case tint::inspector::PipelineStage::kFragment:
+            return "Fragment Shader";
+        case tint::inspector::PipelineStage::kCompute:
+            return "Compute Shader";
+    }
+    return "Unknown";
+}
+
+enum class InputFormat {
+    kUnknown,
+    kWgsl,
+    kSpirvBin,
+    kSpirvAsm,
+};
+
+InputFormat InputFormatFromFilename(const std::string& filename) {
+    auto input_format = InputFormat::kUnknown;
+
+    if (filename.size() > 5 && filename.substr(filename.size() - 5) == ".wgsl") {
+        input_format = InputFormat::kWgsl;
+    } else if (filename.size() > 4 && filename.substr(filename.size() - 4) == ".spv") {
+        input_format = InputFormat::kSpirvBin;
+    } else if (filename.size() > 7 && filename.substr(filename.size() - 7) == ".spvasm") {
+        input_format = InputFormat::kSpirvAsm;
+    }
+    return input_format;
+}
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
+    if (!buffer) {
+        std::cerr << "The buffer pointer was null" << std::endl;
+        return false;
+    }
+
+    FILE* file = nullptr;
+#if defined(_MSC_VER)
+    fopen_s(&file, input_file.c_str(), "rb");
+#else
+    file = fopen(input_file.c_str(), "rb");
+#endif
+    if (!file) {
+        std::cerr << "Failed to open " << input_file << std::endl;
+        return false;
+    }
+
+    fseek(file, 0, SEEK_END);
+    const auto file_size = static_cast<size_t>(ftell(file));
+    if (0 != (file_size % sizeof(T))) {
+        std::cerr << "File " << input_file
+                  << " does not contain an integral number of objects: " << file_size
+                  << " bytes in the file, require " << sizeof(T) << " bytes per object"
+                  << std::endl;
+        fclose(file);
+        return false;
+    }
+    fseek(file, 0, SEEK_SET);
+
+    buffer->clear();
+    buffer->resize(file_size / sizeof(T));
+
+    size_t bytes_read = fread(buffer->data(), 1, file_size, file);
+    fclose(file);
+    if (bytes_read != file_size) {
+        std::cerr << "Failed to read " << input_file << std::endl;
+        return false;
+    }
+
+    return true;
+}
+
+void PrintBindings(tint::inspector::Inspector& inspector, const std::string& ep_name) {
+    auto bindings = inspector.GetResourceBindings(ep_name);
+    if (!inspector.error().empty()) {
+        std::cerr << "Failed to get bindings from Inspector: " << inspector.error() << std::endl;
+        exit(1);
+    }
+    for (auto& binding : bindings) {
+        std::cout << "\t[" << binding.bind_group << "][" << binding.binding << "]:" << std::endl;
+        std::cout << "\t\t resource_type = " << ResourceTypeToString(binding.resource_type)
+                  << std::endl;
+        std::cout << "\t\t dim = " << TextureDimensionToString(binding.dim) << std::endl;
+        std::cout << "\t\t sampled_kind = " << SampledKindToString(binding.sampled_kind)
+                  << std::endl;
+        std::cout << "\t\t image_format = " << TexelFormatToString(binding.image_format)
+                  << std::endl;
+
+        std::cout << std::endl;
+    }
+}
+
+}  // namespace
+
+[[noreturn]] void TintInternalCompilerErrorReporter(const tint::diag::List& diagnostics) {
+    auto printer = tint::diag::Printer::create(stderr, true);
+    tint::diag::Formatter{}.format(diagnostics, printer.get());
+    tint::diag::Style bold_red{tint::diag::Color::kRed, true};
+    constexpr const char* please_file_bug = R"(
+********************************************************************
+*  The tint shader compiler has encountered an unexpected error.   *
+*                                                                  *
+*  Please help us fix this issue by submitting a bug report at     *
+*  crbug.com/tint with the source program that triggered the bug.  *
+********************************************************************
+)";
+    printer->write(please_file_bug, bold_red);
+    exit(1);
+}
+
+void PrintWGSL(std::ostream& out, const tint::Program& program) {
+#if TINT_BUILD_WGSL_WRITER
+    tint::writer::wgsl::Options options;
+    auto result = tint::writer::wgsl::Generate(&program, options);
+    out << std::endl << result.wgsl << std::endl;
+#else
+    (void)out;
+    (void)program;
+#endif
+}
+
+ProgramInfo LoadProgramInfo(const LoadProgramOptions& opts) {
+    std::unique_ptr<tint::Program> program;
+    std::unique_ptr<tint::Source::File> source_file;
+
+    auto input_format = InputFormatFromFilename(opts.filename);
+    switch (input_format) {
+        case InputFormat::kUnknown: {
+            std::cerr << "Unknown input format" << std::endl;
+            exit(1);
+        }
+        case InputFormat::kWgsl: {
+#if TINT_BUILD_WGSL_READER
+            std::vector<uint8_t> data;
+            if (!ReadFile<uint8_t>(opts.filename, &data)) {
+                exit(1);
+            }
+            source_file = std::make_unique<tint::Source::File>(
+                opts.filename, std::string(data.begin(), data.end()));
+            program = std::make_unique<tint::Program>(tint::reader::wgsl::Parse(source_file.get()));
+            break;
+#else
+            std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
+            exit(1);
+#endif  // TINT_BUILD_WGSL_READER
+        }
+        case InputFormat::kSpirvBin: {
+#if TINT_BUILD_SPV_READER
+            std::vector<uint32_t> data;
+            if (!ReadFile<uint32_t>(opts.filename, &data)) {
+                exit(1);
+            }
+            program = std::make_unique<tint::Program>(
+                tint::reader::spirv::Parse(data, opts.spirv_reader_options));
+            break;
+#else
+            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+            exit(1);
+#endif  // TINT_BUILD_SPV_READER
+        }
+        case InputFormat::kSpirvAsm: {
+#if TINT_BUILD_SPV_READER
+            std::vector<char> text;
+            if (!ReadFile<char>(opts.filename, &text)) {
+                exit(1);
+            }
+            // Use Vulkan 1.1, since this is what Tint, internally, is expecting.
+            spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
+            tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& pos,
+                                        const char* msg) {
+                std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl;
+            });
+            std::vector<uint32_t> data;
+            if (!tools.Assemble(text.data(), text.size(), &data,
+                                SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
+                exit(1);
+            }
+            program = std::make_unique<tint::Program>(
+                tint::reader::spirv::Parse(data, opts.spirv_reader_options));
+            break;
+#else
+            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
+            exit(1);
+#endif  // TINT_BUILD_SPV_READER
+        }
+    }
+
+    if (!program) {
+        std::cerr << "Failed to parse input file: " << opts.filename << std::endl;
+        exit(1);
+    }
+    if (program->Diagnostics().count() > 0) {
+        if (!program->IsValid() && input_format != InputFormat::kWgsl) {
+            // Invalid program from a non-wgsl source. Print the WGSL, to help
+            // understand the diagnostics.
+            PrintWGSL(std::cout, *program);
+        }
+
+        auto diag_printer = tint::diag::Printer::create(stderr, true);
+        tint::diag::Formatter diag_formatter;
+        diag_formatter.format(program->Diagnostics(), diag_printer.get());
+    }
+
+    if (!program->IsValid()) {
+        exit(1);
+    }
+
+    return ProgramInfo{
+        std::move(program),
+        std::move(source_file),
+    };
+}
+
+void PrintInspectorData(tint::inspector::Inspector& inspector) {
+    auto entry_points = inspector.GetEntryPoints();
+    if (!inspector.error().empty()) {
+        std::cerr << "Failed to get entry points from Inspector: " << inspector.error()
+                  << std::endl;
+        exit(1);
+    }
+
+    for (auto& entry_point : entry_points) {
+        std::cout << "Entry Point = " << entry_point.name << " ("
+                  << EntryPointStageToString(entry_point.stage) << ")" << std::endl;
+
+        if (entry_point.workgroup_size) {
+            std::cout << "  Workgroup Size (" << entry_point.workgroup_size->x << ", "
+                      << entry_point.workgroup_size->y << ", " << entry_point.workgroup_size->z
+                      << ")" << std::endl;
+        }
+
+        if (!entry_point.input_variables.empty()) {
+            std::cout << "  Input Variables:" << std::endl;
+
+            for (const auto& var : entry_point.input_variables) {
+                std::cout << "\t";
+
+                if (var.has_location_attribute) {
+                    std::cout << "@location(" << var.location_attribute << ") ";
+                }
+                std::cout << var.name << std::endl;
+            }
+        }
+        if (!entry_point.output_variables.empty()) {
+            std::cout << "  Output Variables:" << std::endl;
+
+            for (const auto& var : entry_point.output_variables) {
+                std::cout << "\t";
+
+                if (var.has_location_attribute) {
+                    std::cout << "@location(" << var.location_attribute << ") ";
+                }
+                std::cout << var.name << std::endl;
+            }
+        }
+        if (!entry_point.overrides.empty()) {
+            std::cout << "  Overrides:" << std::endl;
+
+            for (const auto& var : entry_point.overrides) {
+                std::cout << "\tname: " << var.name << std::endl;
+                std::cout << "\tid: " << var.id.value << std::endl;
+            }
+        }
+
+        auto bindings = inspector.GetResourceBindings(entry_point.name);
+        if (!inspector.error().empty()) {
+            std::cerr << "Failed to get bindings from Inspector: " << inspector.error()
+                      << std::endl;
+            exit(1);
+        }
+
+        if (!bindings.empty()) {
+            std::cout << "  Bindings:" << std::endl;
+            PrintBindings(inspector, entry_point.name);
+            std::cout << std::endl;
+        }
+
+        std::cout << std::endl;
+    }
+}
+
+void PrintInspectorBindings(tint::inspector::Inspector& inspector) {
+    std::cout << std::string(80, '-') << std::endl;
+    auto entry_points = inspector.GetEntryPoints();
+    if (!inspector.error().empty()) {
+        std::cerr << "Failed to get entry points from Inspector: " << inspector.error()
+                  << std::endl;
+        exit(1);
+    }
+
+    for (auto& entry_point : entry_points) {
+        std::cout << "Entry Point = " << entry_point.name << std::endl;
+        PrintBindings(inspector, entry_point.name);
+    }
+    std::cout << std::string(80, '-') << std::endl;
+}
+
+}  // namespace tint::cmd
diff --git a/src/tint/cmd/helper.h b/src/tint/cmd/helper.h
new file mode 100644
index 0000000..8ce0c7b
--- /dev/null
+++ b/src/tint/cmd/helper.h
@@ -0,0 +1,67 @@
+// Copyright 2023 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_CMD_HELPER_H_
+#define SRC_TINT_CMD_HELPER_H_
+
+#include <memory>
+#include <string>
+
+#include "tint/tint.h"
+
+namespace tint::cmd {
+
+/// Information on a loaded program
+struct ProgramInfo {
+    /// The loaded program
+    std::unique_ptr<tint::Program> program;
+    /// The source file information
+    std::unique_ptr<tint::Source::File> source_file;
+};
+
+/// Reporter callback for internal tint errors
+/// @param diagnostics the diagnostics to emit
+[[noreturn]] void TintInternalCompilerErrorReporter(const tint::diag::List& diagnostics);
+
+/// PrintWGSL writes the WGSL of the program to the provided ostream, if the
+/// WGSL writer is enabled, otherwise it does nothing.
+/// @param out the output stream to write the WGSL to
+/// @param program the program
+void PrintWGSL(std::ostream& out, const tint::Program& program);
+
+/// Prints inspector data information to stderr
+/// @param inspector the inspector to print.
+void PrintInspectorData(tint::inspector::Inspector& inspector);
+
+/// Prints inspector binding information to stderr
+/// @param inspector the inspector to print.
+void PrintInspectorBindings(tint::inspector::Inspector& inspector);
+
+/// Options for the LoadProgramInfo call
+struct LoadProgramOptions {
+    /// The file to be loaded
+    std::string filename;
+#if TINT_BUILD_SPV_READER
+    /// Spirv-reader options
+    tint::reader::spirv::Options spirv_reader_options;
+#endif
+};
+
+/// Loads the source and program information for the given file
+/// @param opts the loading options
+ProgramInfo LoadProgramInfo(const LoadProgramOptions& opts);
+
+}  // namespace tint::cmd
+
+#endif  // SRC_TINT_CMD_HELPER_H_
diff --git a/src/tint/cmd/info.cc b/src/tint/cmd/info.cc
new file mode 100644
index 0000000..50df90f
--- /dev/null
+++ b/src/tint/cmd/info.cc
@@ -0,0 +1,138 @@
+
+// Copyright 2023 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.
+
+#if TINT_BUILD_SPV_READER
+#include "spirv-tools/libspirv.hpp"
+#endif  // TINT_BUILD_SPV_READER
+
+#include "src/tint/ast/module.h"
+#include "src/tint/cmd/helper.h"
+#include "src/tint/type/struct.h"
+#include "src/tint/utils/io/command.h"
+#include "src/tint/utils/string.h"
+#include "src/tint/utils/transform.h"
+#include "src/tint/val/val.h"
+#include "tint/tint.h"
+
+namespace {
+
+struct Options {
+    bool show_help = false;
+
+#if TINT_BUILD_SPV_READER
+    tint::reader::spirv::Options spirv_reader_options;
+#endif
+
+    std::string input_filename;
+};
+
+const char kUsage[] = R"(Usage: tint [options] <input-file>
+
+ options:
+   -h                        -- This help text
+
+)";
+
+bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
+    for (size_t i = 1; i < args.size(); ++i) {
+        const std::string& arg = args[i];
+        if (arg == "-h" || arg == "--help") {
+            opts->show_help = true;
+        } else if (!arg.empty()) {
+            if (arg[0] == '-') {
+                std::cerr << "Unrecognized option: " << arg << std::endl;
+                return false;
+            }
+            if (!opts->input_filename.empty()) {
+                std::cerr << "More than one input file specified: '" << opts->input_filename
+                          << "' and '" << arg << "'" << std::endl;
+                return false;
+            }
+            opts->input_filename = arg;
+        }
+    }
+    return true;
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+    std::vector<std::string> args(argv, argv + argc);
+    Options options;
+
+    tint::SetInternalCompilerErrorReporter(&tint::cmd::TintInternalCompilerErrorReporter);
+
+    if (!ParseArgs(args, &options)) {
+        std::cerr << "Failed to parse arguments." << std::endl;
+        return 1;
+    }
+
+    if (options.show_help) {
+        std::cout << kUsage << std::endl;
+        return 0;
+    }
+
+    auto diag_printer = tint::diag::Printer::create(stderr, true);
+    tint::diag::Formatter diag_formatter;
+
+    std::unique_ptr<tint::Program> program;
+    std::unique_ptr<tint::Source::File> source_file;
+
+    {
+        tint::cmd::LoadProgramOptions opts;
+        opts.filename = options.input_filename;
+#if TINT_BUILD_SPV_READER
+        opts.spirv_reader_options = options.spirv_reader_options;
+#endif
+
+        auto info = tint::cmd::LoadProgramInfo(opts);
+        program = std::move(info.program);
+        source_file = std::move(info.source_file);
+    }
+
+    tint::inspector::Inspector inspector(program.get());
+
+    if (!inspector.GetUsedExtensionNames().empty()) {
+        std::cout << "Extensions:" << std::endl;
+        for (const auto& name : inspector.GetUsedExtensionNames()) {
+            std::cout << "\t" << name << std::endl;
+        }
+    }
+    std::cout << std::endl;
+
+    tint::cmd::PrintInspectorData(inspector);
+
+    bool has_struct = false;
+    for (const auto* ty : program->Types()) {
+        if (!ty->Is<tint::type::Struct>()) {
+            continue;
+        }
+        has_struct = true;
+        break;
+    }
+
+    if (has_struct) {
+        std::cout << "Structures" << std::endl;
+        for (const auto* ty : program->Types()) {
+            if (!ty->Is<tint::type::Struct>()) {
+                continue;
+            }
+            const auto* s = ty->As<tint::type::Struct>();
+            std::cout << s->Layout(program->Symbols()) << std::endl << std::endl;
+        }
+    }
+
+    return 0;
+}
diff --git a/src/tint/cmd/main.cc b/src/tint/cmd/main.cc
index fa8dbc0..49037a7 100644
--- a/src/tint/cmd/main.cc
+++ b/src/tint/cmd/main.cc
@@ -34,6 +34,7 @@
 #endif  // TINT_BUILD_SPV_READER
 
 #include "src/tint/ast/module.h"
+#include "src/tint/cmd/helper.h"
 #include "src/tint/utils/io/command.h"
 #include "src/tint/utils/string.h"
 #include "src/tint/utils/transform.h"
@@ -48,22 +49,6 @@
 
 namespace {
 
-[[noreturn]] void TintInternalCompilerErrorReporter(const tint::diag::List& diagnostics) {
-    auto printer = tint::diag::Printer::create(stderr, true);
-    tint::diag::Formatter{}.format(diagnostics, printer.get());
-    tint::diag::Style bold_red{tint::diag::Color::kRed, true};
-    constexpr const char* please_file_bug = R"(
-********************************************************************
-*  The tint shader compiler has encountered an unexpected error.   *
-*                                                                  *
-*  Please help us fix this issue by submitting a bug report at     *
-*  crbug.com/tint with the source program that triggered the bug.  *
-********************************************************************
-)";
-    printer->write(please_file_bug, bold_red);
-    exit(1);
-}
-
 /// Prints the given hash value in a format string that the end-to-end test runner can parse.
 void PrintHash(uint32_t hash) {
     std::cout << "<<HASH: 0x" << std::hex << hash << ">>" << std::endl;
@@ -298,113 +283,6 @@
     return result;
 }
 
-std::string TextureDimensionToString(tint::inspector::ResourceBinding::TextureDimension dim) {
-    switch (dim) {
-        case tint::inspector::ResourceBinding::TextureDimension::kNone:
-            return "None";
-        case tint::inspector::ResourceBinding::TextureDimension::k1d:
-            return "1d";
-        case tint::inspector::ResourceBinding::TextureDimension::k2d:
-            return "2d";
-        case tint::inspector::ResourceBinding::TextureDimension::k2dArray:
-            return "2dArray";
-        case tint::inspector::ResourceBinding::TextureDimension::k3d:
-            return "3d";
-        case tint::inspector::ResourceBinding::TextureDimension::kCube:
-            return "Cube";
-        case tint::inspector::ResourceBinding::TextureDimension::kCubeArray:
-            return "CubeArray";
-    }
-
-    return "Unknown";
-}
-
-std::string SampledKindToString(tint::inspector::ResourceBinding::SampledKind kind) {
-    switch (kind) {
-        case tint::inspector::ResourceBinding::SampledKind::kFloat:
-            return "Float";
-        case tint::inspector::ResourceBinding::SampledKind::kUInt:
-            return "UInt";
-        case tint::inspector::ResourceBinding::SampledKind::kSInt:
-            return "SInt";
-        case tint::inspector::ResourceBinding::SampledKind::kUnknown:
-            break;
-    }
-
-    return "Unknown";
-}
-
-std::string TexelFormatToString(tint::inspector::ResourceBinding::TexelFormat format) {
-    switch (format) {
-        case tint::inspector::ResourceBinding::TexelFormat::kR32Uint:
-            return "R32Uint";
-        case tint::inspector::ResourceBinding::TexelFormat::kR32Sint:
-            return "R32Sint";
-        case tint::inspector::ResourceBinding::TexelFormat::kR32Float:
-            return "R32Float";
-        case tint::inspector::ResourceBinding::TexelFormat::kBgra8Unorm:
-            return "Bgra8Unorm";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Unorm:
-            return "Rgba8Unorm";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Snorm:
-            return "Rgba8Snorm";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Uint:
-            return "Rgba8Uint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba8Sint:
-            return "Rgba8Sint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRg32Uint:
-            return "Rg32Uint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRg32Sint:
-            return "Rg32Sint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRg32Float:
-            return "Rg32Float";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Uint:
-            return "Rgba16Uint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Sint:
-            return "Rgba16Sint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba16Float:
-            return "Rgba16Float";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Uint:
-            return "Rgba32Uint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Sint:
-            return "Rgba32Sint";
-        case tint::inspector::ResourceBinding::TexelFormat::kRgba32Float:
-            return "Rgba32Float";
-        case tint::inspector::ResourceBinding::TexelFormat::kNone:
-            return "None";
-    }
-    return "Unknown";
-}
-
-std::string ResourceTypeToString(tint::inspector::ResourceBinding::ResourceType type) {
-    switch (type) {
-        case tint::inspector::ResourceBinding::ResourceType::kUniformBuffer:
-            return "UniformBuffer";
-        case tint::inspector::ResourceBinding::ResourceType::kStorageBuffer:
-            return "StorageBuffer";
-        case tint::inspector::ResourceBinding::ResourceType::kReadOnlyStorageBuffer:
-            return "ReadOnlyStorageBuffer";
-        case tint::inspector::ResourceBinding::ResourceType::kSampler:
-            return "Sampler";
-        case tint::inspector::ResourceBinding::ResourceType::kComparisonSampler:
-            return "ComparisonSampler";
-        case tint::inspector::ResourceBinding::ResourceType::kSampledTexture:
-            return "SampledTexture";
-        case tint::inspector::ResourceBinding::ResourceType::kMultisampledTexture:
-            return "MultisampledTexture";
-        case tint::inspector::ResourceBinding::ResourceType::kWriteOnlyStorageTexture:
-            return "WriteOnlyStorageTexture";
-        case tint::inspector::ResourceBinding::ResourceType::kDepthTexture:
-            return "DepthTexture";
-        case tint::inspector::ResourceBinding::ResourceType::kDepthMultisampledTexture:
-            return "DepthMultisampledTexture";
-        case tint::inspector::ResourceBinding::ResourceType::kExternalTexture:
-            return "ExternalTexture";
-    }
-
-    return "Unknown";
-}
-
 bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
     for (size_t i = 1; i < args.size(); ++i) {
         const std::string& arg = args[i];
@@ -565,54 +443,6 @@
     return true;
 }
 
-/// Copies the content from the file named `input_file` to `buffer`,
-/// assuming each element in the file is of type `T`.  If any error occurs,
-/// writes error messages to the standard error stream and returns false.
-/// Assumes the size of a `T` object is divisible by its required alignment.
-/// @returns true if we successfully read the file.
-template <typename T>
-bool ReadFile(const std::string& input_file, std::vector<T>* buffer) {
-    if (!buffer) {
-        std::cerr << "The buffer pointer was null" << std::endl;
-        return false;
-    }
-
-    FILE* file = nullptr;
-#if defined(_MSC_VER)
-    fopen_s(&file, input_file.c_str(), "rb");
-#else
-    file = fopen(input_file.c_str(), "rb");
-#endif
-    if (!file) {
-        std::cerr << "Failed to open " << input_file << std::endl;
-        return false;
-    }
-
-    fseek(file, 0, SEEK_END);
-    const auto file_size = static_cast<size_t>(ftell(file));
-    if (0 != (file_size % sizeof(T))) {
-        std::cerr << "File " << input_file
-                  << " does not contain an integral number of objects: " << file_size
-                  << " bytes in the file, require " << sizeof(T) << " bytes per object"
-                  << std::endl;
-        fclose(file);
-        return false;
-    }
-    fseek(file, 0, SEEK_SET);
-
-    buffer->clear();
-    buffer->resize(file_size / sizeof(T));
-
-    size_t bytes_read = fread(buffer->data(), 1, file_size, file);
-    fclose(file);
-    if (bytes_read != file_size) {
-        std::cerr << "Failed to read " << input_file << std::endl;
-        return false;
-    }
-
-    return true;
-}
-
 /// Writes the given `buffer` into the file named as `output_file` using the
 /// given `mode`.  If `output_file` is empty or "-", writes to standard
 /// output. If any error occurs, returns false and outputs error message to
@@ -694,21 +524,6 @@
 }
 #endif  // TINT_BUILD_SPV_WRITER
 
-/// PrintWGSL writes the WGSL of the program to the provided ostream, if the
-/// WGSL writer is enabled, otherwise it does nothing.
-/// @param out the output stream to write the WGSL to
-/// @param program the program
-void PrintWGSL(std::ostream& out, const tint::Program& program) {
-#if TINT_BUILD_WGSL_WRITER
-    tint::writer::wgsl::Options options;
-    auto result = tint::writer::wgsl::Generate(&program, options);
-    out << std::endl << result.wgsl << std::endl;
-#else
-    (void)out;
-    (void)program;
-#endif
-}
-
 /// Generate SPIR-V code for a program.
 /// @param program the program to generate
 /// @param options the options that Tint was invoked with
@@ -721,7 +536,7 @@
     gen_options.generate_external_texture_bindings = true;
     auto result = tint::writer::spirv::Generate(program, gen_options);
     if (!result.success) {
-        PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, *program);
         std::cerr << "Failed to generate: " << result.error << std::endl;
         return false;
     }
@@ -827,7 +642,7 @@
     gen_options.generate_external_texture_bindings = true;
     auto result = tint::writer::msl::Generate(input_program, gen_options);
     if (!result.success) {
-        PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, *program);
         std::cerr << "Failed to generate: " << result.error << std::endl;
         return false;
     }
@@ -888,7 +703,7 @@
     gen_options.root_constant_binding_point = options.hlsl_root_constant_binding_point;
     auto result = tint::writer::hlsl::Generate(program, gen_options);
     if (!result.success) {
-        PrintWGSL(std::cerr, *program);
+        tint::cmd::PrintWGSL(std::cerr, *program);
         std::cerr << "Failed to generate: " << result.error << std::endl;
         return false;
     }
@@ -1025,7 +840,7 @@
         gen_options.generate_external_texture_bindings = true;
         auto result = tint::writer::glsl::Generate(prg, gen_options, entry_point_name);
         if (!result.success) {
-            PrintWGSL(std::cerr, *prg);
+            tint::cmd::PrintWGSL(std::cerr, *prg);
             std::cerr << "Failed to generate: " << result.error << std::endl;
             return false;
         }
@@ -1087,7 +902,7 @@
     std::vector<std::string> args(argv, argv + argc);
     Options options;
 
-    tint::SetInternalCompilerErrorReporter(&TintInternalCompilerErrorReporter);
+    tint::SetInternalCompilerErrorReporter(&tint::cmd::TintInternalCompilerErrorReporter);
 
 #if TINT_BUILD_WGSL_WRITER
     tint::Program::printer = [](const tint::Program* program) {
@@ -1254,102 +1069,18 @@
     std::unique_ptr<tint::Program> program;
     std::unique_ptr<tint::Source::File> source_file;
 
-    enum class InputFormat {
-        kUnknown,
-        kWgsl,
-        kSpirvBin,
-        kSpirvAsm,
-    };
-    auto input_format = InputFormat::kUnknown;
-
-    if (options.input_filename.size() > 5 &&
-        options.input_filename.substr(options.input_filename.size() - 5) == ".wgsl") {
-        input_format = InputFormat::kWgsl;
-    } else if (options.input_filename.size() > 4 &&
-               options.input_filename.substr(options.input_filename.size() - 4) == ".spv") {
-        input_format = InputFormat::kSpirvBin;
-    } else if (options.input_filename.size() > 7 &&
-               options.input_filename.substr(options.input_filename.size() - 7) == ".spvasm") {
-        input_format = InputFormat::kSpirvAsm;
-    }
-
-    switch (input_format) {
-        case InputFormat::kUnknown: {
-            std::cerr << "Unknown input format" << std::endl;
-            return 1;
-        }
-        case InputFormat::kWgsl: {
-#if TINT_BUILD_WGSL_READER
-            std::vector<uint8_t> data;
-            if (!ReadFile<uint8_t>(options.input_filename, &data)) {
-                return 1;
-            }
-            source_file = std::make_unique<tint::Source::File>(
-                options.input_filename, std::string(data.begin(), data.end()));
-            program = std::make_unique<tint::Program>(tint::reader::wgsl::Parse(source_file.get()));
-            break;
-#else
-            std::cerr << "Tint not built with the WGSL reader enabled" << std::endl;
-            return 1;
-#endif  // TINT_BUILD_WGSL_READER
-        }
-        case InputFormat::kSpirvBin: {
+    {
+        tint::cmd::LoadProgramOptions opts;
+        opts.filename = options.input_filename;
 #if TINT_BUILD_SPV_READER
-            std::vector<uint32_t> data;
-            if (!ReadFile<uint32_t>(options.input_filename, &data)) {
-                return 1;
-            }
-            program = std::make_unique<tint::Program>(
-                tint::reader::spirv::Parse(data, options.spirv_reader_options));
-            break;
-#else
-            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
-            return 1;
-#endif  // TINT_BUILD_SPV_READER
-        }
-        case InputFormat::kSpirvAsm: {
-#if TINT_BUILD_SPV_READER
-            std::vector<char> text;
-            if (!ReadFile<char>(options.input_filename, &text)) {
-                return 1;
-            }
-            // Use Vulkan 1.1, since this is what Tint, internally, is expecting.
-            spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
-            tools.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t& pos,
-                                        const char* msg) {
-                std::cerr << (pos.line + 1) << ":" << (pos.column + 1) << ": " << msg << std::endl;
-            });
-            std::vector<uint32_t> data;
-            if (!tools.Assemble(text.data(), text.size(), &data,
-                                SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)) {
-                return 1;
-            }
-            program = std::make_unique<tint::Program>(
-                tint::reader::spirv::Parse(data, options.spirv_reader_options));
-            break;
-#else
-            std::cerr << "Tint not built with the SPIR-V reader enabled" << std::endl;
-            return 1;
-#endif  // TINT_BUILD_SPV_READER
-        }
+        opts.spirv_reader_options = options.spirv_reader_options;
+#endif
+
+        auto info = tint::cmd::LoadProgramInfo(opts);
+        program = std::move(info.program);
+        source_file = std::move(info.source_file);
     }
 
-    if (!program) {
-        std::cerr << "Failed to parse input file: " << options.input_filename << std::endl;
-        return 1;
-    }
-    if (program->Diagnostics().count() > 0) {
-        if (!program->IsValid() && input_format != InputFormat::kWgsl) {
-            // Invalid program from a non-wgsl source. Print the WGSL, to help
-            // understand the diagnostics.
-            PrintWGSL(std::cout, *program);
-        }
-        diag_formatter.format(program->Diagnostics(), diag_printer.get());
-    }
-
-    if (!program->IsValid()) {
-        return 1;
-    }
     if (options.parse_only) {
         return 1;
     }
@@ -1374,37 +1105,8 @@
 #endif  // TINT_BUILD_IR
 
     tint::inspector::Inspector inspector(program.get());
-
     if (options.dump_inspector_bindings) {
-        std::cout << std::string(80, '-') << std::endl;
-        auto entry_points = inspector.GetEntryPoints();
-        if (!inspector.error().empty()) {
-            std::cerr << "Failed to get entry points from Inspector: " << inspector.error()
-                      << std::endl;
-            return 1;
-        }
-
-        for (auto& entry_point : entry_points) {
-            auto bindings = inspector.GetResourceBindings(entry_point.name);
-            if (!inspector.error().empty()) {
-                std::cerr << "Failed to get bindings from Inspector: " << inspector.error()
-                          << std::endl;
-                return 1;
-            }
-            std::cout << "Entry Point = " << entry_point.name << std::endl;
-            for (auto& binding : bindings) {
-                std::cout << "\t[" << binding.bind_group << "][" << binding.binding
-                          << "]:" << std::endl;
-                std::cout << "\t\t resource_type = " << ResourceTypeToString(binding.resource_type)
-                          << std::endl;
-                std::cout << "\t\t dim = " << TextureDimensionToString(binding.dim) << std::endl;
-                std::cout << "\t\t sampled_kind = " << SampledKindToString(binding.sampled_kind)
-                          << std::endl;
-                std::cout << "\t\t image_format = " << TexelFormatToString(binding.image_format)
-                          << std::endl;
-            }
-        }
-        std::cout << std::string(80, '-') << std::endl;
+        tint::cmd::PrintInspectorBindings(inspector);
     }
 
     tint::transform::Manager transform_manager;
@@ -1485,7 +1187,7 @@
 
     auto out = transform_manager.Run(program.get(), std::move(transform_inputs));
     if (!out.program.IsValid()) {
-        PrintWGSL(std::cerr, out.program);
+        tint::cmd::PrintWGSL(std::cerr, out.program);
         diag_formatter.format(out.program.Diagnostics(), diag_printer.get());
         return 1;
     }