3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-08-02 17:30: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

@ -1163,9 +1163,9 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
dump_sigspec(f, cell->getPort(ID::Y));
f << stringf(" = ~((");
dump_cell_expr_port(f, cell, "A", false);
f << stringf(cell->type == ID($_AOI3_) ? " & " : " | ");
f << (cell->type == ID($_AOI3_) ? " & " : " | ");
dump_cell_expr_port(f, cell, "B", false);
f << stringf(cell->type == ID($_AOI3_) ? ") |" : ") &");
f << (cell->type == ID($_AOI3_) ? ") |" : ") &");
dump_attributes(f, "", cell->attributes, " ");
f << stringf(" ");
dump_cell_expr_port(f, cell, "C", false);
@ -1178,13 +1178,13 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
dump_sigspec(f, cell->getPort(ID::Y));
f << stringf(" = ~((");
dump_cell_expr_port(f, cell, "A", false);
f << stringf(cell->type == ID($_AOI4_) ? " & " : " | ");
f << (cell->type == ID($_AOI4_) ? " & " : " | ");
dump_cell_expr_port(f, cell, "B", false);
f << stringf(cell->type == ID($_AOI4_) ? ") |" : ") &");
f << (cell->type == ID($_AOI4_) ? ") |" : ") &");
dump_attributes(f, "", cell->attributes, " ");
f << stringf(" (");
dump_cell_expr_port(f, cell, "C", false);
f << stringf(cell->type == ID($_AOI4_) ? " & " : " | ");
f << (cell->type == ID($_AOI4_) ? " & " : " | ");
dump_cell_expr_port(f, cell, "D", false);
f << stringf("));\n");
return true;
@ -1395,10 +1395,10 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
int s_width = cell->getPort(ID::S).size();
std::string func_name = cellname(cell);
f << stringf("%s" "function [%d:0] %s;\n", indent.c_str(), width-1, func_name.c_str());
f << stringf("%s" " input [%d:0] a;\n", indent.c_str(), width-1);
f << stringf("%s" " input [%d:0] b;\n", indent.c_str(), s_width*width-1);
f << stringf("%s" " input [%d:0] s;\n", indent.c_str(), s_width-1);
f << stringf("%s" "function [%d:0] %s;\n", indent, width-1, func_name);
f << stringf("%s" " input [%d:0] a;\n", indent, width-1);
f << stringf("%s" " input [%d:0] b;\n", indent, s_width*width-1);
f << stringf("%s" " input [%d:0] s;\n", indent, s_width-1);
dump_attributes(f, indent + " ", cell->attributes);
if (noparallelcase)
@ -1407,7 +1407,7 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
if (!noattr)
f << stringf("%s" " (* parallel_case *)\n", indent.c_str());
f << stringf("%s" " casez (s)", indent.c_str());
f << stringf(noattr ? " // synopsys parallel_case\n" : "\n");
f << (noattr ? " // synopsys parallel_case\n" : "\n");
}
for (int i = 0; i < s_width; i++)

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

View file

@ -1,5 +1,6 @@
#include <string>
#include <stdarg.h>
#include <type_traits>
#include "kernel/yosys_common.h"
#ifndef YOSYS_IO_H
@ -50,18 +51,391 @@ inline std::string vstringf(const char *fmt, va_list ap)
#endif
}
std::string stringf(const char *fmt, ...) YS_ATTRIBUTE(format(printf, 1, 2));
inline std::string stringf(const char *fmt, ...)
enum ConversionSpecifier : uint8_t
{
std::string string;
va_list ap;
CONVSPEC_NONE,
// Specifier not understood/supported
CONVSPEC_ERROR,
// Consumes a "long long"
CONVSPEC_SIGNED_INT,
// Consumes a "unsigned long long"
CONVSPEC_UNSIGNED_INT,
// Consumes a "double"
CONVSPEC_DOUBLE,
// Consumes a "const char*"
CONVSPEC_CHAR_PTR,
// Consumes a "void*"
CONVSPEC_VOID_PTR,
};
va_start(ap, fmt);
string = vstringf(fmt, ap);
va_end(ap);
constexpr ConversionSpecifier parse_conversion_specifier(char ch, char prev_ch)
{
switch (ch) {
case 'd':
case 'i':
return CONVSPEC_SIGNED_INT;
case 'o':
case 'u':
case 'x':
case 'X':
case 'm':
return CONVSPEC_UNSIGNED_INT;
case 'c':
if (prev_ch == 'l' || prev_ch == 'q' || prev_ch == 'L') {
// wchar not supported
return CONVSPEC_ERROR;
}
return CONVSPEC_UNSIGNED_INT;
case 'e':
case 'E':
case 'f':
case 'F':
case 'g':
case 'G':
case 'a':
case 'A':
return CONVSPEC_DOUBLE;
case 's':
if (prev_ch == 'l' || prev_ch == 'q' || prev_ch == 'L') {
// wchar not supported
return CONVSPEC_ERROR;
}
return CONVSPEC_CHAR_PTR;
case 'p':
return CONVSPEC_VOID_PTR;
case '$': // positional parameters
case 'n':
case 'S':
return CONVSPEC_ERROR;
default:
return CONVSPEC_NONE;
}
}
return string;
enum class DynamicIntCount : uint8_t {
NONE = 0,
ONE = 1,
TWO = 2,
};
// Describes a printf-style format conversion specifier found in a format string.
struct FoundFormatSpec
{
// The start offset of the conversion spec in the format string.
int start;
// The end offset of the conversion spec in the format string.
int end;
ConversionSpecifier spec;
// Number of int args consumed by '*' dynamic width/precision args.
DynamicIntCount num_dynamic_ints;
};
// Ensure there is no format spec.
constexpr void ensure_no_format_spec(std::string_view fmt, int index, bool *has_escapes)
{
int fmt_size = static_cast<int>(fmt.size());
// A trailing '%' is not a format spec.
while (index + 1 < fmt_size) {
if (fmt[index] != '%') {
++index;
continue;
}
if (fmt[index + 1] != '%') {
YOSYS_ABORT("More format conversion specifiers than arguments");
}
*has_escapes = true;
index += 2;
}
}
// Returns the next format conversion specifier (starting with '%').
// Returns CONVSPEC_NONE if there isn't a format conversion specifier.
constexpr FoundFormatSpec find_next_format_spec(std::string_view fmt, int fmt_start, bool *has_escapes)
{
int index = fmt_start;
int fmt_size = static_cast<int>(fmt.size());
while (index < fmt_size) {
if (fmt[index] != '%') {
++index;
continue;
}
int p = index + 1;
uint8_t num_dynamic_ints = 0;
while (true) {
if (p == fmt_size) {
return {0, 0, CONVSPEC_NONE, DynamicIntCount::NONE};
}
if (fmt[p] == '%') {
*has_escapes = true;
index = p + 1;
break;
}
if (fmt[p] == '*') {
if (num_dynamic_ints >= 2) {
return {0, 0, CONVSPEC_ERROR, DynamicIntCount::NONE};
}
++num_dynamic_ints;
}
ConversionSpecifier spec = parse_conversion_specifier(fmt[p], fmt[p - 1]);
if (spec != CONVSPEC_NONE) {
return {index, p + 1, spec, static_cast<DynamicIntCount>(num_dynamic_ints)};
}
++p;
}
}
return {0, 0, CONVSPEC_NONE, DynamicIntCount::NONE};
}
template <typename... Args>
constexpr typename std::enable_if<sizeof...(Args) == 0>::type
check_format(std::string_view fmt, int fmt_start, bool *has_escapes, FoundFormatSpec*, DynamicIntCount)
{
ensure_no_format_spec(fmt, fmt_start, has_escapes);
}
// Check that the format string `fmt.substr(fmt_start)` is valid for the given type arguments.
// Fills `specs` with the FoundFormatSpecs found in the format string.
// `int_args_consumed` is the number of int arguments already consumed to satisfy the
// dynamic width/precision args for the next format conversion specifier.
template <typename Arg, typename... Args>
constexpr void check_format(std::string_view fmt, int fmt_start, bool *has_escapes, FoundFormatSpec* specs,
DynamicIntCount int_args_consumed)
{
FoundFormatSpec found = find_next_format_spec(fmt, fmt_start, has_escapes);
if (found.num_dynamic_ints > int_args_consumed) {
// We need to consume at least one more int for the dynamic
// width/precision of this format conversion specifier.
if constexpr (!std::is_convertible_v<Arg, int>) {
YOSYS_ABORT("Expected dynamic int argument");
}
check_format<Args...>(fmt, fmt_start, has_escapes, specs,
static_cast<DynamicIntCount>(static_cast<uint8_t>(int_args_consumed) + 1));
return;
}
switch (found.spec) {
case CONVSPEC_NONE:
YOSYS_ABORT("Expected format conversion specifier for argument");
break;
case CONVSPEC_ERROR:
YOSYS_ABORT("Found unsupported format conversion specifier");
break;
case CONVSPEC_SIGNED_INT:
if constexpr (!std::is_convertible_v<Arg, long long>) {
YOSYS_ABORT("Expected type convertible to signed integer");
}
*specs = found;
break;
case CONVSPEC_UNSIGNED_INT:
if constexpr (!std::is_convertible_v<Arg, unsigned long long>) {
YOSYS_ABORT("Expected type convertible to unsigned integer");
}
*specs = found;
break;
case CONVSPEC_DOUBLE:
if constexpr (!std::is_convertible_v<Arg, double>) {
YOSYS_ABORT("Expected type convertible to double");
}
*specs = found;
break;
case CONVSPEC_CHAR_PTR:
if constexpr (!std::is_convertible_v<Arg, const char *> &&
!std::is_convertible_v<Arg, const std::string &> &&
!std::is_convertible_v<Arg, const std::string_view &>) {
YOSYS_ABORT("Expected type convertible to char *");
}
*specs = found;
break;
case CONVSPEC_VOID_PTR:
if constexpr (!std::is_convertible_v<Arg, const void *>) {
YOSYS_ABORT("Expected pointer type");
}
*specs = found;
break;
}
check_format<Args...>(fmt, found.end, has_escapes, specs + 1, DynamicIntCount::NONE);
}
// Emit the string representation of `arg` that has been converted to a `long long'.
void format_emit_long_long(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, long long arg);
// Emit the string representation of `arg` that has been converted to a `unsigned long long'.
void format_emit_unsigned_long_long(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, unsigned long long arg);
// Emit the string representation of `arg` that has been converted to a `double'.
void format_emit_double(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, double arg);
// Emit the string representation of `arg` that has been converted to a `const char*'.
void format_emit_char_ptr(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const char *arg);
// Emit the string representation of `arg` that has been converted to a `std::string'.
void format_emit_string(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const std::string &arg);
// Emit the string representation of `arg` that has been converted to a `std::string_view'.
void format_emit_string_view(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, std::string_view arg);
// Emit the string representation of `arg` that has been converted to a `double'.
void format_emit_void_ptr(std::string &result, std::string_view spec, int *dynamic_ints,
DynamicIntCount num_dynamic_ints, const void *arg);
// Emit the string representation of `arg` according to the given `FoundFormatSpec`,
// appending it to `result`.
template <typename Arg>
inline void format_emit_one(std::string &result, std::string_view fmt, const FoundFormatSpec &ffspec,
int *dynamic_ints, const Arg& arg)
{
std::string_view spec = fmt.substr(ffspec.start, ffspec.end - ffspec.start);
DynamicIntCount num_dynamic_ints = ffspec.num_dynamic_ints;
switch (ffspec.spec) {
case CONVSPEC_SIGNED_INT:
if constexpr (std::is_convertible_v<Arg, long long>) {
long long s = arg;
format_emit_long_long(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
break;
case CONVSPEC_UNSIGNED_INT:
if constexpr (std::is_convertible_v<Arg, unsigned long long>) {
unsigned long long s = arg;
format_emit_unsigned_long_long(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
break;
case CONVSPEC_DOUBLE:
if constexpr (std::is_convertible_v<Arg, double>) {
double s = arg;
format_emit_double(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
break;
case CONVSPEC_CHAR_PTR:
if constexpr (std::is_convertible_v<Arg, const char *>) {
const char *s = arg;
format_emit_char_ptr(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
if constexpr (std::is_convertible_v<Arg, const std::string &>) {
const std::string &s = arg;
format_emit_string(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
if constexpr (std::is_convertible_v<Arg, const std::string_view &>) {
const std::string_view &s = arg;
format_emit_string_view(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
break;
case CONVSPEC_VOID_PTR:
if constexpr (std::is_convertible_v<Arg, const void *>) {
const void *s = arg;
format_emit_void_ptr(result, spec, dynamic_ints, num_dynamic_ints, s);
return;
}
break;
default:
break;
}
YOSYS_ABORT("Internal error");
}
// Append the format string `fmt` to `result`, assuming there are no format conversion
// specifiers other than "%%" and therefore no arguments. Unescape "%%".
void format_emit_unescaped(std::string &result, std::string_view fmt);
std::string unescape_format_string(std::string_view fmt);
inline void format_emit(std::string &result, std::string_view fmt, int fmt_start,
bool has_escapes, const FoundFormatSpec*, int*, DynamicIntCount)
{
fmt = fmt.substr(fmt_start);
if (has_escapes) {
format_emit_unescaped(result, fmt);
} else {
result += fmt;
}
}
// Format `args` according to `fmt` (starting at `fmt_start`) and `specs` and append to `result`.
// `num_dynamic_ints` in `dynamic_ints[]` have already been collected to provide as
// dynamic width/precision args for the next format conversion specifier.
template <typename Arg, typename... Args>
inline void format_emit(std::string &result, std::string_view fmt, int fmt_start, bool has_escapes,
const FoundFormatSpec* specs, int *dynamic_ints, DynamicIntCount num_dynamic_ints,
const Arg &arg, const Args &... args)
{
if (specs->num_dynamic_ints > num_dynamic_ints) {
// Collect another int for the dynamic width precision/args
// for the next format conversion specifier.
if constexpr (std::is_convertible_v<Arg, int>) {
dynamic_ints[static_cast<uint8_t>(num_dynamic_ints)] = arg;
} else {
YOSYS_ABORT("Internal error");
}
format_emit(result, fmt, fmt_start, has_escapes, specs, dynamic_ints,
static_cast<DynamicIntCount>(static_cast<uint8_t>(num_dynamic_ints) + 1), args...);
return;
}
std::string_view str = fmt.substr(fmt_start, specs->start - fmt_start);
if (has_escapes) {
format_emit_unescaped(result, str);
} else {
result += str;
}
format_emit_one(result, fmt, *specs, dynamic_ints, arg);
format_emit(result, fmt, specs->end, has_escapes, specs + 1, dynamic_ints, DynamicIntCount::NONE, args...);
}
template <typename... Args>
inline std::string format_emit_toplevel(std::string_view fmt, bool has_escapes, const FoundFormatSpec* specs, const Args &... args)
{
std::string result;
int dynamic_ints[2] = { 0, 0 };
format_emit(result, fmt, 0, has_escapes, specs, dynamic_ints, DynamicIntCount::NONE, args...);
return result;
}
template <>
inline std::string format_emit_toplevel(std::string_view fmt, bool has_escapes, const FoundFormatSpec*)
{
if (!has_escapes) {
return std::string(fmt);
}
return unescape_format_string(fmt);
}
// This class parses format strings to build a list of `FoundFormatSpecs` in `specs`.
// When the compiler supports `consteval` (C++20), this parsing happens at compile time and
// type errors will be reported at compile time. Otherwise the parsing happens at
// runtime and type errors will trigger an `abort()` at runtime.
template <typename... Args>
class FmtString
{
public:
// Implicit conversion from const char * means that users can pass
// C string constants which are automatically parsed.
YOSYS_CONSTEVAL FmtString(const char *p) : fmt(p)
{
check_format<Args...>(fmt, 0, &has_escapes, specs, DynamicIntCount::NONE);
}
std::string format(const Args &... args)
{
return format_emit_toplevel(fmt, has_escapes, specs, args...);
}
private:
std::string_view fmt;
bool has_escapes = false;
FoundFormatSpec specs[sizeof...(Args)] = {};
};
template <typename T> struct WrapType { using type = T; };
template <typename T> using TypeIdentity = typename WrapType<T>::type;
template <typename... Args>
inline std::string stringf(FmtString<TypeIdentity<Args>...> fmt, Args... args)
{
return fmt.format(args...);
}
int readsome(std::istream &f, char *s, int n);

View file

@ -134,6 +134,15 @@
# define YS_COLD
#endif
#ifdef __cpp_consteval
#define YOSYS_CONSTEVAL consteval
#else
// If we can't use consteval we can at least make it constexpr.
#define YOSYS_CONSTEVAL constexpr
#endif
#define YOSYS_ABORT(s) abort()
#include "kernel/io.h"
YOSYS_NAMESPACE_BEGIN