Skip to content

Commit

Permalink
Introduce support for Unliftable for Quasiquotes
Browse files Browse the repository at this point in the history
Unliftable is a type class similar to existing Liftable that lets
users to extract custom data types out of trees with the help of
straightforward type ascription syntax:

  val q“foo.bar(${baz: Baz})” = ...

This will use Unliftable[Baz] to extract custom data type Baz out of
a tree nested inside of the another tree. A simpler example would be
extracting of constant values:

  val q”${x: Int} + ${y: Int}” = q”1 + 2”
  • Loading branch information
densh committed Dec 10, 2013
1 parent 4c899ea commit 1188f95
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 6 deletions.
64 changes: 62 additions & 2 deletions src/compiler/scala/tools/reflect/quasiquotes/Holes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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) }
Expand Down Expand Up @@ -33,6 +34,7 @@ trait Holes { self: Quasiquotes =>
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) ||
Expand Down Expand Up @@ -136,8 +138,66 @@ trait Holes { self: Quasiquotes =>
}

class UnapplyHole(val cardinality: Cardinality, pat: Tree) extends Hole {
val (tree, pos) = pat match {
case Bind(pname, inner) => (Bind(pname, Ident(nme.WILDCARD)), inner.pos)
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))
}
}
}
8 changes: 4 additions & 4 deletions src/compiler/scala/tools/reflect/quasiquotes/Reifiers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,12 @@ trait Reifiers { self: Quasiquotes =>
// cq"$tree if $guard => $succ" :: cq"_ => $fail" :: Nil
CaseDef(tree, guard, succ) :: CaseDef(Ident(nme.WILDCARD), EmptyTree, fail) :: Nil
}
// q"new { def unapply(tree: $AnyClass) = tree match { case ..$cases } }.unapply(..$args)"
// q"new { def unapply(tree: $AnyClass) = { ..${unlifters.preamble()}; tree match { case ..$cases } } }.unapply(..$args)"
Apply(
Select(
SyntacticNew(Nil, Nil, noSelfType, List(
DefDef(NoMods, nme.unapply, Nil, List(List(ValDef(NoMods, nme.tree, TypeTree(AnyClass.toType), EmptyTree))), TypeTree(),
Match(Ident(nme.tree), cases)))),
SyntacticBlock(unlifters.preamble() :+ Match(Ident(nme.tree), cases))))),
nme.unapply),
args)
}
Expand Down Expand Up @@ -198,7 +198,7 @@ trait Reifiers { self: Quasiquotes =>
case Placeholder(hole: ApplyHole) =>
if (!(hole.tpe <:< nameType)) c.abort(hole.pos, s"$nameType expected but ${hole.tpe} found")
hole.tree
case Placeholder(hole: UnapplyHole) => hole.tree
case Placeholder(hole: UnapplyHole) => hole.treeNoUnlift
case FreshName(prefix) if prefix != nme.QUASIQUOTE_NAME_PREFIX =>
def fresh() = c.freshName[TermName](nme.QUASIQUOTE_NAME_PREFIX)
def introduceName() = { val n = fresh(); nameMap(name) += n; n}
Expand Down Expand Up @@ -401,7 +401,7 @@ trait Reifiers { self: Quasiquotes =>
case hole :: Nil =>
if (m.annotations.length != 1) c.abort(hole.pos, "Can't extract modifiers together with annotations, consider extracting just modifiers")
ensureNoExplicitFlags(m, hole.pos)
hole.tree
hole.treeNoUnlift
case _ :: hole :: _ =>
c.abort(hole.pos, "Can't extract multiple modifiers together, consider extracting a single modifiers instance")
case Nil =>
Expand Down
10 changes: 10 additions & 0 deletions src/reflect/scala/reflect/api/BuildUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ private[reflect] trait BuildUtils { self: Universe =>
def unapply(tree: Tree): Option[(List[Tree], Tree)]
}

def UnliftHelper1[T](unliftable: Unliftable[T]): UnliftHelper1[T]
trait UnliftHelper1[T] {
def unapply(lst: List[Tree]): Option[List[T]]
}

def UnliftHelper2[T](unliftable: Unliftable[T]): UnliftHelper2[T]
trait UnliftHelper2[T] {
def unapply(lst: List[List[Tree]]): Option[List[List[T]]]
}

val SyntacticMatch: SyntacticMatchExtractor
trait SyntacticMatchExtractor {
def apply(selector: Tree, cases: List[Tree]): Match
Expand Down
Loading

0 comments on commit 1188f95

Please sign in to comment.