package magnolia import scala.reflect._, macros._ import macrocompat.bundle import scala.util.Try import language.existentials import scala.collection.immutable.ListMap import GlobalMutableState.log case class Pos(pos: api.Position) { override def toString = s"${pos.line}:${pos.column}" } case class DirectlyReentrantException() extends Exception("attempt to recurse directly") object Lazy { def apply[T](method: String): T = ??? } object GlobalMutableState { def log(c: whitebox.Context)(msg: String) = msg.split("/n").foreach { ln => println(" "*stackSize(c)+ln) } private[magnolia] var state: Map[Pos, ListMap[c.universe.Type forSome { val c: whitebox.Context }, c.universe.TermName forSome { val c: whitebox.Context }]] = Map() private[magnolia] def wrap[T](c: whitebox.Context)(key: c.universe.Type, value: c.universe.TermName)(fn: => T): Option[T] = { push(c)(key, value) try Some(fn) catch { case e: Exception => None } finally { pop(c) } } private def push(c: whitebox.Context)(key: c.universe.Type, value: c.universe.TermName): Unit = { state = state.updated(Pos(c.enclosingPosition), state.get(Pos(c.enclosingPosition)).map { m => m.updated(key, value) }.getOrElse(ListMap(key -> value))) } private def pop(c: whitebox.Context): Unit = { state = state.updated(Pos(c.enclosingPosition), state(Pos(c.enclosingPosition)).init) } private[magnolia] def find(c: whitebox.Context)(key: c.universe.Type): Option[c.universe.TermName] = { try state(Pos(c.enclosingPosition)).get(key).asInstanceOf[Option[c.universe.TermName]] catch { case e: Exception => ??? } } private[magnolia] def enclosingTypes(c: whitebox.Context): List[c.universe.Type] = state.get(Pos(c.enclosingPosition)).to[List].flatMap(_.keySet.map(_.asInstanceOf[c.universe.Type]).to[List]) private[magnolia] def stackSize(c: whitebox.Context): Int = state.get(Pos(c.enclosingPosition)).map(_.size).getOrElse(0) private[magnolia] var searchType: Option[Universe#Type] = None } @bundle class Macros(val context: whitebox.Context) extends GenericMacro(context) { protected def classBody(context: whitebox.Context)(genericType: context.Type, implementation: context.Tree): context.Tree = { import context.universe._ q"""def extract(src: _root_.magnolia.Thing): $genericType = $implementation""" } protected def dereferenceValue(context: whitebox.Context)(value: context.Tree, elem: String): context.Tree = { import context.universe._ q"$value.access($elem)" } protected def callDelegateMethod(context: whitebox.Context)(value: context.Tree, argument: context.Tree): context.Tree = { import context.universe._ q"$value.extract($argument)" } protected def coproductReduction(context: whitebox.Context)(left: context.Tree, right: context.Tree): context.Tree = { import context.universe._ q"$left.orElse($right)" } } abstract class GenericMacro(whiteboxContext: whitebox.Context) { val c = whiteboxContext import c.universe._ val transformer = new Transformer { override def transform(tree: Tree): Tree = tree match { case q"_root_.magnolia.Lazy[$returnType](${Literal(Constant(method: String))})" => q"${TermName(method)}" case _ => super.transform(tree) } } def getImplicit(genericType: c.universe.Type, typeConstructor: c.universe.Type, myName: c.universe.TermName): c.Tree = { GlobalMutableState.find(c)(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(GlobalMutableState.find(c)(genericType).isEmpty) { log(c)(s"could not find type $genericType in current context") GlobalMutableState.searchType = Some(genericType) val inferredImplicit = try Some({ val myName: TermName = TermName(c.freshName(genericType.typeSymbol.name.encodedName.toString.toLowerCase+"Extractor")) GlobalMutableState.wrap(c)(genericType, myName) { val imp = c.inferImplicitValue(searchType, false, false) q"""{ def $myName: $searchType = $imp $myName }""" }.get }) catch { case e: Exception => None } inferredImplicit.orElse { directInferImplicit(genericType, typeConstructor) } } else { directInferImplicit(genericType, typeConstructor) } }.getOrElse { log(c)("failed this branch of derivation for type "+genericType) c.abort(c.enclosingPosition, "Could not find extractor for type "+genericType) } } def directInferImplicit(genericType: c.universe.Type, typeConstructor: c.universe.Type): Option[c.Tree] = { log(c)(s"directInferImplicit($genericType) given definitions for ${GlobalMutableState.enclosingTypes(c).mkString("{", ", ", "}")}") val myName: TermName = TermName(c.freshName(genericType.typeSymbol.name.encodedName.toString.toLowerCase+"Extractor")) 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 isAnyVal = genericType <:< typeOf[AnyVal] val resultType = appliedType(typeConstructor, genericType) val construct = if(isCaseClass) { log(c)(s"deriving $genericType as a product type") val implicits = genericType.decls.collect { case m: MethodSymbol if m.isCaseAccessor => m.asMethod }.map { param => log(c)(s"dealing with parameter '${param.name}'") val returnType = param.returnType val imp = GlobalMutableState.wrap(c)(genericType, myName) { getImplicit(returnType, typeConstructor, myName) }.map { imp => val impString = if(imp.toString.contains("\\n")) "" else imp.toString log(c)(s"found an implicit for type $returnType: $impString") imp }.getOrElse { log(c)(s"failed to get implicit for type $genericType") c.abort(c.enclosingPosition, s"failed to get implicit for type $genericType") } val dereferenced = dereferenceValue(c)(q"src", param.name.toString) callDelegateMethod(c)(imp, dereferenced) } Some(q"new $genericType(..$implicits)") } else if(isSealedTrait) { log(c)(s"deriving $genericType as a coproduct type") val subtypes = classType.get.knownDirectSubclasses.to[List] Some { subtypes.map(_.asType.toType).map { searchType => log(c)(s"exploring coproduct subtype $searchType") val imp = GlobalMutableState.wrap(c)(genericType, myName) { getImplicit(searchType, typeConstructor, myName) }.getOrElse { log(c)("exploration was unsuccessful") c.abort(c.enclosingPosition, s"failed to get implicit for type $searchType") } log(c)(s"successful exploration: $imp") imp }.reduce(coproductReduction(c)) }.map { imp => callDelegateMethod(c)(imp, q"src") } } else None construct.map { const => val methodImplementation = classBody(c)(genericType, const) q"""{ def $myName: $resultType = new $resultType { $methodImplementation } $myName }""" } } protected def classBody(c: whitebox.Context)(genericType: c.Type, implementation: c.Tree): c.Tree protected def coproductReduction(c: whitebox.Context)(left: c.Tree, right: c.Tree): c.Tree protected def dereferenceValue(c: whitebox.Context)(value: c.Tree, elem: String): c.Tree protected def callDelegateMethod(c: whitebox.Context)(value: c.Tree, argument: c.Tree): c.Tree def generic[T: c.WeakTypeTag, Typeclass: c.WeakTypeTag]: c.Tree = try { import c.universe._ val genericType: Type = weakTypeOf[T] val directlyReentrant = Some(genericType) == GlobalMutableState.searchType //log(c)(s"previous: ${GlobalMutableState.searchType}; THIS TYPE = $genericType") val result: Option[c.Tree] = if(directlyReentrant) { log(c)(s"detected direct reentry into generic macro for $genericType; aborting") throw DirectlyReentrantException() } else if(GlobalMutableState.searchType != None) { log(c)(s"searching for in-scope implicit for $genericType") GlobalMutableState.find(c)(genericType) match { case None => log(c)(s"could not find suitable implicit, so recursing on $genericType") val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor directInferImplicit(genericType, typeConstructor) case Some(enclosingRef) => log(c)(s"found an enclosing implicit for $enclosingRef") val str = enclosingRef.toString val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor val searchType = appliedType(typeConstructor, genericType) Some(q"$str.asInstanceOf[${searchType}]") } } else { val typeConstructor: Type = weakTypeOf[Typeclass].typeConstructor directInferImplicit(genericType, typeConstructor) } result.map { tree => val depth = GlobalMutableState.stackSize(c) println("depth = "+depth) val transformedTree = if(depth == 0) c.untypecheck(transformer.transform(tree)) else tree val treeString = if(tree.toString.contains("\\n")) "" else tree.toString log(c)(s"returning a complete tree, which may or may not typecheck: $treeString") transformedTree }.getOrElse { log(c)("failed to derive a tree") c.abort(c.enclosingPosition, "Could not infer typeclass. Sorry.") } } catch { case e@DirectlyReentrantException() => throw e case e: Exception => log(c)("Oh no, there was a problem: "+e) e.printStackTrace() ??? } }