Skip to content

Commit 84a3359

Browse files
committed
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 scala#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 scala#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.
1 parent 1cd7a9e commit 84a3359

33 files changed

+327
-0
lines changed

src/compiler/scala/tools/nsc/typechecker/ContextErrors.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,9 @@ trait ContextErrors {
517517
def TooManyArgsPatternError(fun: Tree) =
518518
NormalTypeError(fun, "too many arguments for unapply pattern, maximum = "+definitions.MaxTupleArity)
519519

520+
def WrongShapeExtractorExpansion(fun: Tree) =
521+
NormalTypeError(fun, "extractor macros can only expand into extractor calls")
522+
520523
def WrongNumberOfArgsError(tree: Tree, fun: Tree) =
521524
NormalTypeError(tree, "wrong number of arguments for "+ treeSymTypeMsg(fun))
522525

src/compiler/scala/tools/nsc/typechecker/PatternTypers.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,8 @@ trait PatternTypers {
413413

414414
if (fun1.tpe.isErroneous)
415415
duplErrTree
416+
else if (unapplyMethod.isMacro && !fun1.isInstanceOf[Apply])
417+
duplErrorTree(WrongShapeExtractorExpansion(tree))
416418
else
417419
makeTypedUnApply()
418420
}

test/files/neg/t5903a.check

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Test_2.scala:4: error: wrong number of patterns for <$anon: AnyRef> offering (SomeTree.type, SomeTree.type): expected 2, found 3
2+
case nq"$x + $y + $z" => println((x, y))
3+
^
4+
Test_2.scala:4: error: not found: value x
5+
case nq"$x + $y + $z" => println((x, y))
6+
^
7+
two errors found

test/files/neg/t5903a/Macros_1.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
trait Tree
5+
case object SomeTree extends Tree
6+
7+
object NewQuasiquotes {
8+
implicit class QuasiquoteInterpolation(c: StringContext) {
9+
object nq {
10+
def unapply(t: Tree) = macro QuasiquoteMacros.unapplyImpl
11+
}
12+
}
13+
}
14+
15+
object QuasiquoteMacros {
16+
def unapplyImpl(c: Context)(t: c.Tree) = {
17+
import c.universe._
18+
q"""
19+
new {
20+
def isEmpty = false
21+
def get = this
22+
def _1 = SomeTree
23+
def _2 = SomeTree
24+
def unapply(t: Tree) = this
25+
}.unapply($t)
26+
"""
27+
}
28+
}

test/files/neg/t5903a/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import NewQuasiquotes._
3+
SomeTree match {
4+
case nq"$x + $y + $z" => println((x, y))
5+
}
6+
}

test/files/neg/t5903b.check

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Test_2.scala:4: error: type mismatch;
2+
found : Int
3+
required: String
4+
case t"$x" => println(x)
5+
^
6+
Test_2.scala:4: error: not found: value x
7+
case t"$x" => println(x)
8+
^
9+
two errors found

test/files/neg/t5903b/Macros_1.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply[T](x: T) = macro Macros.unapplyImpl[T]
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
new {
17+
def isEmpty = false
18+
def get = "2"
19+
def unapply(x: String) = this
20+
}.unapply($x)
21+
"""
22+
}
23+
}

test/files/neg/t5903b/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
2 match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/neg/t5903c.check

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Test_2.scala:4: error: String is not supported
2+
case t"$x" => println(x)
3+
^
4+
Test_2.scala:4: error: not found: value x
5+
case t"$x" => println(x)
6+
^
7+
two errors found

test/files/neg/t5903c/Macros_1.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply[T](x: T) = macro Macros.unapplyImpl[T]
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
if (!(c.weakTypeOf[Int] =:= c.weakTypeOf[T])) c.abort(c.enclosingPosition, s"${c.weakTypeOf[T]} is not supported")
16+
else {
17+
q"""
18+
new {
19+
def isEmpty = false
20+
def get = 2
21+
def unapply(x: Int) = this
22+
}.unapply($x)
23+
"""
24+
}
25+
}
26+
}

test/files/neg/t5903c/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
"2" match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/neg/t5903d.check

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Test_2.scala:4: error: extractor macros can only expand into extractor calls
2+
case t"$x" => println(x)
3+
^
4+
Test_2.scala:4: error: not found: value x
5+
case t"$x" => println(x)
6+
^
7+
two errors found

test/files/neg/t5903d/Macros_1.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply(x: Int) = macro Macros.unapplyImpl
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl(c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
class Match(x: Int) {
17+
def isEmpty = false
18+
def get = x
19+
}
20+
new { def unapply(x: Int) = new Match(x) }.unapply($x)
21+
"""
22+
}
23+
}

test/files/neg/t5903d/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
42 match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/neg/t5903e.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Test_2.scala:4: error: value class may not be a member of another class
2+
case t"$x" => println(x)
3+
^
4+
one error found

test/files/neg/t5903e/Macros_1.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply(x: Int) = macro Macros.unapplyImpl
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl(c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
new {
17+
class Match(x: Int) extends AnyVal {
18+
def isEmpty = false
19+
def get = x
20+
}
21+
def unapply(x: Int) = new Match(x)
22+
}.unapply($x)
23+
"""
24+
}
25+
}

test/files/neg/t5903e/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class C {
2+
import Interpolation._
3+
42 match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/run/t5903a.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(SomeTree,SomeTree)

test/files/run/t5903a.flags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xlog-reflective-calls

test/files/run/t5903a/Macros_1.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
trait Tree
5+
case object SomeTree extends Tree
6+
7+
object NewQuasiquotes {
8+
implicit class QuasiquoteInterpolation(c: StringContext) {
9+
object nq {
10+
def unapply(t: Tree) = macro QuasiquoteMacros.unapplyImpl
11+
}
12+
}
13+
}
14+
15+
object QuasiquoteMacros {
16+
def unapplyImpl(c: Context)(t: c.Tree) = {
17+
import c.universe._
18+
q"""
19+
new {
20+
def isEmpty = false
21+
def get = this
22+
def _1 = SomeTree
23+
def _2 = SomeTree
24+
def unapply(t: Tree) = this
25+
}.unapply($t)
26+
"""
27+
}
28+
}

test/files/run/t5903a/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import NewQuasiquotes._
3+
SomeTree match {
4+
case nq"$x + $y" => println((x, y))
5+
}
6+
}

test/files/run/t5903b.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
oops

test/files/run/t5903b.flags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xlog-reflective-calls

test/files/run/t5903b/Macros_1.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply[T](x: T) = macro Macros.unapplyImpl[T]
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
new {
17+
def isEmpty = false
18+
def get = this
19+
def _1 = 2
20+
def unapply(x: Int) = this
21+
override def toString = "oops"
22+
}.unapply($x)
23+
"""
24+
}
25+
}

test/files/run/t5903b/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
2 match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/run/t5903c.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

test/files/run/t5903c.flags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xlog-reflective-calls

test/files/run/t5903c/Macros_1.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply[T](x: T) = macro Macros.unapplyImpl[T]
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl[T: c.WeakTypeTag](c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
new {
17+
def isEmpty = false
18+
def get = 2
19+
def unapply(x: Int) = this
20+
}.unapply($x)
21+
"""
22+
}
23+
}

test/files/run/t5903c/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
2 match {
4+
case t"$x" => println(x)
5+
}
6+
}

test/files/run/t5903d.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
42

test/files/run/t5903d.flags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Xlog-reflective-calls

test/files/run/t5903d/Macros_1.scala

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import scala.reflect.macros.Context
2+
import language.experimental.macros
3+
4+
object Interpolation {
5+
implicit class TestInterpolation(c: StringContext) {
6+
object t {
7+
def unapply(x: Int) = macro Macros.unapplyImpl
8+
}
9+
}
10+
}
11+
12+
object Macros {
13+
def unapplyImpl(c: Context)(x: c.Tree) = {
14+
import c.universe._
15+
q"""
16+
new {
17+
class Match(x: Int) {
18+
def isEmpty = false
19+
def get = x
20+
}
21+
def unapply(x: Int) = new Match(x)
22+
}.unapply($x)
23+
"""
24+
}
25+
}

test/files/run/t5903d/Test_2.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test extends App {
2+
import Interpolation._
3+
42 match {
4+
case t"$x" => println(x)
5+
}
6+
}

0 commit comments

Comments
 (0)