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,

  • If the variable is declared without an initializer, only default-initialized variables will be considered.
  • If the variable is declared with an initializer, the variable must be initialized in order to match (though the style of initialization is not considered). Moreover, the initializing expression in a declaration must match the initializer specified in 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()
  void before() {
    std::string s = "";
  }
};

// Matches any default-initialized `std::function`.
struct StdFunctions : bronto::rewrite_decl {
  template <typename FnType>
  BRONTO_BEFORE()
  void before() {
    std::function<FnType> f;
  }
};

// Matches any integral type initialized with some expression.
struct Integral : bronto::rewrite_decl {
  BRONTO_BEFORE()
  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()
  void before(E expr) requires (not std::same_as<T, E>) {
    T n = expr;
  }
};

BRONTO_AFTER

Requirements

Each struct inheriting from bronto::rewrite_decl must have at most one member function or template member function marked BRONTO_AFTER(). The function must have a single declaration representing the replacement of the matched variable.

Semantics

The variable declared in the function annotated with BRONTO_AFTER represents the intended replacement. Each captured expression or template parameter is used in place of the corresponding parameter or template parameter. The replaced variable name will remain unchanged.

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_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();
    }
  };
};