Render diagnostics through the terminal library (#7683)

Diagnostic rendering was part of the data model: `Loc::FormatLocation`
and
`Loc::FormatSnippet` wrote plain text to a stream, so no capability,
color, or
terminal width could reach them.

A diagnostic is now one message with labels, contexts, and location
information
attached, and `Diagnostics::Renderer` draws it into a
`Terminal::Buffer`. The
design is in `toolchain/docs/diagnostics_rendering.md` (#7668),
authoring in
`toolchain/docs/diagnostics.md`.

Before:

```
example.carbon:6:3: error: 1 argument passed to function expecting 0 arguments
  Run0(1);
  ^~~~~~~
example.carbon:3:1: note: calling function declared here
fn Run0() {}
^~~~~~~~~~~
```

After:

```
error: 1 argument passed to function expecting 0 arguments
       ╭─┤ example.carbon:6:3
       ┆
     3 │ fn Run0() {}
       · ─────┬─────
       ·      ╰──┤ calling function declared here
       ┆
->   6 │   Run0(1);
       ·   ───────
       ╰────────────
```

`--no-diagnostic-snippets` gives a compact form, `example.carbon:6:3-9`.
File
tests use it; the rendering tests under `toolchain/diagnostics/testdata`
ask for
snippets.

C++ interop diagnostics are data now rather than text Clang drew:
`SemIR::ClangSourceLocId` names a `clang::SourceRange`, and a Clang
location is
marked as the point it names.

`StreamConsumer` moves to a `:renderer` target that `:emitter` does not
depend
on, keeping `common/terminal` out of `consumer.h`. Capabilities come
from the
file behind the error stream, so tests get plain ASCII.

`renderer_fuzzer` draws diagnostics built from fuzzer bytes with every
capability combination, since a shape the renderer doesn't expect must
never be
why the compiler dies while reporting a problem.

TODOs mark the follow-ups: line weights and crossing lines, wrapping
that keeps
a quoted type whole, and ordering diagnostics that share a byte offset.

Also: `IncorrectExtension`'s hint moves into its message, since its note
had no
location of its own, and `strip_noise` in `check_diagnostics.py` blanks
multi-line `#define`s instead of deleting them.

Assisted-by: Claude Code
This commit is contained in:
Chandler Carruth authored and GitHub committed 2026-10-15 06:08:57 +00:00
1 parent e1c8c1f855
commit 2c9739799b
1205 files changed
+20291 -24988

No files matched your search

+40 -38
View File
@@ -260,16 +260,16 @@ auto CheckFunctionReturnTypeMatches(Context& context,
: context.emitter().Build(new_function.latest_decl_id(),
FunctionRedeclReturnTypeDiffersNoReturn);
if (prev_return_type_id.has_value()) {
CARBON_DIAGNOSTIC(FunctionRedeclReturnTypePrevious, Note,
"previously declared with return type {0}",
SemIR::TypeId);
diag.Note(prev_function.latest_decl_id(),
FunctionRedeclReturnTypePrevious, prev_return_type_id);
CARBON_DIAGNOSTIC_LABEL(FunctionRedeclReturnTypePrevious, Info,
"previously declared with return type {0}",
SemIR::TypeId);
diag.Attach(prev_function.latest_decl_id(),
FunctionRedeclReturnTypePrevious, prev_return_type_id);
} else {
CARBON_DIAGNOSTIC(FunctionRedeclReturnTypePreviousNoReturn, Note,
"previously declared with no return type");
diag.Note(prev_function.latest_decl_id(),
FunctionRedeclReturnTypePreviousNoReturn);
CARBON_DIAGNOSTIC_LABEL(FunctionRedeclReturnTypePreviousNoReturn, Info,
"previously declared with no return type");
diag.Attach(prev_function.latest_decl_id(),
FunctionRedeclReturnTypePreviousNoReturn);
}
diag.Emit();
return false;
@@ -307,16 +307,17 @@ static auto CheckFunctionEvaluationModeMatches(
"function redeclaration differs because new function is "
"{0:=-1:not `eval`|=-2:not `musteval`|=1:`eval`|=2:`musteval`}",
Diagnostics::IntAsSelect);
CARBON_DIAGNOSTIC(FunctionRedeclEvaluationModePrevious, Note,
"previously {0:<0:not |:}declared as "
"{0:=-1:`eval`|=-2:`musteval`|=1:`eval`|=2:`musteval`}",
Diagnostics::IntAsSelect);
CARBON_DIAGNOSTIC_LABEL(
FunctionRedeclEvaluationModePrevious, Info,
"previously {0:<0:not |:}declared as "
"{0:=-1:`eval`|=-2:`musteval`|=1:`eval`|=2:`musteval`}",
Diagnostics::IntAsSelect);
context.emitter()
.Build(new_function.latest_decl_id(), FunctionRedeclEvaluationModeDiffers,
new_eval_mode_index ? new_eval_mode_index : -prev_eval_mode_index)
.Note(prev_function.latest_decl_id(),
FunctionRedeclEvaluationModePrevious,
prev_eval_mode_index ? prev_eval_mode_index : -new_eval_mode_index)
.Attach(
prev_function.latest_decl_id(), FunctionRedeclEvaluationModePrevious,
prev_eval_mode_index ? prev_eval_mode_index : -new_eval_mode_index)
.Emit();
return false;
}
@@ -356,18 +357,18 @@ auto CheckFunctionReturnPatternType(Context& context, SemIR::LocId loc_id,
if (!RequireConcreteType(
context, arg_type_id, SemIR::LocId(return_pattern_id),
[&](auto& builder) {
CARBON_DIAGNOSTIC(IncompleteTypeInFunctionReturnType, Context,
"function returns incomplete type {0}",
SemIR::TypeId);
builder.Context(loc_id, IncompleteTypeInFunctionReturnType,
arg_type_id);
CARBON_DIAGNOSTIC_CONTEXT(IncompleteTypeInFunctionReturnType,
"function returns incomplete type {0}",
SemIR::TypeId);
builder.Attach(loc_id, IncompleteTypeInFunctionReturnType,
arg_type_id);
},
[&](auto& builder) {
CARBON_DIAGNOSTIC(AbstractTypeInFunctionReturnType, Context,
"function returns abstract type {0}",
SemIR::TypeId);
builder.Context(loc_id, AbstractTypeInFunctionReturnType,
arg_type_id);
CARBON_DIAGNOSTIC_CONTEXT(AbstractTypeInFunctionReturnType,
"function returns abstract type {0}",
SemIR::TypeId);
builder.Attach(loc_id, AbstractTypeInFunctionReturnType,
arg_type_id);
})) {
return SemIR::ErrorInst::TypeId;
}
@@ -419,12 +420,12 @@ auto CheckFunctionDefinitionSignature(Context& context,
RequireCompleteType(
context, context.insts().Get(param_ref_id).type_id(),
SemIR::LocId(param_ref_id), [&](auto& builder) {
CARBON_DIAGNOSTIC(
IncompleteTypeInFunctionParam, Context,
CARBON_DIAGNOSTIC_CONTEXT(
IncompleteTypeInFunctionParam,
"parameter has incomplete type {0} in function definition",
TypeOfInstId);
builder.Context(param_ref_id, IncompleteTypeInFunctionParam,
param_ref_id);
builder.Attach(param_ref_id, IncompleteTypeInFunctionParam,
param_ref_id);
});
}
@@ -684,20 +685,21 @@ static auto CheckDefaults(Context& context, SemIR::Function& function)
if (!state->patterns_missing_defaults.empty()) {
CARBON_DIAGNOSTIC(RequiredPatternDefaultValueMissing, Error,
"this pattern is missing a required default value.");
CARBON_DIAGNOSTIC(RequiredPatternDefaultValueFirstDefault, Note,
"all patterns to the right of this first pattern with "
"a default value must also specify a default value.");
CARBON_DIAGNOSTIC(
RequiredPatternDefaultValueMissingAdditional, Note,
CARBON_DIAGNOSTIC_LABEL(RequiredPatternDefaultValueFirstDefault, Info,
"all patterns to the right of this first pattern "
"with a default value must also specify a "
"default value.");
CARBON_DIAGNOSTIC_LABEL(
RequiredPatternDefaultValueMissingAdditional, Primary,
"this pattern is also missing a required default value.");
auto inst_ref = llvm::ArrayRef(state->patterns_missing_defaults);
auto builder = context.emitter().Build(
inst_ref.consume_front(), RequiredPatternDefaultValueMissing);
for (auto inst_id : inst_ref) {
builder.Note(inst_id, RequiredPatternDefaultValueMissingAdditional);
builder.Attach(inst_id, RequiredPatternDefaultValueMissingAdditional);
}
builder.Note(state->first_pattern_with_default,
RequiredPatternDefaultValueFirstDefault);
builder.Attach(state->first_pattern_with_default,
RequiredPatternDefaultValueFirstDefault);
builder.Emit();
}