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.

Basic Example
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 auto with 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 (const or volatile) 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 specifies const int, both const int and const volatile int will match, but plain int will not. If the pattern specifies int, then int, 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:

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.

Rewriting a declaration and the statements around it
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.

Statements placed before an if statement
// 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.

Replacing a declaration with a statement
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.

BRONTO_USAGE(required) Example
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.

BRONTO_USAGE(forbidden) Example
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.

BRONTO_USAGE(allowed) Example
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();
    }
  };
};

On this page