diff --git a/toolchain/codegen/codegen.cpp b/toolchain/codegen/codegen.cpp index 45c35413d5bc..0d65a4226f6e 100644 --- a/toolchain/codegen/codegen.cpp +++ b/toolchain/codegen/codegen.cpp @@ -15,7 +15,9 @@ namespace Carbon { -auto CodeGen::CreateTargetMachine() -> std::unique_ptr { +auto CodeGen::Create(llvm::Module& module, llvm::StringRef target_triple, + llvm::raw_pwrite_stream& errors) + -> std::optional { // Initialize the target registry etc. llvm::InitializeAllTargetInfos(); llvm::InitializeAllTargets(); @@ -24,66 +26,50 @@ auto CodeGen::CreateTargetMachine() -> std::unique_ptr { 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 reloc_model; - std::unique_ptr 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::EmitCode(llvm::raw_pwrite_stream& dest, - llvm::TargetMachine* target_machine, +auto CodeGen::EmitAssembly(llvm::raw_pwrite_stream& out) -> bool { + return EmitCode(out, llvm::CodeGenFileType::CGFT_AssemblyFile); +} + +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 diff --git a/toolchain/codegen/codegen.h b/toolchain/codegen/codegen.h index 70f627cd63fa..a48b4647ac93 100644 --- a/toolchain/codegen/codegen.h +++ b/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; // 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; + 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 target_machine_; }; } // namespace Carbon diff --git a/toolchain/codegen/testdata/assembly/basic.carbon b/toolchain/codegen/testdata/assembly/basic.carbon index 3337d0637379..221ba3ab1d28 100644 --- a/toolchain/codegen/testdata/assembly/basic.carbon +++ b/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: diff --git a/toolchain/codegen/testdata/assembly/fail_target_triple.carbon b/toolchain/codegen/testdata/fail_target_triple.carbon similarity index 70% rename from toolchain/codegen/testdata/assembly/fail_target_triple.carbon rename to toolchain/codegen/testdata/fail_target_triple.carbon index 882720f049fb..883ab5b59aad 100644 --- a/toolchain/codegen/testdata/assembly/fail_target_triple.carbon +++ b/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; } diff --git a/toolchain/codegen/testdata/objcode/basic.carbon b/toolchain/codegen/testdata/objcode/basic.carbon index 5f676af540f9..96beac6de170 100644 --- a/toolchain/codegen/testdata/objcode/basic.carbon +++ b/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! diff --git a/toolchain/codegen/testdata/objcode/fail_no_input_file.carbon b/toolchain/codegen/testdata/objcode/fail_no_input_file.carbon deleted file mode 100644 index db0479d40f34..000000000000 --- a/toolchain/codegen/testdata/objcode/fail_no_input_file.carbon +++ /dev/null @@ -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; } diff --git a/toolchain/codegen/testdata/objcode/fail_no_output_file.carbon b/toolchain/codegen/testdata/objcode/fail_no_output_file.carbon deleted file mode 100644 index b0deeab576c3..000000000000 --- a/toolchain/codegen/testdata/objcode/fail_no_output_file.carbon +++ /dev/null @@ -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; } diff --git a/toolchain/codegen/testdata/objcode/fail_target_triple.carbon b/toolchain/codegen/testdata/objcode/fail_target_triple.carbon deleted file mode 100644 index a961c1a601c5..000000000000 --- a/toolchain/codegen/testdata/objcode/fail_target_triple.carbon +++ /dev/null @@ -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; } diff --git a/toolchain/driver/BUILD b/toolchain/driver/BUILD index 5bbc7c37bf14..d28566bfcbb6 100644 --- a/toolchain/driver/BUILD +++ b/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", ], ) diff --git a/toolchain/driver/driver.cpp b/toolchain/driver/driver.cpp index a6d2db23521e..f6452a598c65 100644 --- a/toolchain/driver/driver.cpp +++ b/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. + +Error messages are written to the standard error stream. + +Different phases of the compiler can be selected to run, and intermediate state +can be written to standard output as these phases progress. +)""", + }; + + enum class Phase { + Lex, + Parse, + Check, + Lower, + CodeGen, + }; + + 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; + } + + 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); }); + } + + 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; }; -auto GetSubcommand(llvm::StringRef name) -> Subcommand { - return llvm::StringSwitch(name) -#define CARBON_SUBCOMMAND(Name, Spelling, ...) .Case(Spelling, Subcommand::Name) -#include "toolchain/driver/flags.def" - .Default(Subcommand::Unknown); +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 +)""", + }; + + enum class Subcommand { + Compile, + }; + + 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(); + } + + bool verbose; + Subcommand subcommand; + + CompileOptions compile_options; +}; + +auto Driver::ParseArgs(llvm::ArrayRef args, Options& options) + -> CommandLine::ParseResult { + return CommandLine::Parse( + args, output_stream_, error_stream_, Options::Info, + [&](CommandLine::CommandBuilder& b) { options.Build(b); }); } -} // namespace +auto Driver::RunCommand(llvm::ArrayRef 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; + } -auto Driver::RunFullCommand(llvm::ArrayRef args) -> bool { - StreamDiagnosticConsumer stream_consumer(error_stream_); - DiagnosticConsumer* consumer = &stream_consumer; - std::unique_ptr sorting_consumer; - // TODO: Figure out a command-line support library, this is temporary. - if (!args.empty() && args[0] == "-v") { - args = args.drop_front(); + if (options.verbose) { // 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(*consumer); - consumer = sorting_consumer.get(); } - if (args.empty()) { - error_stream_ << "ERROR: No subcommand specified.\n"; - return false; - } - - 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" + switch (options.subcommand) { + case Options::Subcommand::Compile: + return Compile(options.compile_options); } llvm_unreachable("All subcommands handled!"); } -auto Driver::RunHelpSubcommand(DiagnosticConsumer& /*consumer*/, - llvm::ArrayRef 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; +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; + } + [[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; } - - output_stream_ << "List of subcommands:\n\n"; - - constexpr llvm::StringLiteral SubcommandsAndHelp[][2] = { -#define CARBON_SUBCOMMAND(Name, Spelling, HelpText) {Spelling, HelpText}, -#include "toolchain/driver/flags.def" - }; - - int max_subcommand_width = 0; - for (const auto* subcommand_and_help : SubcommandsAndHelp) { - max_subcommand_width = std::max( - max_subcommand_width, static_cast(subcommand_and_help[0].size())); - } - - 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"; - } - - output_stream_ << "\n"; return true; } -enum class DumpMode { - TokenizedBuffer, - ParseTree, - RawSemanticsIR, - SemanticsIR, - LLVMIR, - Assembly, - ObjectCode, - Unknown -}; +auto Driver::Compile(const CompileOptions& options) -> bool { + using Phase = CompileOptions::Phase; -auto Driver::RunDumpSubcommand(DiagnosticConsumer& consumer, - llvm::ArrayRef args) -> bool { - if (args.empty()) { - error_stream_ << "ERROR: No dump mode specified.\n"; + if (!ValidateCompileOptions(options)) { return false; } - auto dump_mode = llvm::StringSwitch(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"; - return false; - } - 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; + StreamDiagnosticConsumer stream_consumer(error_stream_); + DiagnosticConsumer* consumer = &stream_consumer; + std::unique_ptr sorting_consumer; + if (vlog_stream_ == nullptr && !options.stream_errors) { + sorting_consumer = std::make_unique(*consumer); + consumer = sorting_consumer.get(); } - 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; - } - - 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(); - } - - 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; - } - } - - if (output_file.empty()) { - error_stream_ << "ERROR: Must provide an output file.\n"; - return false; - } - } - - if (args.empty()) { - error_stream_ << "ERROR: No input file specified.\n"; - return false; - } - - llvm::StringRef input_file_name = args.front(); - args = args.drop_front(); - if (!args.empty()) { - ReportExtraArgs("dump", args); - return false; - } - - 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; - - CARBON_VLOG() << "*** ParseTree::Parse ***\n"; - 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); + if (options.phase == Phase::Lex) { return !has_errors; } + + CARBON_VLOG() << "*** ParseTree::Parse ***\n"; + auto parse_tree = ParseTree::Parse(tokenized_source, *consumer, vlog_stream_); + has_errors |= parse_tree.has_errors(); + CARBON_VLOG() << "*** ParseTree::Parse done ***\n"; + 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 module = - LowerToLLVM(llvm_context, input_file_name, semantics_ir, vlog_stream_); + const std::unique_ptr 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 (dump_mode == DumpMode::Assembly) { - CodeGen codegen(*module, target_triple, error_stream_, output_stream_); - has_errors |= !codegen.PrintAssembly(); - return !has_errors; + if (options.phase == Phase::Lower) { + return true; } - if (dump_mode == DumpMode::ObjectCode) { + CARBON_VLOG() << "*** CodeGen ***\n"; + std::optional codegen = + CodeGen::Create(*module, options.target, error_stream_); + if (!codegen) { + return false; + } + if (vlog_stream_) { + CARBON_VLOG() << "assembly:\n"; + codegen->EmitAssembly(*vlog_stream_); + } + + 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 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 diff --git a/toolchain/driver/driver.h b/toolchain/driver/driver.h index 770481a2d0fa..3802ddf4067d 100644 --- a/toolchain/driver/driver.h +++ b/toolchain/driver/driver.h @@ -7,6 +7,7 @@ #include +#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 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 args) -> bool; - - // 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 args) -> bool; + auto RunCommand(llvm::ArrayRef args) -> bool; private: - auto ReportExtraArgs(llvm::StringRef subcommand_text, - llvm::ArrayRef args) -> void; + struct Options; + struct CompileOptions; + + // 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 args, Options& options) + -> CommandLine::ParseResult; + + // 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 diff --git a/toolchain/driver/driver_file_test_base.h b/toolchain/driver/driver_file_test_base.h index b6cf4de85059..33d8e74010bc 100644 --- a/toolchain/driver/driver_file_test_base.h +++ b/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); } }; diff --git a/toolchain/driver/driver_fuzzer.cpp b/toolchain/driver/driver_fuzzer.cpp index 3b9debdc90e4..264316eaed4b 100644 --- a/toolchain/driver/driver_fuzzer.cpp +++ b/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; diff --git a/toolchain/driver/driver_main.cpp b/toolchain/driver/driver_main.cpp index b3dbc841c063..5bf03c98c380 100644 --- a/toolchain/driver/driver_main.cpp +++ b/toolchain/driver/driver_main.cpp @@ -30,6 +30,6 @@ auto main(int argc, char** argv) -> int { llvm::SmallVector 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; } diff --git a/toolchain/driver/driver_test.cpp b/toolchain/driver/driver_test.cpp index 286e788045fe..7b3673f4c0a1 100644 --- a/toolchain/driver/driver_test.cpp +++ b/toolchain/driver/driver_test.cpp @@ -7,7 +7,13 @@ #include #include +#include +#include +#include + +#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})); - EXPECT_THAT(test_error_stream_.TakeStr(), StrEq("")); - EXPECT_THAT(test_output_stream_.TakeStr(), StrEq(tokenized_text)); -} - -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")); - - 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_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")); } TEST_F(DriverTest, DumpParseTree) { - auto file = CreateTestFile("var v: Int = 42;"); - EXPECT_TRUE(driver_.RunDumpSubcommand(ConsoleDiagnosticConsumer(), - {"parse-tree", file})); + 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("")); // Verify there is output without examining it. EXPECT_FALSE(test_output_stream_.TakeStr().empty()); +} - // Check that the subcommand dispatch works. - EXPECT_TRUE(driver_.RunFullCommand({"dump", "parse-tree", file})); +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_TRUE(driver_.RunCommand({"compile", "--output=-", "test.carbon"})); EXPECT_THAT(test_error_stream_.TakeStr(), StrEq("")); - // Verify there is output without examining it. - EXPECT_FALSE(test_output_stream_.TakeStr().empty()); + // The default is textual assembly. + EXPECT_THAT(test_output_stream_.TakeStr(), ContainsRegex("Main:")); + + 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, 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("")); + // 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()); + + // 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("")); + // TODO: This may need to be tailored to other assembly formats. + EXPECT_THAT(ReadFile("test.s"), ContainsRegex("Main:")); } } // namespace diff --git a/toolchain/driver/testdata/fail_errors_sorted.carbon b/toolchain/driver/testdata/fail_errors_sorted.carbon index 39b0a3ad0b28..43d58520c1df 100644 --- a/toolchain/driver/testdata/fail_errors_sorted.carbon +++ b/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: ] diff --git a/toolchain/driver/testdata/fail_errors_streamed.carbon b/toolchain/driver/testdata/fail_errors_streamed.carbon index cf7e839731b4..a04dc6adf7d2 100644 --- a/toolchain/driver/testdata/fail_errors_streamed.carbon +++ b/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: ] diff --git a/toolchain/lexer/lexer_file_test.cpp b/toolchain/lexer/lexer_file_test.cpp index 54196743357b..4b8e4a300c05 100644 --- a/toolchain/lexer/lexer_file_test.cpp +++ b/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 override { - return {"dump", "tokens", "%s"}; + return {"compile", "--phase=lex", "--dump-tokens", "%s"}; } auto GetLineNumberReplacement(llvm::ArrayRef /*filenames*/) diff --git a/toolchain/lowering/lowering_file_test.cpp b/toolchain/lowering/lowering_file_test.cpp index e987537c5cd3..d2597a5aab65 100644 --- a/toolchain/lowering/lowering_file_test.cpp +++ b/toolchain/lowering/lowering_file_test.cpp @@ -15,7 +15,7 @@ class LoweringFileTest : public DriverFileTestBase { using DriverFileTestBase::DriverFileTestBase; auto GetDefaultArgs() -> llvm::SmallVector override { - return {"dump", "llvm-ir", "%s"}; + return {"compile", "--phase=lower", "--dump-llvm-ir", "%s"}; } }; diff --git a/toolchain/parser/parse_tree_file_test.cpp b/toolchain/parser/parse_tree_file_test.cpp index 7ca668c099d4..82d2713f890c 100644 --- a/toolchain/parser/parse_tree_file_test.cpp +++ b/toolchain/parser/parse_tree_file_test.cpp @@ -15,7 +15,7 @@ class ParseTreeFileTest : public DriverFileTestBase { using DriverFileTestBase::DriverFileTestBase; auto GetDefaultArgs() -> llvm::SmallVector override { - return {"dump", "parse-tree", "%s"}; + return {"compile", "--phase=parse", "--dump-parse-tree", "%s"}; } }; diff --git a/toolchain/semantics/semantics_file_test.cpp b/toolchain/semantics/semantics_file_test.cpp index efe7445f7ba3..296d72b463b8 100644 --- a/toolchain/semantics/semantics_file_test.cpp +++ b/toolchain/semantics/semantics_file_test.cpp @@ -15,8 +15,10 @@ class SemanticsFileTest : public DriverFileTestBase { using DriverFileTestBase::DriverFileTestBase; auto GetDefaultArgs() -> llvm::SmallVector 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"}; } }; diff --git a/toolchain/semantics/semantics_fuzzer.cpp b/toolchain/semantics/semantics_fuzzer.cpp index 24400ec7ef77..ea33a7ab65f0 100644 --- a/toolchain/semantics/semantics_fuzzer.cpp +++ b/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; } diff --git a/toolchain/semantics/semantics_ir_test.cpp b/toolchain/semantics/semantics_ir_test.cpp index 65e1535c9793..489df4b0ed39 100644 --- a/toolchain/semantics/semantics_ir_test.cpp +++ b/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. diff --git a/toolchain/semantics/testdata/basics/builtin_nodes.carbon b/toolchain/semantics/testdata/basics/builtin_nodes.carbon index 52c7a6e370bd..5cad77f51b51 100644 --- a/toolchain/semantics/testdata/basics/builtin_nodes.carbon +++ b/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 diff --git a/toolchain/semantics/testdata/basics/textual_ir.carbon b/toolchain/semantics/testdata/basics/textual_ir.carbon index 75376b6bacdf..00db0e60f289 100644 --- a/toolchain/semantics/testdata/basics/textual_ir.carbon +++ b/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 diff --git a/toolchain/semantics/testdata/basics/verbose.carbon b/toolchain/semantics/testdata/basics/verbose.carbon index 0439be973150..48aa8e41c6da 100644 --- a/toolchain/semantics/testdata/basics/verbose.carbon +++ b/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