Declaration pattern matching
To find and replace a declaration, create a struct that inherits publicly from
bronto::rewrite_decl (either directly or indirectly) and use the
BRONTO_BEFORE() and BRONTO_AFTER() annotations to identify declarations and
define their replacements.
struct StringConstructedFromEmpty : bronto::rewrite_decl {
// Matches string variable declarations initialized with an empty
// string literal.
BRONTO_BEFORE()
void before() { std::string var = ""; }
// Removes the initializer, since this is equivalent to default
// construction.
BRONTO_AFTER()
void after() { std::string var; }
};BRONTO_BEFORE
Requirements
Each struct inheriting from bronto::rewrite_decl must have at least one member
function or template member function marked BRONTO_BEFORE(). These functions
must contain a single variable declaration, representing the variable
declaration to be matched.
Semantics
The declared variable in the function annotated with BRONTO_BEFORE() defines
the pattern to be matched. The declared variable's type and initialization
determine what it matches against as explained below.
Specifically, for the variable type,
- If a concrete type is specified, only variables of that type will be considered.
- If the type is deduced via unconstrained
auto, variables of any type will be considered. - If the type is deduced via
autowith concept constraints, only variables whose type matches those constraints will be considered. - If the variable's type is dependent on some template parameter, only variables whose type matches that pattern will be considered.
- If any qualifiers (
constorvolatile) are specified in the pattern, the type must also have those qualifiers. If the qualifiers are not present, the type may (but is not required to) be qualified. For example, if the pattern specifiesconst int, bothconst intandconst volatile intwill match, but plainintwill not. If the pattern specifiesint, thenint,const int,volatile int, etc. will all match.
For initializers, matching occurs in one of two modes. One can pass a string
literal "strict" or "loose" as an argument to BRONTO_BEFORE(). If nothing
is provided, matching defaults to "loose".
Loose initializer matching
- If the variable is declared without an initializer, any declaration of a variable with a matching type will match, regardless of whether it is initialized.
- If the variable is declared with an initializer, the particular syntax
(parentheses, braces, or
=) is ignored, and only the initializing expressions are required to match.
Strict initializer matching
- If the variable is declared without an initializer, only default-initialized variables will be considered.
- If the variable is declared with an initializer, the initialization style
(parentheses, braces, or with
=) and the initializing expression(s) must match the pattern.
Notably, variable names are not considered or modified.
Here are several examples:
// Matches an string variable initialized with an empty string literal.
// This includes any initialization form such as `std::string s{""}`
// or `std::string s("")`;
struct InitializedWithStringLiteral : bronto::rewrite_decl {
BRONTO_BEFORE("loose")
void before() {
std::string s = "";
}
};
// Matches any default-initialized `std::function`.
struct StdFunctions : bronto::rewrite_decl {
template <typename FnType>
BRONTO_BEFORE("strict")
void before() {
std::function<FnType> f;
}
};
// Matches any integral type initialized with some expression.
struct Integral : bronto::rewrite_decl {
BRONTO_BEFORE() // Or equivalently, BRONTO_BEFORE("loose")
void before(auto expr) {
std::integral auto n = expr;
}
};
// Matches any integral type initialized with some expression of a
// different type.
struct IntegralWithCast : bronto::rewrite_decl {
template <std::integral T, typename E>
BRONTO_BEFORE() // Or equivalently, BRONTO_BEFORE("loose")
void before(E expr) requires (not std::same_as<T, E>) {
T n = expr;
}
};
A parameter binding can also be restricted to particular kinds of expressions.
Annotating it with BRONTO_BINDS matches an expression only when it matches the
specified predicate, as described in
Restricting what a pattern parameter binds.
BRONTO_AFTER
Requirements
Each struct inheriting from bronto::rewrite_decl must have at most one member
function or template member function marked BRONTO_AFTER(). Its body may
contain any statements, but may declare at most one variable outside of nested
scopes. With more than one, such as a declaration of several variables or
several declaration statements, it would be unclear which variable replaces the
matched one. Structured bindings, even of a single element, are not supported
outside of nested scopes.
Structured bindings are never matched by a bronto::rewrite_decl rule. They are
left unchanged.
Semantics
In all cases, each captured expression or template parameter is used in place of the corresponding parameter or template parameter.
With a variable declaration
When the body declares a variable at its top level, that variable stands in for
the matched one. The matched declaration is rewritten to match it, keeping its
name and its decl-specifiers (such as static or constexpr). Any other
statements in the body are placed before or after the matched declaration, in
the same order, and references to the variable are replaced with the name of the
matched variable.
struct ReserveVector : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(size_t n) { std::vector<int> v(n); }
BRONTO_AFTER()
void after(size_t n) {
std::vector<int> v;
v.reserve(n);
}
};When the body consists of only the variable declaration, this applies to
variables declared anywhere, including at namespace scope and in the condition
of an if statement.
Statements written before the variable declaration are placed before the matched
declaration. If the matched declaration is in the header of a statement in a
block and is evaluated once on entry to it, as in the init-statement or
condition of an if or switch, or the init-statement of a for loop, they
are placed before that statement instead.
// Rewriting `int var = init` to `{ Log(init); long var = init; }` turns
if (int n = x) { Use(n); }
// into
Log(x);
if (long n = x) { Use(n); }Statements written after the variable declaration require the matched declaration to form its own statement inside a block.
Without a variable declaration
When the body declares no variable at its top level, the entire declaration
statement, including its semicolon, is replaced by the body of the function as
written. An empty body deletes the declaration. Bronto does not check whether
the deleted variable is still used, so such a rule should only match variables
with no remaining uses, for example by way of a BRONTO_USAGE(forbidden) rule.
struct DiscardedResult : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(Status expr) { Status s = expr; }
BRONTO_AFTER()
void after(Status expr) { expr.IgnoreError(); }
// Only rewrite variables which are never used.
struct BRONTO_USAGE(forbidden) Usage : bronto::rewrite_expr {
BRONTO_BEFORE()
auto before(Status usage) { return usage; }
};
};This only applies to declarations that form their own statement inside a block.
Unsupported locations
Matches in locations where the replacement cannot be placed are reported as
errors and left unchanged. Statements written after the variable declaration, or
a replacement without a variable declaration, cannot be placed for declarations
at namespace or class scope, in the header of an if, switch, or loop, or
sharing a statement with other declarations. Statements written before the
variable declaration additionally cannot be placed for declarations evaluated on
every iteration of a loop, such as in the condition of a while loop or the
variable of a range-based for loop.
BRONTO_USAGE
Declaration patterns may also have nested structs annotated with
BRONTO_USAGE(...) to indicate that they place constraints on how the variable
is used. Such rules match against references to the declared variable and work
the same as expression patterns.
There are three versions of BRONTO_USAGE(...) described below. Multiple
constraints can be placed on variable usages, and all of them must hold for the
variable to be rewritten.
BRONTO_USAGE(required)
Constrains the rewrite to occur only when at least one instance of the required pattern is found. The rule may also specify how those usages are rewritten.
struct Rule : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(auto expr) {
time_t t = expr;
}
BRONTO_AFTER()
void after(auto expr) {
auto t = std::chrono::system_clock::from_time_t(expr);
}
struct BRONTO_USAGE(required) Usage : bronto::rewrite_expr {
// Only do this rewrite if the variable is wrapped in a
// `std::chrono::time_point<std::chrono::system_clock>` constructor.
BRONTO_BEFORE()
auto before(time_t usage) {
return std::chrono::system_clock::from_time_t(usage);
}
// And remove that wrapper anywhere it is present.
BRONTO_AFTER()
auto before(
std::chrono::time_point<std::chrono::system_clock> usage) {
return usage;
}
};
};BRONTO_USAGE(forbidden)
Constrains the rewrite to occur only when no usages match the pattern.
struct Rule : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(auto expr) {
time_t t = expr;
}
BRONTO_AFTER()
void after(auto expr) {
std::chrono::time_point<std::chrono::system_clock> t(expr);
}
struct BRONTO_USAGE(forbidden) Usage : bronto::rewrite_expr {
// Do not apply this rewrite if the address of the variable is taken.
BRONTO_BEFORE()
auto before(time_t usage) { return &usage; }
};
};bronto::where and BRONTO_USAGE(forbidden)
A forbidden usage pattern withholds the rewrite when it matches, so a forbidden
usage that does not match is permission to rewrite. That makes this the one
context where a bronto::where rejection causes a rewrite rather
than stopping one.
A bronto::where inside a forbidden usage pattern rejects silently, including
at a site whose binding is not a constant expression. Such a usage does not
match, so it does not forbid, and the declaration is rewritten.
struct Rule : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(int init) { int var = init; }
BRONTO_AFTER()
void after(int init) { long var = init; }
struct BRONTO_USAGE(forbidden) Usage : bronto::rewrite_expr {
BRONTO_BEFORE()
void before(int var, int n) {
bronto::where(n > 0);
sink(var, n);
}
};
};
void forbidden() {
int var = 1;
sink(var, 2); // `2` evaluates and satisfies the predicate, so the usage
// matches and the declaration is left alone.
}
void permitted(int x) {
int var = 1;
sink(var, x); // `x` has no value at compile time, so the predicate cannot be
// evaluated, the usage does not match, and the declaration IS
// rewritten.
}Consider whether the usage can be framed as a required usage, with a negated
where predicate.
BRONTO_USAGE(allowed)
Makes no additional constraints about when a rewrite will occur, but offers rewrites to be applied to usages of the declared variable.
struct Rule : bronto::rewrite_decl {
BRONTO_BEFORE()
void before(auto expr) { time_t t = expr; }
BRONTO_AFTER()
void after(auto expr) {
std::chrono::time_point<std::chrono::system_clock> t(expr);
}
struct BRONTO_USAGE(allowed) Usage : bronto::rewrite_expr {
// Any usage of the variable matching this pattern should be
// rewritten.
BRONTO_BEFORE()
auto before(time_t usage) { return usage = time(0); }
BRONTO_AFTER()
auto after(
std::chrono::time_point<std::chrono::system_clock> usage) {
return usage = std::chrono::system_clock::now();
}
};
};