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])) { 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/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/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/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..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,16 +1,24 @@ package me.rexim.morganey.meta import me.rexim.morganey.ast._ +import me.rexim.morganey.meta.QuotationMacro._ -import scala.reflect.macros.whitebox.Context import StringContext.treatEscapes +import scala.reflect.macros.whitebox -private[meta] class QuotationMacro(val c: Context) { +private[meta] object QuotationMacro { + + val bigNumberThreshold = 10 + +} + +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 +30,54 @@ private[meta] class QuotationMacro(val c: Context) { private val (parts, method, args) = macroApplication() - private def buildProgram(): String = { + 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 = + IterableClass.typeParams.head.asType.toType + private def iterableT(tpe: Type): Type = + IterableTParam.asSeenFrom(tpe, IterableClass) + + 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]]] + 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[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[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(decls: Tree*): Lifted = + Lifted(exp, preamble ++ decls.toList) + + 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 +92,269 @@ 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) 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)" + } } - 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 } - } + + def prepend(terms: List[LambdaTerm], tree: Lifted): Lifted = + terms.foldRight(tree) { + case (ele, acc) => + liftComplexTerm(ele).wrap2(acc) { + case (a, b) if isUnapply => pq"$a +: $b" + case (a, b) => q"$a +: $b" + } + } + + def append(tree: Lifted, terms: List[LambdaTerm]): Lifted = + terms.foldLeft(tree) { + case (acc, ele) => + acc.wrap2(liftComplexTerm(ele)) { + 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) => + + 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) => + + 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) => + liftPrimitiveTerm(term) + + case _ => + c.abort(c.enclosingPosition, "Illegal usage of ..!") + } + + case simple => + liftPrimitiveTerm(simple) } - 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 @ _)" + 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 { - 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 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) } - case _ => - pq"$x @ _" + } + + 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 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)" - } + 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) - private def transform(term: LambdaTerm): Tree = method match { - case TermName("apply") => liftAst(term) - case TermName("unapply") => extractor(term) - } + def unQuote(tpe: Type, tpeCtor: Type): (Lifted, Type) = { + val unliftTpe = c.inferImplicitValue(unliftableT(tpe), silent = true) - 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") + if (unliftTpe.nonEmpty) { + if (!dotted) { + (Lifted(pq"$unliftTpe($x @ _)"), tpeCtor) + } else { + 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) } - (q"_root_.scala.Some((..$ys))", q"_root_.scala.None") + } 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) + } } - /* - * 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) - """ + 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 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} + """ - def quote(args: Tree*): Tree = expand() - def unquote(arg: Tree): Tree = expand() + /** + * - 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 + 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") + } + + 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) + """ + } } 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 new file mode 100644 index 0000000..cdad317 --- /dev/null +++ b/macros/src/main/scala/me/rexim/morganey/meta/UnapplyEach.scala @@ -0,0 +1,21 @@ +package me.rexim.morganey.meta + +import me.rexim.morganey.ast.LambdaTerm +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]]]) { + + def unapply(terms: CC[LambdaTerm]): Option[CC[T]] = { + val unliftedElements = (cbfo() ++= (terms map unliftT.unapply _)).result() + sequence[CC, T](unliftedElements)(cbf) + } + +} 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) 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" + } + } + } 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..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) @@ -59,7 +65,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 +106,160 @@ class InterpolatorSpec extends FlatSpec with Matchers with TestTerms { rest should be (lfunc("d", one)) } + "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]" + 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]`) + } + + // 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 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)) + } + { + val m"[0, ..$unspliced]" = `[0 .. 2]` + unspliced should be(List(one, two)) + } + { + val m"[..$unspliced, 2]" = `[0 .. 2]` + 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)) + } + } + }