Files
carbon-lang/common/check_internal.cpp
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

93 lines
3.4 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
#include "common/check_internal.h"
#include <cstdlib>
#include <string>
#include "common/ostream.h"
#include "llvm/Support/FormatCommon.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/raw_ostream.h"
namespace Carbon::Internal {
namespace {
// Renders `fmt` over the externally-built, type-erased `adapters` into `out`,
// with the same semantics as `llvm::formatv` (including runtime format-string
// validation).
//
// TODO: We should add a type-erased helper to upstream LLVM instead of rolling
// our own type-erased version of `format` here.
auto FormatvInto(
llvm::raw_ostream& out, llvm::StringRef format_str,
llvm::ArrayRef<llvm::support::detail::format_adapter*> adapters) -> void {
for (const llvm::ReplacementItem& replacement :
llvm::formatv_object_base::parseFormatString(format_str, adapters.size(),
/*Validate=*/true)) {
if (replacement.Type == llvm::ReplacementType::Literal ||
replacement.Index >= adapters.size()) {
out << replacement.Spec;
continue;
}
llvm::FmtAlign(*adapters[replacement.Index], replacement.Where,
replacement.Width, replacement.Pad)
.format(out, replacement.Options);
}
}
} // namespace
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 {
// Render the final check string directly into one stream. The extra message
// is rendered in place from its format string and type-erased adapters, so
// we never materialize a separate string just for it.
//
// `llvm::raw_string_ostream` (rather than `common/raw_string_ostream.h`) is
// used to avoid a dependency cycle: `RawStringOstream` itself uses
// `CARBON_CHECK`. It is unbuffered, so `message` is populated directly.
std::string message;
llvm::raw_string_ostream message_stream(message);
message_stream << kind << " failure at " << file << ":" << line;
if (*condition_str != '\0') {
message_stream << ": " << condition_str;
}
if (*extra_format != '\0') {
message_stream << ": ";
FormatvInto(message_stream, extra_format, extra_adapters);
}
message_stream << "\n";
// This macro is defined by `--config=non-fatal-checks`.
#ifdef CARBON_NON_FATAL_CHECKS
#ifdef NDEBUG
#error "--config=non-fatal-checks is incompatible with -c opt"
#endif
// TODO: It'd be nice to print the LLVM PrettyStackTrace, but LLVM doesn't
// expose functionality to do so.
llvm::sys::PrintStackTrace(llvm::errs());
llvm::errs() << message;
#else
// Register another signal handler to print the message. This is because we
// want it at the bottom of output, after LLVM's builtin stack output, rather
// than the top.
llvm::sys::AddSignalHandler(
[](void* str) { llvm::errs() << reinterpret_cast<char*>(str); },
const_cast<char*>(message.c_str()));
// It's useful to exit the program with `std::abort()` for integration with
// debuggers and other tools. We also assume LLVM's exit handling is
// installed, which will stack trace on `std::abort()`.
std::abort();
#endif
}
} // namespace Carbon::Internal