A.J.Rouvoet
A.J.Rouvoet

Reputation: 1213

Scala compile time macro, filter argument list based on (parent) type

I have a method/constructor that takes a bunch of parameters. Using a scala macro I can ofcourse extract the Tree representing the type of those parameters.

But I cannot find out how to convert this tree to something "useful", i.e. that I can get the parent types of, check if it is a primitive, etc.

Lets say I have a concrete type C and if want all parameters that inherit from C or are subtypes of Seq[C].

For a bit of context:

case cd@q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: tail =>

    // extract everything that is subtype of C or Seq[C]
    val cs = paramss.head.map {
        case q"$mods val $name: $tpt = $default" => ???
    }

Everything that goes in a macro should be typechecked, right? So $tpt should have a "type"? How do I get it and what exactly do I get back?

Upvotes: 1

Views: 486

Answers (2)

som-snytt
som-snytt

Reputation: 39577

I haven't used them in anger, but it looks like you're using macro annotations.

No, you get untyped annotees.

http://docs.scala-lang.org/overviews/macros/annotations.html

macro annottees are untyped, so that we can change their signatures

Given a client:

package thingy

class C

@testThing class Thingy(val c: C, i: Int) { def j = 2*i }

object Test extends App {
  Console println Thingy(42).stuff
}

then then the macro implementation sees:

val t = (annottees map (c typecheck _.tree)) match {
  case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: _ =>
    val p @ q"$mods val $name: ${tpt: Type} = $default" = paramss.head.head
    Console println showRaw(tpt)
    Console println tpt.baseClasses
    toEmit
}

where the tree has been typechecked explicitly and the type is annotated in the extraction:

case q"val $name: ${tpt: Type} = ???" =>

I realize my spelling of annotee differs from the official docs. I just can't figure out why it should be different from devotee.

Here is the toy skeleton code, for other beginners:

package thingy

import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

class testThing extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro testThing.impl
}

object testThing {
  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    val toEmit = q"""{
class Thingy(i: Int) {
  def stuff = println(i)
}

object Thingy {
  def apply(x: Int) = new Thingy(x)
}
()
}"""
    def dummy = Literal(Constant(()))
    //val t = (annottees map (_.tree)) match {
    val t = (annottees map (c typecheck _.tree)) match {
      case Nil =>
        c.abort(c.enclosingPosition, "No test target")
        dummy
      case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$_ } with ..$_ { $self => ..$stats }" :: _ =>
        //val p @ q"$mods val $name: $tpt = $default" = paramss.head.head
        val p @ q"$mods val $name: ${tpt: Type} = $default" = paramss.head.head
        Console println showRaw(tpt)
        Console println tpt.baseClasses
        toEmit
/*
      case (classDeclaration: ClassDef) :: Nil =>
        println("No companion provided")
        toEmit
      case (classDeclaration: ClassDef) :: (companionDeclaration: ModuleDef) :: Nil =>
        println("Companion provided")
        toEmit
*/
      case _ => c.abort(c.enclosingPosition, "Invalid test target")
        dummy
    }
    c.Expr[Any](t)
  }
}

Upvotes: 0

Zim-Zam O'Pootertoot
Zim-Zam O'Pootertoot

Reputation: 18148

This is how I determine if a type inherits from Iterable and not from Map, and if a type inherits from Map

val iterableType = typeOf[Iterable[_]].typeSymbol
val mapType = typeOf[Map[_, _]].typeSymbol

def isIterable(tpe: Type): Boolean = tpe.baseClasses.contains(iterableType) && !isMap(tpe)
def isMap(tpe: Type): Boolean = tpe.baseClasses.contains(mapType)

I use this on e.g. the fields in a class's primary constructor:

// tpe is a Type
val fields = tpe.declarations.collectFirst {
  case m: MethodSymbol if m.isPrimaryConstructor => m
}.get.paramss.head

val iterableFields = fields.filter(f => isIterable(f.typeSignature))
val mapFields = fields.filter(f => isMap(f.typeSignature))

Upvotes: 1

Related Questions