paulp/scala — pull request #8 by xeno-by (2013-08-17 08:49:05+00:00)
Title: SI-5903 extractor macros

Establishes a pattern that can be used to implement extractor macros
that give the programmer control over signatures of unapplications
at compile-time.

=== The pattern ===

In a nutshell, given an unapply method (for simplicity, in this
example the scrutinee is of a concrete type, but it's also possible
to have the extractor be polymorphic, as demonstrated in the tests):

def unapply(x: SomeType) = ???

One can write a macro that generates extraction signatures for unapply
on per-call basis, using the target of the calls (c.prefix) and the type
of the scrutinee (that comes with x), and then communicate these signatures
to the typechecker.

For example, here's how one can define a macro that simply passes
the scrutinee back to the pattern match (for information on how to
express signatures that involve multiple extractees, visit
https://github.com/scala/scala/pull/2835).

def unapply(x: SomeType) = macro impl
def impl(c: Context)(x: c.Tree) = {
q"""
new {
class Match(x: SomeType) {
def isEmpty = false
def get = x
}
def unapply(x: SomeType) = new Match(x)
}.unapply($x)
"""
}

In addition to the matcher, which implements domain-specific
matching logic, there's quite a bit of boilerplate here, but
every part of it looks necessary to arrange a non-frustrating dialogue
with the typer. Maybe something better can be done in this department,
but I can't see how, without introducing modifications to the typechecker.

Even though the pattern uses structural types, somehow no reflective calls
are being generated (as verified by -Xlog-reflective-calls and then
by manual examination of the produced code). That's a mystery to me, but
that's also good news, since that means that extractor macros aren't
going to induce performance penalties.

Almost. Unfortunately, I couldn't turn matchers into value classes
because one can't declare value classes local. Nevertheless,
I'm leaving a canary in place (neg/t5903e) that will let us know
once this restriction is lifted.

=== Use cases ===

In particular, the pattern can be used to implement shapeshifting
pattern matchers for string interpolators without resorting to dirty
tricks. For example, quasiquote unapplications can be unhardcoded now:

def doTypedApply(tree: Tree, fun0: Tree, args: List[Tree], ...) = {
...
fun.tpe match {
case ExtractorType(unapply) if mode.inPatternMode =>
// this hardcode in Typers.scala is no longer necessary
if (unapply == QuasiquoteClass_api_unapply) macroExpandUnapply(...)
else doTypedUnapply(tree, fun0, fun, args, mode, pt)
}
}

Rough implementation strategy here would involve writing an extractor
macro that destructures c.prefix, analyzes parts of StringContext and
then generates an appropriate matcher as outlined above.

=== Implementation details ===

No modifications to core logic of typer or patmat are necessary,
as we're just piggybacking on https://github.com/scala/scala/pull/2835.

The only minor change I introduced is a guard against misbehaving
extractor macros that don't conform to the pattern (e.g. expand into
blocks or whatever else). Without the guard we'd crash with an NPE,
with the guard we get a sane compilation error.