package scala.tools.reflect
package quasiquotes
import scala.collection.{immutable, mutable}
import scala.reflect.internal.Flags._
import scala.reflect.macros.TypecheckException
class Cardinality private[Cardinality](val value: Int) extends AnyVal {
def pred = { assert(value - 1 >= 0); new Cardinality(value - 1) }
def succ = new Cardinality(value + 1)
override def toString = if (value == 0) "no dots" else "." * (value + 1)
}
object Cardinality {
val NoDot = new Cardinality(0)
val DotDot = new Cardinality(1)
val DotDotDot = new Cardinality(2)
object Dot { def unapply(card: Cardinality) = card != NoDot }
def parseDots(part: String) = {
if (part.endsWith("...")) (part.stripSuffix("..."), DotDotDot)
else if (part.endsWith("..")) (part.stripSuffix(".."), DotDot)
else (part, NoDot)
}
}
/** Defines abstractions that provide support for splicing into Scala syntax.
*/
trait Holes { self: Quasiquotes =>
import global._
import Cardinality._
import definitions._
import universeTypes._
protected lazy val IterableTParam = IterableClass.typeParams(0).asType.toType
protected def inferParamImplicit(tfun: Type, targ: Type) = c.inferImplicitValue(appliedType(tfun, List(targ)), silent = true)
protected def inferLiftable(tpe: Type): Tree = inferParamImplicit(liftableType, tpe)
protected def inferUnliftable(tpe: Type): Tree = inferParamImplicit(unliftableType, tpe)
protected def isLiftableType(tpe: Type) = inferLiftable(tpe) != EmptyTree
protected def isNativeType(tpe: Type) =
(tpe <:< treeType) || (tpe <:< nameType) || (tpe <:< modsType) ||
(tpe <:< flagsType) || (tpe <:< symbolType)
protected def isBottomType(tpe: Type) =
tpe <:< NothingClass.tpe || tpe <:< NullClass.tpe
protected def stripIterable(tpe: Type, limit: Option[Cardinality] = None): (Cardinality, Type) =
if (limit.map { _ == NoDot }.getOrElse { false }) (NoDot, tpe)
else if (tpe != null && !isIterableType(tpe)) (NoDot, tpe)
else if (isBottomType(tpe)) (NoDot, tpe)
else {
val targ = IterableTParam.asSeenFrom(tpe, IterableClass)
val (card, innerTpe) = stripIterable(targ, limit.map { _.pred })
(card.succ, innerTpe)
}
protected def iterableTypeFromCard(n: Cardinality, tpe: Type): Type = {
if (n == NoDot) tpe
else appliedType(IterableClass.toType, List(iterableTypeFromCard(n.pred, tpe)))
}
/** Hole encapsulates information about splices in quasiquotes.
* It packs together a cardinality of a splice, pre-reified tree
* representation (possibly preprocessed) and position.
*/
abstract class Hole {
val tree: Tree
val pos: Position
val cardinality: Cardinality
}
object Hole {
def apply(card: Cardinality, tree: Tree): Hole =
if (method != nme.unapply) new ApplyHole(card, tree)
else new UnapplyHole(card, tree)
def unapply(hole: Hole): Some[(Tree, Cardinality)] = Some((hole.tree, hole.cardinality))
}
class ApplyHole(card: Cardinality, splicee: Tree) extends Hole {
val (strippedTpe: Type, tpe: Type) = {
if (stripIterable(splicee.tpe)._1.value < card.value) cantSplice()
val (_, strippedTpe) = stripIterable(splicee.tpe, limit = Some(card))
if (isBottomType(strippedTpe)) cantSplice()
else if (isNativeType(strippedTpe)) (strippedTpe, iterableTypeFromCard(card, strippedTpe))
else if (isLiftableType(strippedTpe)) (strippedTpe, iterableTypeFromCard(card, treeType))
else cantSplice()
}
val tree = {
def inner(itpe: Type)(tree: Tree) =
if (isNativeType(itpe)) tree
else if (isLiftableType(itpe)) lifted(itpe)(tree)
else global.abort("unreachable")
if (card == NoDot) inner(strippedTpe)(splicee)
else iterated(card, strippedTpe, inner(strippedTpe))(splicee)
}
val pos = splicee.pos
val cardinality = stripIterable(tpe)._1
protected def cantSplice(): Nothing = {
val (iterableCard, iterableType) = stripIterable(splicee.tpe)
val holeCardMsg = if (card != NoDot) s" with $card" else ""
val action = "splice " + splicee.tpe + holeCardMsg
val suggestCard = card != iterableCard || card != NoDot
val spliceeCardMsg = if (card != iterableCard && iterableCard != NoDot) s"using $iterableCard" else "omitting the dots"
val cardSuggestion = if (suggestCard) spliceeCardMsg else ""
val suggestLifting = (card == NoDot || iterableCard != NoDot) && !(iterableType <:< treeType) && !isLiftableType(iterableType)
val liftedTpe = if (card != NoDot) iterableType else splicee.tpe
val liftSuggestion = if (suggestLifting) s"providing an implicit instance of Liftable[$liftedTpe]" else ""
val advice =
if (isBottomType(iterableType)) "bottom type values often indicate programmer mistake"
else "consider " + List(cardSuggestion, liftSuggestion).filter(_ != "").mkString(" or ")
c.abort(splicee.pos, s"Can't $action, $advice")
}
protected def lifted(tpe: Type)(tree: Tree): Tree = {
val lifter = inferLiftable(tpe)
assert(lifter != EmptyTree, s"couldnt find a liftable for $tpe")
val lifted = Apply(lifter, List(tree))
val targetType = Select(u, tpnme.Tree)
atPos(tree.pos)(TypeApply(Select(lifted, nme.asInstanceOf_), List(targetType)))
}
protected def iterated(card: Cardinality, tpe: Type, elementTransform: Tree => Tree = identity)(tree: Tree): Tree = {
assert(card != NoDot)
def reifyIterable(tree: Tree, n: Cardinality): Tree = {
def loop(tree: Tree, n: Cardinality): Tree =
if (n == NoDot) elementTransform(tree)
else {
val x: TermName = c.freshName()
val wrapped = reifyIterable(Ident(x), n.pred)
val xToWrapped = Function(List(ValDef(Modifiers(PARAM), x, TypeTree(), EmptyTree)), wrapped)
Select(Apply(Select(tree, nme.map), List(xToWrapped)), nme.toList)
}
if (tree.tpe != null && (tree.tpe <:< listTreeType || tree.tpe <:< listListTreeType)) tree
else atPos(tree.pos)(loop(tree, n))
}
reifyIterable(tree, card)
}
}
class UnapplyHole(val cardinality: Cardinality, pat: Tree) extends Hole {
val (placeholderName, pos, tptopt) = pat match {
case Bind(pname, inner @ Bind(_, Typed(Ident(nme.WILDCARD), tpt))) => (pname, inner.pos, Some(tpt))
case Bind(pname, inner @ Typed(Ident(nme.WILDCARD), tpt)) => (pname, inner.pos, Some(tpt))
case Bind(pname, inner) => (pname, inner.pos, None)
}
val treeNoUnlift = Bind(placeholderName, Ident(nme.WILDCARD))
lazy val tree =
tptopt.map { tpt =>
val TypeDef(_, _, _, typedTpt) =
try c.typeCheck(TypeDef(NoMods, TypeName("T"), Nil, tpt))
catch { case TypecheckException(pos, msg) => c.abort(pos.asInstanceOf[c.Position], msg) }
val tpe = typedTpt.tpe
val (iterableCard, _) = stripIterable(tpe)
if (iterableCard.value < cardinality.value)
c.abort(pat.pos, s"Can't extract $tpe with $cardinality, consider using $iterableCard")
val (_, strippedTpe) = stripIterable(tpe, limit = Some(cardinality))
if (strippedTpe <:< treeType) treeNoUnlift
else
unlifters.spawn(strippedTpe, cardinality).map {
Apply(_, treeNoUnlift :: Nil)
}.getOrElse {
c.abort(pat.pos, s"Can't find $unliftableType[$strippedTpe], consider providing it")
}
}.getOrElse { treeNoUnlift }
}
/** Full support for unliftable implies that it's possible to interleave
* deconstruction with higher cardinality and unlifting of the values.
* In particular extraction of List[Tree] as List[T: Unliftable] requires
* helper extractors that would do the job: UnliftHelper1[T]. Similarly
* List[List[Tree]] needs UnliftHelper2[T].
*
* See also "unlift list" tests in UnapplyProps.scala
*/
object unlifters {
private var records = List.empty[(Type, Cardinality)]
// Request an UnliftHelperN[T] where n == card and T == tpe.
// If card == 0 then helper is not needed and plain instance
// of unliftable is returned.
def spawn(tpe: Type, card: Cardinality): Option[Tree] = {
val unlifter = inferUnliftable(tpe)
if (unlifter == EmptyTree) None
else if (card == NoDot) Some(unlifter)
else {
val idx = records.indexWhere { p => p._1 =:= tpe && p._2 == card }
val resIdx = if (idx != -1) idx else { records +:= (tpe, card); records.length - 1}
Some(Ident(TermName(nme.QUASIQUOTE_UNLIFT_HELPER + resIdx)))
}
}
// Returns a list of vals that will defined required unlifters
def preamble(): List[Tree] =
records.zipWithIndex.map { case ((tpe, card), idx) =>
val name = TermName(nme.QUASIQUOTE_UNLIFT_HELPER + idx)
val helperName = card match { case DotDot => nme.UnliftHelper1 case DotDotDot => nme.UnliftHelper2 }
val lifter = inferUnliftable(tpe)
assert(helperName.isTermName)
// q"val $name: $u.build.${helperName.toTypeName} = $u.build.$helperName($lifter)"
ValDef(NoMods, name,
AppliedTypeTree(Select(Select(u, nme.build), helperName.toTypeName), List(TypeTree(tpe))),
Apply(Select(Select(u, nme.build), helperName), lifter :: Nil))
}
}
}