parboiled2 Custom Rules

There are use cases when developing custom low-level DSL rules might be very effective. It is possible without changing source code of parboiled2. And requirements to achive that are as follows:

First, we need a RuleDSLActionsCustom trait that contains rules declarations:

trait RuleDSLActionsCustom {
  @compileTimeOnly("Calls to `debug` must be inside `rule` macro")
  def debug(msg: String): Rule0 = `n/a`

Second, we need to mix it in a class ParserCustom:

abstract class ParserCustom extends RuleTypes
      with RuleDSLBasics with RuleDSLCombinators
      with RuleDSLActions with RuleDSLActionsCustom

Next we need to deliver debug rule to a OpTreeContextCustom class: it should extend OpTreeContext.opTreePF function with proper implementation of code generation for debug rule:

class OpTreeContextCustom[OpTreeCtx <: reflect.macros.blackbox.Context](val c1: OpTreeCtx) extends OpTreeContext[OpTreeCtx](c1) {
  import c.universe._

  override def opTreePF: PartialFunction[Tree, OpTree] = {
    case q"$a.this.debug($msg)" => Debug(OpTree(arg))
    case t => super.opTreePF(t)

  case class Debug(msg: Tree) extends OpTree {
    def renderInner(wrapped: Boolean): Tree = q"println($msg)"

And here is the place where code starts to smell. The complication arises from the implementation currently not designed for such extensions. Parser tighgly depends on ParserMacros, that in its turn tightly depends on OpTreeContext. It is still possible to extend those things (like it experimentally done for CapturePos for custom debug rule. Overall, it all implies that custom extension should copy-paste almost entire inner implementation of ParserMacros and OpTreeContext, and even worse keep it updated with original upstream to properly merge updates.

The approach for custom rule implementation is useless. It is possible though to redesign parboiled2 inner API to make it easier to extend Parser with custom rules. But, it would make much harder to evolve that API. Practically, it is easier to keep a simple patch to a fork then to maintain classes derived from parboiled2 inner API.