Skip to content

Commit f1c90fb

Browse files
committed
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, so fix a few places where that isn't true.
1 parent e57a2b9 commit f1c90fb

File tree

4 files changed

+323
-13
lines changed

4 files changed

+323
-13
lines changed

backends/verilog/verilog_backend.cc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,9 +1163,9 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
11631163
dump_sigspec(f, cell->getPort(ID::Y));
11641164
f << stringf(" = ~((");
11651165
dump_cell_expr_port(f, cell, "A", false);
1166-
f << stringf(cell->type == ID($_AOI3_) ? " & " : " | ");
1166+
f << (cell->type == ID($_AOI3_) ? " & " : " | ");
11671167
dump_cell_expr_port(f, cell, "B", false);
1168-
f << stringf(cell->type == ID($_AOI3_) ? ") |" : ") &");
1168+
f << (cell->type == ID($_AOI3_) ? ") |" : ") &");
11691169
dump_attributes(f, "", cell->attributes, " ");
11701170
f << stringf(" ");
11711171
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)
11781178
dump_sigspec(f, cell->getPort(ID::Y));
11791179
f << stringf(" = ~((");
11801180
dump_cell_expr_port(f, cell, "A", false);
1181-
f << stringf(cell->type == ID($_AOI4_) ? " & " : " | ");
1181+
f << (cell->type == ID($_AOI4_) ? " & " : " | ");
11821182
dump_cell_expr_port(f, cell, "B", false);
1183-
f << stringf(cell->type == ID($_AOI4_) ? ") |" : ") &");
1183+
f << (cell->type == ID($_AOI4_) ? ") |" : ") &");
11841184
dump_attributes(f, "", cell->attributes, " ");
11851185
f << stringf(" (");
11861186
dump_cell_expr_port(f, cell, "C", false);
1187-
f << stringf(cell->type == ID($_AOI4_) ? " & " : " | ");
1187+
f << (cell->type == ID($_AOI4_) ? " & " : " | ");
11881188
dump_cell_expr_port(f, cell, "D", false);
11891189
f << stringf("));\n");
11901190
return true;
@@ -1407,7 +1407,7 @@ bool dump_cell_expr(std::ostream &f, std::string indent, RTLIL::Cell *cell)
14071407
if (!noattr)
14081408
f << stringf("%s" " (* parallel_case *)\n", indent.c_str());
14091409
f << stringf("%s" " casez (s)", indent.c_str());
1410-
f << stringf(noattr ? " // synopsys parallel_case\n" : "\n");
1410+
f << (noattr ? " // synopsys parallel_case\n" : "\n");
14111411
}
14121412

14131413
for (int i = 0; i < s_width; i++)

kernel/io.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,4 +384,16 @@ std::string escape_filename_spaces(const std::string& filename)
384384
return out;
385385
}
386386

387+
void format_emit_no_conversions(std::string &result, std::string_view fmt)
388+
{
389+
result.reserve(result.size() + fmt.size());
390+
for (size_t i = 0; i < fmt.size(); ++i) {
391+
char ch = fmt[i];
392+
result.push_back(ch);
393+
if (ch == '%' && i + 1 < fmt.size() && fmt[i + 1] == '%') {
394+
++i;
395+
}
396+
}
397+
}
398+
387399
YOSYS_NAMESPACE_END

kernel/io.h

Lines changed: 297 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <string>
22
#include <stdarg.h>
3+
#include <type_traits>
34
#include "kernel/yosys_common.h"
45

56
#ifndef YOSYS_IO_H
@@ -50,18 +51,307 @@ inline std::string vstringf(const char *fmt, va_list ap)
5051
#endif
5152
}
5253

53-
std::string stringf(const char *fmt, ...) YS_ATTRIBUTE(format(printf, 1, 2));
54+
enum class ConversionSpecifier : char
55+
{
56+
NONE,
57+
// Specifier not understood/supported
58+
ERROR,
59+
// Consumes a "long long"
60+
SIGNED_INT,
61+
// Consumes a "unsigned long long"
62+
UNSIGNED_INT,
63+
// Consumes a "double"
64+
DOUBLE,
65+
// Consumes a "const char*"
66+
CHAR_PTR,
67+
// Consumes a "void*"
68+
VOID_PTR,
69+
};
5470

55-
inline std::string stringf(const char *fmt, ...)
71+
constexpr ConversionSpecifier parse_conversion_specifier(char ch)
5672
{
57-
std::string string;
58-
va_list ap;
73+
switch (ch) {
74+
case 'd':
75+
case 'i':
76+
return ConversionSpecifier::SIGNED_INT;
77+
case 'o':
78+
case 'u':
79+
case 'x':
80+
case 'X':
81+
case 'c':
82+
case 'm':
83+
return ConversionSpecifier::UNSIGNED_INT;
84+
case 'e':
85+
case 'E':
86+
case 'f':
87+
case 'F':
88+
case 'g':
89+
case 'G':
90+
case 'a':
91+
case 'A':
92+
return ConversionSpecifier::DOUBLE;
93+
case 's':
94+
return ConversionSpecifier::CHAR_PTR;
95+
case 'p':
96+
return ConversionSpecifier::VOID_PTR;
97+
case '$': // positional parameters
98+
case 'n':
99+
case 'S':
100+
return ConversionSpecifier::ERROR;
101+
default:
102+
return ConversionSpecifier::NONE;
103+
}
104+
}
105+
106+
// Describes a printf-style format conversion specifier found in a format string.
107+
struct FoundFormatSpec
108+
{
109+
// The start offset of the conversion spec in the format string.
110+
int start;
111+
// The end offset of the conversion spec in the format string.
112+
int end;
113+
ConversionSpecifier spec;
114+
// Number of int args consumed by '*' dynamic width/precision args.
115+
uint8_t num_dynamic_ints;
116+
};
117+
118+
// Returns the next format conversion specifier (starting with '%').
119+
// Returns ConversionSpecifier::NONE if there isn't a format conversion specifier.
120+
constexpr FoundFormatSpec find_next_format_spec(std::string_view fmt, int fmt_start)
121+
{
122+
int index = fmt_start;
123+
int fmt_size = static_cast<int>(fmt.size());
124+
while (index < fmt_size) {
125+
if (fmt[index] != '%') {
126+
++index;
127+
continue;
128+
}
129+
int p = index + 1;
130+
uint8_t num_dynamic_ints = 0;
131+
while (true) {
132+
if (p == fmt_size) {
133+
return {0, 0, ConversionSpecifier::NONE, 0};
134+
}
135+
if (fmt[p] == '%') {
136+
index = p + 1;
137+
break;
138+
}
139+
if (fmt[p] == '*') {
140+
if (num_dynamic_ints >= 2) {
141+
return {0, 0, ConversionSpecifier::ERROR, 0};
142+
}
143+
++num_dynamic_ints;
144+
}
145+
ConversionSpecifier spec = parse_conversion_specifier(fmt[p]);
146+
if (spec != ConversionSpecifier::NONE) {
147+
return {index, p + 1, spec, num_dynamic_ints};
148+
}
149+
++p;
150+
}
151+
}
152+
return {0, 0, ConversionSpecifier::NONE, 0};
153+
}
59154

60-
va_start(ap, fmt);
61-
string = vstringf(fmt, ap);
155+
template <typename... Args>
156+
constexpr typename std::enable_if<sizeof...(Args) == 0>::type
157+
check_format(std::string_view fmt, int fmt_start, FoundFormatSpec*, uint8_t)
158+
{
159+
FoundFormatSpec found = find_next_format_spec(fmt, fmt_start);
160+
if (found.spec != ConversionSpecifier::NONE) {
161+
YOSYS_ABORT("More format conversion specifiers than arguments");
162+
}
163+
}
164+
165+
// Check that the format string `fmt.substr(fmt_start)` is valid for the given type arguments.
166+
// Fills `specs` with the FoundFormatSpecs found in the format string.
167+
// `int_args_consumed` is the number of int arguments already consumed to satisfy the
168+
// dynamic width/precision args for the next format conversion specifier.
169+
template <typename Arg, typename... Args>
170+
constexpr void check_format(std::string_view fmt, int fmt_start, FoundFormatSpec* specs,
171+
uint8_t int_args_consumed)
172+
{
173+
FoundFormatSpec found = find_next_format_spec(fmt, fmt_start);
174+
if (found.num_dynamic_ints > int_args_consumed) {
175+
// We need to consume at least one more int for the dynamic
176+
// width/precision of this format conversion specifier.
177+
if constexpr (!std::is_convertible_v<Arg, int>) {
178+
YOSYS_ABORT("Expected dynamic int argument");
179+
}
180+
check_format<Args...>(fmt, fmt_start, specs, int_args_consumed + 1);
181+
return;
182+
}
183+
switch (found.spec) {
184+
case ConversionSpecifier::NONE:
185+
YOSYS_ABORT("Expected format conversion specifier for argument");
186+
break;
187+
case ConversionSpecifier::ERROR:
188+
YOSYS_ABORT("Found unsupported format conversion specifier");
189+
break;
190+
case ConversionSpecifier::SIGNED_INT:
191+
if constexpr (!std::is_convertible_v<Arg, long long>) {
192+
YOSYS_ABORT("Expected type convertible to signed integer");
193+
}
194+
*specs = found;
195+
break;
196+
case ConversionSpecifier::UNSIGNED_INT:
197+
if constexpr (!std::is_convertible_v<Arg, unsigned long long>) {
198+
YOSYS_ABORT("Expected type convertible to unsigned integer");
199+
}
200+
*specs = found;
201+
break;
202+
case ConversionSpecifier::DOUBLE:
203+
if constexpr (!std::is_convertible_v<Arg, double>) {
204+
YOSYS_ABORT("Expected type convertible to double");
205+
}
206+
*specs = found;
207+
break;
208+
case ConversionSpecifier::CHAR_PTR:
209+
if constexpr (!std::is_convertible_v<Arg, const char *>) {
210+
YOSYS_ABORT("Expected type convertible to char *");
211+
}
212+
*specs = found;
213+
break;
214+
case ConversionSpecifier::VOID_PTR:
215+
if constexpr (!std::is_convertible_v<Arg, const void *>) {
216+
YOSYS_ABORT("Expected pointer type");
217+
}
218+
*specs = found;
219+
break;
220+
}
221+
check_format<Args...>(fmt, found.end, specs + 1, 0);
222+
}
223+
224+
inline std::string string_view_stringf(std::string_view spec, ...)
225+
{
226+
va_list ap;
227+
va_start(ap, spec);
228+
std::string result = vstringf(std::string(spec).c_str(), ap);
62229
va_end(ap);
230+
return result;
231+
}
63232

64-
return string;
233+
// Emit the string representation of `arg` by converting it to type `T` and passing
234+
// it to `vstringf()` via `string_view_stringf()`, then appending to `result`.
235+
// If `arg` can't be converted to `T` then abort; `check_format()` should guarantee
236+
// this doesn't happen, but the C++ compiler doesn't know that.
237+
template <typename Arg, typename T>
238+
inline void format_emit_type(std::string &result, std::string_view spec, int *dynamic_ints,
239+
uint8_t num_dynamic_ints, const Arg &arg)
240+
{
241+
if constexpr (std::is_convertible_v<Arg, T>) {
242+
T v = arg;
243+
switch (num_dynamic_ints) {
244+
case 0:
245+
result += string_view_stringf(spec, v);
246+
return;
247+
case 1:
248+
result += string_view_stringf(spec, dynamic_ints[0], v);
249+
return;
250+
case 2:
251+
result += string_view_stringf(spec, dynamic_ints[0], dynamic_ints[1], v);
252+
return;
253+
}
254+
}
255+
YOSYS_ABORT("Internal error");
256+
}
257+
258+
// Emit the string representation of `arg` according to the given `FoundFormatSpec`,
259+
// appending it to `result`.
260+
template <typename Arg>
261+
inline void format_emit_one(std::string &result, std::string_view fmt, const FoundFormatSpec &ffspec,
262+
int *dynamic_ints, const Arg& arg)
263+
{
264+
std::string_view spec = fmt.substr(ffspec.start, ffspec.end - ffspec.start);
265+
int num_dynamic_ints = ffspec.num_dynamic_ints;
266+
switch (ffspec.spec) {
267+
case ConversionSpecifier::SIGNED_INT:
268+
format_emit_type<Arg, long long>(result, spec, dynamic_ints, num_dynamic_ints, arg);
269+
return;
270+
case ConversionSpecifier::UNSIGNED_INT:
271+
format_emit_type<Arg, unsigned long long>(result, spec, dynamic_ints, num_dynamic_ints, arg);
272+
return;
273+
case ConversionSpecifier::DOUBLE:
274+
format_emit_type<Arg, double>(result, spec, dynamic_ints, num_dynamic_ints, arg);
275+
return;
276+
case ConversionSpecifier::CHAR_PTR:
277+
format_emit_type<Arg, const char *>(result, spec, dynamic_ints, num_dynamic_ints, arg);
278+
return;
279+
case ConversionSpecifier::VOID_PTR:
280+
format_emit_type<Arg, void *>(result, spec, dynamic_ints, num_dynamic_ints, arg);
281+
return;
282+
default:
283+
break;
284+
}
285+
YOSYS_ABORT("Internal error");
286+
}
287+
288+
// Append the format string `fmt` to `result`, assuming there are no format conversion
289+
// specifiers other than "%%" and therefore no arguments. Unescape "%%".
290+
void format_emit_no_conversions(std::string &result, std::string_view fmt);
291+
292+
inline void format_emit(std::string &result, std::string_view fmt, int fmt_start,
293+
const FoundFormatSpec*, int*, uint8_t)
294+
{
295+
format_emit_no_conversions(result, fmt.substr(fmt_start));
296+
}
297+
// Format `args` according to `fmt` (starting at `fmt_start`) and `specs` and append to `result`.
298+
// `num_dynamic_ints` in `dynamic_ints[]` have already been collected to provide as
299+
// dynamic width/precision args for the next format conversion specifier.
300+
template <typename Arg, typename... Args>
301+
inline void format_emit(std::string &result, std::string_view fmt, int fmt_start,
302+
const FoundFormatSpec* specs, int *dynamic_ints, uint8_t num_dynamic_ints,
303+
const Arg &arg, const Args &... args)
304+
{
305+
if (specs->num_dynamic_ints > num_dynamic_ints) {
306+
// Collect another int for the dynamic width precision/args
307+
// for the next format conversion specifier.
308+
if constexpr (std::is_convertible_v<Arg, int>) {
309+
dynamic_ints[num_dynamic_ints] = arg;
310+
} else {
311+
YOSYS_ABORT("Internal error");
312+
}
313+
format_emit(result, fmt, fmt_start, specs, dynamic_ints,
314+
num_dynamic_ints + 1, args...);
315+
return;
316+
}
317+
format_emit_no_conversions(result, fmt.substr(fmt_start, specs->start - fmt_start));
318+
format_emit_one(result, fmt, *specs, dynamic_ints, arg);
319+
format_emit(result, fmt, specs->end, specs + 1, dynamic_ints, 0, args...);
320+
}
321+
322+
// This class parses format strings to build a list of `FoundFormatSpecs` in `specs`.
323+
// When the compiler supports `consteval` (C++20), this parsing happens at compile time and
324+
// type errors will be reported at compile time. Otherwise the parsing happens at
325+
// runtime and type errors will trigger an `abort()` at runtime.
326+
template <typename... Args>
327+
class FmtString
328+
{
329+
public:
330+
// Implicit conversion from const char * means that users can pass
331+
// C string constants which are automatically parsed.
332+
YOSYS_CONSTEVAL FmtString(const char *p) : fmt(p)
333+
{
334+
check_format<Args...>(fmt, 0, specs, 0);
335+
}
336+
std::string format(const Args &... args)
337+
{
338+
std::string result;
339+
int dynamic_ints[2];
340+
format_emit(result, fmt, 0, specs, dynamic_ints, 0, args...);
341+
return result;
342+
}
343+
private:
344+
std::string_view fmt;
345+
FoundFormatSpec specs[sizeof...(Args)] = {};
346+
};
347+
348+
template <typename T> struct WrapType { using type = T; };
349+
template <typename T> using TypeIdentity = typename WrapType<T>::type;
350+
351+
template <typename... Args>
352+
inline std::string stringf(FmtString<TypeIdentity<Args>...> fmt, Args... args)
353+
{
354+
return fmt.format(args...);
65355
}
66356

67357
int readsome(std::istream &f, char *s, int n);

kernel/yosys_common.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@
134134
# define YS_COLD
135135
#endif
136136

137+
#ifdef __cpp_consteval
138+
#define YOSYS_CONSTEVAL consteval
139+
#else
140+
#define YOSYS_CONSTEVAL
141+
#endif
142+
143+
#define YOSYS_ABORT(s) abort()
144+
137145
#include "kernel/io.h"
138146

139147
YOSYS_NAMESPACE_BEGIN

0 commit comments

Comments
 (0)