Here's a solution that uses macros
. The general approach here is to enrich Boolean
so that it has a macro
method that looks at the prefix
of the context to find the comparison that was used to generate that Boolean
.
For example, suppose we have:
implicit class RichBooleanComparison(val x: Boolean) extends AnyVal {
def <(rightConstant: Int): Boolean = macro Compare.ltImpl
}
And a macro
definition with method header:
def ltImpl(c: Context)(rightConstant: c.Expr[Int]): c.Expr[Boolean]
Now suppose that the compiler is parsing the expression 1 < 2 < 3
. We could apparently use c.prefix
to get at the expression 1 < 2
while evaluating the macro method body. However, the concept of constant folding prevents us from doing so here. Constant folding is the process by which the compiler computes predetermined constants at compile time. So by the time macros are being evaluated, the c.prefix
has already been folded to be just true
in this case. We have lost the 1 < 2
expression that led to true
. You can read more about constant folding and their interactions with Scala macros on this issue and a little bit on this question.
If we can limit the scope of the discussion to only expressions of the form C1 < x < C2
, where C1
and C2
are constants, and x
is a variable, then this becomes doable, since this type of expression won't be affected by constant folding. Here is an implementation:
object Compare {
def ltImpl(c: Context)(rightConstant: c.Expr[Int]): c.Expr[Boolean] = {
import c.universe._
c.prefix.tree match {
case Apply(_, Apply(Select(lhs@Literal(Constant(_)), _), (x@Select(_, TermName(_))) :: Nil) :: Nil) =>
val leftConstant = c.Expr[Int](lhs)
val variable = c.Expr[Int](x)
reify((leftConstant.splice < variable.splice) && (variable.splice < rightConstant.splice))
case _ => c.abort(c.enclosingPosition, s"Invalid format. Must have format c1<x<c2, where c1 and c2 are constants, and x is variable.")
}
}
}
Here, we match the context prefix
to the expected type, extract the relevant parts (lhs
and x
), construct new subtrees using c.Expr[Int]
, and construct a new full expression tree using reify
and splice
to make the desired 3-way comparison. If there is no match with the expected type, this will fail to compile.
This allows us to do:
val x = 5
1 < x < 5 //true
6 < x < 7 //false
3 < x < 4 //false
As desired!
The docs about macros, trees, and this presentation are good resources to learn more about macros.