Constant evaluation in replacements
In a replacement, bronto::eval(expr) constant-evaluates expr and writes the
result into the rewritten source. For example, bronto::eval(1 + 2) writes the
value 3 rather than the spliced text 1 + 2. Captured expressions can appear
inside expr, so a replacement can compute a new constant from the values
matched at each site.
constexpr int Square(int n) { return n * n; }
struct SquareTheArgument : bronto::rewrite_expr {
BRONTO_BEFORE()
void find(int n) { foo(n); }
BRONTO_AFTER()
void repl(int n) { bar(bronto::eval(Square(n))); }
};With this rewrite, the call foo(3) becomes bar(9). Without bronto::eval it
would instead become bar(Square(3)).
bronto::eval
Requirements
bronto::eval may only appear in a function annotated with BRONTO_AFTER().
Using it in a function annotated with BRONTO_BEFORE() is an error.
bronto::eval takes either one argument or two. The one-argument form,
described here, writes the value as a plain literal. The
two-argument form writes it as a user-defined literal
instead.
After the captured expressions are substituted for the parameters they bind to, the first argument must be a constant expression. If the substitution fails, or the substituted expression cannot be constant-evaluated, the matched site is left unchanged and a diagnostic explains why.
Semantics
The argument is constant-evaluated and the resulting value is written into the replacement as a literal denoting the value exactly. The type of the expression determines the kind of literal that is generated.
| Expression type | Generated literal | Example |
|---|---|---|
bool | boolean literal | true |
int | integer literal, no suffix | 42 |
unsigned | integer literal, u suffix | 42u |
long | integer literal, l suffix | 42l |
unsigned long | integer literal, ul suffix | 42ul |
long long | integer literal, ll suffix | 42ll |
unsigned long long | integer literal, ull suffix | 42ull |
signed char, unsigned char, short, unsigned short | static_cast of an integer literal | static_cast<short>(1000) |
char | character literal, no prefix | 'a' |
wchar_t | character literal, L prefix | L'a' |
char8_t | character literal, u8 prefix | u8'a' |
char16_t | character literal, u prefix | u'a' |
char32_t | character literal, U prefix | U'a' |
float | floating-point literal, f suffix | 1.5f |
double | floating-point literal, no suffix | 1.5 |
long double | floating-point literal, L suffix | 1.5L |
| string-like (see below) | string literal, prefix matching the code unit | "abc", L"abc" |
| character pointer (see below) | string literal, prefix matching the code unit | "abc", L"abc" |
Character results
Printable ASCII characters are written verbatim, mnemonic escapes such as \n
are used where they exist portably, and any other value uses a numeric escape.
Note that signed char and unsigned char are treated as integer types, not
character types.
Floating-point results
The expression is constant-evaluated with IEEE 754 arithmetic in the result
type's format on the target described by your compile_commands.json. For
example, the format of long double varies by target. The following applies:
- Values are rounded to nearest with ties to even.
- Operations are never contracted. For example,
a * b + cis evaluated as a rounded multiply followed by a rounded add, never as a fused multiply-add, so the result may differ from what the same expression would compute at runtime under flags like-ffp-contract=fast. - The emitted literal denotes the evaluated value exactly. Any compiler that parses floating-point literals with correct rounding reproduces the value bit for bit. It is not necessarily the shortest spelling of that value.
String results
To output a string literal, the evaluated expression must be of a type that has
a constexpr data() member function returning a pointer to a standard character
type and a constexpr size() member function returning its length.
std::string_view satisfies these requirements in C++17, and std::string does
as well in C++20. The literal denotes each code unit of the result exactly,
using the same escaping rules as character literals.
An expression whose type is itself a pointer to a standard character type, such
as char const*, is also accepted. It is read as a NUL-terminated string. The
pointer must point to a NUL-terminated constant, so a null pointer or an array
without a terminator fails to constant-evaluate.
constexpr std::string KebabCase(std::string_view s) {
std::string result;
for (char c : s) { result.push_back(c == '_' ? '-' : c); }
return result;
}
struct MigrateOptionNames : bronto::rewrite_expr {
BRONTO_BEFORE()
void find(char const* name) { get_option(name); }
BRONTO_AFTER()
void repl(char const* name) { get_flag(bronto::eval(KebabCase(name))); }
};With this rewrite, the call get_option("max_retry_count") becomes
get_flag("max-retry-count"). The string literal captured at each call site is
substituted for name before the expression is constant-evaluated, so every
call site gets its own precomputed literal.
User-defined literals
Passing a second argument makes bronto::eval emit its result as a user-defined
literal. bronto::eval(value, tag) constant-evaluates value and writes it as
a literal token carrying the suffix and prefix of tag. For example, given a
literal operator Bytes operator""_kb(unsigned long long), a replacement
written bronto::eval(n, 0_kb) writes 512_kb when n evaluates to 512.
The tag is a user-defined literal token whose value is ignored. It names three
things: the literal operator to emit, the token category (e.g., character,
integer, floating-point, or string literals), and the prefix. The value to emit
is always the first argument. The operator is never called by the tool, so it
need not be constexpr.
Bytes operator""_kb(unsigned long long n);
struct ScaleReserve : bronto::rewrite_expr {
BRONTO_BEFORE()
void find(unsigned long long bytes) { reserve(bytes); }
BRONTO_AFTER()
void repl(unsigned long long bytes) {
reserve(bronto::eval(bytes / 1024, 0_kb));
}
};With this rewrite, the call reserve(2048) becomes reserve(2_kb).
Supported operator forms
The tag must resolve to one of these literal operator forms, where CharT is a
standard character type.
| Operator form | Emitted token | Example |
|---|---|---|
operator""_x(unsigned long long) | integer | 512_x |
operator""_x(long double) | floating-point | 1.5_x |
operator""_x(CharT) | character | u'a'_x |
operator""_x(CharT const*, size_t) | string | u"abc"_x |
template <FixedString s> operator""_x() | string | u"abc"_x |
For the character and string forms, the prefix of the tag determines which types
are accepted for the constant-evaluated expression. For example, if the tag is
L""_x, we will only accept a wchar_t* or a type with a constexpr data()
returning wchar_t*.
The raw literal operator operator""_x(char const*) and the numeric literal
operator template are not accepted. Those forms observe the token's exact
spelling, and bronto::eval produces a value rather than a spelling, so it
cannot guarantee what such an operator would see. A literal operator that
returns an rvalue reference is also rejected, because bronto::eval's signature
cannot model it faithfully.
Value types
The type of value must be one the tag's form can render as a matching token.
| Operator form | Accepted value types |
|---|---|
| integer | an integer type other than bool, a character type, or an enumeration |
| floating-point | a floating-point type, or one of the integer types above |
| character | exactly the operator's character type |
| string | a string-like type, or a pointer to the tag's character type |
As in the one-argument form, signed char and unsigned char are considered
integer types rather than character types. For the floating-point form, an
integral value must be exactly representable in the target's long double. For
the string form, a string-like type is one with a constexpr data() returning a
pointer to the operator's character type and a constexpr size(), and a
character-pointer value is read as a NUL-terminated string, exactly as in the
one-argument string form. Because a string is the only
literal a class type can produce, a class-typed value is accepted only by the
string form.