From dd266c94023b5f6cc1c1ed7c8fbecf699e86893e Mon Sep 17 00:00:00 2001 From: keddelzz Date: Sun, 16 Apr 2017 23:47:01 +0200 Subject: [PATCH 01/11] Implement quotation macro from scratch - 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` --- .../me/rexim/morganey/meta/DottedHole.scala | 9 + .../scala/me/rexim/morganey/meta/Hole.scala | 2 - .../me/rexim/morganey/meta/MetaParser.scala | 10 +- .../rexim/morganey/meta/QuotationMacro.scala | 282 ++++++++++++------ 4 files changed, 204 insertions(+), 99 deletions(-) create mode 100644 macros/src/main/scala/me/rexim/morganey/meta/DottedHole.scala diff --git a/macros/src/main/scala/me/rexim/morganey/meta/DottedHole.scala b/macros/src/main/scala/me/rexim/morganey/meta/DottedHole.scala new file mode 100644 index 0000000..7441e3a --- /dev/null +++ b/macros/src/main/scala/me/rexim/morganey/meta/DottedHole.scala @@ -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 + +} diff --git a/macros/src/main/scala/me/rexim/morganey/meta/Hole.scala b/macros/src/main/scala/me/rexim/morganey/meta/Hole.scala index 441e3a0..b2aae85 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/Hole.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/Hole.scala @@ -1,7 +1,5 @@ package me.rexim.morganey.meta -import java.util.regex.Pattern - private[meta] object Hole { private val holePattern = "$hole" diff --git a/macros/src/main/scala/me/rexim/morganey/meta/MetaParser.scala b/macros/src/main/scala/me/rexim/morganey/meta/MetaParser.scala index f942222..701125c 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/MetaParser.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/MetaParser.scala @@ -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 } diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index ef19105..6bc3735 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -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 { @@ -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 @@ -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) + """ + } } From 29f291d478d5c9447379e8ee2293977d0c77a1f5 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Mon, 17 Apr 2017 13:02:55 +0200 Subject: [PATCH 02/11] Reimplement unquoting and implement unsplicing (#345) --- .../rexim/morganey/meta/QuotationMacro.scala | 66 +++++++++++++++---- .../me/rexim/morganey/meta/UnapplyEach.scala | 11 ++++ 2 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index 6bc3735..4100adc 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -32,9 +32,10 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { 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 lazy val liftList = implicitly[Lift[List[LambdaTerm]]] + private lazy val liftList_ = c.inferImplicitValue(typeOf[Lift[List[LambdaTerm]]], silent = true) + private lazy val unliftList = implicitly[Unlift[List[LambdaTerm]]] + private lazy val unliftList_ = c.inferImplicitValue(typeOf[Unlift[List[LambdaTerm]]], silent = true) private type Lift[T] = me.rexim.morganey.meta.Liftable[T] private lazy val LiftableTpe = typeOf[Lift[_]] @@ -104,6 +105,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private def liftComplexTerm(term: LambdaTerm): Lifted = term match { case unliftList(terms) => + val unapply = TermName("unapply") == method + def isDottedHole(term: LambdaTerm): Boolean = term match { case LambdaVar(DottedHole(_)) => true case _ => false @@ -130,13 +133,15 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { case (Nil, LambdaVar(DottedHole(hole)) :: rest) => val holeContent = replaceHole(hole, dotted = true) val app = append(holeContent, rest) - app.wrap(x => q"$liftList_($x)") + if (unapply) app + else 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)") + if (unapply) prep + else prep.wrap(x => q"$liftList_($x)") // no dotted list case (_, Nil) => @@ -169,9 +174,9 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { * 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)" + val liftTpe = c.inferImplicitValue(liftableT(tpe), silent = true) + if (liftTpe.nonEmpty) { + q"$liftTpe($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)" @@ -188,7 +193,44 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { } case TermName("unapply") => - ??? + val x = TermName(s"x$hole") + val subpattern = c.internal.subpatterns(args.head).get.apply(hole) + + def unQuote(tpe: Type): Lifted = { + val unliftTpe = c.inferImplicitValue(unliftableT(tpe), silent = true) + + if (unliftTpe.nonEmpty) { + if (!dotted) { + Lifted(pq"$unliftTpe($x @ _)") + } else { + val each = TermName(c.freshName(s"each$hole")) + val preamble = + q""" + val $each: _root_.me.rexim.morganey.meta.UnapplyEach[$tpe] = + new _root_.me.rexim.morganey.meta.UnapplyEach[$tpe]($unliftTpe) + """ + val unapplyEach = pq"$each($x @ _)" + val unapplyTerms = unliftList_ + + Lifted(q"$unapplyTerms($unapplyEach)", List(preamble)) + } + } 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) + } + } + + subpattern match { + case pq"$_: $tpt" => + val typedTpt = c.typecheck(tpt, c.TYPEmode) + val tpe = + if (!dotted) typedTpt.tpe + else iterableT(typedTpt.tpe) + unQuote(tpe) + case _ => + unQuote(LambdaTermTpe) + } } private def wrapAst(term: LambdaTerm): Tree = method match { @@ -208,12 +250,12 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { */ case TermName("unapply") => val ps = parts.indices.init - lazy val (ifp, elp) = parts match { - case Nil => + val (ifp, elp) = parts match { + case Nil => c.abort(c.enclosingPosition, "Internal error: \"parts\" is empty.") case _ :: Nil => (q"true", q"false") - case _ => + case _ => val ys = ps map { i => TermName(s"x$i") } diff --git a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala new file mode 100644 index 0000000..6b0e0d6 --- /dev/null +++ b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala @@ -0,0 +1,11 @@ +package me.rexim.morganey.meta + +import me.rexim.morganey.ast.LambdaTerm +import me.rexim.morganey.monad.sequence + +class UnapplyEach[T](unliftT: Unliftable[T]) { + + def unapply(terms: List[LambdaTerm]): Option[List[T]] = + sequence(terms map unliftT.unapply) + +} From 61dbcb100308245982a9079a37f099b1251b378b Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 09:28:27 +0200 Subject: [PATCH 03/11] Add test for new marker (dotted holes) (#345) --- .../me/rexim/morganey/meta/HoleSpec.scala | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/macros/src/test/scala/me/rexim/morganey/meta/HoleSpec.scala b/macros/src/test/scala/me/rexim/morganey/meta/HoleSpec.scala index 3733511..08eab0c 100644 --- a/macros/src/test/scala/me/rexim/morganey/meta/HoleSpec.scala +++ b/macros/src/test/scala/me/rexim/morganey/meta/HoleSpec.scala @@ -4,7 +4,9 @@ import org.scalatest.{FlatSpec, Matchers} class HoleSpec extends FlatSpec with Matchers { - "A hole" should "mark positions, where lambda-terms were inserted during interpolation" in { + behavior of "A hole" + + it should "mark positions, where lambda-terms were inserted during interpolation" in { Hole(0) should be ("$hole0") Hole(1) should be ("$hole1") Hole(2) should be ("$hole2") @@ -18,4 +20,28 @@ class HoleSpec extends FlatSpec with Matchers { val Hole(999) = "$hole999" } + it should "throw a MatchError, if the extractor is used with no hole" in { + a[MatchError] should be thrownBy { + val Hole(_) = "no hole" + } + } + + behavior of "A dotted hole" + + it can "be used to get the number back, which was used to create the dotted hole" in { + val DottedHole(0) = "..$hole0" + val DottedHole(1) = "..$hole1" + val DottedHole(2) = "..$hole2" + val DottedHole(999) = "..$hole999" + } + + it should "throw a MatchError, if the extractor is used with no dotted hole" in { + a[MatchError] should be thrownBy { + val DottedHole(_) = "no hole" + } + a[MatchError] should be thrownBy { + val DottedHole(0) = "$hole0" + } + } + } From f519f6912d8e4ff072aca467b3bdda30012081f7 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 09:54:45 +0200 Subject: [PATCH 04/11] Add tests for splicing/unsplicing terms in/from lists (#345) --- .../me/rexim/morganey/helpers/TestTerms.scala | 3 ++ .../morganey/meta/InterpolatorSpec.scala | 35 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala b/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala index 62ac6ca..095c1b8 100644 --- a/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala +++ b/kernel/src/test/scala/me/rexim/morganey/helpers/TestTerms.scala @@ -34,4 +34,7 @@ trait TestTerms { val zero = lnested(List("f", "x"), lvar("x")) val one = lnested(List("f", "x"), lapp(lvar("f"), lvar("x"))) val two = lnested(List("f", "x"), lapp(lvar("f"), lapp(lvar("f"), lvar("x")))) + + val `[0 .. 2]` = pair(zero, pair(one, pair(two, zero))) + } diff --git a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala index 97d988a..078c519 100644 --- a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala +++ b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala @@ -59,7 +59,7 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { val m"${nTwo: Int}" = two nTwo should be (2) - val m"${numbers: List[Int]}" = pair(zero, pair(one, pair(two, zero))) + val m"${numbers: List[Int]}" = `[0 .. 2]` numbers should be (List(0, 1, 2)) val m"${list: List[Int]}" = zero @@ -100,4 +100,37 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { rest should be (lfunc("d", one)) } + "Splicing of terms into lists" should "be supported by the quotation macro" in { + { + val terms = List(zero, one, two) + val spliced = m"[..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = List(one, two) + val spliced = m"[0, ..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = List(zero, one) + val spliced = m"[..$terms, 2]" + spliced should be (`[0 .. 2]`) + } + } + + "Unsplicing of terms out of lists" should "be supported by the unquotation macro" in { + { + val m"[..$unspliced]" = `[0 .. 2]` + unspliced should be (List(zero, one, two)) + } + { + val m"[0, ..$unspliced]" = `[0 .. 2]` + unspliced should be (List(one, two)) + } + { + val m"[..$unspliced, 2]" = `[0 .. 2]` + unspliced should be (List(zero, one)) + } + } + } From 6018ad26e9e554934aa9f9648c02cd4b5cf07832 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 09:56:48 +0200 Subject: [PATCH 05/11] Fix creating expression in pattern position (#345) --- .../me/rexim/morganey/meta/QuotationMacro.scala | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index 4100adc..97d1bcb 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -116,7 +116,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { terms.foldRight(tree) { case (ele, acc) => liftComplexTerm(ele).wrap2(acc) { - case (a, b) => q"$a :: $b" + case (a, b) if unapply => pq"$a :: $b" + case (a, b) => q"$a :: $b" } } @@ -124,7 +125,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { terms.foldLeft(tree) { case (acc, ele) => acc.wrap2(liftComplexTerm(ele)) { - (a, b) => q"$a :+ $b" + case (a, b) if unapply => pq"$a :+ $b" + case (a, b) => q"$a :+ $b" } } @@ -133,14 +135,14 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { case (Nil, LambdaVar(DottedHole(hole)) :: rest) => val holeContent = replaceHole(hole, dotted = true) val app = append(holeContent, rest) - if (unapply) app + if (unapply) app.wrap(x => pq"$unliftList_($x)") else app.wrap(x => q"$liftList_($x)") // [_*, ..$xs] case (init, LambdaVar(DottedHole(hole)) :: Nil) => val holeContent = replaceHole(hole, dotted = true) val prep = prepend(init, holeContent) - if (unapply) prep + if (unapply) prep.wrap(x => pq"$unliftList_($x)") else prep.wrap(x => q"$liftList_($x)") // no dotted list @@ -209,10 +211,7 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { val $each: _root_.me.rexim.morganey.meta.UnapplyEach[$tpe] = new _root_.me.rexim.morganey.meta.UnapplyEach[$tpe]($unliftTpe) """ - val unapplyEach = pq"$each($x @ _)" - val unapplyTerms = unliftList_ - - Lifted(q"$unapplyTerms($unapplyEach)", List(preamble)) + Lifted(pq"$each($x @ _)", List(preamble)) } } else { val reason = s"Because no implicit value of type me.rexim.morganey.meta.Unliftable[$tpe] could be found!" From 192b9ed2b362ef827c00f0c7c04e62f63bcb7393 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 13:07:51 +0200 Subject: [PATCH 06/11] Fix splicing of sequences (other than lists) not working (#345) --- .../rexim/morganey/meta/QuotationMacro.scala | 4 +- .../morganey/meta/InterpolatorSpec.scala | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index 97d1bcb..3af8e8f 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -116,8 +116,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { terms.foldRight(tree) { case (ele, acc) => liftComplexTerm(ele).wrap2(acc) { - case (a, b) if unapply => pq"$a :: $b" - case (a, b) => q"$a :: $b" + case (a, b) if unapply => pq"$a +: $b" + case (a, b) => q"$a +: $b" } } diff --git a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala index 078c519..5ae0715 100644 --- a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala +++ b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala @@ -100,7 +100,8 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { rest should be (lfunc("d", one)) } - "Splicing of terms into lists" should "be supported by the quotation macro" in { + "Splicing sequences of terms into lists" should "be supported by the quotation macro" in { + // Lists { val terms = List(zero, one, two) val spliced = m"[..$terms]" @@ -116,9 +117,43 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { val spliced = m"[..$terms, 2]" spliced should be (`[0 .. 2]`) } + + // Vectors + { + val terms = Vector(zero, one, two) + val spliced = m"[..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = Vector(one, two) + val spliced = m"[0, ..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = Vector(zero, one) + val spliced = m"[..$terms, 2]" + spliced should be (`[0 .. 2]`) + } + + // Seqs + { + val terms = Seq(zero, one, two) + val spliced = m"[..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = Seq(one, two) + val spliced = m"[0, ..$terms]" + spliced should be (`[0 .. 2]`) + } + { + val terms = Seq(zero, one) + val spliced = m"[..$terms, 2]" + spliced should be (`[0 .. 2]`) + } } - "Unsplicing of terms out of lists" should "be supported by the unquotation macro" in { + "Unsplicing sequences of terms out of lists" should "be supported by the unquotation macro" in { { val m"[..$unspliced]" = `[0 .. 2]` unspliced should be (List(zero, one, two)) From 2c4934bc9d05b45131d1690d82e6584455c9ed4d Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 17:49:09 +0200 Subject: [PATCH 07/11] Add tests for unsplicing and autoconversion during unlifing (#345) --- .../morganey/meta/InterpolatorSpec.scala | 96 ++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala index 5ae0715..ea89439 100644 --- a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala +++ b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala @@ -153,18 +153,106 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { } } - "Unsplicing sequences of terms out of lists" should "be supported by the unquotation macro" in { + "Unsplicing sequences of terms out of lists (without specified types)" should "be supported by the unquotation macro" in { { val m"[..$unspliced]" = `[0 .. 2]` - unspliced should be (List(zero, one, two)) + unspliced should be(List(zero, one, two)) } { val m"[0, ..$unspliced]" = `[0 .. 2]` - unspliced should be (List(one, two)) + unspliced should be(List(one, two)) } { val m"[..$unspliced, 2]" = `[0 .. 2]` - unspliced should be (List(zero, one)) + unspliced should be(List(zero, one)) + } + } + + "Unsplicing sequences of terms out of lists (with specified types)" should "be supported by the unquotation macro" in { + // Lists + { + val m"[..${unspliced: List[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be(List(zero, one, two)) + } + { + val m"[0, ..${unspliced: List[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be(List(one, two)) + } + { + val m"[..${unspliced: List[LambdaTerm]}, 2]" = `[0 .. 2]` + unspliced should be(List(zero, one)) + } + + // Vectors + { + val m"[..${unspliced: Vector[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be (Vector(zero, one, two)) + } + { + val m"[0, ..${unspliced: Vector[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be (Vector(one, two)) + } + { + val m"[..${unspliced: Vector[LambdaTerm]}, 2]" = `[0 .. 2]` + unspliced should be (Vector(zero, one)) + } + + // Seqs + { + val m"[..${unspliced: Seq[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be (Seq(zero, one, two)) + } + { + val m"[0, ..${unspliced: Vector[LambdaTerm]}]" = `[0 .. 2]` + unspliced should be (Seq(one, two)) + } + { + val m"[..${unspliced: Vector[LambdaTerm]}, 2]" = `[0 .. 2]` + unspliced should be (Seq(zero, one)) + } + } + + "Unsplicing sequences of terms out of lists (with specified types and autoconversion)" should "be supported by the unquotation macro" in { + // Lists + { + val m"[..${unspliced: List[Int]}]" = `[0 .. 2]` + unspliced should be(List(0, 1, 2)) + } + { + val m"[0, ..${unspliced: List[Int]}]" = `[0 .. 2]` + unspliced should be(List(1, 2)) + } + { + val m"[..${unspliced: List[Int]}, 2]" = `[0 .. 2]` + unspliced should be(List(0, 1)) + } + + // Vectors + { + val m"[..${unspliced: Vector[Int]}]" = `[0 .. 2]` + unspliced should be (Vector(0, 1, 2)) + } + { + val m"[0, ..${unspliced: Vector[Int]}]" = `[0 .. 2]` + unspliced should be (Vector(1, 2)) + } + { + val m"[..${unspliced: Vector[Int]}, 2]" = `[0 .. 2]` + unspliced should be (Vector(0, 1)) + } + + // Seqs + { + val m"[..${unspliced: Seq[Int]}]" = `[0 .. 2]` + unspliced should be (Seq(0, 1, 2)) + } + { + val m"[0, ..${unspliced: Vector[Int]}]" = `[0 .. 2]` + unspliced should be (Seq(1, 2)) + } + { + val m"[..${unspliced: Vector[Int]}, 2]" = `[0 .. 2]` + unspliced should be (Seq(0, 1)) } } From 5280f96f458ca72e52248ade4b226bf179a52fb5 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 17:50:52 +0200 Subject: [PATCH 08/11] Make 'sequence'-function generic (#345) --- .../scala/me/rexim/morganey/monad/package.scala | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/kernel/src/main/scala/me/rexim/morganey/monad/package.scala b/kernel/src/main/scala/me/rexim/morganey/monad/package.scala index b906d24..453688b 100644 --- a/kernel/src/main/scala/me/rexim/morganey/monad/package.scala +++ b/kernel/src/main/scala/me/rexim/morganey/monad/package.scala @@ -1,16 +1,25 @@ package me.rexim.morganey +import scala.collection.generic.CanBuildFrom import scala.util.Try +import scala.language.higherKinds package object monad { /** * Returns 'None', if one of the Options in 'lst' is 'None', * otherwise the elements are collected in a 'Some'. */ - def sequence[T](lst: List[Option[T]]): Option[List[T]] = - lst.foldRight(Option(List.empty[T])) { - case (ele, acc) => acc.flatMap(lst => ele.map(_ :: lst)) - } + def sequence[CC[+T] <: TraversableOnce[T], T](lst: CC[Option[T]]) + (implicit cbf: CanBuildFrom[Nothing, T, CC[T]]): Option[CC[T]] = { + var out = Option(cbf.apply()) + val i = lst.toIterator + while (out.isDefined && i.hasNext) + i.next() match { + case Some(elem) => out.map(_ += elem) + case None => out = None + } + out.map(_.result()) + } def sequence[T](lst: List[Try[T]]): Try[List[T]] = lst.foldRight(Try(List.empty[T])) { From 07435250a65f955929ea48aaee369d8ea593324c Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 17:52:28 +0200 Subject: [PATCH 09/11] Make 'UnapplyEach' work with every TraversableOnce (#345) --- .../me/rexim/morganey/meta/UnapplyEach.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala index 6b0e0d6..b7f2239 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala @@ -1,11 +1,18 @@ package me.rexim.morganey.meta import me.rexim.morganey.ast.LambdaTerm -import me.rexim.morganey.monad.sequence +import me.rexim.morganey.monad._ -class UnapplyEach[T](unliftT: Unliftable[T]) { +import scala.collection.generic.CanBuildFrom +import scala.language.higherKinds - def unapply(terms: List[LambdaTerm]): Option[List[T]] = - sequence(terms map unliftT.unapply) +class UnapplyEach[T, CC[+X] <: TraversableOnce[X]](unliftT: Unliftable[T]) + (implicit cbf: CanBuildFrom[CC[LambdaTerm], T, CC[T]], + cbfo: CanBuildFrom[CC[LambdaTerm], Option[T], CC[Option[T]]]) { + + def unapply(terms: CC[LambdaTerm]): Option[CC[T]] = { + val unliftedElements = (cbfo() ++= (terms map unliftT.unapply _)).result() + sequence[CC, T](unliftedElements)(cbf) + } } From 472fb9fa551ef9bb7a71b9e967c1a9bcff657730 Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 17:59:23 +0200 Subject: [PATCH 10/11] Unsplicing with autoconversion to annotated types (#345) --- .../me/rexim/morganey/meta/Liftable.scala | 5 +- .../rexim/morganey/meta/QuotationMacro.scala | 206 +++++++++++------- .../me/rexim/morganey/meta/Unliftable.scala | 7 +- 3 files changed, 129 insertions(+), 89 deletions(-) diff --git a/macros/src/main/scala/me/rexim/morganey/meta/Liftable.scala b/macros/src/main/scala/me/rexim/morganey/meta/Liftable.scala index ebee508..f81d223 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/Liftable.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/Liftable.scala @@ -15,8 +15,7 @@ trait DefaultLiftableInstances { implicit val liftInt = Liftable[Int](encodeNumber) implicit val liftChar = Liftable[Char](c => encodeNumber(c.toInt)) - implicit val liftString = Liftable[String] { s => - val lift = implicitly[Liftable[Seq[Char]]] + implicit def liftString(implicit lift: Liftable[Seq[Char]]) = Liftable[String] { s => lift(s.toSeq) } @@ -25,7 +24,7 @@ trait DefaultLiftableInstances { case (a, b) => encodePair( (liftA(a), liftB(b)) ) } - implicit def liftColl[X, CC[X] <: Traversable[X], A](implicit lift: Liftable[A]): Liftable[CC[A]] = + implicit def liftColl[CC[X] <: TraversableOnce[X], A](implicit lift: Liftable[A]): Liftable[CC[A]] = Liftable[CC[A]] { xs => val ys = xs map lift encodeList(ys.toList) diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index 3af8e8f..0926f4f 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -23,6 +23,9 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private val (parts, method, args) = macroApplication() + private def isUnapply = TermName("unapply") == method + private def isApply = TermName("apply") == method + private lazy val IterableClass: TypeSymbol = typeOf[Iterable[_]].typeSymbol.asType private lazy val IterableTParam: Type = @@ -36,16 +39,20 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private lazy val liftList_ = c.inferImplicitValue(typeOf[Lift[List[LambdaTerm]]], silent = true) private lazy val unliftList = implicitly[Unlift[List[LambdaTerm]]] private lazy val unliftList_ = c.inferImplicitValue(typeOf[Unlift[List[LambdaTerm]]], silent = true) + private def unliftColl_ (tpe: Type) = c.inferImplicitValue(unliftableT(tpe), silent = true) private type Lift[T] = me.rexim.morganey.meta.Liftable[T] - private lazy val LiftableTpe = typeOf[Lift[_]] + private lazy val LiftableTpe = typeOf[me.rexim.morganey.meta.Liftable[_]] 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 lazy val UnliftableTpe = typeOf[me.rexim.morganey.meta.Unliftable[_]] private def unliftableT(tpe: Type): Type = appliedType(UnliftableTpe, tpe) private case class Lifted(exp: Tree, preamble: List[Tree] = Nil) { + def define(decl: Tree): Lifted = + Lifted(exp, preamble :+ decl) + def wrap(f: Tree => Tree): Lifted = Lifted(f(exp), preamble) @@ -89,7 +96,10 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { case LambdaVar(DottedHole(_)) => c.abort(c.enclosingPosition, "Illegal usage of ..!") case LambdaVar(Hole(hole)) => - replaceHole(hole, dotted = false) + replaceHole(hole, dotted = false) match { + case Left(x) => x + case Right((x, _)) => x + } case LambdaVar(name) => Lifted(q"_root_.me.rexim.morganey.ast.LambdaVar($name)") case LambdaFunc(param, body) => @@ -105,8 +115,6 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private def liftComplexTerm(term: LambdaTerm): Lifted = term match { case unliftList(terms) => - val unapply = TermName("unapply") == method - def isDottedHole(term: LambdaTerm): Boolean = term match { case LambdaVar(DottedHole(_)) => true case _ => false @@ -116,8 +124,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { terms.foldRight(tree) { case (ele, acc) => liftComplexTerm(ele).wrap2(acc) { - case (a, b) if unapply => pq"$a +: $b" - case (a, b) => q"$a +: $b" + case (a, b) if isUnapply => pq"$a +: $b" + case (a, b) => q"$a +: $b" } } @@ -125,25 +133,55 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { terms.foldLeft(tree) { case (acc, ele) => acc.wrap2(liftComplexTerm(ele)) { - case (a, b) if unapply => pq"$a :+ $b" - case (a, b) => q"$a :+ $b" + case (a, b) if isUnapply => pq"$a :+ $b" + case (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) - if (unapply) app.wrap(x => pq"$unliftList_($x)") - else app.wrap(x => q"$liftList_($x)") + + replaceHole(hole, dotted = true) match { + // apply + case Left(holeContent) => + val app = append(holeContent, rest) + app.wrap(x => q"$liftList_($x)") + + // unapply + case Right((holeContent, tpeCtor)) => + val app = append(holeContent, rest) + val exactly = TermName(c.freshName("exactly")) + val LTermsTpe = appliedType(tpeCtor, LambdaTermTpe) + val preamble = + q""" + val $exactly: _root_.me.rexim.morganey.meta.Unliftable[$LTermsTpe] = + ${unliftColl_(LTermsTpe)} + """ + app.wrap(x => pq"$exactly($x)").define(preamble) + } // [_*, ..$xs] case (init, LambdaVar(DottedHole(hole)) :: Nil) => - val holeContent = replaceHole(hole, dotted = true) - val prep = prepend(init, holeContent) - if (unapply) prep.wrap(x => pq"$unliftList_($x)") - else prep.wrap(x => q"$liftList_($x)") + + replaceHole(hole, dotted = true) match { + // apply + case Left(holeContent) => + val prep = prepend(init, holeContent) + prep.wrap(x => q"$liftList_($x)") + + // unapply + case Right((holeContent, tpeCtor)) => + val prep = prepend(init, holeContent) + val exactly = TermName(c.freshName("exactly")) + val LTermsTpe = appliedType(tpeCtor, LambdaTermTpe) + val preamble = + q""" + val $exactly: _root_.me.rexim.morganey.meta.Unliftable[$LTermsTpe] = + ${unliftColl_(LTermsTpe)} + """ + prep.wrap(x => pq"$exactly($x)").define(preamble) + } // no dotted list case (_, Nil) => @@ -157,79 +195,83 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { liftPrimitiveTerm(simple) } - 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 + private def replaceHole(hole: Int, dotted: Boolean): Either[Lifted, (Lifted, Type /* of collection */)] = + if (isApply) Left(replaceHoleWithExp(hole, dotted)) + else Right(replaceHoleWithPattern(hole, dotted)) + + private def replaceHoleWithExp(hole: Int, dotted: Boolean): Lifted = { + 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 liftTpe = c.inferImplicitValue(liftableT(tpe), silent = true) + if (liftTpe.nonEmpty) { + q"$liftTpe($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 liftTpe = c.inferImplicitValue(liftableT(tpe), silent = true) - if (liftTpe.nonEmpty) { - q"$liftTpe($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) - } + 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) } - - 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") => - val x = TermName(s"x$hole") - val subpattern = c.internal.subpatterns(args.head).get.apply(hole) - - def unQuote(tpe: Type): Lifted = { - val unliftTpe = c.inferImplicitValue(unliftableT(tpe), silent = true) - - if (unliftTpe.nonEmpty) { - if (!dotted) { - Lifted(pq"$unliftTpe($x @ _)") - } else { - val each = TermName(c.freshName(s"each$hole")) - val preamble = - q""" - val $each: _root_.me.rexim.morganey.meta.UnapplyEach[$tpe] = - new _root_.me.rexim.morganey.meta.UnapplyEach[$tpe]($unliftTpe) - """ - Lifted(pq"$each($x @ _)", List(preamble)) - } + if (!dotted) { + Lifted(quote(arg)) + } else { + val parName = TermName(c.freshName()) + val list = q"$arg.map { ($parName: $tpe) => ${quote(q"$parName")} }.toList" + Lifted(list) + } + } + + private def replaceHoleWithPattern(hole: Int, dotted: Boolean): (Lifted, Type) = { + val x = TermName(s"x$hole") + val subpattern = c.internal.subpatterns(args.head).get.apply(hole) + + def unQuote(tpe: Type, tpeCtor: Type): (Lifted, Type) = { + val unliftTpe = c.inferImplicitValue(unliftableT(tpe), silent = true) + + if (unliftTpe.nonEmpty) { + if (!dotted) { + (Lifted(pq"$unliftTpe($x @ _)"), tpeCtor) } 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) + val each = TermName(c.freshName(s"each$hole")) + val preamble = + q""" + val $each: _root_.me.rexim.morganey.meta.UnapplyEach[$tpe, $tpeCtor] = + new _root_.me.rexim.morganey.meta.UnapplyEach[$tpe, $tpeCtor]($unliftTpe) + """ + (Lifted(pq"$each($x @ _)", List(preamble)), tpeCtor) } + } 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) } + } - subpattern match { - case pq"$_: $tpt" => - val typedTpt = c.typecheck(tpt, c.TYPEmode) - val tpe = - if (!dotted) typedTpt.tpe - else iterableT(typedTpt.tpe) - unQuote(tpe) - case _ => - unQuote(LambdaTermTpe) - } + subpattern match { + case pq"$_: $tpt" => + val typedTpt = c.typecheck(tpt, c.TYPEmode) + val tpe = + if (!dotted) typedTpt.tpe + else iterableT(typedTpt.tpe) + unQuote(tpe, typedTpt.tpe.typeConstructor) + case _ => + unQuote(LambdaTermTpe, typeOf[List[_]].typeConstructor) + } } private def wrapAst(term: LambdaTerm): Tree = method match { diff --git a/macros/src/main/scala/me/rexim/morganey/meta/Unliftable.scala b/macros/src/main/scala/me/rexim/morganey/meta/Unliftable.scala index dc5c966..0769fdf 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/Unliftable.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/Unliftable.scala @@ -17,8 +17,7 @@ trait DefaultUnliftableInstances { implicit val unliftInt = Unliftable[Int](decodeNumber) implicit val unliftChar = Unliftable[Char](decodeChar) - implicit val unliftString = Unliftable[String] { s => - val unlift = implicitly[Unliftable[Seq[Char]]] + implicit def unliftString(implicit unlift: Unliftable[Seq[Char]]) = Unliftable[String] { s => unlift.unapply(s).map(_.mkString) } @@ -31,8 +30,8 @@ trait DefaultUnliftableInstances { } yield (a, b) } - implicit def unliftColl[X, CC[X] <: Traversable[X], A] - (implicit unlift: Unliftable[A], cbf: CanBuildFrom[List[A], A, CC[A]]): Unliftable[CC[A]] = + implicit def unliftColl[CC[X] <: TraversableOnce[X], A] + (implicit unlift: Unliftable[A], cbf: CanBuildFrom[Nothing, A, CC[A]]): Unliftable[CC[A]] = Unliftable[CC[A]] { t => decodeList(t).flatMap { xs => val decodeResults = xs.map(unlift.unapply) From ebc3942e8059e71c32ff148c750c7261ac26eb4d Mon Sep 17 00:00:00 2001 From: keddelzz Date: Tue, 18 Apr 2017 23:14:54 +0200 Subject: [PATCH 11/11] Don't slay the compiler (#345) I created too huge patterns to be matched against. Those crashed the compiler. This change optimizes the pattern (and the runtime const) of huge constants (numbers and characters) and other terms, whose representation is very large (int-lists and char-lists/strings). --- .../rexim/morganey/meta/QuotationMacro.scala | 37 ++++++++++++++++++- .../me/rexim/morganey/meta/UnapplyConst.scala | 15 ++++++++ .../me/rexim/morganey/meta/UnapplyEach.scala | 3 ++ .../morganey/meta/InterpolatorSpec.scala | 6 +++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 macros/src/main/scala/me/rexim/morganey/meta/UnapplyConst.scala diff --git a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala index 0926f4f..831fb66 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/QuotationMacro.scala @@ -1,10 +1,17 @@ package me.rexim.morganey.meta import me.rexim.morganey.ast._ +import me.rexim.morganey.meta.QuotationMacro._ import StringContext.treatEscapes import scala.reflect.macros.whitebox +private[meta] object QuotationMacro { + + val bigNumberThreshold = 10 + +} + private[meta] class QuotationMacro(val c: whitebox.Context) { import c.universe._ @@ -35,6 +42,9 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private val LambdaTermTpe = typeOf[LambdaTerm] + private lazy val morganeyNumber = implicitly[Unlift[Int]] + private lazy val morganeyIntList = implicitly[Unlift[List[Int]]] + private lazy val liftList = implicitly[Lift[List[LambdaTerm]]] private lazy val liftList_ = c.inferImplicitValue(typeOf[Lift[List[LambdaTerm]]], silent = true) private lazy val unliftList = implicitly[Unlift[List[LambdaTerm]]] @@ -50,8 +60,8 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { private def unliftableT(tpe: Type): Type = appliedType(UnliftableTpe, tpe) private case class Lifted(exp: Tree, preamble: List[Tree] = Nil) { - def define(decl: Tree): Lifted = - Lifted(exp, preamble :+ decl) + def define(decls: Tree*): Lifted = + Lifted(exp, preamble ++ decls.toList) def wrap(f: Tree => Tree): Lifted = Lifted(f(exp), preamble) @@ -95,17 +105,40 @@ private[meta] class QuotationMacro(val c: whitebox.Context) { 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) match { case Left(x) => x case Right((x, _)) => x } + case LambdaVar(name) => Lifted(q"_root_.me.rexim.morganey.ast.LambdaVar($name)") + + // generate special code for detecting int lists (also strings) + case morganeyIntList(xs) if isUnapply => + val const = TermName(c.freshName("const")) + val preamble = + q""" + val $const = + new _root_.me.rexim.morganey.meta.UnapplyConst[_root_.scala.collection.immutable.List[Int]]($xs) + """ + Lifted(pq"$const()").define(preamble) + + // generate special code for detecting big constant numbers and characters + case morganeyNumber(n) if isUnapply && n > bigNumberThreshold => + val const = TermName(c.freshName("const")) + val preamble = + q""" + val $const = new _root_.me.rexim.morganey.meta.UnapplyConst[Int]($n) + """ + Lifted(pq"$const()").define(preamble) + 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)" diff --git a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyConst.scala b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyConst.scala new file mode 100644 index 0000000..9e30566 --- /dev/null +++ b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyConst.scala @@ -0,0 +1,15 @@ +package me.rexim.morganey.meta + +import me.rexim.morganey.ast.LambdaTerm + +/** + * Helper class to match a constant in the quotation at runtime + */ +class UnapplyConst[T](n: T)(implicit morganeyValue: Unliftable[T]) { + + def unapply(term: LambdaTerm): Boolean = term match { + case morganeyValue(m) => n == m + case _ => false + } + +} diff --git a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala index b7f2239..cdad317 100644 --- a/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala +++ b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala @@ -6,6 +6,9 @@ import me.rexim.morganey.monad._ import scala.collection.generic.CanBuildFrom import scala.language.higherKinds +/** + * Helper class to convert each LambdaTerm to T in a collection + */ class UnapplyEach[T, CC[+X] <: TraversableOnce[X]](unliftT: Unliftable[T]) (implicit cbf: CanBuildFrom[CC[LambdaTerm], T, CC[T]], cbfo: CanBuildFrom[CC[LambdaTerm], Option[T], CC[Option[T]]]) { diff --git a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala index ea89439..b5de56d 100644 --- a/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala +++ b/macros/src/test/scala/me/rexim/morganey/meta/InterpolatorSpec.scala @@ -49,6 +49,12 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { m"$numbers" should be (pair(zero, pair(one, pair(two, zero, "z"), "z"), "z")) } + "Matching against big strings" should "be possible with the quotation mechanism" in { + val string = "The quick brown fox jumps over the lazy dog" + // `string` is represented by 4057 applications in morganey + val m""" "The quick brown fox jumps over the lazy dog" """ = m"$string" + } + "Lambda terms" should "be converted back to Scala values during unlifting" in { val m"${nZero: Int}" = zero nZero should be (0)