3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-08-04 02:10:24 +00:00

Replace stringf() with a templated function which does compile-time format string checking.

Checking only happens at compile time if -std=c++20 (or greater) is enabled. Otherwise
the checking happens at run time.

This requires the format string to be a compile-time constant (when compiling with
C++20), so fix a few places where that isn't true.

The format string behavior is a bit more lenient than C printf. For %d/%u
you can pass any integer type and it will be converted and output without
truncating bits, i.e. any length specifier is ignored and the conversion is
always treated as 'll'. Any truncation needs to be done by casting the argument itself.
For %f/%g you can pass anything that converts to double, including integers.

Performance results with clang 19 -O3 on Linux:
```
hyperfine './yosys -dp "read_rtlil /usr/local/google/home/rocallahan/Downloads/jpeg.synth.il; dump"'
```
C++17 before: Time (mean ± σ):     101.3 ms ±   0.8 ms    [User: 85.6 ms, System: 15.6 ms]
C++17 after:  Time (mean ± σ):      98.4 ms ±   1.2 ms    [User: 82.1 ms, System: 16.1 ms]
C++20 before: Time (mean ± σ):     100.9 ms ±   1.1 ms    [User: 87.0 ms, System: 13.8 ms]
C++20 after:  Time (mean ± σ):      97.8 ms ±   1.4 ms    [User: 83.1 ms, System: 14.7 ms]

The generated code is reasonably efficient. E.g. with clang 19, `stringf()` with a format
with no %% escapes and no other parameters (a weirdly common case) often compiles to a fully
inlined `std::string` construction. In general the format string parsing is often (not always)
compiled away.
This commit is contained in:
Robert O'Callahan 2025-07-09 02:31:33 +00:00
parent 8f6d7a3043
commit 6ee3cd8ffd
4 changed files with 551 additions and 19 deletions

View file

@ -384,4 +384,153 @@ std::string escape_filename_spaces(const std::string& filename)
return out;
}
void format_emit_unescaped(std::string &result, std::string_view fmt)
{
result.reserve(result.size() + fmt.size());
for (size_t i = 0; i < fmt.size(); ++i) {
char ch = fmt[i];
result.push_back(ch);
if (ch == '%' && i + 1 < fmt.size() && fmt[i + 1] == '%') {
++i;
}
}
}
std::string unescape_format_string(std::string_view fmt)
{
std::string result;
format_emit_unescaped(result, fmt);
return result;
}
static std::string string_view_stringf(std::string_view spec, ...)
{
std::string fmt(spec);
char format_specifier = fmt[fmt.size() - 1];
switch (format_specifier) {
case 'd':
case 'i':
case 'o':
case 'u':
case 'x':
case 'X': {
// Strip any length modifier off `fmt`
std::string long_fmt;
for (size_t i = 0; i + 1 < fmt.size(); ++i) {
char ch = fmt[i];
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
break;
}
long_fmt.push_back(ch);
}
// Add `lld` or whatever
long_fmt += "ll";
long_fmt.push_back(format_specifier);
fmt = long_fmt;
break;
}
default:
break;
}
va_list ap;
va_start(ap, spec);
std::string result = vstringf(fmt.c_str(), ap);
va_end(ap);
return result;
}
template <typename Arg>
static void format_emit_stringf(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, Arg arg)
{
// Delegate nontrivial formats to the C library.
switch (num_dynamic_ints) {
case DynamicIntCount::NONE:
result += string_view_stringf(spec, arg);
return;
case DynamicIntCount::ONE:
result += string_view_stringf(spec, dynamic_ints[0], arg);
return;
case DynamicIntCount::TWO:
result += string_view_stringf(spec, dynamic_ints[0], dynamic_ints[1], arg);
return;
}
YOSYS_ABORT("Internal error");
}
void format_emit_long_long(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, long long arg)
{
if (spec == "%d") {
// Format checking will have guaranteed num_dynamic_ints == 0.
result += std::to_string(arg);
return;
}
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg);
}
void format_emit_unsigned_long_long(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, unsigned long long arg)
{
if (spec == "%u") {
// Format checking will have guaranteed num_dynamic_ints == 0.
result += std::to_string(arg);
return;
}
if (spec == "%c") {
result += static_cast<char>(arg);
return;
}
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg);
}
void format_emit_double(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, double arg)
{
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg);
}
void format_emit_char_ptr(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const char *arg)
{
if (spec == "%s") {
// Format checking will have guaranteed num_dynamic_ints == 0.
result += arg;
return;
}
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg);
}
void format_emit_string(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const std::string &arg)
{
if (spec == "%s") {
// Format checking will have guaranteed num_dynamic_ints == 0.
result += arg;
return;
}
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg.c_str());
}
void format_emit_string_view(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, std::string_view arg)
{
if (spec == "%s") {
// Format checking will have guaranteed num_dynamic_ints == 0.
// We can output the string without creating a temporary copy.
result += arg;
return;
}
// Delegate nontrivial formats to the C library. We need to construct
// a temporary string to ensure null termination.
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, std::string(arg).c_str());
}
void format_emit_void_ptr(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const void *arg)
{
format_emit_stringf(result, spec, dynamic_ints, num_dynamic_ints, arg);
}
YOSYS_NAMESPACE_END