소스 검색

Switch the toolchain to the new CLI library. (#2979)

This also tries to restructure the command line interface to the
toolchain a bit to make it start operating more like a compiler that
could be integrated into a build system rather than primarily as
a testing tool.

1) This switches form a `dump` subcommand to a `compile` subcommand
   which has "dump" actions that can be enabled within it.

2) A distinct set of compile _phases_ that match the toolchain
   structure:
   - `lex` to run the lexer
   - `parse` to run the parser
   - `check` to fully check that the code is valid
   - `lower` to lower to LLVM's IR
   - `codegen` to generate executable code

3) The codegen phase has two output formats: textual assembly and
   a binary object. These outputs can be configured, with a default for
   an object when writing to a file and more firm default for textual
   assembly when writing to stdout.

4) Select and expose the use of the LLVM host detection to compute
   a default code generation target in the driver so that the command
   line interface can reflect this. For example, the `help` output will
   include the default target.

5) The `//toolchain/codegen` library APIs have been restructured a bit
   to make the code flow a bit more naturally when implementing the new
   command line structure. No real changes to the logic though.

There are also some minor tweaks to the command line interface based on
trying to use the shortest names for things that still seem likely to be
learnable for users:

- Switched `target-triple` to just `target`: the "triple" component to
  this name is historical and can be confusing. For example, almost all
  "triple" strings have more than three components today.

- Switched to just `--output` as now the fact that it is a file can be
  configured in the documentation -- it will render as `--output=FILE`.

This also adds support for two custom output filename modes. First, when
no output is specified, we now compute one in the conventional way for
compilers by removing the file extension of the input file and replacing
it with `.o` for an object file output or `.s` for an assembly file
output. This matches the behavior of Clang and GCC for example.

Second, output to stdout is enabled with the special output file name of
`-` since it is no longer the default. This also follows the convention
of most compilers and many other command line tools to use `-` as a file
name to signify using standard in/out pipes.

There are still some rough edges here that I suspect could be improved,
but this seems like a good start of switching over to a complete
argument parser.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Co-authored-by: Lucile Rose Nihlen <luci.the.rose@gmail.com>
Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Chandler Carruth 2 년 전
부모
커밋
a2b4cabeaa

+ 26 - 40
toolchain/codegen/codegen.cpp

@@ -15,7 +15,9 @@
 
 namespace Carbon {
 
-auto CodeGen::CreateTargetMachine() -> std::unique_ptr<llvm::TargetMachine> {
+auto CodeGen::Create(llvm::Module& module, llvm::StringRef target_triple,
+                     llvm::raw_pwrite_stream& errors)
+    -> std::optional<CodeGen> {
   // Initialize the target registry etc.
   llvm::InitializeAllTargetInfos();
   llvm::InitializeAllTargets();
@@ -24,66 +26,50 @@ auto CodeGen::CreateTargetMachine() -> std::unique_ptr<llvm::TargetMachine> {
   llvm::InitializeAllAsmPrinters();
 
   std::string error;
-  llvm::StringRef triple = target_triple;
-  std::string host_triple;
-  if (target_triple.empty()) {
-    host_triple = llvm::sys::getDefaultTargetTriple();
-    triple = host_triple;
-  }
-  const auto* target = llvm::TargetRegistry::lookupTarget(triple, error);
+  const llvm::Target* target =
+      llvm::TargetRegistry::lookupTarget(target_triple, error);
 
   if (!target) {
-    error_stream_ << "ERROR: Invalid -target_triple: " << error << "\n";
-    return nullptr;
+    errors << "ERROR: Invalid target: " << error << "\n";
+    return {};
   }
+  module.setTargetTriple(target_triple);
 
   constexpr llvm::StringLiteral CPU = "generic";
   constexpr llvm::StringLiteral Features = "";
 
   llvm::TargetOptions target_opts;
   std::optional<llvm::Reloc::Model> reloc_model;
-  std::unique_ptr<llvm::TargetMachine> target_machine(
-      target->createTargetMachine(target_triple, CPU, Features, target_opts,
-                                  reloc_model));
-  return target_machine;
+  CodeGen codegen(module, errors);
+  codegen.target_machine_.reset(target->createTargetMachine(
+      target_triple, CPU, Features, target_opts, reloc_model));
+  return codegen;
+}
+
+auto CodeGen::EmitAssembly(llvm::raw_pwrite_stream& out) -> bool {
+  return EmitCode(out, llvm::CodeGenFileType::CGFT_AssemblyFile);
 }
 
-auto CodeGen::EmitCode(llvm::raw_pwrite_stream& dest,
-                       llvm::TargetMachine* target_machine,
+auto CodeGen::EmitObject(llvm::raw_pwrite_stream& out) -> bool {
+  return EmitCode(out, llvm::CodeGenFileType::CGFT_ObjectFile);
+}
+
+auto CodeGen::EmitCode(llvm::raw_pwrite_stream& out,
                        llvm::CodeGenFileType file_type) -> bool {
-  Module.setDataLayout(target_machine->createDataLayout());
-  Module.setTargetTriple(target_triple);
+  module_.setDataLayout(target_machine_->createDataLayout());
 
   // Using the legacy PM to generate the assembly since the new PM
   // does not work with this yet.
   // TODO: make the new PM work with the codegen pipeline.
-
   llvm::legacy::PassManager pass;
-
-  if (target_machine->addPassesToEmitFile(pass, dest, nullptr, file_type)) {
-    error_stream_ << "Error: Nothing to write to object file\n";
+  // Note that this returns true on an error.
+  if (target_machine_->addPassesToEmitFile(pass, out, nullptr, file_type)) {
+    errors_ << "ERROR: Unable to emit to this file.\n";
     return false;
   }
 
-  pass.run(Module);
+  pass.run(module_);
   return true;
 }
 
-auto CodeGen::PrintAssembly() -> bool {
-  auto target_machine = CreateTargetMachine();
-  if (target_machine == nullptr) {
-    return false;
-  }
-  return EmitCode(output_stream_, target_machine.get(),
-                  llvm::CodeGenFileType::CGFT_AssemblyFile);
-}
-
-auto CodeGen::GenerateObjectCode() -> bool {
-  auto target_machine = CreateTargetMachine();
-  if (target_machine == nullptr) {
-    return false;
-  }
-  return EmitCode(output_stream_, target_machine.get(),
-                  llvm::CodeGenFileType::CGFT_ObjectFile);
-}
 }  // namespace Carbon

+ 18 - 21
toolchain/codegen/codegen.h

@@ -14,41 +14,38 @@ namespace Carbon {
 
 class CodeGen {
  public:
-  CodeGen(llvm::Module& module, llvm::StringRef triple,
-          llvm::raw_pwrite_stream& error_stream,
-          llvm::raw_pwrite_stream& output_stream)
-      : Module(module),
-        output_stream_(output_stream),
-        error_stream_(error_stream),
-        target_triple(triple){};
+  static auto Create(llvm::Module& module, llvm::StringRef target_triple,
+                     llvm::raw_pwrite_stream& errors) -> std::optional<CodeGen>;
 
   // Generates the object code file.
   // Returns false in case of failure, and any information about the failure is
   // printed to the error stream.
-  auto GenerateObjectCode() -> bool;
+  //
+  // Note that unlike the error stream, this requires a `pwrite` stream to allow
+  // patching the output.
+  auto EmitObject(llvm::raw_pwrite_stream& out) -> bool;
 
   // Prints the assembly to stdout.
   // Returns false in case of failure, and any information about the failure is
   // printed to the error stream.
-  auto PrintAssembly() -> bool;
+  //
+  // Note that unlike the error stream, this requires a `pwrite` stream to allow
+  // patching the output.
+  auto EmitAssembly(llvm::raw_pwrite_stream& out) -> bool;
 
  private:
-  llvm::Module& Module;
-  llvm::raw_pwrite_stream& output_stream_;
-  llvm::raw_pwrite_stream& error_stream_;
-  llvm::StringRef target_triple;
-
-  // Creates the target machine for triple.
-  // Returns nullptr in case of failure, and any information about the failure
-  // is printed to the error stream.
-  auto CreateTargetMachine() -> std::unique_ptr<llvm::TargetMachine>;
+  explicit CodeGen(llvm::Module& module, llvm::raw_pwrite_stream& errors)
+      : module_(module), errors_(errors) {}
 
   // Using the llvm pass emits either assembly or object code to dest.
   // Returns false in case of failure, and any information about the failure is
   // printed to the error stream.
-  auto EmitCode(llvm::raw_pwrite_stream& dest,
-                llvm::TargetMachine* target_machine,
-                llvm::CodeGenFileType file_type) -> bool;
+  auto EmitCode(llvm::raw_pwrite_stream& out, llvm::CodeGenFileType file_type)
+      -> bool;
+
+  llvm::Module& module_;
+  llvm::raw_pwrite_stream& errors_;
+  std::unique_ptr<llvm::TargetMachine> target_machine_;
 };
 
 }  // namespace Carbon

+ 1 - 1
toolchain/codegen/testdata/assembly/basic.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump assembly --target_triple=x86_64-unknown-linux-gnu %s
+// ARGS: compile --target=x86_64-unknown-linux-gnu --output=- %s
 // NOAUTOUPDATE
 // SET-CHECK-SUBSET
 // CHECK:STDOUT: Main:

+ 2 - 2
toolchain/codegen/testdata/assembly/fail_target_triple.carbon → toolchain/codegen/testdata/fail_target_triple.carbon

@@ -2,9 +2,9 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump assembly --target_triple=x86_687-unknown-linux-gnu %s
+// ARGS: compile --target=x86_687-unknown-linux-gnu --output=- %s
 // No autoupdate because the message comes from LLVM.
 // NOAUTOUPDATE
-// CHECK:STDERR: ERROR: Invalid -target_triple:{{.*}}
+// CHECK:STDERR: ERROR: Invalid target: {{.*}}x86_687{{.*}}
 
 fn Main() -> i32 { return 0; }

+ 3 - 3
toolchain/codegen/testdata/objcode/basic.carbon

@@ -2,9 +2,9 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump objcode --target_triple=x86_64-unknown-linux-gnu --output_file=%t %s
+// ARGS: compile --target=x86_64-unknown-linux-gnu --output=%t %s
+//
+// TODO: Add a way to write some basic tests for object file outputs.
 // AUTOUPDATE
 
 fn Main() -> i32 { return 0; }
-
-// CHECK:STDOUT: Success: Object file is generated!

+ 0 - 9
toolchain/codegen/testdata/objcode/fail_no_input_file.carbon

@@ -1,9 +0,0 @@
-// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
-// Exceptions. See /LICENSE for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-// ARGS: dump objcode --output_file=%t --target_triple=x86_64-unknown-linux-gnu
-// AUTOUPDATE
-// CHECK:STDERR: ERROR: No input file specified.
-
-fn Main() -> i32 { return 0; }

+ 0 - 9
toolchain/codegen/testdata/objcode/fail_no_output_file.carbon

@@ -1,9 +0,0 @@
-// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
-// Exceptions. See /LICENSE for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-// ARGS: dump objcode --target_triple=x86_64-unknown-linux-gnu %s
-// AUTOUPDATE
-// CHECK:STDERR: ERROR: Must provide an output file.
-
-fn Main() -> i32 { return 0; }

+ 0 - 10
toolchain/codegen/testdata/objcode/fail_target_triple.carbon

@@ -1,10 +0,0 @@
-// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
-// Exceptions. See /LICENSE for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-// ARGS: dump objcode --target_triple=x86_684-unknown-linux-gnu --output_file=%t %s
-// No autoupdate because the message comes from LLVM.
-// NOAUTOUPDATE
-// CHECK:STDERR: ERROR: Invalid -target_triple:{{.*}}
-
-fn Main() -> i32 { return 0; }

+ 3 - 0
toolchain/driver/BUILD

@@ -14,6 +14,7 @@ cc_library(
     hdrs = ["driver.h"],
     textual_hdrs = ["flags.def"],
     deps = [
+        "//common:command_line",
         "//common:vlog",
         "//toolchain/codegen",
         "//toolchain/diagnostics:diagnostic_emitter",
@@ -26,6 +27,7 @@ cc_library(
         "//toolchain/source:source_buffer",
         "@llvm-project//llvm:Core",
         "@llvm-project//llvm:Support",
+        "@llvm-project//llvm:TargetParser",
     ],
 )
 
@@ -51,6 +53,7 @@ cc_test(
         "//toolchain/diagnostics:diagnostic_emitter",
         "//toolchain/lexer:tokenized_buffer_test_helpers",
         "@com_google_googletest//:gtest",
+        "@llvm-project//llvm:Object",
         "@llvm-project//llvm:Support",
     ],
 )

+ 418 - 202
toolchain/driver/driver.cpp

@@ -4,6 +4,7 @@
 
 #include "toolchain/driver/driver.h"
 
+#include "common/command_line.h"
 #include "common/vlog.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/ScopeExit.h"
@@ -12,6 +13,8 @@
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/Support/Format.h"
+#include "llvm/Support/Path.h"
+#include "llvm/TargetParser/Host.h"
 #include "toolchain/codegen/codegen.h"
 #include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/diagnostics/sorting_diagnostic_consumer.h"
@@ -24,261 +27,451 @@
 
 namespace Carbon {
 
-namespace {
+struct Driver::CompileOptions {
+  static constexpr CommandLine::CommandInfo Info = {
+      .name = "compile",
+      .help = R"""(
+Compile Carbon source code.
 
-enum class Subcommand {
-#define CARBON_SUBCOMMAND(Name, ...) Name,
-#include "toolchain/driver/flags.def"
-  Unknown,
-};
+This subcommand runs the Carbon compiler over input source code, checking it for
+errors and producing the requested output.
 
-auto GetSubcommand(llvm::StringRef name) -> Subcommand {
-  return llvm::StringSwitch<Subcommand>(name)
-#define CARBON_SUBCOMMAND(Name, Spelling, ...) .Case(Spelling, Subcommand::Name)
-#include "toolchain/driver/flags.def"
-      .Default(Subcommand::Unknown);
-}
+Error messages are written to the standard error stream.
 
-}  // namespace
+Different phases of the compiler can be selected to run, and intermediate state
+can be written to standard output as these phases progress.
+)""",
+  };
 
-auto Driver::RunFullCommand(llvm::ArrayRef<llvm::StringRef> args) -> bool {
-  StreamDiagnosticConsumer stream_consumer(error_stream_);
-  DiagnosticConsumer* consumer = &stream_consumer;
-  std::unique_ptr<SortingDiagnosticConsumer> sorting_consumer;
-  // TODO: Figure out a command-line support library, this is temporary.
-  if (!args.empty() && args[0] == "-v") {
-    args = args.drop_front();
-    // Note this implies streamed output in order to interleave.
-    vlog_stream_ = &error_stream_;
-  } else if (!args.empty() && args[0] == "--print-errors=streamed") {
-    args = args.drop_front();
-  } else {
-    sorting_consumer = std::make_unique<SortingDiagnosticConsumer>(*consumer);
-    consumer = sorting_consumer.get();
-  }
+  enum class Phase {
+    Lex,
+    Parse,
+    Check,
+    Lower,
+    CodeGen,
+  };
 
-  if (args.empty()) {
-    error_stream_ << "ERROR: No subcommand specified.\n";
-    return false;
+  friend auto operator<<(llvm::raw_ostream& out, Phase phase)
+      -> llvm::raw_ostream& {
+    switch (phase) {
+      case Phase::Lex:
+        out << "lex";
+        break;
+      case Phase::Parse:
+        out << "parse";
+        break;
+      case Phase::Check:
+        out << "check";
+        break;
+      case Phase::Lower:
+        out << "lower";
+        break;
+      case Phase::CodeGen:
+        out << "codegen";
+        break;
+    }
+    return out;
   }
 
-  llvm::StringRef subcommand_text = args[0];
-  args = args.drop_front();
-  switch (GetSubcommand(subcommand_text)) {
-    case Subcommand::Unknown:
-      error_stream_ << "ERROR: Unknown subcommand '" << subcommand_text
-                    << "'.\n";
-      return false;
-
-#define CARBON_SUBCOMMAND(Name, ...) \
-  case Subcommand::Name:             \
-    return Run##Name##Subcommand(*consumer, args);
-#include "toolchain/driver/flags.def"
+  void Build(CommandLine::CommandBuilder& b) {
+    b.AddStringPositionalArg(
+        {
+            .name = "FILE",
+            .help = R"""(
+The input Carbon source file to compile.
+)""",
+        },
+        [&](auto& arg_b) {
+          arg_b.Required(true);
+          arg_b.Set(&input_file_name);
+        });
+
+    b.AddOneOfOption(
+        {
+            .name = "phase",
+            .help = R"""(
+Selects the compilation phase to run. These phases are always run in sequence,
+so every phase before the one selected will also be run. The default is to
+compile to machine code.
+)""",
+        },
+        [&](auto& arg_b) {
+          arg_b.SetOneOf(
+              {
+                  arg_b.OneOfValue("lex", Phase::Lex),
+                  arg_b.OneOfValue("parse", Phase::Parse),
+                  arg_b.OneOfValue("check", Phase::Check),
+                  arg_b.OneOfValue("lower", Phase::Lower),
+                  arg_b.OneOfValue("codegen", Phase::CodeGen).Default(true),
+              },
+              &phase);
+        });
+
+    // TODO: Rearrange the code setting this option and two related ones to
+    // allow them to reference each other instead of hard-coding their names.
+    b.AddStringOption(
+        {
+            .name = "output",
+            .value_name = "FILE",
+            .help = R"""(
+The output filename for codegen.
+
+When this is a file name, either textual assembly or a binary object will be
+written to it based on the flag `--asm-output`. The default is to write a binary
+object file.
+
+Passing `--output=-` will write the output to stdout. In that
+case, the flag `--asm-output` is ignored and the output defaults to textual
+assembly. Binary object output can be forced by enabling `--force-obj-output`.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&output_file_name); });
+
+    b.AddStringOption(
+        {
+            .name = "target",
+            .help = R"""(
+Select a target platform. Uses the LLVM target syntax. Also known as a "triple"
+for historical reasons.
+
+This corresponds to the `target` flag to Clang and accepts the same strings
+documented there:
+https://clang.llvm.org/docs/CrossCompilation.html#target-triple
+)""",
+        },
+        [&](auto& arg_b) {
+          arg_b.Default(host);
+          arg_b.Set(&target);
+        });
+
+    b.AddFlag(
+        {
+            .name = "asm-output",
+            .help = R"""(
+Write textual assembly rather than a binary object file to the code generation
+output.
+
+This flag only applies when writing to a file. When writing to stdout, the
+default is textual assembly and this flag is ignored.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&asm_output); });
+
+    b.AddFlag(
+        {
+            .name = "force-obj-output",
+            .help = R"""(
+Force binary object output, even with `--output=-`.
+
+When `--output=-` is set, the default is textual assembly; this forces printing
+of a binary object file instead. Ignored for other `--output` values.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&force_obj_output); });
+
+    b.AddFlag(
+        {
+            .name = "stream-errors",
+            .help = R"""(
+Stream error messages to stderr as they are generated rather than sorting them
+and displaying them in source order.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&stream_errors); });
+
+    b.AddFlag(
+        {
+            .name = "dump-tokens",
+            .help = R"""(
+Dump the tokens to stdout when lexed.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_tokens); });
+    b.AddFlag(
+        {
+            .name = "dump-parse-tree",
+            .help = R"""(
+Dump the parse tree to stdout when parsed.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_parse_tree); });
+    b.AddFlag(
+        {
+            .name = "preorder-parse-tree",
+            .help = R"""(
+When dumping the parse tree, reorder it so that it is in preorder rather than
+postorder.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&preorder_parse_tree); });
+    b.AddFlag(
+        {
+            .name = "dump-raw-semantics-ir",
+            .help = R"""(
+Dump the raw JSON structure of semantics IR to stdout when built.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_raw_semantics_ir); });
+    b.AddFlag(
+        {
+            .name = "dump-semantics-ir",
+            .help = R"""(
+Dump the semantics IR to stdout when built.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_semantics_ir); });
+    b.AddFlag(
+        {
+            .name = "builtin-semantics-ir",
+            .help = R"""(
+Include the semantics IR for builtins when dumping it.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&builtin_semantics_ir); });
+    b.AddFlag(
+        {
+            .name = "dump-llvm-ir",
+            .help = R"""(
+Dump the LLVM IR to stdout after lowering.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_llvm_ir); });
+    b.AddFlag(
+        {
+            .name = "dump-asm",
+            .help = R"""(
+Dump the generated assembly to stdout after codegen.
+)""",
+        },
+        [&](auto& arg_b) { arg_b.Set(&dump_asm); });
   }
-  llvm_unreachable("All subcommands handled!");
-}
 
-auto Driver::RunHelpSubcommand(DiagnosticConsumer& /*consumer*/,
-                               llvm::ArrayRef<llvm::StringRef> args) -> bool {
-  // TODO: We should support getting detailed help on a subcommand by looking
-  // for it as a positional parameter here.
-  if (!args.empty()) {
-    ReportExtraArgs("help", args);
-    return false;
-  }
-
-  output_stream_ << "List of subcommands:\n\n";
+  Phase phase;
+
+  std::string host = llvm::sys::getDefaultTargetTriple();
+  llvm::StringRef target;
+
+  llvm::StringRef output_file_name;
+  llvm::StringRef input_file_name;
+
+  bool asm_output = false;
+  bool force_obj_output = false;
+  bool dump_tokens = false;
+  bool dump_parse_tree = false;
+  bool dump_raw_semantics_ir = false;
+  bool dump_semantics_ir = false;
+  bool dump_llvm_ir = false;
+  bool dump_asm = false;
+  bool stream_errors = false;
+  bool preorder_parse_tree = false;
+  bool builtin_semantics_ir = false;
+};
 
-  constexpr llvm::StringLiteral SubcommandsAndHelp[][2] = {
-#define CARBON_SUBCOMMAND(Name, Spelling, HelpText) {Spelling, HelpText},
-#include "toolchain/driver/flags.def"
+struct Driver::Options {
+  static constexpr CommandLine::CommandInfo Info = {
+      .name = "carbon",
+      // TODO: Setup more detailed version information and use that here.
+      .version = R"""(
+Carbon Language toolchain -- version 0.0.0
+)""",
+      .help = R"""(
+This is the unified Carbon Language toolchain driver. It's subcommands provide
+all of the core behavior of the toolchain, including compilation, linking, and
+developer tools. Each of these has its own subcommand, and you can pass a
+specific subcommand to the `help` subcommand to get details about is usage.
+)""",
+      .help_epilogue = R"""(
+For questions, issues, or bug reports, please use our GitHub project:
+
+  https://github.com/carbon-language/carbon-lang
+)""",
   };
 
-  int max_subcommand_width = 0;
-  for (const auto* subcommand_and_help : SubcommandsAndHelp) {
-    max_subcommand_width = std::max(
-        max_subcommand_width, static_cast<int>(subcommand_and_help[0].size()));
-  }
+  enum class Subcommand {
+    Compile,
+  };
 
-  for (const auto* subcommand_and_help : SubcommandsAndHelp) {
-    llvm::StringRef subcommand_text = subcommand_and_help[0];
-    // TODO: We should wrap this to the number of columns left after the
-    // subcommand on the terminal, and using a hanging indent.
-    llvm::StringRef help_text = subcommand_and_help[1];
-    output_stream_ << "  "
-                   << llvm::left_justify(subcommand_text, max_subcommand_width)
-                   << " - " << help_text << "\n";
+  void Build(CommandLine::CommandBuilder& b) {
+    b.AddFlag(
+        {
+            .name = "verbose",
+            .short_name = "v",
+            .help = "Enable verbose logging to the stderr stream.",
+        },
+        [&](CommandLine::FlagBuilder& arg_b) { arg_b.Set(&verbose); });
+
+    b.AddSubcommand(CompileOptions::Info,
+                    [&](CommandLine::CommandBuilder& sub_b) {
+                      compile_options.Build(sub_b);
+                      sub_b.Do([&] { subcommand = Subcommand::Compile; });
+                    });
+
+    b.RequiresSubcommand();
   }
 
-  output_stream_ << "\n";
-  return true;
-}
+  bool verbose;
+  Subcommand subcommand;
 
-enum class DumpMode {
-  TokenizedBuffer,
-  ParseTree,
-  RawSemanticsIR,
-  SemanticsIR,
-  LLVMIR,
-  Assembly,
-  ObjectCode,
-  Unknown
+  CompileOptions compile_options;
 };
 
-auto Driver::RunDumpSubcommand(DiagnosticConsumer& consumer,
-                               llvm::ArrayRef<llvm::StringRef> args) -> bool {
-  if (args.empty()) {
-    error_stream_ << "ERROR: No dump mode specified.\n";
-    return false;
-  }
+auto Driver::ParseArgs(llvm::ArrayRef<llvm::StringRef> args, Options& options)
+    -> CommandLine::ParseResult {
+  return CommandLine::Parse(
+      args, output_stream_, error_stream_, Options::Info,
+      [&](CommandLine::CommandBuilder& b) { options.Build(b); });
+}
 
-  auto dump_mode = llvm::StringSwitch<DumpMode>(args.front())
-                       .Case("tokens", DumpMode::TokenizedBuffer)
-                       .Case("parse-tree", DumpMode::ParseTree)
-                       .Case("raw-semantics-ir", DumpMode::RawSemanticsIR)
-                       .Case("semantics-ir", DumpMode::SemanticsIR)
-                       .Case("llvm-ir", DumpMode::LLVMIR)
-                       .Case("assembly", DumpMode::Assembly)
-                       .Case("objcode", DumpMode::ObjectCode)
-                       .Default(DumpMode::Unknown);
-  if (dump_mode == DumpMode::Unknown) {
-    error_stream_ << "ERROR: Dump mode should be one of tokens, parse-tree, "
-                     "semantics-ir, llvm-ir, assembly, or objcode.\n";
+auto Driver::RunCommand(llvm::ArrayRef<llvm::StringRef> args) -> bool {
+  Options options;
+  CommandLine::ParseResult result = ParseArgs(args, options);
+  if (result == CommandLine::ParseResult::Error) {
     return false;
+  } else if (result == CommandLine::ParseResult::MetaSuccess) {
+    return true;
   }
-  args = args.drop_front();
 
-  bool parse_tree_preorder = false;
-  if (dump_mode == DumpMode::ParseTree && !args.empty() &&
-      args.front() == "--preorder") {
-    args = args.drop_front();
-    parse_tree_preorder = true;
-  }
-
-  bool semantics_ir_include_raw = false;
-  if (dump_mode == DumpMode::SemanticsIR && !args.empty() &&
-      args.front() == "--include_raw") {
-    args = args.drop_front();
-    semantics_ir_include_raw = true;
-  }
-
-  bool semantics_ir_include_builtins = false;
-  if ((dump_mode == DumpMode::RawSemanticsIR || semantics_ir_include_raw) &&
-      !args.empty() && args.front() == "--include_builtins") {
-    args = args.drop_front();
-    semantics_ir_include_builtins = true;
+  if (options.verbose) {
+    // Note this implies streamed output in order to interleave.
+    vlog_stream_ = &error_stream_;
   }
 
-  llvm::StringRef target_triple;
-  if (dump_mode == DumpMode::Assembly && !args.empty() &&
-      args.front().starts_with("--target_triple=")) {
-    target_triple = args.front().split("=").second;
-    args = args.drop_front();
+  switch (options.subcommand) {
+    case Options::Subcommand::Compile:
+      return Compile(options.compile_options);
   }
+  llvm_unreachable("All subcommands handled!");
+}
 
-  llvm::StringRef output_file;
-  if (dump_mode == DumpMode::ObjectCode) {
-    while (!args.empty()) {
-      if (args.front().starts_with("--target_triple=")) {
-        target_triple = args.front().split("=").second;
-        args = args.drop_front();
-      } else if (args.front().starts_with("--output_file=")) {
-        output_file = args.front().split("=").second;
-        args = args.drop_front();
-      } else {
-        break;
+auto Driver::ValidateCompileOptions(const CompileOptions& options) const
+    -> bool {
+  using Phase = CompileOptions::Phase;
+  switch (options.phase) {
+    case Phase::Lex:
+      if (options.dump_parse_tree) {
+        error_stream_ << "ERROR: Requested dumping the parse tree but compile "
+                         "phase is limited to '"
+                      << options.phase << "'\n";
+        return false;
       }
-    }
-
-    if (output_file.empty()) {
-      error_stream_ << "ERROR: Must provide an output file.\n";
-      return false;
-    }
+      [[clang::fallthrough]];
+    case Phase::Parse:
+      if (options.dump_semantics_ir) {
+        error_stream_ << "ERROR: Requested dumping the semantics IR but "
+                         "compile phase is limited to '"
+                      << options.phase << "'\n";
+        return false;
+      }
+      [[clang::fallthrough]];
+    case Phase::Check:
+      if (options.dump_llvm_ir) {
+        error_stream_ << "ERROR: Requested dumping the LLVM IR but compile "
+                         "phase is limited to '"
+                      << options.phase << "'\n";
+        return false;
+      }
+      [[clang::fallthrough]];
+    case Phase::Lower:
+    case Phase::CodeGen:
+      // Everything can be dumped in these phases.
+      break;
   }
+  return true;
+}
+
+auto Driver::Compile(const CompileOptions& options) -> bool {
+  using Phase = CompileOptions::Phase;
 
-  if (args.empty()) {
-    error_stream_ << "ERROR: No input file specified.\n";
+  if (!ValidateCompileOptions(options)) {
     return false;
   }
 
-  llvm::StringRef input_file_name = args.front();
-  args = args.drop_front();
-  if (!args.empty()) {
-    ReportExtraArgs("dump", args);
-    return false;
+  StreamDiagnosticConsumer stream_consumer(error_stream_);
+  DiagnosticConsumer* consumer = &stream_consumer;
+  std::unique_ptr<SortingDiagnosticConsumer> sorting_consumer;
+  if (vlog_stream_ == nullptr && !options.stream_errors) {
+    sorting_consumer = std::make_unique<SortingDiagnosticConsumer>(*consumer);
+    consumer = sorting_consumer.get();
   }
 
-  CARBON_VLOG() << "*** SourceBuffer::CreateFromFile ***\n";
-  auto source = SourceBuffer::CreateFromFile(fs_, input_file_name);
+  CARBON_VLOG() << "*** SourceBuffer::CreateFromFile on '"
+                << options.input_file_name << "' ***\n";
+  auto source = SourceBuffer::CreateFromFile(fs_, options.input_file_name);
   CARBON_VLOG() << "*** SourceBuffer::CreateFromFile done ***\n";
   // Require flushing the consumer before the source buffer is destroyed,
   // because diagnostics may reference the buffer.
-  auto flush = llvm::make_scope_exit([&]() { consumer.Flush(); });
+  auto flush = llvm::make_scope_exit([&]() { consumer->Flush(); });
   if (!source.ok()) {
     error_stream_ << "ERROR: Unable to open input source file: "
                   << source.error();
     return false;
   }
-
-  bool has_errors = false;
+  CARBON_VLOG() << "*** file:\n```\n" << source->text() << "\n```\n";
 
   CARBON_VLOG() << "*** TokenizedBuffer::Lex ***\n";
-  auto tokenized_source = TokenizedBuffer::Lex(*source, consumer);
-  has_errors |= tokenized_source.has_errors();
+  auto tokenized_source = TokenizedBuffer::Lex(*source, *consumer);
+  bool has_errors = tokenized_source.has_errors();
   CARBON_VLOG() << "*** TokenizedBuffer::Lex done ***\n";
-  if (dump_mode == DumpMode::TokenizedBuffer) {
+  if (options.dump_tokens) {
     CARBON_VLOG() << "Finishing output.";
+    consumer->Flush();
     output_stream_ << tokenized_source;
-    return !has_errors;
   }
   CARBON_VLOG() << "tokenized_buffer: " << tokenized_source;
+  if (options.phase == Phase::Lex) {
+    return !has_errors;
+  }
 
   CARBON_VLOG() << "*** ParseTree::Parse ***\n";
-  auto parse_tree = ParseTree::Parse(tokenized_source, consumer, vlog_stream_);
+  auto parse_tree = ParseTree::Parse(tokenized_source, *consumer, vlog_stream_);
   has_errors |= parse_tree.has_errors();
   CARBON_VLOG() << "*** ParseTree::Parse done ***\n";
-  if (dump_mode == DumpMode::ParseTree) {
-    parse_tree.Print(output_stream_, parse_tree_preorder);
-    return !has_errors;
+  if (options.dump_parse_tree) {
+    consumer->Flush();
+    parse_tree.Print(output_stream_, options.preorder_parse_tree);
   }
   CARBON_VLOG() << "parse_tree: " << parse_tree;
+  if (options.phase == Phase::Parse) {
+    return !has_errors;
+  }
 
   const SemanticsIR builtin_ir = SemanticsIR::MakeBuiltinIR();
   CARBON_VLOG() << "*** SemanticsIR::MakeFromParseTree ***\n";
   const SemanticsIR semantics_ir = SemanticsIR::MakeFromParseTree(
-      builtin_ir, tokenized_source, parse_tree, consumer, vlog_stream_);
+      builtin_ir, tokenized_source, parse_tree, *consumer, vlog_stream_);
   has_errors |= semantics_ir.has_errors();
   CARBON_VLOG() << "*** SemanticsIR::MakeFromParseTree done ***\n";
-  if (dump_mode == DumpMode::RawSemanticsIR) {
-    semantics_ir.Print(output_stream_, semantics_ir_include_builtins);
-    return !has_errors;
-  }
-  if (dump_mode == DumpMode::SemanticsIR) {
-    if (semantics_ir_include_raw) {
-      semantics_ir.Print(output_stream_, semantics_ir_include_builtins);
+  if (options.dump_raw_semantics_ir) {
+    consumer->Flush();
+    semantics_ir.Print(output_stream_, options.builtin_semantics_ir);
+    if (options.dump_semantics_ir) {
       output_stream_ << "\n";
     }
+  }
+  if (options.dump_semantics_ir) {
     FormatSemanticsIR(tokenized_source, parse_tree, semantics_ir,
                       output_stream_);
-    return !has_errors;
   }
   CARBON_VLOG() << "semantics_ir: " << semantics_ir;
+  if (options.phase == Phase::Check) {
+    return !has_errors;
+  }
 
   // Unlike previous steps, errors block further progress.
   if (has_errors) {
-    CARBON_VLOG() << "Unable to dump llvm-ir due to prior errors.";
+    CARBON_VLOG() << "*** Stopping before lowering due to syntax errors ***";
     return false;
   }
+  consumer->Flush();
 
   CARBON_VLOG() << "*** LowerToLLVM ***\n";
   llvm::LLVMContext llvm_context;
-  const std::unique_ptr<llvm::Module> module =
-      LowerToLLVM(llvm_context, input_file_name, semantics_ir, vlog_stream_);
+  const std::unique_ptr<llvm::Module> module = LowerToLLVM(
+      llvm_context, options.input_file_name, semantics_ir, vlog_stream_);
   CARBON_VLOG() << "*** LowerToLLVM done ***\n";
-  if (dump_mode == DumpMode::LLVMIR) {
+  if (options.dump_llvm_ir) {
     module->print(output_stream_, /*AAW=*/nullptr,
                   /*ShouldPreserveUseListOrder=*/true);
-    return !has_errors;
   }
   if (vlog_stream_) {
     CARBON_VLOG() << "module: ";
@@ -286,40 +479,63 @@ auto Driver::RunDumpSubcommand(DiagnosticConsumer& consumer,
                   /*ShouldPreserveUseListOrder=*/false,
                   /*IsForDebug=*/true);
   }
+  if (options.phase == Phase::Lower) {
+    return true;
+  }
 
-  if (dump_mode == DumpMode::Assembly) {
-    CodeGen codegen(*module, target_triple, error_stream_, output_stream_);
-    has_errors |= !codegen.PrintAssembly();
-    return !has_errors;
+  CARBON_VLOG() << "*** CodeGen ***\n";
+  std::optional<CodeGen> codegen =
+      CodeGen::Create(*module, options.target, error_stream_);
+  if (!codegen) {
+    return false;
+  }
+  if (vlog_stream_) {
+    CARBON_VLOG() << "assembly:\n";
+    codegen->EmitAssembly(*vlog_stream_);
   }
 
-  if (dump_mode == DumpMode::ObjectCode) {
+  if (options.output_file_name == "-") {
+    // TODO: the output file name, forcing object output, and requesting textual
+    // assembly output are all somewhat linked flags. We should add some
+    // validation that they are used correctly.
+    if (options.force_obj_output) {
+      if (!codegen->EmitObject(output_stream_)) {
+        return false;
+      }
+    } else {
+      if (!codegen->EmitAssembly(output_stream_)) {
+        return false;
+      }
+    }
+  } else {
+    llvm::SmallString<256> output_file_name = options.output_file_name;
+    if (output_file_name.empty()) {
+      output_file_name = options.input_file_name;
+      llvm::sys::path::replace_extension(output_file_name,
+                                         options.asm_output ? ".s" : ".o");
+    }
+    CARBON_VLOG() << "Writing output to: " << output_file_name << "\n";
+
     std::error_code ec;
-    llvm::raw_fd_ostream dest(output_file, ec, llvm::sys::fs::OF_None);
+    llvm::raw_fd_ostream output_file(output_file_name, ec,
+                                     llvm::sys::fs::OF_None);
     if (ec) {
-      error_stream_ << "Error: Could not open file: " << ec.message() << "\n";
+      error_stream_ << "ERROR: Could not open output file '" << output_file_name
+                    << "': " << ec.message() << "\n";
       return false;
     }
-    CodeGen codegen(*module, target_triple, error_stream_, dest);
-    has_errors |= !codegen.GenerateObjectCode();
-    if (!has_errors) {
-      output_stream_ << "Success: Object file is generated!\n";
+    if (options.asm_output) {
+      if (!codegen->EmitAssembly(output_file)) {
+        return false;
+      }
+    } else {
+      if (!codegen->EmitObject(output_file)) {
+        return false;
+      }
     }
-    return !has_errors;
   }
-
-  llvm_unreachable("should handle all dump modes");
-}
-
-auto Driver::ReportExtraArgs(llvm::StringRef subcommand_text,
-                             llvm::ArrayRef<llvm::StringRef> args) -> void {
-  error_stream_ << "ERROR: Unexpected additional arguments to the '"
-                << subcommand_text << "' subcommand:";
-  for (auto arg : args) {
-    error_stream_ << " " << arg;
-  }
-
-  error_stream_ << "\n";
+  CARBON_VLOG() << "*** CodeGen done ***\n";
+  return true;
 }
 
 }  // namespace Carbon

+ 16 - 25
toolchain/driver/driver.h

@@ -7,6 +7,7 @@
 
 #include <cstdint>
 
+#include "common/command_line.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Debug.h"
@@ -37,38 +38,28 @@ class Driver {
   // Returns true if the operation succeeds. If the operation fails, returns
   // false and any information about the failure is printed to the registered
   // error stream (stderr by default).
-  auto RunFullCommand(llvm::ArrayRef<llvm::StringRef> args) -> bool;
+  auto RunCommand(llvm::ArrayRef<llvm::StringRef> args) -> bool;
 
-  // Subcommand that prints available help text to the error stream.
-  //
-  // Optionally one positional parameter may be provided to select a particular
-  // subcommand or detailed section of help to print.
-  //
-  // Returns true if appropriate help text was found and printed. If an invalid
-  // positional parameter (or flag) is provided, returns false.
-  auto RunHelpSubcommand(DiagnosticConsumer& consumer,
-                         llvm::ArrayRef<llvm::StringRef> args) -> bool;
+ private:
+  struct Options;
+  struct CompileOptions;
 
-  // Subcommand that dumps internal compilation information for the provided
-  // source file.
-  //
-  // Requires exactly one positional parameter to designate the source file to
-  // read. May be `-` to read from stdin.
-  //
-  // Returns true if the operation succeeds. If the operation fails, this
-  // returns false and any information about the failure is printed to the
-  // registered error stream (stderr by default).
-  auto RunDumpSubcommand(DiagnosticConsumer& consumer,
-                         llvm::ArrayRef<llvm::StringRef> args) -> bool;
+  // Delegates to the command line library to parse the arguments and store the
+  // results in a custom `Options` structure that the rest of the driver uses.
+  auto ParseArgs(llvm::ArrayRef<llvm::StringRef> args, Options& options)
+      -> CommandLine::ParseResult;
 
- private:
-  auto ReportExtraArgs(llvm::StringRef subcommand_text,
-                       llvm::ArrayRef<llvm::StringRef> args) -> void;
+  // Does custom validation of the compile-subcommand options structure beyond
+  // what the command line parsing library supports.
+  auto ValidateCompileOptions(const CompileOptions& options) const -> bool;
+
+  // Implements the compile subcommand of the driver.
+  auto Compile(const CompileOptions& options) -> bool;
 
   llvm::vfs::FileSystem& fs_;
   llvm::raw_pwrite_stream& output_stream_;
   llvm::raw_pwrite_stream& error_stream_;
-  llvm::raw_ostream* vlog_stream_ = nullptr;
+  llvm::raw_pwrite_stream* vlog_stream_ = nullptr;
 };
 
 }  // namespace Carbon

+ 1 - 1
toolchain/driver/driver_file_test_base.h

@@ -37,7 +37,7 @@ class DriverFileTestBase : public FileTestBase {
     }
 
     Driver driver(fs, stdout, stderr);
-    return driver.RunFullCommand(test_args);
+    return driver.RunCommand(test_args);
   }
 };
 

+ 1 - 1
toolchain/driver/driver_fuzzer.cpp

@@ -71,7 +71,7 @@ extern "C" auto LLVMFuzzerTestOneInput(const unsigned char* data, size_t size)
   TestRawOstream error_stream;
   llvm::raw_null_ostream dest;
   Driver d(fs, dest, error_stream);
-  if (!d.RunFullCommand(args)) {
+  if (!d.RunCommand(args)) {
     if (error_stream.TakeStr().find("ERROR:") == std::string::npos) {
       llvm::errs() << "No error message on a failure!\n";
       return 1;

+ 1 - 1
toolchain/driver/driver_main.cpp

@@ -30,6 +30,6 @@ auto main(int argc, char** argv) -> int {
   llvm::SmallVector<llvm::StringRef> args(argv + 1, argv + argc);
   auto fs = llvm::vfs::getRealFileSystem();
   Carbon::Driver driver(*fs, llvm::outs(), llvm::errs());
-  bool success = driver.RunFullCommand(args);
+  bool success = driver.RunCommand(args);
   return success ? EXIT_SUCCESS : EXIT_FAILURE;
 }

+ 127 - 69
toolchain/driver/driver_test.cpp

@@ -7,7 +7,13 @@
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
+#include <filesystem>
+#include <fstream>
+#include <utility>
+
+#include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/SmallString.h"
+#include "llvm/Object/Binary.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/SourceMgr.h"
 #include "testing/base/test_raw_ostream.h"
@@ -17,74 +23,111 @@
 namespace Carbon::Testing {
 namespace {
 
+using ::testing::ContainsRegex;
 using ::testing::ElementsAre;
 using ::testing::HasSubstr;
 using ::testing::StrEq;
 
+// Reads a file to string.
+// TODO: Extract this to a helper and share it with other tests.
+static auto ReadFile(std::filesystem::path path) -> std::string {
+  std::ifstream proto_file(path);
+  std::stringstream buffer;
+  buffer << proto_file.rdbuf();
+  proto_file.close();
+  return buffer.str();
+}
+
 class DriverTest : public testing::Test {
  protected:
-  DriverTest() : driver_(fs_, test_output_stream_, test_error_stream_) {}
+  DriverTest() : driver_(fs_, test_output_stream_, test_error_stream_) {
+    char* tmpdir_env = getenv("TEST_TMPDIR");
+    CARBON_CHECK(tmpdir_env != nullptr);
+    test_tmpdir_ = tmpdir_env;
+  }
 
-  auto CreateTestFile(llvm::StringRef text) -> llvm::StringRef {
-    static constexpr llvm::StringLiteral TestFileName = "test_file.carbon";
-    fs_.addFile(TestFileName, /*ModificationTime=*/0,
+  auto CreateTestFile(llvm::StringRef text,
+                      llvm::StringRef file_name = "test_file.carbon")
+      -> llvm::StringRef {
+    fs_.addFile(file_name, /*ModificationTime=*/0,
                 llvm::MemoryBuffer::getMemBuffer(text));
-    return TestFileName;
+    return file_name;
+  }
+
+  // Makes a temp directory and changes the working directory to it. Returns an
+  // LLVM `scope_exit` that will restore the working directory and remove the
+  // temporary directory (and everything it contains) when destroyed.
+  auto ScopedTempWorkingDir() {
+    // Save our current working directory.
+    std::error_code ec;
+    auto original_dir = std::filesystem::current_path(ec);
+    CARBON_CHECK(!ec) << ec.message();
+
+    const auto* unit_test = ::testing::UnitTest::GetInstance();
+    const auto* test_info = unit_test->current_test_info();
+    std::filesystem::path test_dir = test_tmpdir_.append(
+        llvm::formatv("{0}_{1}", test_info->test_suite_name(),
+                      test_info->name())
+            .str());
+    std::filesystem::create_directory(test_dir, ec);
+    CARBON_CHECK(!ec) << "Could not create test working dir '" << test_dir
+                      << "': " << ec.message();
+    std::filesystem::current_path(test_dir, ec);
+    CARBON_CHECK(!ec) << "Could not change the current working dir to '"
+                      << test_dir << "': " << ec.message();
+    return llvm::make_scope_exit([original_dir, test_dir] {
+      std::error_code ec;
+      std::filesystem::current_path(original_dir, ec);
+      CARBON_CHECK(!ec) << "Could not change the current working dir to '"
+                        << original_dir << "': " << ec.message();
+      std::filesystem::remove_all(test_dir, ec);
+      CARBON_CHECK(!ec) << "Could not remove the test working dir '" << test_dir
+                        << "': " << ec.message();
+    });
   }
 
   llvm::vfs::InMemoryFileSystem fs_;
   TestRawOstream test_output_stream_;
   TestRawOstream test_error_stream_;
+
+  // Some tests work directly with files in the test temporary directory.
+  std::filesystem::path test_tmpdir_;
+
   Driver driver_;
 };
 
-TEST_F(DriverTest, FullCommandErrors) {
-  EXPECT_FALSE(driver_.RunFullCommand({}));
+TEST_F(DriverTest, BadCommandErrors) {
+  EXPECT_FALSE(driver_.RunCommand({}));
   EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
 
-  EXPECT_FALSE(driver_.RunFullCommand({"foo"}));
+  EXPECT_FALSE(driver_.RunCommand({"foo"}));
   EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
 
-  EXPECT_FALSE(driver_.RunFullCommand({"foo --bar --baz"}));
+  EXPECT_FALSE(driver_.RunCommand({"foo --bar --baz"}));
   EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
 }
 
-TEST_F(DriverTest, Help) {
-  EXPECT_TRUE(driver_.RunHelpSubcommand(ConsoleDiagnosticConsumer(), {}));
-  EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  auto help_text = test_output_stream_.TakeStr();
-
-  // Help text should mention each subcommand.
-#define CARBON_SUBCOMMAND(Name, Spelling, ...) \
-  EXPECT_THAT(help_text, HasSubstr(Spelling));
-#include "toolchain/driver/flags.def"
-
-  // Check that the subcommand dispatch works.
-  EXPECT_TRUE(driver_.RunFullCommand({"help"}));
-  EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(help_text));
-}
-
-TEST_F(DriverTest, HelpErrors) {
-  EXPECT_FALSE(driver_.RunHelpSubcommand(ConsoleDiagnosticConsumer(), {"foo"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
-
+TEST_F(DriverTest, CompileCommandErrors) {
+  // No input file. This error message is important so check all of it.
+  EXPECT_FALSE(driver_.RunCommand({"compile"}));
+  EXPECT_THAT(
+      test_error_stream_.TakeStr(),
+      StrEq("ERROR: Not all required positional arguments were provided. First "
+            "missing and required positional argument: 'FILE'\n"));
+
+  // Invalid output filename. No reliably error message here.
+  // TODO: Likely want a different filename on Windows.
+  auto empty_file = CreateTestFile("");
   EXPECT_FALSE(
-      driver_.RunHelpSubcommand(ConsoleDiagnosticConsumer(), {"help"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
-
-  EXPECT_FALSE(
-      driver_.RunHelpSubcommand(ConsoleDiagnosticConsumer(), {"--xyz"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
+      driver_.RunCommand({"compile", "--output=/dev/empty", empty_file}));
+  EXPECT_THAT(test_error_stream_.TakeStr(),
+              ContainsRegex("ERROR: .*/dev/empty.*"));
 }
 
 TEST_F(DriverTest, DumpTokens) {
   auto file = CreateTestFile("Hello World");
   EXPECT_TRUE(
-      driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(), {"tokens", file}));
+      driver_.RunCommand({"compile", "--phase=lex", "--dump-tokens", file}));
   EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
   auto tokenized_text = test_output_stream_.TakeStr();
 
@@ -112,47 +155,62 @@ TEST_F(DriverTest, DumpTokens) {
                                      {"column", "12"},
                                      {"indent", "1"},
                                      {"spelling", ""}}}));
+}
 
-  // Check that the subcommand dispatch works.
-  EXPECT_TRUE(driver_.RunFullCommand({"dump", "tokens", file}));
+TEST_F(DriverTest, DumpParseTree) {
+  auto file = CreateTestFile("var v: i32 = 42;");
+  EXPECT_TRUE(driver_.RunCommand(
+      {"compile", "--phase=parse", "--dump-parse-tree", file}));
   EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(tokenized_text));
+  // Verify there is output without examining it.
+  EXPECT_FALSE(test_output_stream_.TakeStr().empty());
 }
 
-TEST_F(DriverTest, DumpErrors) {
-  EXPECT_FALSE(driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(), {"foo"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
+TEST_F(DriverTest, StdoutOutput) {
+  // Use explicit filenames so we can look for those to validate output.
+  CreateTestFile("fn Main() -> i32 { return 0; }", "test.carbon");
 
-  EXPECT_FALSE(
-      driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(), {"--xyz"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
-
-  EXPECT_FALSE(
-      driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(), {"tokens"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
+  EXPECT_TRUE(driver_.RunCommand({"compile", "--output=-", "test.carbon"}));
+  EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
+  // The default is textual assembly.
+  EXPECT_THAT(test_output_stream_.TakeStr(), ContainsRegex("Main:"));
 
-  EXPECT_FALSE(driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(),
-                                         {"tokens", "/not/a/real/file/name"}));
-  EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(""));
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("ERROR"));
+  EXPECT_TRUE(driver_.RunCommand(
+      {"compile", "--output=-", "--force-obj-output", "test.carbon"}));
+  EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
+  std::string output = test_output_stream_.TakeStr();
+  auto result =
+      llvm::object::createBinary(llvm::MemoryBufferRef(output, "test_output"));
+  if (auto error = result.takeError()) {
+    FAIL() << toString(std::move(error));
+  }
+  EXPECT_TRUE(result->get()->isObject());
 }
 
-TEST_F(DriverTest, DumpParseTree) {
-  auto file = CreateTestFile("var v: Int = 42;");
-  EXPECT_TRUE(driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(),
-                                        {"parse-tree", file}));
+TEST_F(DriverTest, FileOutput) {
+  auto scope = ScopedTempWorkingDir();
+
+  // Use explicit filenames as the default output filename is computed from
+  // this, and we can use this to validate output.
+  CreateTestFile("fn Main() -> i32 { return 0; }", "test.carbon");
+
+  // Object output (the default) uses `.o`.
+  // TODO: This should actually reflect the platform defaults.
+  EXPECT_TRUE(driver_.RunCommand({"compile", "test.carbon"}));
   EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  // Verify there is output without examining it.
-  EXPECT_FALSE(test_output_stream_.TakeStr().empty());
+  // Ensure we wrote an object file of some form with the correct name.
+  auto result = llvm::object::createBinary("test.o");
+  if (auto error = result.takeError()) {
+    FAIL() << toString(std::move(error));
+  }
+  EXPECT_TRUE(result->getBinary()->isObject());
 
-  // Check that the subcommand dispatch works.
-  EXPECT_TRUE(driver_.RunFullCommand({"dump", "parse-tree", file}));
+  // Assembly output uses `.s`.
+  // TODO: This should actually reflect the platform defaults.
+  EXPECT_TRUE(driver_.RunCommand({"compile", "--asm-output", "test.carbon"}));
   EXPECT_THAT(test_error_stream_.TakeStr(), StrEq(""));
-  // Verify there is output without examining it.
-  EXPECT_FALSE(test_output_stream_.TakeStr().empty());
+  // TODO: This may need to be tailored to other assembly formats.
+  EXPECT_THAT(ReadFile("test.s"), ContainsRegex("Main:"));
 }
 
 }  // namespace

+ 1 - 22
toolchain/driver/testdata/fail_errors_sorted.carbon

@@ -2,9 +2,8 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump tokens %s
+// ARGS: compile --phase=lex %s
 //
-// TODO: Disable token output, it's not interesting for these tests.
 // AUTOUPDATE
 
 // CHECK:STDERR: fail_errors_sorted.carbon:[[@LINE+3]]:24: Closing symbol does not match most recent opening symbol.
@@ -17,23 +16,3 @@ fn run(String program) {
 // CHECK:STDERR: var x = 3a;
 // CHECK:STDERR:          ^
 var x = 3a;
-
-// CHECK:STDOUT: [
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: ]

+ 1 - 22
toolchain/driver/testdata/fail_errors_streamed.carbon

@@ -2,9 +2,8 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: --print-errors=streamed dump tokens %s
+// ARGS: compile --phase=lex --stream-errors %s
 //
-// TODO: Disable token output, it's not interesting for these tests.
 // AUTOUPDATE
 
 fn run(String program) {
@@ -17,23 +16,3 @@ fn run(String program) {
 // CHECK:STDERR: fn run(String program) {
 // CHECK:STDERR:                        ^
 var x = 3a;
-
-// CHECK:STDOUT: [
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: {{.*}}
-// CHECK:STDOUT: ]

+ 1 - 1
toolchain/lexer/lexer_file_test.cpp

@@ -18,7 +18,7 @@ class LexerFileTest : public DriverFileTestBase {
         end_of_file_re_((R"((EndOfFile.*column: )( *\d+))")) {}
 
   auto GetDefaultArgs() -> llvm::SmallVector<std::string> override {
-    return {"dump", "tokens", "%s"};
+    return {"compile", "--phase=lex", "--dump-tokens", "%s"};
   }
 
   auto GetLineNumberReplacement(llvm::ArrayRef<llvm::StringRef> /*filenames*/)

+ 1 - 1
toolchain/lowering/lowering_file_test.cpp

@@ -15,7 +15,7 @@ class LoweringFileTest : public DriverFileTestBase {
   using DriverFileTestBase::DriverFileTestBase;
 
   auto GetDefaultArgs() -> llvm::SmallVector<std::string> override {
-    return {"dump", "llvm-ir", "%s"};
+    return {"compile", "--phase=lower", "--dump-llvm-ir", "%s"};
   }
 };
 

+ 1 - 1
toolchain/parser/parse_tree_file_test.cpp

@@ -15,7 +15,7 @@ class ParseTreeFileTest : public DriverFileTestBase {
   using DriverFileTestBase::DriverFileTestBase;
 
   auto GetDefaultArgs() -> llvm::SmallVector<std::string> override {
-    return {"dump", "parse-tree", "%s"};
+    return {"compile", "--phase=parse", "--dump-parse-tree", "%s"};
   }
 };
 

+ 4 - 2
toolchain/semantics/semantics_file_test.cpp

@@ -15,8 +15,10 @@ class SemanticsFileTest : public DriverFileTestBase {
   using DriverFileTestBase::DriverFileTestBase;
 
   auto GetDefaultArgs() -> llvm::SmallVector<std::string> override {
-    // TODO: Remove the "--include_raw" once the textual IR format stabilizes.
-    return {"dump", "semantics-ir", "--include_raw", "%s"};
+    // TODO: Remove the "--dump-raw-semantics-ir" once the textual IR format
+    // stabilizes.
+    return {"compile", "--phase=check", "--dump-raw-semantics-ir",
+            "--dump-semantics-ir", "%s"};
   }
 };
 

+ 2 - 2
toolchain/semantics/semantics_fuzzer.cpp

@@ -31,10 +31,10 @@ extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data,
 
   // TODO: Get semantics-ir to a point where it can handle invalid parse trees
   // without crashing.
-  if (!driver.RunFullCommand({"dump", "parse-tree", TestFileName})) {
+  if (!driver.RunCommand({"compile", "--phase=parse", TestFileName})) {
     return 0;
   }
-  driver.RunFullCommand({"dump", "semantics-ir", TestFileName});
+  driver.RunCommand({"compile", "--phase=check", TestFileName});
   return 0;
 }
 

+ 2 - 1
toolchain/semantics/semantics_ir_test.cpp

@@ -32,7 +32,8 @@ TEST(SemanticsIRTest, YAML) {
                           llvm::MemoryBuffer::getMemBuffer("var x: i32 = 0;")));
   TestRawOstream print_stream;
   Driver d(fs, print_stream, llvm::errs());
-  d.RunFullCommand({"dump", "raw-semantics-ir", "test.carbon"});
+  d.RunCommand(
+      {"compile", "--phase=check", "--dump-raw-semantics-ir", "test.carbon"});
 
   // Matches the ID of a node. The numbers may change because of builtin
   // cross-references, so this code is only doing loose structural checks.

+ 1 - 1
toolchain/semantics/testdata/basics/builtin_nodes.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump raw-semantics-ir --include_builtins %s
+// ARGS: compile --phase=check --dump-raw-semantics-ir --builtin-semantics-ir %s
 //
 // AUTOUPDATE
 

+ 1 - 1
toolchain/semantics/testdata/basics/textual_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: dump semantics-ir %s
+// ARGS: compile --phase=check --dump-semantics-ir %s
 //
 // Just check that the raw IR format isn't included in `dump semantics-ir` mode.
 // AUTOUPDATE

+ 1 - 1
toolchain/semantics/testdata/basics/verbose.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: -v dump raw-semantics-ir %s
+// ARGS: -v compile --phase=check %s
 //
 // Only checks a couple statements in order to minimize manual update churn.
 // NOAUTOUPDATE