Skip to content

Commit

Permalink
Implement quotation macro from scratch
Browse files Browse the repository at this point in the history
- Extend custom `MetaParser` to allow syntax-changes in the quasiquote
- Add helper class for detecting dotted arguments e.g. `[..$xs]`
- Add support for splicing multiple `LambdaTerms` into lists

Todo:
- Other possible ways to splice terms into other terms
- Unlifting and unsplicing `LambdaTerms`
  • Loading branch information
keddelzz committed Apr 16, 2017
1 parent e3b0106 commit dd266c9
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 99 deletions.
9 changes: 9 additions & 0 deletions macros/src/main/scala/me/rexim/morganey/meta/DottedHole.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package me.rexim.morganey.meta

private[meta] object DottedHole {

def unapply(arg: String): Option[Int] =
if (arg startsWith "..") Hole.unapply(arg stripPrefix "..")
else None

}
2 changes: 0 additions & 2 deletions macros/src/main/scala/me/rexim/morganey/meta/Hole.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package me.rexim.morganey.meta

import java.util.regex.Pattern

private[meta] object Hole {

private val holePattern = "$hole"
Expand Down
10 changes: 6 additions & 4 deletions macros/src/main/scala/me/rexim/morganey/meta/MetaParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import me.rexim.morganey.ast._
import me.rexim.morganey.syntax.LambdaParser

/**
* Parser, which allows dollar signs as the start of identifiers,
* as special-marker for holes, which will be used during quotation
* of syntax nodes into another nodes.
* Special version of morganey's parser, that allows
* - two dots ('..') and
* - a dollar character ('$')
* as the start of identifiers, as special-markers for holes.
* Both markers will be used during quotation.
*/
object MetaParser extends LambdaParser {

override def variable: Parser[LambdaVar] =
"\\$?[a-zA-Z][a-zA-Z0-9]*".r ^^ LambdaVar
"((\\.\\.)?\\$)?[a-zA-Z][a-zA-Z0-9]*".r ^^ LambdaVar

}
282 changes: 189 additions & 93 deletions macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package me.rexim.morganey.meta

import me.rexim.morganey.ast._

import scala.reflect.macros.whitebox.Context
import StringContext.treatEscapes
import scala.reflect.macros.whitebox

private[meta] class QuotationMacro(val c: Context) {
private[meta] class QuotationMacro(val c: whitebox.Context) {
import c.universe._

private type Lift[T] = me.rexim.morganey.meta.Liftable[T]
private type Unlift[T] = me.rexim.morganey.meta.Unliftable[T]
private val LambdaTermTpe = typeOf[LambdaTerm]
def quote(args: Tree*): Tree = expand()
def unquote(arg: Tree): Tree = expand()

private def expand(): Tree = wrapAst(buildAndParseProgram())

private def macroApplication() =
Option(c.macroApplication) collect {
Expand All @@ -22,7 +23,43 @@ private[meta] class QuotationMacro(val c: Context) {

private val (parts, method, args) = macroApplication()

private def buildProgram(): String = {
private lazy val IterableClass: TypeSymbol =
typeOf[Iterable[_]].typeSymbol.asType
private lazy val IterableTParam: Type =
IterableClass.typeParams.head.asType.toType
private def iterableT(tpe: Type): Type =
IterableTParam.asSeenFrom(tpe, IterableClass)

private val LambdaTermTpe = typeOf[LambdaTerm]

private lazy val liftList_ = c.inferImplicitValue(typeOf[Lift[List[LambdaTerm]]], silent = true)
private lazy val unliftList = implicitly[Unlift[List[LambdaTerm]]]
private lazy val liftList = implicitly[Lift[List[LambdaTerm]]]

private type Lift[T] = me.rexim.morganey.meta.Liftable[T]
private lazy val LiftableTpe = typeOf[Lift[_]]
private def liftableT(tpe: Type): Type = appliedType(LiftableTpe, tpe)

private type Unlift[T] = me.rexim.morganey.meta.Unliftable[T]
private lazy val UnliftableTpe = typeOf[Unlift[_]]
private def unliftableT(tpe: Type): Type = appliedType(UnliftableTpe, tpe)

private case class Lifted(exp: Tree, preamble: List[Tree] = Nil) {
def wrap(f: Tree => Tree): Lifted =
Lifted(f(exp), preamble)

def wrap2(other: Lifted)(f: (Tree, Tree) => Tree): Lifted =
Lifted(f(exp, other.exp), preamble ++ other.preamble)
}

/**
* Create the morganey source program, by joining the `parts` of the string context.
* For each argument in `args` a hole is created. A hole is a special mangled identifier,
* that won't be accepted by the default parser. The holes are later replaced by another
* syntax tree.
* The `MetaParser` allows parsing these special mangled identifiers.
*/
private def buildAndParseProgram(): LambdaTerm = {
val b = new StringBuilder()
val parts = this.parts.iterator
var i = 0
Expand All @@ -37,112 +74,171 @@ private[meta] class QuotationMacro(val c: Context) {
}
}

b.toString
}

private def parse(program: String): LambdaTerm = {
val par = MetaParser
par.parseAll(par.term, program) match {
case par.Success(res, _) => res
case par.NoSuccess(err, input) =>
import MetaParser._
parseAll(term, b.toString) match {
case Success(res, _) => res
case NoSuccess(err, input) =>
val pos = input.pos; import pos._
val msg = s"Error at line $line column $column in interpolated string:\n$err"
c.abort(c.enclosingPosition, msg)
}
}

private def argument(i: Int): Tree = method match {
case TermName("apply") => quoteArg(i)
case TermName("unapply") => unquoteArg(i)
private def liftPrimitiveTerm(term: LambdaTerm): Lifted = term match {
case LambdaVar(DottedHole(_)) =>
c.abort(c.enclosingPosition, "Illegal usage of ..!")
case LambdaVar(Hole(hole)) =>
replaceHole(hole, dotted = false)
case LambdaVar(name) =>
Lifted(q"_root_.me.rexim.morganey.ast.LambdaVar($name)")
case LambdaFunc(param, body) =>
liftPrimitiveTerm(param).wrap2(liftComplexTerm(body)) {
case (paramTree, bodyTree) => q"_root_.me.rexim.morganey.ast.LambdaFunc($paramTree, $bodyTree)"
}
case LambdaApp(left, right) =>
liftComplexTerm(left).wrap2(liftComplexTerm(right)) {
case (leftTree, rightTree) => q"_root_.me.rexim.morganey.ast.LambdaApp($leftTree, $rightTree)"
}
}

private def quoteArg(i: Int): Tree = {
val arg = args(i)
val tpe = arg.tpe
if (tpe <:< typeOf[LambdaTerm]) {
arg
} else {
val liftT = appliedType(typeOf[Lift[_]], tpe)
val lift = c.inferImplicitValue(liftT, silent = true)
if (lift.nonEmpty) {
q"$lift($arg)"
} else {
val reason = s"Because no implicit value of type me.rexim.morganey.meta.Liftable[$tpe] could be found!"
val msg = s"Couldn't lift a value of type $tpe to lambda term! ($reason)"
c.abort(arg.pos, msg)
private def liftComplexTerm(term: LambdaTerm): Lifted = term match {
case unliftList(terms) =>

def isDottedHole(term: LambdaTerm): Boolean = term match {
case LambdaVar(DottedHole(_)) => true
case _ => false
}
}
}

private def unquoteArg(i: Int): Tree = {
val x = TermName(s"x$i")
val subpattern = c.internal.subpatterns(args.head).get.apply(i)
subpattern match {
case pq"$_: $tpt" =>
val tpe = c.typecheck(tpt, c.TYPEmode).tpe
val UnliftT = appliedType(typeOf[Unlift[_]], tpe)
val unlift = c.inferImplicitValue(UnliftT, silent = true)
if (unlift.nonEmpty) {
pq"$unlift($x @ _)"
} else {
val reason = s"Because no implicit value of type me.rexim.morganey.meta.Unliftable[$tpe] could be found!"
val msg = s"Couldn't unlift a lambda term to a value of type $tpe! ($reason)"
c.abort(subpattern.pos, msg)
def prepend(terms: List[LambdaTerm], tree: Lifted): Lifted =
terms.foldRight(tree) {
case (ele, acc) =>
liftComplexTerm(ele).wrap2(acc) {
case (a, b) => q"$a :: $b"
}
}
case _ =>
pq"$x @ _"
}
}

private implicit def liftAst[A <: LambdaTerm]: Liftable[A] = Liftable {
case LambdaVar(Hole(n)) =>
argument(n)
case LambdaVar(name) =>
q"_root_.me.rexim.morganey.ast.LambdaVar($name)"
case LambdaFunc(param, body) =>
q"_root_.me.rexim.morganey.ast.LambdaFunc($param, $body)"
case LambdaApp(left, right) =>
q"_root_.me.rexim.morganey.ast.LambdaApp($left, $right)"
}
def append(tree: Lifted, terms: List[LambdaTerm]): Lifted =
terms.foldLeft(tree) {
case (acc, ele) =>
acc.wrap2(liftComplexTerm(ele)) {
(a, b) => q"$a :+ $b"
}
}

terms.span(!isDottedHole(_)) match {
// [..$xs, _*]
case (Nil, LambdaVar(DottedHole(hole)) :: rest) =>
val holeContent = replaceHole(hole, dotted = true)
val app = append(holeContent, rest)
app.wrap(x => q"$liftList_($x)")

// [_*, ..$xs]
case (init, LambdaVar(DottedHole(hole)) :: Nil) =>
val holeContent = replaceHole(hole, dotted = true)
val prep = prepend(init, holeContent)
prep.wrap(x => q"$liftList_($x)")

// no dotted list
case (_, Nil) =>
liftPrimitiveTerm(term)

case _ =>
c.abort(c.enclosingPosition, "Illegal usage of ..!")
}

private def transform(term: LambdaTerm): Tree = method match {
case TermName("apply") => liftAst(term)
case TermName("unapply") => extractor(term)
case simple =>
liftPrimitiveTerm(simple)
}

private def extractor(term: LambdaTerm): Tree = {
val ps = parts.indices.init
lazy val (ifp, elp) = parts match {
case Nil =>
c.abort(c.enclosingPosition, "Internal error: \"parts\" is empty.")
case hd :: Nil =>
(q"true", q"false")
case _ =>
val ys = ps map { i =>
TermName(s"x$i")
private def replaceHole(hole: Int, dotted: Boolean): Lifted = method match {
case TermName("apply") =>
val arg = args(hole)
val tpe =
if (!dotted) arg.tpe
else iterableT(arg.tpe)

def quote(tree: Tree): Tree =
if (tpe <:< LambdaTermTpe) {
/*
* A value of type `LambdaTerm` will be inserted into the hole.
*/
tree
} else {
/*
* A value of type `tpe` will be inserted into the hole.
* To be able to do so, it has to be converted to a value of type `LambdaTerm`.
* Instances of the typeclass me.rexim.morganey.meta.Liftable know how to do the conversion.
*/
val lift = c.inferImplicitValue(liftableT(tpe), silent = true)
if (lift.nonEmpty) {
q"$lift($tree)"
} else {
val reason = s"Because no implicit value of type me.rexim.morganey.meta.Liftable[$tpe] could be found!"
val msg = s"Couldn't lift a value of type $tpe to lambda term! ($reason)"
c.abort(arg.pos, msg)
}
}
(q"_root_.scala.Some((..$ys))", q"_root_.scala.None")
}

/*
* Workaround to avoid the warning: patterns after a variable pattern cannot match (SLS 8.1.1)
* (See http://alvinalexander.com/scala/scala-unreachable-code-due-to-variable-pattern-message)
*/
val identName = TermName(c.freshName())
q"""
new {
val $identName = true
def unapply(input: $LambdaTermTpe) = input match {
case $term if $identName => $ifp
case _ => $elp
}
}.unapply(..$args)
"""
if (!dotted) {
Lifted(quote(arg))
} else {
val parName = TermName(c.freshName())
val list = q"$arg.map { ($parName: $tpe) => ${quote(q"$parName")} }.toList"
Lifted(list)
}

case TermName("unapply") =>
???
}

private def expand() = transform(parse(buildProgram()))
private def wrapAst(term: LambdaTerm): Tree = method match {
/**
* - create an expression, that yields `term` at runtime, by lifting the tree (containing holes)
* - replace holes with appropriate expressions, which themselves have to be lifted, too
*/
case TermName("apply") =>
val lifted = liftComplexTerm(term)
q"""
..${lifted.preamble}
${lifted.exp}
"""

/**
* - create a pattern, which allows to extract the values at the positions, which are marked by holes
*/
case TermName("unapply") =>
val ps = parts.indices.init
lazy val (ifp, elp) = parts match {
case Nil =>
c.abort(c.enclosingPosition, "Internal error: \"parts\" is empty.")
case _ :: Nil =>
(q"true", q"false")
case _ =>
val ys = ps map { i =>
TermName(s"x$i")
}
(q"_root_.scala.Some((..$ys))", q"_root_.scala.None")
}

def quote(args: Tree*): Tree = expand()
def unquote(arg: Tree): Tree = expand()
val lifted = liftComplexTerm(term)

/*
* Workaround to avoid the warning: patterns after a variable pattern cannot match (SLS 8.1.1)
* (See http://alvinalexander.com/scala/scala-unreachable-code-due-to-variable-pattern-message)
*/
val identName = TermName(c.freshName())
q"""
new {
val $identName = true

..${lifted.preamble}

def unapply(input: $LambdaTermTpe) = input match {
case ${lifted.exp} if $identName => $ifp
case _ => $elp
}
}.unapply(..$args)
"""
}

}

0 comments on commit dd266c9

Please sign in to comment.