Bläddra i källkod

Switch custom error stream output to diagnostic (#4846)

This switches most error printing to use diagnostics instead of direct
stream writes, even when not a specific file diagnostic. I'm allowing
empty filenames for this use-case.

This allows a little more specific testing to validate coverage of
output using the diagnostic coverage test. I'm adding a few tests to
cover things that weren't previously tested.

Separately, this also forces a little more standardization in format...
considering how changes like #4568 show effort being spent to _mirror_
diagnostic style, my thought is now to just use diagnostic code where
possible.

Note this also allows incrementally better testing of the language
server; I'm changing the crash fix from #4847 in favor of diagnostic
testing.

---------

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Jon Ross-Perkins 1 år sedan
förälder
incheckning
7befe2ce9f
57 ändrade filer med 379 tillägg och 174 borttagningar
  1. 1 1
      toolchain/check/check_unit.cpp
  2. 1 1
      toolchain/check/context.cpp
  3. 1 1
      toolchain/check/context.h
  4. 1 1
      toolchain/check/testdata/basics/builtin_insts.carbon
  5. 17 0
      toolchain/check/testdata/interop/cpp/no_prelude/fail_fuzzing.carbon
  6. 1 1
      toolchain/codegen/testdata/assembly/basic.carbon
  7. 1 1
      toolchain/codegen/testdata/fail_target_triple.carbon
  8. 1 1
      toolchain/codegen/testdata/objcode/basic.carbon
  9. 9 0
      toolchain/diagnostics/BUILD
  10. 4 2
      toolchain/diagnostics/coverage_test.cpp
  11. 4 0
      toolchain/diagnostics/diagnostic.cpp
  12. 3 2
      toolchain/diagnostics/diagnostic.h
  13. 5 6
      toolchain/diagnostics/diagnostic_consumer.cpp
  14. 6 4
      toolchain/diagnostics/diagnostic_consumer.h
  15. 34 0
      toolchain/diagnostics/diagnostic_emitter.h
  16. 15 0
      toolchain/diagnostics/diagnostic_kind.def
  17. 37 0
      toolchain/diagnostics/file_diagnostics.h
  18. 2 2
      toolchain/diagnostics/testdata/fail_multiline_token.carbon
  19. 4 2
      toolchain/driver/clang_subcommand.cpp
  20. 56 59
      toolchain/driver/compile_subcommand.cpp
  21. 3 3
      toolchain/driver/compile_subcommand.h
  22. 24 5
      toolchain/driver/driver.cpp
  23. 14 4
      toolchain/driver/driver_env.h
  24. 0 5
      toolchain/driver/driver_test.cpp
  25. 7 8
      toolchain/driver/format_subcommand.cpp
  26. 7 8
      toolchain/driver/language_server_subcommand.cpp
  27. 1 1
      toolchain/driver/testdata/compile/multifile_raw_and_textual_ir.carbon
  28. 1 1
      toolchain/driver/testdata/compile/multifile_raw_ir.carbon
  29. 1 1
      toolchain/driver/testdata/compile/raw_and_textual_ir.carbon
  30. 1 1
      toolchain/driver/testdata/compile/raw_ir.carbon
  31. 1 1
      toolchain/driver/testdata/compile/textual_ir.carbon
  32. 1 1
      toolchain/driver/testdata/dump_mem_usage.carbon
  33. 1 1
      toolchain/driver/testdata/dump_shared_values.carbon
  34. 1 1
      toolchain/driver/testdata/dump_timings.carbon
  35. 15 0
      toolchain/driver/testdata/fail_clang_fuzzing.cpp
  36. 1 1
      toolchain/driver/testdata/fail_clang_no_args.cpp
  37. 13 0
      toolchain/driver/testdata/fail_dump_phase_conflict.carbon
  38. 2 2
      toolchain/driver/testdata/fail_flag.carbon
  39. 4 4
      toolchain/driver/testdata/fail_flush_errors.carbon
  40. 2 2
      toolchain/driver/testdata/fail_input_is_directory.carbon
  41. 1 1
      toolchain/driver/testdata/fail_missing_file.carbon
  42. 2 2
      toolchain/driver/testdata/fail_missing_stdin_output.carbon
  43. 15 0
      toolchain/driver/testdata/fail_output_is_directory.carbon
  44. 1 1
      toolchain/driver/testdata/stdin.carbon
  45. 1 1
      toolchain/format/testdata/basics/fail_invalid_comment.carbon
  46. 12 0
      toolchain/format/testdata/basics/fail_multi_file_one_output.carbon
  47. 2 2
      toolchain/format/testdata/basics/fail_nonexistent.carbon
  48. 1 1
      toolchain/language_server/BUILD
  49. 8 4
      toolchain/language_server/language_server.cpp
  50. 4 3
      toolchain/language_server/language_server.h
  51. 1 1
      toolchain/language_server/testdata/fail_empty_stdin.carbon
  52. 15 0
      toolchain/language_server/testdata/fail_no_stdin.carbon
  53. 1 2
      toolchain/lex/tokenized_buffer_benchmark.cpp
  54. 1 1
      toolchain/lower/testdata/debug/nodebug.carbon
  55. 1 0
      toolchain/source/BUILD
  56. 3 13
      toolchain/source/source_buffer.cpp
  57. 7 8
      toolchain/testing/file_test.cpp

+ 1 - 1
toolchain/check/check_unit.cpp

@@ -366,7 +366,7 @@ auto CheckUnit::ProcessNodeIds() -> bool {
     auto converted = unit_and_imports_->unit->node_converter->ConvertLoc(
         node_id, [](DiagnosticLoc, const DiagnosticBase<>&) {});
     converted.loc.FormatLocation(output);
-    output << ": checking " << context_.parse_tree().node_kind(node_id) << "\n";
+    output << "checking " << context_.parse_tree().node_kind(node_id) << "\n";
     // Crash output has a tab indent; try to indent slightly past that.
     converted.loc.FormatSnippet(output, /*indent=*/10);
   });

+ 1 - 1
toolchain/check/context.cpp

@@ -223,7 +223,7 @@ auto Context::DiagnoseDuplicateName(SemIRLoc dup_def, SemIRLoc prev_def)
       .Emit();
 }
 
-auto Context::DiagnosePoisonedName(SemIRLoc loc) -> void {
+auto Context::DiagnosePoisonedName(SemIR::InstId loc) -> void {
   // TODO: Improve the diagnostic to replace NodeId::None with the location
   // where the name was poisoned. See discussion in
   // https://github.com/carbon-language/carbon-lang/pull/4654#discussion_r1876607172

+ 1 - 1
toolchain/check/context.h

@@ -266,7 +266,7 @@ class Context {
   auto DiagnoseDuplicateName(SemIRLoc dup_def, SemIRLoc prev_def) -> void;
 
   // Prints a diagnostic for a poisoned name.
-  auto DiagnosePoisonedName(SemIRLoc loc) -> void;
+  auto DiagnosePoisonedName(SemIR::InstId loc) -> void;
 
   // Prints a diagnostic for a missing name.
   auto DiagnoseNameNotFound(SemIRLoc loc, SemIR::NameId name_id) -> void;

+ 1 - 1
toolchain/check/testdata/basics/builtin_insts.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --phase=check --dump-raw-sem-ir --builtin-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --phase=check --dump-raw-sem-ir --builtin-sem-ir %s
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:

+ 17 - 0
toolchain/check/testdata/interop/cpp/no_prelude/fail_fuzzing.carbon

@@ -0,0 +1,17 @@
+// 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: --include-diagnostic-kind --fuzzing compile --no-prelude-import --phase=check %s
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/interop/cpp/no_prelude/fail_fuzzing.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/interop/cpp/no_prelude/fail_fuzzing.carbon
+
+// CHECK:STDERR: fail_fuzzing.carbon:[[@LINE+4]]:1: error: `Cpp` import found during fuzzing [CppInteropFuzzing]
+// CHECK:STDERR: import Cpp library "file.h";
+// CHECK:STDERR: ^~~~~~
+// CHECK:STDERR:
+import Cpp library "file.h";

+ 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: compile --no-prelude-import --target=x86_64-unknown-linux-gnu --output=- %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --target=x86_64-unknown-linux-gnu --output=- %s
 //
 // To test this file alone, run:
 //   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/codegen/testdata/assembly/basic.carbon

+ 1 - 1
toolchain/codegen/testdata/fail_target_triple.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --target=x86_687-unknown-linux-gnu --output=- %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --target=x86_687-unknown-linux-gnu --output=- %s
 // No autoupdate because the message comes from LLVM.
 // To test this file alone, run:
 //   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/codegen/testdata/fail_target_triple.carbon

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

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --target=x86_64-unknown-linux-gnu --output=%t %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --target=x86_64-unknown-linux-gnu --output=%t %s
 //
 // TODO: Add a way to write some basic tests for object file outputs.
 // AUTOUPDATE

+ 9 - 0
toolchain/diagnostics/BUILD

@@ -81,6 +81,15 @@ cc_test(
     ],
 )
 
+cc_library(
+    name = "file_diagnostics",
+    hdrs = ["file_diagnostics.h"],
+    deps = [
+        ":diagnostic_emitter",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
 cc_library(
     name = "format_providers",
     srcs = ["format_providers.cpp"],

+ 4 - 2
toolchain/diagnostics/coverage_test.cpp

@@ -24,8 +24,10 @@ constexpr DiagnosticKind UntestedDiagnosticKinds[] = {
     DiagnosticKind::TestDiagnostic,
     DiagnosticKind::TestDiagnosticNote,
 
-    // Driver specific.
-    DiagnosticKind::CppInteropFuzzing,
+    // Diagnosing erroneous install conditions, but test environments are
+    // typically correct.
+    DiagnosticKind::CompilePreludeManifestError,
+    DiagnosticKind::DriverInstallInvalid,
 
     // These diagnose filesystem issues that are hard to unit test.
     DiagnosticKind::ErrorReadingFile,

+ 4 - 0
toolchain/diagnostics/diagnostic.cpp

@@ -10,6 +10,9 @@
 namespace Carbon {
 
 auto DiagnosticLoc::FormatLocation(llvm::raw_ostream& out) const -> void {
+  if (filename.empty()) {
+    return;
+  }
   out << filename;
   if (line_number > 0) {
     out << ":" << line_number;
@@ -17,6 +20,7 @@ auto DiagnosticLoc::FormatLocation(llvm::raw_ostream& out) const -> void {
       out << ":" << column_number;
     }
   }
+  out << ": ";
 }
 
 auto DiagnosticLoc::FormatSnippet(llvm::raw_ostream& out, int indent) const

+ 3 - 2
toolchain/diagnostics/diagnostic.h

@@ -50,8 +50,9 @@ enum class DiagnosticLevel : int8_t {
 // is required to be less than SourceBuffer that it refers to due to the
 // contained filename and line references.
 struct DiagnosticLoc {
-  // Write the filename, line number, and column number corresponding to this
-  // location to the given stream.
+  // Writes the location to the given stream. It will be formatted as
+  // `<filename>:<line_number>:<column_number>: ` with parts dropped when
+  // unknown.
   auto FormatLocation(llvm::raw_ostream& out) const -> void;
 
   // Write the source snippet corresponding to this location to the given

+ 5 - 6
toolchain/diagnostics/diagnostic_consumer.cpp

@@ -20,18 +20,18 @@ auto StreamDiagnosticConsumer::HandleDiagnostic(Diagnostic diagnostic) -> void {
     message.loc.FormatLocation(*stream_);
     switch (message.level) {
       case DiagnosticLevel::Error:
-        *stream_ << ": error";
+        *stream_ << "error: ";
         break;
       case DiagnosticLevel::Warning:
-        *stream_ << ": warning";
+        *stream_ << "warning: ";
         break;
       case DiagnosticLevel::Note:
-        *stream_ << ": note";
+        *stream_ << "note: ";
         break;
       case DiagnosticLevel::LocationInfo:
         break;
     }
-    *stream_ << ": " << message.format_fn(message);
+    *stream_ << message.format_fn(message);
     if (include_diagnostic_kind_) {
       *stream_ << " [" << message.kind << "]";
     }
@@ -46,8 +46,7 @@ auto StreamDiagnosticConsumer::HandleDiagnostic(Diagnostic diagnostic) -> void {
 }
 
 auto ConsoleDiagnosticConsumer() -> DiagnosticConsumer& {
-  static auto* consumer = new StreamDiagnosticConsumer(
-      llvm::errs(), /*include_diagnostic_kind=*/false);
+  static auto* consumer = new StreamDiagnosticConsumer(&llvm::errs());
   return *consumer;
 }
 

+ 6 - 4
toolchain/diagnostics/diagnostic_consumer.h

@@ -39,14 +39,16 @@ class DiagnosticConsumer {
 // A diagnostic consumer that prints to a stream.
 class StreamDiagnosticConsumer : public DiagnosticConsumer {
  public:
-  explicit StreamDiagnosticConsumer(llvm::raw_ostream& stream,
-                                    bool include_diagnostic_kind)
-      : stream_(&stream), include_diagnostic_kind_(include_diagnostic_kind) {}
+  explicit StreamDiagnosticConsumer(llvm::raw_ostream* stream)
+      : stream_(stream) {}
 
   auto HandleDiagnostic(Diagnostic diagnostic) -> void override;
   auto Flush() -> void override { stream_->flush(); }
 
   auto set_stream(llvm::raw_ostream* stream) -> void { stream_ = stream; }
+  auto set_include_diagnostic_kind(bool value) -> void {
+    include_diagnostic_kind_ = value;
+  }
 
  private:
   auto Print(const DiagnosticMessage& message, llvm::StringRef prefix) -> void;
@@ -54,7 +56,7 @@ class StreamDiagnosticConsumer : public DiagnosticConsumer {
   llvm::raw_ostream* stream_;
 
   // Whether to include the diagnostic kind when printing.
-  bool include_diagnostic_kind_;
+  bool include_diagnostic_kind_ = false;
 
   // Whethere we've printed a diagnostic. Used for printing separators.
   bool printed_diagnostic_ = false;

+ 34 - 0
toolchain/diagnostics/diagnostic_emitter.h

@@ -173,9 +173,12 @@ class DiagnosticEmitter {
 
   // The `converter` and `consumer` are required to outlive the diagnostic
   // emitter.
+  // TODO: Adjust construction to take stored arguments as pointers, and
+  // consider taking `converter` by value (move/copy) instead of reference.
   explicit DiagnosticEmitter(DiagnosticConverter<LocT>& converter,
                              DiagnosticConsumer& consumer)
       : converter_(&converter), consumer_(&consumer) {}
+
   ~DiagnosticEmitter() = default;
 
   // Emits an error.
@@ -229,6 +232,37 @@ class DiagnosticEmitter {
       annotate_fns_;
 };
 
+// This relies on `void*` location handling on `DiagnosticEmitter`.
+//
+// TODO: Based on how this ends up used or if we get more distinct emitters, it
+// might be worth considering having diagnostics specify that they don't apply
+// to source-location carrying emitters. For example, this might look like a
+// `CARBON_NO_LOC_DIAGNOSTIC` macro, or some other factoring. But it might end
+// up being more noise than it is worth.
+class NoLocDiagnosticEmitter : public DiagnosticEmitter<void*> {
+ public:
+  // This constructor only applies to NoLocDiagnosticEmitter.
+  explicit NoLocDiagnosticEmitter(DiagnosticConsumer* consumer)
+      : DiagnosticEmitter(converter_, *consumer) {}
+
+  // Emits an error. This specialization only applies to
+  // `NoLocDiagnosticEmitter`.
+  template <typename... Args>
+  auto Emit(const DiagnosticBase<Args...>& diagnostic_base,
+            Internal::NoTypeDeduction<Args>... args) -> void {
+    DiagnosticEmitter::Emit(nullptr, diagnostic_base, args...);
+  }
+
+ private:
+  struct Converter : DiagnosticConverter<void*> {
+    auto ConvertLoc(void* /*loc*/, ContextFnT /*context_fn*/) const
+        -> ConvertedDiagnosticLoc override {
+      return {.loc = {.filename = ""}, .last_byte_offset = -1};
+    }
+  };
+  Converter converter_;
+};
+
 // An RAII object that denotes a scope in which any diagnostic produced should
 // be annotated in some way.
 //

+ 15 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -16,6 +16,21 @@
 #error "Must define the x-macro to use this file."
 #endif
 
+// ============================================================================
+// Driver and subcommand diagnostics
+// ============================================================================
+
+CARBON_DIAGNOSTIC_KIND(DriverInstallInvalid)
+CARBON_DIAGNOSTIC_KIND(DriverCommandLineParseFailed)
+CARBON_DIAGNOSTIC_KIND(ClangFuzzingDisallowed)
+CARBON_DIAGNOSTIC_KIND(CompilePhaseFlagConflict)
+CARBON_DIAGNOSTIC_KIND(CompilePreludeManifestError)
+CARBON_DIAGNOSTIC_KIND(CompileInputNotRegularFile)
+CARBON_DIAGNOSTIC_KIND(CompileOutputFileOpenError)
+CARBON_DIAGNOSTIC_KIND(FormatMultipleFilesToOneOutput)
+CARBON_DIAGNOSTIC_KIND(LanguageServerMissingInputStream)
+CARBON_DIAGNOSTIC_KIND(LanguageServerTransportError)
+
 // ============================================================================
 // SourceBuffer diagnostics
 // ============================================================================

+ 37 - 0
toolchain/diagnostics/file_diagnostics.h

@@ -0,0 +1,37 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_DIAGNOSTICS_FILE_DIAGNOSTICS_H_
+#define CARBON_TOOLCHAIN_DIAGNOSTICS_FILE_DIAGNOSTICS_H_
+
+#include "toolchain/diagnostics/diagnostic_emitter.h"
+
+namespace Carbon {
+
+// We frequently want a `DiagnosticEmitter` that directly uses a filename. Note
+// that an empty string can be used for a diagnostic that has no particular
+// location.
+//
+// Note this provides no way to set a line or column on diagnostics. More
+// specific emitters must be used for that.
+class FileDiagnosticEmitter : public DiagnosticEmitter<llvm::StringRef> {
+ public:
+  explicit FileDiagnosticEmitter(DiagnosticConsumer* consumer)
+      : DiagnosticEmitter<llvm::StringRef>(converter_, *consumer) {}
+
+ private:
+  // Converts a filename directly to the diagnostic location.
+  struct FileDiagnosticConverter : DiagnosticConverter<llvm::StringRef> {
+    auto ConvertLoc(llvm::StringRef filename, ContextFnT /*context_fn*/) const
+        -> ConvertedDiagnosticLoc override {
+      return {.loc = {.filename = filename}, .last_byte_offset = -1};
+    }
+  };
+
+  FileDiagnosticConverter converter_;
+};
+
+}  // namespace Carbon
+
+#endif  // CARBON_TOOLCHAIN_DIAGNOSTICS_FILE_DIAGNOSTICS_H_

+ 2 - 2
toolchain/diagnostics/testdata/fail_multiline_token.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --phase=parse %s
+// ARGS: --include-diagnostic-kind compile --phase=parse %s
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
@@ -10,7 +10,7 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/diagnostics/testdata/fail_multiline_token.carbon
 
-// CHECK:STDERR: fail_multiline_token.carbon:[[@LINE+4]]:1: error: unrecognized declaration introducer
+// CHECK:STDERR: fail_multiline_token.carbon:[[@LINE+4]]:1: error: unrecognized declaration introducer [UnrecognizedDecl]
 // CHECK:STDERR: '''
 // CHECK:STDERR: ^~~
 // CHECK:STDERR:

+ 4 - 2
toolchain/driver/clang_subcommand.cpp

@@ -51,8 +51,10 @@ auto ClangSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
   // Don't run Clang when fuzzing, it is known to not be reliable under fuzzing
   // due to many unfixed issues.
   if (driver_env.fuzzing) {
-    *driver_env.error_stream
-        << "error: cannot run `clang` subcommand productively when fuzzing\n";
+    CARBON_DIAGNOSTIC(
+        ClangFuzzingDisallowed, Error,
+        "preventing fuzzing of `clang` subcommand due to library crashes");
+    driver_env.emitter.Emit(ClangFuzzingDisallowed);
     return {.success = false};
   }
 

+ 56 - 59
toolchain/driver/compile_subcommand.cpp

@@ -11,6 +11,7 @@
 #include "toolchain/base/timings.h"
 #include "toolchain/check/check.h"
 #include "toolchain/codegen/codegen.h"
+#include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/diagnostics/sorting_diagnostic_consumer.h"
 #include "toolchain/lex/lex.h"
 #include "toolchain/lower/lower.h"
@@ -22,28 +23,6 @@
 
 namespace Carbon {
 
-auto operator<<(llvm::raw_ostream& out, CompileOptions::Phase phase)
-    -> llvm::raw_ostream& {
-  switch (phase) {
-    case CompileOptions::Phase::Lex:
-      out << "lex";
-      break;
-    case CompileOptions::Phase::Parse:
-      out << "parse";
-      break;
-    case CompileOptions::Phase::Check:
-      out << "check";
-      break;
-    case CompileOptions::Phase::Lower:
-      out << "lower";
-      break;
-    case CompileOptions::Phase::CodeGen:
-      out << "codegen";
-      break;
-  }
-  return out;
-}
-
 auto CompileOptions::Build(CommandLine::CommandBuilder& b) -> void {
   b.AddStringPositionalArg(
       {
@@ -128,15 +107,6 @@ of a binary object file instead. Ignored for other `--output` values.
       },
       [&](auto& arg_b) { arg_b.Set(&force_obj_output); });
 
-  b.AddFlag(
-      {
-          .name = "include-diagnostic-kind",
-          .help = R"""(
-When printing diagnostics, include the diagnostic kind as part of output. This
-applies to each message that forms a diagnostic, not just the primary message.
-)""",
-      },
-      [&](auto& arg_b) { arg_b.Set(&include_diagnostic_kind); });
   b.AddFlag(
       {
           .name = "stream-errors",
@@ -296,30 +266,49 @@ can be written to standard output as these phases progress.
 
 CompileSubcommand::CompileSubcommand() : DriverSubcommand(SubcommandInfo) {}
 
-// Returns an error for trying to dump a non-executed phase's output.
-static auto DumpPhaseError(llvm::StringLiteral requested_dump,
-                           CompileOptions::Phase phase) -> Error {
-  return Error(llvm::formatv(
-      "requested dumping {0} but compile phase is limited to `{1}`",
-      requested_dump, phase));
+// Returns a string for printing the phase in a diagnostic.
+static auto PhaseToString(CompileOptions::Phase phase) -> std::string {
+  switch (phase) {
+    case CompileOptions::Phase::Lex:
+      return "lex";
+    case CompileOptions::Phase::Parse:
+      return "parse";
+    case CompileOptions::Phase::Check:
+      return "check";
+    case CompileOptions::Phase::Lower:
+      return "lower";
+    case CompileOptions::Phase::CodeGen:
+      return "codegen";
+  }
 }
 
-auto CompileSubcommand::ValidateOptions() const -> ErrorOr<Success> {
+auto CompileSubcommand::ValidateOptions(NoLocDiagnosticEmitter& emitter) const
+    -> bool {
+  CARBON_DIAGNOSTIC(
+      CompilePhaseFlagConflict, Error,
+      "requested dumping {0} but compile phase is limited to `{1}`",
+      std::string, std::string);
   using Phase = CompileOptions::Phase;
   switch (options_.phase) {
     case Phase::Lex:
       if (options_.dump_parse_tree) {
-        return DumpPhaseError("parse tree", options_.phase);
+        emitter.Emit(CompilePhaseFlagConflict, "parse tree",
+                     PhaseToString(options_.phase));
+        return false;
       }
       [[fallthrough]];
     case Phase::Parse:
       if (options_.dump_sem_ir) {
-        return DumpPhaseError("SemIR", options_.phase);
+        emitter.Emit(CompilePhaseFlagConflict, "SemIR",
+                     PhaseToString(options_.phase));
+        return false;
       }
       [[fallthrough]];
     case Phase::Check:
       if (options_.dump_llvm_ir) {
-        return DumpPhaseError("LLVM IR", options_.phase);
+        emitter.Emit(CompilePhaseFlagConflict, "LLVM IR",
+                     PhaseToString(options_.phase));
+        return false;
       }
       [[fallthrough]];
     case Phase::Lower:
@@ -327,7 +316,7 @@ auto CompileSubcommand::ValidateOptions() const -> ErrorOr<Success> {
       // Everything can be dumped in these phases.
       break;
   }
-  return Success();
+  return true;
 }
 
 namespace {
@@ -673,9 +662,13 @@ auto CompilationUnit::RunCodeGenHelper() -> bool {
     if (output_filename.empty()) {
       if (!source_->is_regular_file()) {
         // Don't invent file names like `-.o` or `/dev/stdin.o`.
-        *driver_env_->error_stream
-            << "error: output file name must be specified for input `"
-            << input_filename_ << "` that is not a regular file\n";
+        // TODO: Consider rephrasing the diagnostic to use the file as the
+        // `Emit` location.
+        CARBON_DIAGNOSTIC(CompileInputNotRegularFile, Error,
+                          "output file name must be specified for input `{0}` "
+                          "that is not a regular file",
+                          std::string);
+        driver_env_->emitter.Emit(CompileInputNotRegularFile, input_filename_);
         return false;
       }
       output_filename = input_filename_;
@@ -694,9 +687,13 @@ auto CompilationUnit::RunCodeGenHelper() -> bool {
     llvm::raw_fd_ostream output_file(output_filename, ec,
                                      llvm::sys::fs::OF_None);
     if (ec) {
-      *driver_env_->error_stream << "error: could not open output file '"
-                                 << output_filename << "': " << ec.message()
-                                 << "\n";
+      // TODO: Consider rephrasing the diagnostic to use the file as the `Emit`
+      // location.
+      CARBON_DIAGNOSTIC(CompileOutputFileOpenError, Error,
+                        "could not open output file `{0}`: {1}", std::string,
+                        std::string);
+      driver_env_->emitter.Emit(CompileOutputFileOpenError,
+                                output_filename.str().str(), ec.message());
       return false;
     }
     if (options_.asm_output) {
@@ -745,8 +742,7 @@ auto CompilationUnit::IncludeInDumps(llvm::StringRef filename) const -> bool {
 }  // namespace
 
 auto CompileSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
-  if (auto validate = ValidateOptions(); !validate.ok()) {
-    *driver_env.error_stream << "error: " << validate.error() << "\n";
+  if (!ValidateOptions(driver_env.emitter)) {
     return {.success = false};
   }
 
@@ -759,27 +755,28 @@ auto CompileSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
     if (auto find = driver_env.installation->ReadPreludeManifest(); find.ok()) {
       prelude = std::move(*find);
     } else {
-      *driver_env.error_stream << "error: " << find.error() << "\n";
+      // TODO: Change ReadPreludeManifest to produce diagnostics.
+      CARBON_DIAGNOSTIC(CompilePreludeManifestError, Error, "{0}", std::string);
+      driver_env.emitter.Emit(CompilePreludeManifestError,
+                              PrintToString(find.error()));
       return {.success = false};
     }
   }
 
   // Prepare CompilationUnits before building scope exit handlers.
-  StreamDiagnosticConsumer stream_consumer(*driver_env.error_stream,
-                                           options_.include_diagnostic_kind);
   llvm::SmallVector<std::unique_ptr<CompilationUnit>> units;
   units.reserve(prelude.size() + options_.input_filenames.size());
 
   // Add the prelude files.
   for (const auto& input_filename : prelude) {
     units.push_back(std::make_unique<CompilationUnit>(
-        driver_env, options_, &stream_consumer, input_filename));
+        driver_env, options_, &driver_env.consumer, input_filename));
   }
 
   // Add the input source files.
   for (const auto& input_filename : options_.input_filenames) {
     units.push_back(std::make_unique<CompilationUnit>(
-        driver_env, options_, &stream_consumer, input_filename));
+        driver_env, options_, &driver_env.consumer, input_filename));
   }
 
   auto on_exit = llvm::make_scope_exit([&]() {
@@ -789,7 +786,7 @@ auto CompileSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
       unit->PostCompile();
     }
 
-    stream_consumer.Flush();
+    driver_env.consumer.Flush();
   });
 
   PrettyStackTraceFunction flush_on_crash([&](llvm::raw_ostream& out) {
@@ -801,14 +798,14 @@ auto CompileSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
       out << "Flushing diagnostics\n";
     } else {
       out << "Pending diagnostics:\n";
-      stream_consumer.set_stream(&out);
+      driver_env.consumer.set_stream(&out);
     }
 
     for (auto& unit : units) {
       unit->FlushForStackTrace();
     }
-    stream_consumer.Flush();
-    stream_consumer.set_stream(driver_env.error_stream);
+    driver_env.consumer.Flush();
+    driver_env.consumer.set_stream(driver_env.error_stream);
   });
 
   // Returns a DriverResult object. Called whenever Compile returns.

+ 3 - 3
toolchain/driver/compile_subcommand.h

@@ -52,7 +52,6 @@ struct CompileOptions {
   bool dump_asm = false;
   bool dump_mem_usage = false;
   bool dump_timings = false;
-  bool include_diagnostic_kind = false;
   bool stream_errors = false;
   bool preorder_parse_tree = false;
   bool builtin_sem_ir = false;
@@ -75,8 +74,9 @@ class CompileSubcommand : public DriverSubcommand {
 
  private:
   // Does custom validation of the compile-subcommand options structure beyond
-  // what the command line parsing library supports.
-  auto ValidateOptions() const -> ErrorOr<Success>;
+  // what the command line parsing library supports. Diagnoses and returns false
+  // on failure.
+  auto ValidateOptions(NoLocDiagnosticEmitter& emitter) const -> bool;
 
   CompileOptions options_;
 };

+ 24 - 5
toolchain/driver/driver.cpp

@@ -24,8 +24,9 @@ struct Options {
 
   auto Build(CommandLine::CommandBuilder& b) -> void;
 
-  bool verbose;
-  bool fuzzing;
+  bool verbose = false;
+  bool fuzzing = false;
+  bool include_diagnostic_kind = false;
 
   ClangSubcommand clang;
   CompileSubcommand compile;
@@ -73,6 +74,16 @@ auto Options::Build(CommandLine::CommandBuilder& b) -> void {
       },
       [&](CommandLine::FlagBuilder& arg_b) { arg_b.Set(&fuzzing); });
 
+  b.AddFlag(
+      {
+          .name = "include-diagnostic-kind",
+          .help = R"""(
+When printing diagnostics, include the diagnostic kind as part of output. This
+applies to each message that forms a diagnostic, not just the primary message.
+)""",
+      },
+      [&](auto& arg_b) { arg_b.Set(&include_diagnostic_kind); });
+
   clang.AddTo(b, &selected_subcommand);
   compile.AddTo(b, &selected_subcommand);
   format.AddTo(b, &selected_subcommand);
@@ -84,8 +95,9 @@ auto Options::Build(CommandLine::CommandBuilder& b) -> void {
 
 auto Driver::RunCommand(llvm::ArrayRef<llvm::StringRef> args) -> DriverResult {
   if (driver_env_.installation->error()) {
-    *driver_env_.error_stream << "error: " << *driver_env_.installation->error()
-                              << "\n";
+    CARBON_DIAGNOSTIC(DriverInstallInvalid, Error, "{0}", std::string);
+    driver_env_.emitter.Emit(DriverInstallInvalid,
+                             driver_env_.installation->error()->str());
     return {.success = false};
   }
 
@@ -95,8 +107,15 @@ auto Driver::RunCommand(llvm::ArrayRef<llvm::StringRef> args) -> DriverResult {
       args, *driver_env_.output_stream, Options::Info,
       [&](CommandLine::CommandBuilder& b) { options.Build(b); });
 
+  // Regardless of whether the parse succeeded, try to use the diagnostic kind
+  // flag.
+  driver_env_.consumer.set_include_diagnostic_kind(
+      options.include_diagnostic_kind);
+
   if (!result.ok()) {
-    *driver_env_.error_stream << "error: " << result.error() << "\n";
+    CARBON_DIAGNOSTIC(DriverCommandLineParseFailed, Error, "{0}", std::string);
+    driver_env_.emitter.Emit(DriverCommandLineParseFailed,
+                             PrintToString(result.error()));
     return {.success = false};
   } else if (*result == CommandLine::ParseResult::MetaSuccess) {
     return {.success = true};

+ 14 - 4
toolchain/driver/driver_env.h

@@ -6,9 +6,11 @@
 #define CARBON_TOOLCHAIN_DRIVER_DRIVER_ENV_H_
 
 #include <cstdio>
+#include <utility>
 
 #include "common/ostream.h"
 #include "llvm/Support/VirtualFileSystem.h"
+#include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/install/install_paths.h"
 
 namespace Carbon {
@@ -24,7 +26,9 @@ struct DriverEnv {
         input_stream(input_stream),
         output_stream(output_stream),
         error_stream(error_stream),
-        fuzzing(fuzzing) {}
+        fuzzing(fuzzing),
+        consumer(error_stream),
+        emitter(&consumer) {}
 
   // The filesystem for source code.
   llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs;
@@ -39,13 +43,19 @@ struct DriverEnv {
   // Error output; stderr.
   llvm::raw_pwrite_stream* error_stream;
 
-  // For CARBON_VLOG.
-  llvm::raw_pwrite_stream* vlog_stream = nullptr;
-
   // Tracks when the driver is being fuzzed. This allows specific commands to
   // error rather than perform operations that aren't well behaved during
   // fuzzing.
   bool fuzzing;
+
+  // A diagnostic consumer, to be able to connect output.
+  StreamDiagnosticConsumer consumer;
+
+  // A diagnostic emitter that has no locations.
+  NoLocDiagnosticEmitter emitter;
+
+  // For CARBON_VLOG.
+  llvm::raw_pwrite_stream* vlog_stream = nullptr;
 };
 
 }  // namespace Carbon

+ 0 - 5
toolchain/driver/driver_test.cpp

@@ -225,10 +225,5 @@ TEST_F(DriverTest, FileOutput) {
   EXPECT_THAT(ReadFile("test.s"), ContainsRegex("Main:"));
 }
 
-TEST_F(DriverTest, LanguageServerNoStdin) {
-  EXPECT_FALSE(driver_.RunCommand({"language-server"}).success);
-  EXPECT_THAT(test_error_stream_.TakeStr(), HasSubstr("requires input_stream"));
-}
-
 }  // namespace
 }  // namespace Carbon

+ 7 - 8
toolchain/driver/format_subcommand.cpp

@@ -56,9 +56,10 @@ auto FormatSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
   DriverResult result = {.success = true};
   if (options_.input_filenames.size() > 1 &&
       !options_.output_filename.empty()) {
-    *driver_env.error_stream
-        << "error: cannot format multiple input files when "
-           "--output is set\n";
+    CARBON_DIAGNOSTIC(FormatMultipleFilesToOneOutput, Error,
+                      "multiple input files are being provided; --output only "
+                      "works with one input");
+    driver_env.emitter.Emit(FormatMultipleFilesToOneOutput);
     result.success = false;
     return result;
   }
@@ -68,22 +69,20 @@ auto FormatSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
     result.per_file_success.back().second = false;
   };
 
-  StreamDiagnosticConsumer consumer(*driver_env.error_stream,
-                                    /*include_diagnostic_kind=*/false);
   for (auto& f : options_.input_filenames) {
     // Push a result, which we'll update on failure.
     result.per_file_success.push_back({f.str(), true});
 
     // TODO: Consider refactoring this for sharing with compile.
     // TODO: Decide what to do with `-` when there are multiple arguments.
-    auto source =
-        SourceBuffer::MakeFromFileOrStdin(*driver_env.fs, f, consumer);
+    auto source = SourceBuffer::MakeFromFileOrStdin(*driver_env.fs, f,
+                                                    driver_env.consumer);
     if (!source) {
       mark_per_file_error();
       continue;
     }
     SharedValueStores value_stores;
-    auto tokens = Lex::Lex(value_stores, *source, consumer);
+    auto tokens = Lex::Lex(value_stores, *source, driver_env.consumer);
 
     RawStringOstream buffer;
     if (Format::Format(tokens, buffer)) {

+ 7 - 8
toolchain/driver/language_server_subcommand.cpp

@@ -4,6 +4,7 @@
 
 #include "toolchain/driver/language_server_subcommand.h"
 
+#include "toolchain/diagnostics/diagnostic_consumer.h"
 #include "toolchain/language_server/language_server.h"
 
 namespace Carbon {
@@ -20,18 +21,16 @@ LanguageServerSubcommand::LanguageServerSubcommand()
 
 auto LanguageServerSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
   if (!driver_env.input_stream) {
-    *driver_env.error_stream
-        << "error: language-server requires input_stream\n";
+    CARBON_DIAGNOSTIC(LanguageServerMissingInputStream, Error,
+                      "language-server requires input_stream");
+    driver_env.emitter.Emit(LanguageServerMissingInputStream);
     return {.success = false};
   }
 
-  auto err =
+  bool success =
       LanguageServer::Run(driver_env.input_stream, *driver_env.output_stream,
-                          *driver_env.error_stream);
-  if (!err.ok()) {
-    *driver_env.error_stream << "error: " << err.error() << "\n";
-  }
-  return {.success = err.ok()};
+                          *driver_env.error_stream, driver_env.consumer);
+  return {.success = success};
 }
 
 }  // namespace Carbon

+ 1 - 1
toolchain/driver/testdata/compile/multifile_raw_and_textual_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --phase=check --dump-sem-ir --dump-raw-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --phase=check --dump-sem-ir --dump-raw-sem-ir %s
 //
 // Check that we can combine textual IR and raw IR dumping in one compile.
 //

+ 1 - 1
toolchain/driver/testdata/compile/multifile_raw_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import  --phase=check --dump-raw-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import  --phase=check --dump-raw-sem-ir %s
 //
 // Check that raw IR dumping works as expected.
 //

+ 1 - 1
toolchain/driver/testdata/compile/raw_and_textual_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --phase=check --dump-sem-ir --dump-raw-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --phase=check --dump-sem-ir --dump-raw-sem-ir %s
 //
 // Check that we can combine textual IR and raw IR dumping in one compile.
 //

+ 1 - 1
toolchain/driver/testdata/compile/raw_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --phase=check --dump-raw-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --phase=check --dump-raw-sem-ir %s
 //
 // Check that raw IR dumping works as expected.
 //

+ 1 - 1
toolchain/driver/testdata/compile/textual_ir.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import --phase=check --dump-sem-ir %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import --phase=check --dump-sem-ir %s
 //
 // Check that the command-line flag to dump textual IR works.
 //

+ 1 - 1
toolchain/driver/testdata/dump_mem_usage.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --phase=check --dump-mem-usage %s
+// ARGS: --include-diagnostic-kind compile --phase=check --dump-mem-usage %s
 //
 // To test this file alone, run:
 //   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/dump_mem_usage.carbon

+ 1 - 1
toolchain/driver/testdata/dump_shared_values.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --phase=lex --dump-shared-values %s
+// ARGS: --include-diagnostic-kind compile --phase=lex --dump-shared-values %s
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:

+ 1 - 1
toolchain/driver/testdata/dump_timings.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --dump-timings --phase=check %s
+// ARGS: --include-diagnostic-kind compile --dump-timings --phase=check %s
 //
 // SET-CHECK-SUBSET
 //

+ 15 - 0
toolchain/driver/testdata/fail_clang_fuzzing.cpp

@@ -0,0 +1,15 @@
+// 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: --include-diagnostic-kind --fuzzing clang foo.cpp
+//
+// SET-CAPTURE-CONSOLE-OUTPUT
+// clang-format off
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_clang_fuzzing.cpp
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_clang_fuzzing.cpp
+// CHECK:STDERR: error: preventing fuzzing of `clang` subcommand due to library crashes [ClangFuzzingDisallowed]
+// CHECK:STDERR:

+ 1 - 1
toolchain/driver/testdata/fail_clang_no_args.cpp

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: clang --
+// ARGS: --include-diagnostic-kind clang --
 //
 // SET-CAPTURE-CONSOLE-OUTPUT
 // clang-format off

+ 13 - 0
toolchain/driver/testdata/fail_dump_phase_conflict.carbon

@@ -0,0 +1,13 @@
+// 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: --include-diagnostic-kind compile --phase=parse --dump-sem-ir %s
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_dump_phase_conflict.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_dump_phase_conflict.carbon
+// CHECK:STDERR: error: requested dumping SemIR but compile phase is limited to `parse` [CompilePhaseFlagConflict]
+// CHECK:STDERR:

+ 2 - 2
toolchain/driver/testdata/fail_flag.carbon

@@ -2,12 +2,12 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --non-existent-flag
+// ARGS: --include-diagnostic-kind compile --non-existent-flag
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
 // TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_flag.carbon
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_flag.carbon
-// CHECK:STDERR: error: unknown option `--non-existent-flag`
+// CHECK:STDERR: error: unknown option `--non-existent-flag` [DriverCommandLineParseFailed]
 // CHECK:STDERR:

+ 4 - 4
toolchain/driver/testdata/fail_flush_errors.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import %s
+// ARGS: --include-diagnostic-kind compile --no-prelude-import %s
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
@@ -13,7 +13,7 @@
 fn F() {
   // Create diagnostics containing string references, and trigger reallocation
   // of the string table.
-  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared1` not found
+  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared1` not found [NameNotFound]
   // CHECK:STDERR:   undeclared1;
   // CHECK:STDERR:   ^~~~~~~~~~~
   // CHECK:STDERR:
@@ -23,7 +23,7 @@ fn F() {
   // literal storage. Use a hex escape to ensure that the tokenized buffer
   // allocates separate storage for the result.
   "undec\x6Cared2";
-  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared2` not found
+  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared2` not found [NameNotFound]
   // CHECK:STDERR:   undeclared2;
   // CHECK:STDERR:   ^~~~~~~~~~~
   // CHECK:STDERR:
@@ -31,7 +31,7 @@ fn F() {
 
   // Add the name into the string table via a declaration rather than an expression.
   if (true) { var undeclared3: () = (); }
-  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared3` not found
+  // CHECK:STDERR: fail_flush_errors.carbon:[[@LINE+4]]:3: error: name `undeclared3` not found [NameNotFound]
   // CHECK:STDERR:   undeclared3;
   // CHECK:STDERR:   ^~~~~~~~~~~
   // CHECK:STDERR:

+ 2 - 2
toolchain/driver/testdata/fail_input_is_directory.carbon

@@ -2,12 +2,12 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile .
+// ARGS: --include-diagnostic-kind compile .
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
 // TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_input_is_directory.carbon
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_input_is_directory.carbon
-// CHECK:STDERR: .: error: error opening file for read: Invalid argument
+// CHECK:STDERR: .: error: error opening file for read: Invalid argument [ErrorOpeningFile]
 // CHECK:STDERR:

+ 1 - 1
toolchain/driver/testdata/fail_missing_file.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --include-diagnostic-kind not_file.carbon
+// ARGS: --include-diagnostic-kind compile not_file.carbon
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:

+ 2 - 2
toolchain/driver/testdata/fail_missing_stdin_output.carbon

@@ -2,12 +2,12 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-prelude-import -
+// ARGS: --include-diagnostic-kind compile --no-prelude-import -
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
 // TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_missing_stdin_output.carbon
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_missing_stdin_output.carbon
-// CHECK:STDERR: error: output file name must be specified for input `-` that is not a regular file
+// CHECK:STDERR: error: output file name must be specified for input `-` that is not a regular file [CompileInputNotRegularFile]
 // CHECK:STDERR:

+ 15 - 0
toolchain/driver/testdata/fail_output_is_directory.carbon

@@ -0,0 +1,15 @@
+// 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: --include-diagnostic-kind compile --no-prelude-import --output=/ %s
+//
+// NOAUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_output_is_directory.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_output_is_directory.carbon
+
+// Don't depend on a specific filesystem error.
+// CHECK:STDERR: error: could not open output file `/`: {{.*}} [CompileOutputFileOpenError]
+// CHECK:STDERR:

+ 1 - 1
toolchain/driver/testdata/stdin.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile - --no-prelude-import --phase=check --dump-sem-ir
+// ARGS: --include-diagnostic-kind compile - --no-prelude-import --phase=check --dump-sem-ir
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:

+ 1 - 1
toolchain/format/testdata/basics/fail_invalid_comment.carbon

@@ -14,7 +14,7 @@
 
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDERR: fail_test.carbon:2:3: error: whitespace is required after '//'
+// CHECK:STDERR: fail_test.carbon:2:3: error: whitespace is required after '//' [NoWhitespaceAfterCommentIntroducer]
 // CHECK:STDERR: //f
 // CHECK:STDERR:   ^
 // CHECK:STDERR:

+ 12 - 0
toolchain/format/testdata/basics/fail_multi_file_one_output.carbon

@@ -0,0 +1,12 @@
+// 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: --include-diagnostic-kind format --output=foo.carbon bar.carbon baz.carbon
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/format/testdata/basics/fail_multi_file_one_output.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/format/testdata/basics/fail_multi_file_one_output.carbon
+// CHECK:STDERR: error: multiple input files are being provided; --output only works with one input [FormatMultipleFilesToOneOutput]
+// CHECK:STDERR:

+ 2 - 2
toolchain/format/testdata/basics/fail_nonexistent.carbon

@@ -2,11 +2,11 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: format fail_target_file.carbon
+// ARGS: --include-diagnostic-kind format fail_target_file.carbon
 // AUTOUPDATE
 // TIP: To test this file alone, run:
 // TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/format/testdata/basics/fail_nonexistent.carbon
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/format/testdata/basics/fail_nonexistent.carbon
-// CHECK:STDERR: fail_target_file.carbon: error: error opening file for read: No such file or directory
+// CHECK:STDERR: fail_target_file.carbon: error: error opening file for read: No such file or directory [ErrorOpeningFile]
 // CHECK:STDERR:

+ 1 - 1
toolchain/language_server/BUILD

@@ -19,9 +19,9 @@ cc_library(
         ":context",
         ":incoming_messages",
         ":outgoing_messages",
-        "//common:error",
         "//common:ostream",
         "//common:raw_string_ostream",
+        "//toolchain/diagnostics:diagnostic_emitter",
         "@llvm-project//clang-tools-extra/clangd:ClangDaemon",
     ],
 )

+ 8 - 4
toolchain/language_server/language_server.cpp

@@ -8,6 +8,7 @@
 #include "clang-tools-extra/clangd/Transport.h"
 #include "clang-tools-extra/clangd/support/Logger.h"
 #include "common/raw_string_ostream.h"
+#include "toolchain/diagnostics/diagnostic_emitter.h"
 #include "toolchain/language_server/context.h"
 #include "toolchain/language_server/incoming_messages.h"
 #include "toolchain/language_server/outgoing_messages.h"
@@ -15,7 +16,8 @@
 namespace Carbon::LanguageServer {
 
 auto Run(FILE* input_stream, llvm::raw_ostream& output_stream,
-         llvm::raw_ostream& error_stream) -> ErrorOr<Success> {
+         llvm::raw_ostream& error_stream, DiagnosticConsumer& consumer)
+    -> bool {
   // TODO: Consider implementing a custom logger that splits vlog to
   // vlog_stream when provided. For now, this disables verbose logging.
   clang::clangd::StreamLogger logger(error_stream, clang::clangd::Logger::Info);
@@ -36,10 +38,12 @@ auto Run(FILE* input_stream, llvm::raw_ostream& output_stream,
   if (err) {
     RawStringOstream out;
     out << err;
-    return Error(out.TakeStr());
-  } else {
-    return Success();
+    CARBON_DIAGNOSTIC(LanguageServerTransportError, Error, "{0}", std::string);
+    NoLocDiagnosticEmitter emitter(&consumer);
+    emitter.Emit(LanguageServerTransportError, out.TakeStr());
+    return false;
   }
+  return true;
 }
 
 }  // namespace Carbon::LanguageServer

+ 4 - 3
toolchain/language_server/language_server.h

@@ -5,15 +5,16 @@
 #ifndef CARBON_TOOLCHAIN_LANGUAGE_SERVER_LANGUAGE_SERVER_H_
 #define CARBON_TOOLCHAIN_LANGUAGE_SERVER_LANGUAGE_SERVER_H_
 
-#include "common/error.h"
 #include "common/ostream.h"
+#include "toolchain/diagnostics/diagnostic_consumer.h"
 
 namespace Carbon::LanguageServer {
 
 // Start the language server. input_stream and output_stream are used by LSP;
-// error_stream is primarily for errors that don't fit into LSP.
+// error_stream is primarily for errors that don't fit into LSP. Returns true if
+// the server cleanly exits.
 auto Run(FILE* input_stream, llvm::raw_ostream& output_stream,
-         llvm::raw_ostream& error_stream) -> ErrorOr<Success>;
+         llvm::raw_ostream& error_stream, DiagnosticConsumer& consumer) -> bool;
 
 }  // namespace Carbon::LanguageServer
 

+ 1 - 1
toolchain/language_server/testdata/fail_empty_stdin.carbon

@@ -11,6 +11,6 @@
 // --- STDIN
 // --- AUTOUPDATE-SPLIT
 
-// CHECK:STDERR: error: Input/output error
+// CHECK:STDERR: error: Input/output error [LanguageServerTransportError]
 // CHECK:STDERR:
 // CHECK:STDOUT:

+ 15 - 0
toolchain/language_server/testdata/fail_no_stdin.carbon

@@ -0,0 +1,15 @@
+// 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
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/fail_no_stdin.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/fail_no_stdin.carbon
+
+// --- AUTOUPDATE-SPLIT
+
+// CHECK:STDERR: error: language-server requires input_stream [LanguageServerMissingInputStream]
+// CHECK:STDERR:
+// CHECK:STDOUT:

+ 1 - 2
toolchain/lex/tokenized_buffer_benchmark.cpp

@@ -220,8 +220,7 @@ class LexerBenchHelper {
 
   auto DiagnoseErrors() -> std::string {
     RawStringOstream result;
-    StreamDiagnosticConsumer consumer(result,
-                                      /*include_diagnostic_kind=*/false);
+    StreamDiagnosticConsumer consumer(&result);
     auto buffer = Lex::Lex(value_stores_, source_, consumer);
     consumer.Flush();
     CARBON_CHECK(buffer.has_errors(),

+ 1 - 1
toolchain/lower/testdata/debug/nodebug.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// ARGS: compile --no-debug-info --phase=lower --dump-llvm-ir --output=- --exclude-dump-file-prefix=%{core_package_dir} %s
+// ARGS: --include-diagnostic-kind compile --no-debug-info --phase=lower --dump-llvm-ir --output=- --exclude-dump-file-prefix=%{core_package_dir} %s
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:

+ 1 - 0
toolchain/source/BUILD

@@ -13,6 +13,7 @@ cc_library(
     deps = [
         "//common:error",
         "//toolchain/diagnostics:diagnostic_emitter",
+        "//toolchain/diagnostics:file_diagnostics",
         "//toolchain/diagnostics:format_providers",
         "@llvm-project//llvm:Support",
     ],

+ 3 - 13
toolchain/source/source_buffer.cpp

@@ -7,18 +7,10 @@
 #include <limits>
 
 #include "llvm/Support/ErrorOr.h"
+#include "toolchain/diagnostics/file_diagnostics.h"
 
 namespace Carbon {
 
-namespace {
-struct FilenameConverter : DiagnosticConverter<llvm::StringRef> {
-  auto ConvertLoc(llvm::StringRef filename, ContextFnT /*context_fn*/) const
-      -> ConvertedDiagnosticLoc override {
-    return {.loc = {.filename = filename}, .last_byte_offset = -1};
-  }
-};
-}  // namespace
-
 auto SourceBuffer::MakeFromStdin(DiagnosticConsumer& consumer)
     -> std::optional<SourceBuffer> {
   return MakeFromMemoryBuffer(llvm::MemoryBuffer::getSTDIN(), "<stdin>",
@@ -29,8 +21,7 @@ auto SourceBuffer::MakeFromFile(llvm::vfs::FileSystem& fs,
                                 llvm::StringRef filename,
                                 DiagnosticConsumer& consumer)
     -> std::optional<SourceBuffer> {
-  FilenameConverter converter;
-  DiagnosticEmitter<llvm::StringRef> emitter(converter, consumer);
+  FileDiagnosticEmitter emitter(&consumer);
 
   llvm::ErrorOr<std::unique_ptr<llvm::vfs::File>> file =
       fs.openFileForRead(filename);
@@ -64,8 +55,7 @@ auto SourceBuffer::MakeFromMemoryBuffer(
     llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> buffer,
     llvm::StringRef filename, bool is_regular_file,
     DiagnosticConsumer& consumer) -> std::optional<SourceBuffer> {
-  FilenameConverter converter;
-  DiagnosticEmitter<llvm::StringRef> emitter(converter, consumer);
+  FileDiagnosticEmitter emitter(&consumer);
 
   if (buffer.getError()) {
     CARBON_DIAGNOSTIC(ErrorReadingFile, Error, "error reading file: {0}",

+ 7 - 8
toolchain/testing/file_test.cpp

@@ -104,10 +104,6 @@ auto ToolchainFileTest::Run(
     llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem>& fs,
     FILE* input_stream, llvm::raw_pwrite_stream& output_stream,
     llvm::raw_pwrite_stream& error_stream) -> ErrorOr<RunResult> {
-  if (component_ == "language_server" && !input_stream) {
-    return Error("language_server tests must provide STDIN");
-  }
-
   CARBON_ASSIGN_OR_RETURN(auto prelude, installation_.ReadPreludeManifest());
   if (!is_no_prelude()) {
     for (const auto& file : prelude) {
@@ -147,14 +143,17 @@ auto ToolchainFileTest::Run(
 }
 
 auto ToolchainFileTest::GetDefaultArgs() -> llvm::SmallVector<std::string> {
+  llvm::SmallVector<std::string> args = {"--include-diagnostic-kind"};
+
   if (component_ == "format") {
-    return {"format", "%s"};
+    args.insert(args.end(), {"format", "%s"});
+    return args;
   } else if (component_ == "language_server") {
-    return {"language-server"};
+    args.insert(args.end(), {"language-server"});
+    return args;
   }
 
-  llvm::SmallVector<std::string> args = {"compile", "--include-diagnostic-kind",
-                                         "--phase=" + component_.str()};
+  args.insert(args.end(), {"compile", "--phase=" + component_.str()});
 
   if (component_ == "lex") {
     args.insert(args.end(), {"--dump-tokens", "--omit-file-boundary-tokens"});