.agents/skills/diagnostics/SKILL.md
The Carbon compiler features a highly-engineered, context-aware diagnostics framework designed to deliver precise, readable, and highly targetable diagnostic output (errors, warnings, notes). This document establishes strict rules for declaring, formatting, emitting, testing, and styling compiler diagnostics.
graph TD
Kind[kind.def Registry] -->|Registration| Enum[Kind Enum ID]
Enum -->|Build/Emit| Emitter[Emitter LocT]
Emitter -->|ConvertLoc| Loc[Converted Physical Loc]
Emitter -->|formatv serialization| Formatting[format_providers.h / Custom Types]
Emitter -->|Emit Messages| Consumer[Console / Sorting Consumer]
Consumer -->|stable sort| StdErr[Compiler Standard Error]
Diagnostics are handled via three decoupled core components:
LocT like Token or LocId) that convert raw tokens
to standardized physical source locations (file, line, column, and text
snippet).SortingConsumer buffers and stable-sorts diagnostics based on
their last_byte_offset matching compiler traversal order to ensure perfect
causal ordering.All diagnostic types must pass structural uniqueness and coverage verifications.
Every diagnostic kind must be registered globally as an enum option under kind.def:
// toolchain/diagnostics/kind.def
CARBON_DIAGNOSTIC_KIND(RealLiteralTooLargeForUnsizedInt)
To ensure optimal compile-time and analysis integrity, every diagnostic kind
declared in kind.def MUST be mapped to one and only one C++ macro
declaration (CARBON_DIAGNOSTIC or CARBON_DIAGNOSTIC_ON_SCOPE).
DiagnosticBase<Args...>.Emit trigger:
void ConvertFloatValueToInt(...) {
CARBON_DIAGNOSTIC(FloatNaNConvertedToInt, Error,
"cannot convert NaN to integer type {0}", SemIR::TypeId);
context.emitter().Emit(loc_id, FloatNaNConvertedToInt, dest_type_id);
}
.cpp file.extern where applicable, ensuring the
macro is only invoked once.Carbon diagnostics leverage LLVM's formatv engine. Parameters must be passed
using strongly-typed arguments to preserve translation capability.
llvm::StringRef is DISALLOWED: Do not pass StringRef as a parameter
type to CARBON_DIAGNOSTIC due to unsafe lifetime and buffer-allocation
boundaries.llvm::StringLiteral is DISALLOWED: Do not use literal types as
arguments as they prevent future diagnostic localization and translations.std::string: If string formatting or custom allocations are
required, declare the parameter storage type as std::string.format_providers.h)Use specialized formatting wrappers under format_providers.h to express clean inline options in format strings:
| Wrapper | Target Format Style | Example Usage | Output |
|---|---|---|---|
BoolAsSelect | {Index:true|false} | "{0:is signed|is unsigned}" | Maps bool to selection string. |
IntAsSelect | {Index:=Val:String|:Default} | "{0:=1:is|:are}" | Matches exact options. |
IntAsSelect (Plural) | {Index:s} | "{0} argument{0:s}" | Prints "s" if value != 1 (e.g., "1 argument", "3 arguments"). |
Custom structures can define how they serialize inside diagnostics using the
DiagnosticType tag mapping to Diagnostics::TypeInfo<StorageType>:
check/diagnostic_helpers.h):
NameId: Formats raw identifier spelling, safely escaping keyword
conflicts under backticks automatically.LibraryNameId: Formats custom library descriptors cleanly (e.g.
default library or library "foo").TypedInt: Formats an APInt constant exactly, extracting target
signedness representation automatically from its bound type
representation.TypeOfInstId (Preferred): Resolves the backing type of an
InstId, preserving programmatic aliasing, constraints, and source
spelling context. Enclosed under backticks automatically.InstIdAsType: Converts an InstId for a type expression, printing
custom type layouts under backticks.TypeId (Fallback): Canonical description of the type. Avoid when
possible because type canonicalization loses intermediate source
program spelling and aliasing metadata.*AsRawType (e.g. InstIdAsRawType, TypeIdAsRawType): Formats
the type layout exactly like their counter-structures above, but
omits enclosing backticks (useful when inserting types inside larger
code snippets).For compound diagnostics requiring multiple sub-notes, carets, or custom code
overrides, use Build to chain actions fluently:
context.emitter()
.Build(second_node, ModifierRepeated, context.token_kind(second_node))
.Note(first_node, ModifierPrevious, context.token_kind(first_node))
.OverrideSnippet("custom snippet...")
.Emit();
[!SAFETY] Emitter builders are marked
[[nodiscard]]. To prevent a developer from creating a builder but failing to terminal-chain.Emit(), the builder uses an rvalue overloadEmit() &&that triggers a compile-timestatic_assert(false). You must save the builder to an lvalue or execute the chain exactly asemitter.Build(...).Note(...).Emit().
Manage large checking structures requiring blanket note context using RAII block scopes:
ContextScope: Automatically converts any diagnostics emitted within its
scope into sub-notes under a high-level operation descriptor:
ContextScope context_scope(&context.emitter(), [&](ContextBuilder& builder) {
builder.Context(eval_loc, InCallToEvalFn);
});
// any checker error emitted here will automatically append the 'InCallToEvalFn' note
AnnotationScope: RAII block scope that automatically attaches blanket note
annotations to all scoped diagnostics.Refer to the official Diagnostic message style guide for complete details.
To maintain message consistency and integrate cleanly with Clang diagnostics in interoperable code, adhere strictly to these rules:
"cannot convert..." or "`self` declared...")."`{0}` is bad").a, an, the) unless necessary for logical
clarity. Semicolons can be used to separate fragments within a message."redeclaration of X" (implies that redeclaration is
not permitted)."`self` declared in invalid context; can only be declared in implicit parameter list"."allowed",
"legal", "permitted", "valid", and related passive wording. You may
use "cannot" if needed, but try to use phrasing that does not require it:
"`export` in `impl` file" (Avoids "allowed")"`export` is only allowed in API files""`extern library` specifies current library" (Avoids
"cannot")"`extern library` cannot specify the current library""cannot implicitly convert `i32` to `String`; add `as String` for explicit conversion""add `as String` to convert `i32` to `String`" (Lacks
the core violation message).Carbon strictly enforces testing coverage at build-time.
kind.def (which is not blacklisted in the UntestedKinds array under
coverage_test.cpp)
MUST be verified by at least one testcase file inside
toolchain/*/testdata/.// CHECK:STDERR: fail_bounds.carbon:[[@LINE+1]]:15: error: cannot convert NaN to integer type `i32` [FloatNaNConvertedToInt]
let a: i32 = Convert(nan_val);
//toolchain/diagnostics:coverage_test.