package magnolia
import scala.reflect._, macros._
import macrocompat.bundle
import scala.util.Try
import language.existentials
abstract class Transformation[C <: whitebox.Context](val c: C) {
def typeclassBody(genericType: c.Type, implementation: c.Tree): c.Tree
def coproductReduction(left: c.Tree, right: c.Tree): c.Tree
def dereferenceValue(value: c.Tree, elem: String): c.Tree
def callDelegateMethod(value: c.Tree, argument: c.Tree): c.Tree
}
abstract class MagnoliaMacro(val c: whitebox.Context) {
import c.universe._
import CompileTimeState._
protected def transformation(c: whitebox.Context): Transformation[c.type]
private def findType(key: c.universe.Type): Option[c.universe.TermName] =
recursionStack(c.enclosingPosition).find(_._1 == key).map(_._2.asInstanceOf[c.universe.TermName])
private def recurse[T](key: c.universe.Type, value: c.universe.TermName)(fn: => T): Option[T] = {
recursionStack = recursionStack.updated(
c.enclosingPosition,
recursionStack.get(c.enclosingPosition).map { m =>
m ::: List((key, value))
}.getOrElse(List(key -> value))
)
try Some(fn) catch { case e: Exception => None } finally {
recursionStack = recursionStack.updated(c.enclosingPosition,
recursionStack(c.enclosingPosition).init)
}
}
private val transformer = new Transformer {
override def transform(tree: Tree): Tree = tree match {
case q"magnolia.Lazy.apply[$returnType](${Literal(Constant(method: String))})" =>
q"${TermName(method)}"
case _ =>
super.transform(tree)
}
}
private def getImplicit(genericType: c.universe.Type,
typeConstructor: c.universe.Type,
assignedName: c.universe.TermName): c.Tree = {
findType(genericType).map { methodName =>
val methodAsString = methodName.encodedName.toString
val searchType = appliedType(typeConstructor, genericType)
q"_root_.magnolia.Lazy[$searchType]($methodAsString)"
}.orElse {
val searchType = appliedType(typeConstructor, genericType)
if(findType(genericType).isEmpty) {
lastSearchType = Some(genericType)
val inferredImplicit = try Some({
val genericTypeName: String = genericType.typeSymbol.name.encodedName.toString.toLowerCase
val assignedName: TermName = TermName(c.freshName(s"${genericTypeName}Typeclass"))
recurse(genericType, assignedName) {
val imp = c.inferImplicitValue(searchType, false, false)
q"""{
def $assignedName: $searchType = $imp
$assignedName
}"""
}.get
}) catch {
case e: Exception => None
}
inferredImplicit.orElse {
directInferImplicit(genericType, typeConstructor)
}
} else {
directInferImplicit(genericType, typeConstructor)
}
}.getOrElse {
c.abort(c.enclosingPosition, "Could not find extractor for type "+genericType)
}
}
private def directInferImplicit(genericType: c.universe.Type,
typeConstructor: c.universe.Type): Option[c.Tree] = {
val genericTypeName: String = genericType.typeSymbol.name.encodedName.toString.toLowerCase
val assignedName: TermName = TermName(c.freshName(s"${genericTypeName}Typeclass"))
val typeSymbol = genericType.typeSymbol
val classType = if(typeSymbol.isClass) Some(typeSymbol.asClass) else None
val isCaseClass = classType.map(_.isCaseClass).getOrElse(false)
val isSealedTrait = classType.map(_.isSealed).getOrElse(false)
val isValueClass = genericType <:< typeOf[AnyVal]
val resultType = appliedType(typeConstructor, genericType)
val construct = if(isCaseClass) {
val implicits = genericType.decls.collect {
case m: MethodSymbol if m.isCaseAccessor => m.asMethod
}.map { param =>
val derivedImplicit = recurse(genericType, assignedName) {
getImplicit(param.returnType, typeConstructor, assignedName)
}.getOrElse {
c.abort(c.enclosingPosition, s"failed to get implicit for type $genericType")
}
val dereferencedValue = transformation(c).dereferenceValue(q"src", param.name.toString)
transformation(c).callDelegateMethod(
derivedImplicit,
dereferencedValue
)
}
Some(q"new $genericType(..$implicits)")
} else if(isSealedTrait) {
val subtypes = classType.get.knownDirectSubclasses.to[List]
Some(
transformation(c)callDelegateMethod(subtypes.map(_.asType.toType).map { searchType =>
recurse(genericType, assignedName) {
getImplicit(searchType, typeConstructor, assignedName)
}.getOrElse {
c.abort(c.enclosingPosition, s"failed to get implicit for type $searchType")
}
}.reduce(transformation(c).coproductReduction), q"src")
)
} else None
construct.map { const =>
val bodyImplementation = transformation(c).typeclassBody(genericType, const)
q"""{
def $assignedName: $resultType = new $resultType { $bodyImplementation }
$assignedName
}"""
}
}
def magnolia[T: c.WeakTypeTag, Typeclass: c.WeakTypeTag]: c.Tree = try {
import c.universe._
val genericType: Type = weakTypeOf[T]
val directlyReentrant = Some(genericType) == lastSearchType
if(directlyReentrant) throw DirectlyReentrantException()
val result: Option[c.Tree] = if(lastSearchType != None) {
findType(genericType) match {
case None =>
val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor
directInferImplicit(genericType, typeConstructor)
case Some(enclosingRef) =>
val methodAsString = enclosingRef.toString
val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor
val searchType = appliedType(typeConstructor, genericType)
Some(q"_root_.magnolia.Lazy[$searchType]($methodAsString)")
}
} else {
val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor
directInferImplicit(genericType, typeConstructor)
}
result.map { tree =>
val recursionDepth = recursionStack.get(c.enclosingPosition).map(_.size).getOrElse(0)
if(recursionDepth == 0) c.untypecheck(transformer.transform(tree)) else tree
}.getOrElse {
c.abort(c.enclosingPosition, "Could not infer typeclass. Sorry.")
}
} catch {
case e@DirectlyReentrantException() => throw e
case e: Exception =>
e.printStackTrace()
???
}
}
private[magnolia] case class DirectlyReentrantException() extends
Exception("attempt to recurse directly")
private[magnolia] object Lazy { def apply[T](method: String): T = ??? }
private[magnolia] object CompileTimeState {
private[magnolia] var recursionStack: Map[api.Position, List[
(c.universe.Type, c.universe.TermName) forSome { val c: whitebox.Context }
]] = Map()
private[magnolia] var lastSearchType: Option[Universe#Type] = None
}
@bundle
class Macros(val context: whitebox.Context) extends MagnoliaMacro(context) {
protected def transformation(c: whitebox.Context): Transformation[c.type] =
new Transformation[c.type](c) {
import c.universe._
def typeclassBody(genericType: c.Type, implementation: c.Tree): c.Tree =
q"""def extract(src: _root_.magnolia.Thing): $genericType = $implementation"""
def dereferenceValue(value: c.Tree, elem: String): c.Tree = q"$value.access($elem)"
def callDelegateMethod(value: c.Tree, argument: c.Tree): c.Tree = q"$value.extract($argument)"
def coproductReduction(left: c.Tree, right: c.Tree): c.Tree = q"$left.orElse($right)"
}
}