diff options
author | Eugene Burmako <xeno.by@gmail.com> | 2013-08-17 09:29:28 +0200 |
---|---|---|
committer | Eugene Burmako <xeno.by@gmail.com> | 2013-08-18 07:21:16 +0200 |
commit | 84a335916556cb0fe939d1c51f27d80d9cf980dc (patch) | |
tree | 6205d34ee60c6aaaaeece2bf3dafe2a4978a31c1 | |
parent | 1cd7a9e840158dab17a3aafc0ce849605706a561 (diff) | |
download | scala-84a335916556cb0fe939d1c51f27d80d9cf980dc.tar.gz scala-84a335916556cb0fe939d1c51f27d80d9cf980dc.tar.bz2 scala-84a335916556cb0fe939d1c51f27d80d9cf980dc.zip |
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/2848).
```
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/2848.
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.
33 files changed, 327 insertions, 0 deletions
diff --git a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala index 7f4bf0dfbc..1f4d5cbac2 100644 --- a/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala +++ b/src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala @@ -517,6 +517,9 @@ trait ContextErrors { def TooManyArgsPatternError(fun: Tree) = NormalTypeError(fun, "too many arguments for unapply pattern, maximum = "+definitions.MaxTupleArity) + def WrongShapeExtractorExpansion(fun: Tree) = + NormalTypeError(fun, "extractor macros can only expand into extractor calls") + def WrongNumberOfArgsError(tree: Tree, fun: Tree) = NormalTypeError(tree, "wrong number of arguments for "+ treeSymTypeMsg(fun)) diff --git a/src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala b/src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala index 13926ca18b..cc0ffe2ac2 100644 --- a/src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala @@ -413,6 +413,8 @@ trait PatternTypers { if (fun1.tpe.isErroneous) duplErrTree + else if (unapplyMethod.isMacro && !fun1.isInstanceOf[Apply]) + duplErrorTree(WrongShapeExtractorExpansion(tree)) else makeTypedUnApply() } diff --git a/test/files/neg/t5903a.check b/test/files/neg/t5903a.check new file mode 100644 index 0000000000..cbdcfd1bdd --- /dev/null +++ b/test/files/neg/t5903a.check @@ -0,0 +1,7 @@ +Test_2.scala:4: error: wrong number of patterns for <$anon: AnyRef> offering (SomeTree.type, SomeTree.type): expected 2, found 3 + case nq"$x + $y + $z" => println((x, y)) + ^ +Test_2.scala:4: error: not found: value x + case nq"$x + $y + $z" => println((x, y)) + ^ +two errors found diff --git a/test/files/neg/t5903a/Macros_1.scala b/test/files/neg/t5903a/Macros_1.scala new file mode 100644 index 0000000000..e82be0fc68 --- /dev/null +++ b/test/files/neg/t5903a/Macros_1.scala @@ -0,0 +1,28 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +trait Tree +case object SomeTree extends Tree + +object NewQuasiquotes { + implicit class QuasiquoteInterpolation(c: StringContext) { + object nq { + def unapply(t: Tree) = macro QuasiquoteMacros.unapplyImpl + } + } +} + +object QuasiquoteMacros { + def unapplyImpl(c: Context)(t: c.Tree) = { + import c.universe._ + q""" + new { + def isEmpty = false + def get = this + def _1 = SomeTree + def _2 = SomeTree + def unapply(t: Tree) = this + }.unapply($t) + """ + } +} diff --git a/test/files/neg/t5903a/Test_2.scala b/test/files/neg/t5903a/Test_2.scala new file mode 100644 index 0000000000..4d78dfb5e5 --- /dev/null +++ b/test/files/neg/t5903a/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import NewQuasiquotes._ + SomeTree match { + case nq"$x + $y + $z" => println((x, y)) + } +} diff --git a/test/files/neg/t5903b.check b/test/files/neg/t5903b.check new file mode 100644 index 0000000000..faeb73ad03 --- /dev/null +++ b/test/files/neg/t5903b.check @@ -0,0 +1,9 @@ +Test_2.scala:4: error: type mismatch; + found : Int + required: String + case t"$x" => println(x) + ^ +Test_2.scala:4: error: not found: value x + case t"$x" => println(x) + ^ +two errors found diff --git a/test/files/neg/t5903b/Macros_1.scala b/test/files/neg/t5903b/Macros_1.scala new file mode 100644 index 0000000000..b1b875969d --- /dev/null +++ b/test/files/neg/t5903b/Macros_1.scala @@ -0,0 +1,23 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply[T](x: T) = macro Macros.unapplyImpl[T] + } + } +} + +object Macros { + def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = { + import c.universe._ + q""" + new { + def isEmpty = false + def get = "2" + def unapply(x: String) = this + }.unapply($x) + """ + } +} diff --git a/test/files/neg/t5903b/Test_2.scala b/test/files/neg/t5903b/Test_2.scala new file mode 100644 index 0000000000..0f6f80d327 --- /dev/null +++ b/test/files/neg/t5903b/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + 2 match { + case t"$x" => println(x) + } +} diff --git a/test/files/neg/t5903c.check b/test/files/neg/t5903c.check new file mode 100644 index 0000000000..c9476edd11 --- /dev/null +++ b/test/files/neg/t5903c.check @@ -0,0 +1,7 @@ +Test_2.scala:4: error: String is not supported + case t"$x" => println(x) + ^ +Test_2.scala:4: error: not found: value x + case t"$x" => println(x) + ^ +two errors found diff --git a/test/files/neg/t5903c/Macros_1.scala b/test/files/neg/t5903c/Macros_1.scala new file mode 100644 index 0000000000..70efab3101 --- /dev/null +++ b/test/files/neg/t5903c/Macros_1.scala @@ -0,0 +1,26 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply[T](x: T) = macro Macros.unapplyImpl[T] + } + } +} + +object Macros { + def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = { + import c.universe._ + if (!(c.weakTypeOf[Int] =:= c.weakTypeOf[T])) c.abort(c.enclosingPosition, s"${c.weakTypeOf[T]} is not supported") + else { + q""" + new { + def isEmpty = false + def get = 2 + def unapply(x: Int) = this + }.unapply($x) + """ + } + } +} diff --git a/test/files/neg/t5903c/Test_2.scala b/test/files/neg/t5903c/Test_2.scala new file mode 100644 index 0000000000..a1fd31dd49 --- /dev/null +++ b/test/files/neg/t5903c/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + "2" match { + case t"$x" => println(x) + } +} diff --git a/test/files/neg/t5903d.check b/test/files/neg/t5903d.check new file mode 100644 index 0000000000..d5d3fdcc28 --- /dev/null +++ b/test/files/neg/t5903d.check @@ -0,0 +1,7 @@ +Test_2.scala:4: error: extractor macros can only expand into extractor calls + case t"$x" => println(x) + ^ +Test_2.scala:4: error: not found: value x + case t"$x" => println(x) + ^ +two errors found diff --git a/test/files/neg/t5903d/Macros_1.scala b/test/files/neg/t5903d/Macros_1.scala new file mode 100644 index 0000000000..15ff226cff --- /dev/null +++ b/test/files/neg/t5903d/Macros_1.scala @@ -0,0 +1,23 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply(x: Int) = macro Macros.unapplyImpl + } + } +} + +object Macros { + def unapplyImpl(c: Context)(x: c.Tree) = { + import c.universe._ + q""" + class Match(x: Int) { + def isEmpty = false + def get = x + } + new { def unapply(x: Int) = new Match(x) }.unapply($x) + """ + } +} diff --git a/test/files/neg/t5903d/Test_2.scala b/test/files/neg/t5903d/Test_2.scala new file mode 100644 index 0000000000..95c717a9d8 --- /dev/null +++ b/test/files/neg/t5903d/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + 42 match { + case t"$x" => println(x) + } +} diff --git a/test/files/neg/t5903e.check b/test/files/neg/t5903e.check new file mode 100644 index 0000000000..3bdeb091a0 --- /dev/null +++ b/test/files/neg/t5903e.check @@ -0,0 +1,4 @@ +Test_2.scala:4: error: value class may not be a member of another class + case t"$x" => println(x) + ^ +one error found diff --git a/test/files/neg/t5903e/Macros_1.scala b/test/files/neg/t5903e/Macros_1.scala new file mode 100644 index 0000000000..4e1ce89c9f --- /dev/null +++ b/test/files/neg/t5903e/Macros_1.scala @@ -0,0 +1,25 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply(x: Int) = macro Macros.unapplyImpl + } + } +} + +object Macros { + def unapplyImpl(c: Context)(x: c.Tree) = { + import c.universe._ + q""" + new { + class Match(x: Int) extends AnyVal { + def isEmpty = false + def get = x + } + def unapply(x: Int) = new Match(x) + }.unapply($x) + """ + } +} diff --git a/test/files/neg/t5903e/Test_2.scala b/test/files/neg/t5903e/Test_2.scala new file mode 100644 index 0000000000..d69d472436 --- /dev/null +++ b/test/files/neg/t5903e/Test_2.scala @@ -0,0 +1,6 @@ +class C { + import Interpolation._ + 42 match { + case t"$x" => println(x) + } +} diff --git a/test/files/run/t5903a.check b/test/files/run/t5903a.check new file mode 100644 index 0000000000..ce6efd812d --- /dev/null +++ b/test/files/run/t5903a.check @@ -0,0 +1 @@ +(SomeTree,SomeTree) diff --git a/test/files/run/t5903a.flags b/test/files/run/t5903a.flags new file mode 100644 index 0000000000..02ecab49e7 --- /dev/null +++ b/test/files/run/t5903a.flags @@ -0,0 +1 @@ +-Xlog-reflective-calls
\ No newline at end of file diff --git a/test/files/run/t5903a/Macros_1.scala b/test/files/run/t5903a/Macros_1.scala new file mode 100644 index 0000000000..e82be0fc68 --- /dev/null +++ b/test/files/run/t5903a/Macros_1.scala @@ -0,0 +1,28 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +trait Tree +case object SomeTree extends Tree + +object NewQuasiquotes { + implicit class QuasiquoteInterpolation(c: StringContext) { + object nq { + def unapply(t: Tree) = macro QuasiquoteMacros.unapplyImpl + } + } +} + +object QuasiquoteMacros { + def unapplyImpl(c: Context)(t: c.Tree) = { + import c.universe._ + q""" + new { + def isEmpty = false + def get = this + def _1 = SomeTree + def _2 = SomeTree + def unapply(t: Tree) = this + }.unapply($t) + """ + } +} diff --git a/test/files/run/t5903a/Test_2.scala b/test/files/run/t5903a/Test_2.scala new file mode 100644 index 0000000000..3a0b68b568 --- /dev/null +++ b/test/files/run/t5903a/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import NewQuasiquotes._ + SomeTree match { + case nq"$x + $y" => println((x, y)) + } +} diff --git a/test/files/run/t5903b.check b/test/files/run/t5903b.check new file mode 100644 index 0000000000..75891bc672 --- /dev/null +++ b/test/files/run/t5903b.check @@ -0,0 +1 @@ +oops diff --git a/test/files/run/t5903b.flags b/test/files/run/t5903b.flags new file mode 100644 index 0000000000..02ecab49e7 --- /dev/null +++ b/test/files/run/t5903b.flags @@ -0,0 +1 @@ +-Xlog-reflective-calls
\ No newline at end of file diff --git a/test/files/run/t5903b/Macros_1.scala b/test/files/run/t5903b/Macros_1.scala new file mode 100644 index 0000000000..c0124850b8 --- /dev/null +++ b/test/files/run/t5903b/Macros_1.scala @@ -0,0 +1,25 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply[T](x: T) = macro Macros.unapplyImpl[T] + } + } +} + +object Macros { + def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = { + import c.universe._ + q""" + new { + def isEmpty = false + def get = this + def _1 = 2 + def unapply(x: Int) = this + override def toString = "oops" + }.unapply($x) + """ + } +} diff --git a/test/files/run/t5903b/Test_2.scala b/test/files/run/t5903b/Test_2.scala new file mode 100644 index 0000000000..0f6f80d327 --- /dev/null +++ b/test/files/run/t5903b/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + 2 match { + case t"$x" => println(x) + } +} diff --git a/test/files/run/t5903c.check b/test/files/run/t5903c.check new file mode 100644 index 0000000000..0cfbf08886 --- /dev/null +++ b/test/files/run/t5903c.check @@ -0,0 +1 @@ +2 diff --git a/test/files/run/t5903c.flags b/test/files/run/t5903c.flags new file mode 100644 index 0000000000..02ecab49e7 --- /dev/null +++ b/test/files/run/t5903c.flags @@ -0,0 +1 @@ +-Xlog-reflective-calls
\ No newline at end of file diff --git a/test/files/run/t5903c/Macros_1.scala b/test/files/run/t5903c/Macros_1.scala new file mode 100644 index 0000000000..f8baa2275b --- /dev/null +++ b/test/files/run/t5903c/Macros_1.scala @@ -0,0 +1,23 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply[T](x: T) = macro Macros.unapplyImpl[T] + } + } +} + +object Macros { + def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = { + import c.universe._ + q""" + new { + def isEmpty = false + def get = 2 + def unapply(x: Int) = this + }.unapply($x) + """ + } +} diff --git a/test/files/run/t5903c/Test_2.scala b/test/files/run/t5903c/Test_2.scala new file mode 100644 index 0000000000..0f6f80d327 --- /dev/null +++ b/test/files/run/t5903c/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + 2 match { + case t"$x" => println(x) + } +} diff --git a/test/files/run/t5903d.check b/test/files/run/t5903d.check new file mode 100644 index 0000000000..d81cc0710e --- /dev/null +++ b/test/files/run/t5903d.check @@ -0,0 +1 @@ +42 diff --git a/test/files/run/t5903d.flags b/test/files/run/t5903d.flags new file mode 100644 index 0000000000..02ecab49e7 --- /dev/null +++ b/test/files/run/t5903d.flags @@ -0,0 +1 @@ +-Xlog-reflective-calls
\ No newline at end of file diff --git a/test/files/run/t5903d/Macros_1.scala b/test/files/run/t5903d/Macros_1.scala new file mode 100644 index 0000000000..88d714e17b --- /dev/null +++ b/test/files/run/t5903d/Macros_1.scala @@ -0,0 +1,25 @@ +import scala.reflect.macros.Context +import language.experimental.macros + +object Interpolation { + implicit class TestInterpolation(c: StringContext) { + object t { + def unapply(x: Int) = macro Macros.unapplyImpl + } + } +} + +object Macros { + def unapplyImpl(c: Context)(x: c.Tree) = { + import c.universe._ + q""" + new { + class Match(x: Int) { + def isEmpty = false + def get = x + } + def unapply(x: Int) = new Match(x) + }.unapply($x) + """ + } +} diff --git a/test/files/run/t5903d/Test_2.scala b/test/files/run/t5903d/Test_2.scala new file mode 100644 index 0000000000..95c717a9d8 --- /dev/null +++ b/test/files/run/t5903d/Test_2.scala @@ -0,0 +1,6 @@ +object Test extends App { + import Interpolation._ + 42 match { + case t"$x" => println(x) + } +} |