Files
carbon-lang/common/check_internal.h
T
Chandler Carruth 41e3c9bd82 Type-erase CARBON_CHECK message formatting. (#7325)
`CARBON_CHECK` and `CARBON_FATAL` messages are formatted with
`llvm::formatv`. Previously each check site that had a message
instantiated its own copy of the formatv machinery -- a `formatv_object`
over a tuple of per-argument format adapters, plus that tuple -- in
every translation unit, keyed on the site's file, line, condition, and
format strings. A translation unit with many checks paid for that
machinery over and over.

This restructures check failure so that the formatting machinery is
compiled exactly once, and only a single small adapter is instantiated
per distinct value type per translation unit:

- `CheckFailImpl` (out-of-line) now takes the message's format string
and an array of already-type-erased `format_adapter`s, and renders the
whole failure message -- prefix plus the extra message -- directly into
one stream. The extra message is rendered in place, so no separate
string is ever materialized for it.

- `FormatvInto` (in the `.cpp`) renders a format string over that
adapter array. Rather than instantiate `llvm::formatv`, it drives the
formatv replacement loop over the public
`formatv_object_base::parseFormatString`, so this rendering code exists
exactly once. (A TODO notes that we should add a type-erased entry point
upstream in LLVM rather than reimplement the loop here.)

- The lowering from the macro down to that out-of-line call is split so
that the only per-check-site instantiation is trivial:
- `CheckFail<...>` is instantiated once per site, since its file, line,
condition, and format template-string parameters are unique to the site.
It just lowers those compile-time strings to ordinary arguments and
forwards to `CheckFailFormat`.
- `CheckFailFormat<Ts...>` is instantiated once per distinct value-type
sequence and shared across sites; it builds one type-erased adapter per
value.
- `CheckFailWithAdapters<Adapters...>` collects pointers to those
adapters into an array and calls `CheckFailImpl`. It is a distinct
function so the adapter temporaries stay alive while pointers to their
base class are in flight.

Format semantics, including runtime format-string validation, are
unchanged, and the rendered message is byte-for-byte identical.

For `DCHECK` in optimized builds the check is dead code; its arguments
are now routed through a trivial `IgnoreDeadCheckArgs` no-op rather than
`CheckFail`. This still type-checks the arguments so they cannot bitrot,
without instantiating any formatting machinery for them and without
provoking unused-variable warnings.

Measured full-rebuild impact (353 first-party translation units,
fastbuild): -112.6s CPU, -6.9% relative to trunk.

Assisted-by: Claude
2026-06-10 06:49:00 +00:00

213 lines
9.2 KiB
C++

// 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_COMMON_CHECK_INTERNAL_H_
#define CARBON_COMMON_CHECK_INTERNAL_H_
#include "common/template_string.h"
#include "llvm/Support/FormatVariadic.h"
namespace Carbon::Internal {
// Evaluates a condition in a CHECK. This diagnoses if the condition evaluates
// to the constant `true` or `false`.
[[clang::always_inline]] constexpr bool
// Trailing GNU function attributes are incompatible with trailing return types.
// Filed as https://github.com/llvm/llvm-project/issues/118697
// NOLINTNEXTLINE(modernize-use-trailing-return-type)
CheckCondition(bool condition)
__attribute__((diagnose_if(condition,
"CHECK condition is always true; replace with "
"static_assert if this is intended",
"error")))
__attribute__((diagnose_if(!condition,
"CHECK condition is always false; replace with "
"CARBON_FATAL if this is intended",
"error"))) {
return condition;
}
// Implements the check failure message printing.
//
// This is out-of-line and will arrange to stop the program, print any debugging
// information and this string. In `!NDEBUG` mode (`dbg` and `fastbuild`), check
// failures can be made non-fatal by a build flag, so this is not `[[noreturn]]`
// in that case.
//
// This API uses `const char*` C string arguments rather than `llvm::StringRef`
// because we know that these are available as C strings and passing them that
// way lets the code size of calling it be smaller: it only needs to materialize
// a single pointer argument for each. The runtime cost of re-computing the size
// should be minimal.
//
// The user can provide an extra format string along with an array of
// type-erased format adapters. This will be rendered into the final message.
#ifdef NDEBUG
[[noreturn]]
#endif
auto CheckFailImpl(
const char* kind, const char* file, int line, const char* condition_str,
const char* extra_format,
llvm::ArrayRef<llvm::support::detail::format_adapter*> extra_adapters)
-> void;
// Allow converting format values; the default behaviour is to just pass them
// through.
template <typename T>
auto ConvertFormatValue(T&& t) -> T&& {
return std::forward<T>(t);
}
// Convert enums to larger integers so that byte-sized enums are not confused
// with being chars and printed as invalid (or nul-terminating) characters.
// Scoped enums are explicitly converted to integers so they can be printed
// without the user writing a cast.
template <typename T>
requires(std::is_enum_v<std::remove_reference_t<T>>)
auto ConvertFormatValue(T&& t) -> auto {
if constexpr (std::is_signed_v<
std::underlying_type_t<std::remove_reference_t<T>>>) {
return static_cast<int64_t>(t);
} else {
return static_cast<uint64_t>(t);
}
}
// Collects pointers to the given type-erased format adapters and passes them,
// with the rest of the check metadata, to the out-of-line `CheckFailImpl`.
//
// We need a separate function accepting all the adapters as arguments to ensure
// those objects stay alive for pointers to their base class to be put into an
// array and passed to the type erased implementation.
template <typename... Adapters>
#ifdef NDEBUG
[[noreturn]]
#endif
auto CheckFailWithAdapters(const char* kind, const char* file, int line,
const char* condition_str, const char* extra_format,
Adapters&&... adapters) -> void {
std::array<llvm::support::detail::format_adapter*, sizeof...(Adapters)>
adapter_pointers = {&adapters...};
CheckFailImpl(kind, file, line, condition_str, extra_format,
adapter_pointers);
}
// Builds one type-erased format adapter per value -- forwarding each value
// through the conversion machinery -- and hands them to
// `CheckFailWithAdapters`.
//
// This is templated only on the value types, not on the per-check-site
// metadata (file, line, etc., which are passed as ordinary arguments), so the
// adapter-building is instantiated once per distinct sequence of value types in
// the TU.
template <typename... Ts>
#ifdef NDEBUG
[[noreturn]]
#endif
auto CheckFailFormat(const char* kind, const char* file, int line,
const char* condition_str, const char* extra_format,
Ts&&... values) -> void {
CheckFailWithAdapters(kind, file, line, condition_str, extra_format,
llvm::support::detail::build_format_adapter(
ConvertFormatValue(std::forward<Ts>(values)))...);
}
// Prints a check failure, including rendering any user-provided message using
// a format string.
//
// The check-site metadata is passed as compile-time template strings to avoid
// runtime cost of parameter setup in optimized builds. This function is
// instantiated once per check site (its template arguments are unique to the
// site), so it is kept trivial: it just lowers those template strings to
// ordinary arguments and forwards everything to `CheckFailFormat`, where the
// adapter-building is shared across sites with the same value types.
template <TemplateString Kind, TemplateString File, int Line,
TemplateString ConditionStr, TemplateString FormatStr, typename... Ts>
#ifdef NDEBUG
[[noreturn]]
#endif
[[gnu::cold, clang::noinline]] auto CheckFail(Ts&&... values) -> void {
CheckFailFormat(Kind.c_str(), File.c_str(), Line, ConditionStr.c_str(),
FormatStr.c_str(), std::forward<Ts>(values)...);
}
// Type-checks the arguments of a `DCHECK` in optimized builds, where the check
// itself is dead code, without instantiating any formatting machinery for them
// and without provoking unused-variable warnings. It is only ever named from
// dead code, so it is never actually called.
template <typename... Ts>
auto IgnoreDeadCheckArgs(Ts&&... /*values*/) -> void {}
} // namespace Carbon::Internal
// Evaluates the condition of a CHECK as a boolean value.
//
// This performs a contextual conversion to bool, diagnoses if the condition is
// always true or always false, and returns its value.
#define CARBON_INTERNAL_CHECK_CONDITION(cond) \
(Carbon::Internal::CheckCondition(true && (cond)))
// Implements check messages without any formatted values.
//
// Passes each of the provided components of the message to the template
// parameters of the check failure printing function above, including an empty
// string for the format string. Because there are multiple template arguments,
// the entire call is wrapped in parentheses.
#define CARBON_INTERNAL_CHECK_IMPL(kind, file, line, condition_str) \
(Carbon::Internal::CheckFail<kind, file, line, condition_str, "">())
// Implements check messages with a format string and potentially formatted
// values.
//
// Each of the main components is passed as a template arguments, and then any
// formatted values are passed as arguments. Because there are multiple template
// arguments, the entire call is wrapped in parentheses.
#define CARBON_INTERNAL_CHECK_IMPL_FORMAT(kind, file, line, condition_str, \
format_str, ...) \
(Carbon::Internal::CheckFail<kind, file, line, condition_str, format_str>( \
__VA_ARGS__))
// Implements the failure of a check.
//
// Collects all the metadata about the failure to be printed, such as source
// location and stringified condition, and passes those, any format string and
// formatted arguments to the correct implementation macro above.
#define CARBON_INTERNAL_CHECK(condition, ...) \
CARBON_INTERNAL_CHECK_IMPL##__VA_OPT__(_FORMAT)( \
"CHECK", __FILE__, __LINE__, #condition __VA_OPT__(, ) __VA_ARGS__)
// Implements the fatal macro.
//
// Similar to the check failure macro, but tags the message as a fatal one and
// leaves the stringified condition empty.
#define CARBON_INTERNAL_FATAL(...) \
(CARBON_INTERNAL_CHECK_IMPL##__VA_OPT__(_FORMAT)( \
"FATAL", __FILE__, __LINE__, "" __VA_OPT__(, ) __VA_ARGS__), \
CARBON_INTERNAL_FATAL_NORETURN_SUFFIX())
#ifdef NDEBUG
// For `DCHECK` in optimized builds the check is dead code, but we still want to
// type-check its arguments so they can't bitrot. We route them through
// `IgnoreDeadCheckArgs`, which uses the arguments (avoiding unused-variable
// warnings) but builds no format adapters, so the dead check doesn't pull in
// the formatting machinery -- in particular not the per-value-type adapters
// that the live `CheckFail` path would. The format string is a literal, so it
// needs no type-checking and is dropped.
#define CARBON_INTERNAL_DEAD_DCHECK(condition, ...) \
CARBON_INTERNAL_DEAD_DCHECK_IMPL##__VA_OPT__(_FORMAT)(__VA_ARGS__)
#define CARBON_INTERNAL_DEAD_DCHECK_IMPL() \
Carbon::Internal::IgnoreDeadCheckArgs()
#define CARBON_INTERNAL_DEAD_DCHECK_IMPL_FORMAT(format_str, ...) \
Carbon::Internal::IgnoreDeadCheckArgs(__VA_ARGS__)
// The `CheckFail` function itself is noreturn in NDEBUG.
#define CARBON_INTERNAL_FATAL_NORETURN_SUFFIX() void()
#else
#define CARBON_INTERNAL_FATAL_NORETURN_SUFFIX() std::abort()
#endif
#endif // CARBON_COMMON_CHECK_INTERNAL_H_