Permalink
Browse files

[scalac-plugin] Add MatchPlugin (rich pattern matching)

  • Loading branch information...
cchantep
cchantep committed Feb 21, 2014
1 parent 4b64595 commit 88aaedde8f2d6a90bcc3e012810701f72eac3781
View
@@ -3,10 +3,11 @@ import Keys._
// Multi-module project build
object Acolyte extends Build
with Core with ScalaMacros with Scala with Studio {
with Core with ScalaCompiler with Scala with Studio {
lazy val root = Project(id = "acolyte", base = file(".")).
aggregate(core, scalaMacros, scala, studio).settings(
aggregate(core, scalaCompiler, scala, studio).
settings(
version := "1.0.14",
scalaVersion := "2.10.3",
publishTo := Some(Resolver.file("file",
@@ -0,0 +1,81 @@
import sbt._
import Keys._
trait ScalaCompiler {
lazy val scalaCompiler =
Project(id = "scala-compiler", base = file("scala-compiler")).
aggregate(plugin, specs).settings(
version in ThisBuild := "1.0.14")
private lazy val plugin = Project(
id = "scala-compiler-plugin",
base = file("scala-compiler/plugin")).settings(
name := "scala-compiler-plugin",
organization := "org.eu.acolyte",
scalaVersion := "2.10.3",
javaOptions ++= Seq("-source", "1.6", "-target", "1.6"),
javacOptions in Test ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"),
scalacOptions ++= Seq("-feature", "-deprecation"),
resolvers += "Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.10.3",
publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository"))),
pomExtra := (
<url>https://github.com/cchantep/acolyte/</url>
<licenses>
<license>
<name>GNU Lesser General Public License, Version 2.1</name>
<url>
https://raw.github.com/cchantep/acolyte/master/LICENSE.txt
</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:git@github.com:cchantep/acolyte.git</connection>
<developerConnection>
scm:git:git@github.com:cchantep/acolyte.git
</developerConnection>
<url>git@github.com:cchantep/acolyte.git</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/cchantep/acolyte/issues</url>
</issueManagement>
<ciManagement>
<system>Travis CI</system>
<url>https://travis-ci.org/cchantep/acolyte</url>
</ciManagement>
<developers>
<developer>
<id>cchantep</id>
<name>Cedric Chantepie</name>
</developer>
</developers>))
private lazy val specs = Project(
id = "scala-compiler-specs",
base = file("scala-compiler/specs")).
settings(
name := "scala-compiler-specs",
organization := "org.eu.acolyte",
scalaVersion := "2.10.3",
javaOptions ++= Seq("-source", "1.6", "-target", "1.6"),
javacOptions in Test ++= Seq("-Xlint:unchecked", "-Xlint:deprecation"),
compile in Test <<= (compile in Test).dependsOn(clean in Test).dependsOn(
packageBin in (plugin, Compile)/* make sure plugin.jar is available */),
scalacOptions <++= (version in ThisBuild).
zip(baseDirectory in (plugin, Compile)).
zip(name in (plugin, Compile)) map { d =>
val ((v, b), n) = d
val j = b / "target" / "scala-2.10" / "%s_2.10-%s.jar".format(n, v)
Seq("-feature", "-deprecation",
"-Xplugin:%s".format(j.getAbsolutePath))
},
resolvers += "Typesafe Snapshots" at "http://repo.typesafe.com/typesafe/snapshots/",
autoCompilerPlugins := true,
addCompilerPlugin("org.eu.acolyte" %% "scala-compiler-plugin" % "1.0.14"),
libraryDependencies ++= Seq("org.specs2" %% "specs2" % "2.3.2"),
publishTo := None).dependsOn(plugin)
}
@@ -0,0 +1,4 @@
<plugin>
<name>rich-match</name>
<classname>acolyte.MatchPlugin</classname>
</plugin>
@@ -0,0 +1,155 @@
package acolyte
import scala.tools.nsc.Global
import scala.tools.nsc.plugins.{ Plugin, PluginComponent }
import scala.tools.nsc.transform.Transform
class MatchPlugin(val global: Global) extends Plugin {
val name = "rich-match"
val description = "Rich or refactored pattern matching"
val components = List[PluginComponent](MatchComponent)
private object MatchComponent extends PluginComponent with Transform {
val global: MatchPlugin.this.global.type = MatchPlugin.this.global
override val runsRightAfter = Some("parser")
override val runsAfter = runsRightAfter.toList
override val runsBefore = List[String]("typer")
val phaseName = "rich-patmat"
def newTransformer(unit: global.CompilationUnit) = MatchTransformer
object MatchTransformer extends global.Transformer {
import scala.collection.mutable.ListBuffer
import global.{
abort,
reporter,
Block,
CaseDef,
Match,
Position,
Tree,
ValDef
}
override def transform(tree: Tree): Tree = tree match {
case m @ Match(_, _) refactorMatch(m)
case _ super.transform(tree)
}
val tildeTerm = global.newTermName("$tilde")
@inline private def refactorMatch(orig: Match): Tree =
orig match {
case Match(t, cs) {
import global.{
Apply,
Bind,
Block,
Constant,
Ident,
Literal
}
val vds = ListBuffer[ValDef]()
val cds = cs.map {
case ocd @ CaseDef(Apply(Ident(it), xt), g, by) if (
it == tildeTerm)
(xt.headOption, xt.tail) match {
case (Some(xt @ Apply(ex, xa)), Apply(_, ua) :: Nil)
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
ua, g, by)
vds += vd
cd
case (Some(xt @ Apply(ex, xa)), Bind(uf, ua) :: Nil)
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
List(Bind(uf, ua)), g, by)
vds += vd
cd
case (Some(xt @ Apply(ex, xa)), Ident(i) :: Nil)
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
List(Ident(i)), g, by)
vds += vd
cd
/*
case (Some(xt @ Apply(ex, xa)),
Literal(Constant(())) :: Nil) ⇒
val (vd, cd) = caseDef(vds.size, ocd.pos, xt.pos, ex, xa,
List(Literal(Constant(()))), g, by)
vds += vd
cd
*/
case _
reporter.error(ocd.pos, "Invalid ~ pattern")
abort("Invalid ~ pattern")
}
case cd cd
}
if (vds.isEmpty) orig // revert to original Match
else Block(vds.toList, Match(t, cds).setPos(orig.pos))
}
case _
reporter.error(orig.pos, "Invalid Match")
abort("Invalid Match")
}
@inline private def caseDef[T](i: Int, cp: Position, xp: Position, ex: Tree, xa: List[Tree], ua: List[Tree], g: Tree, b: Tree): (ValDef, CaseDef) = {
import scala.reflect.io.VirtualFile
import scala.reflect.internal.util.BatchSourceFile
import global.{
atPos,
show,
Apply,
Ident,
Literal,
Modifiers,
TypeTree
}
val of = xp.source.file
val file = new VirtualFile(of.name, of.path + "#refactored-match-" + i)
// ValDef
val xn = global.treeBuilder.freshTermName("Xtr")
val vd = ValDef(Modifiers(), xn, TypeTree(), Apply(ex, xa))
val vdc =
s"${show(vd)} // generated from ln ${xp.line}, col ${xp.column - 1}"
val vdp = xp.withPoint(0).withSource(new BatchSourceFile(file, vdc), 0)
/*
[error] /path/to/file.scala#refactored-match-M:1: Compilation error.
[error] Error details.
[error] val Xtr1 = B() // generated from ln L, col C
*/
// CaseDef
/*
val pat = ua match {
case Literal(Constant(())) :: Nil ⇒ Apply(Ident(xn), Nil)
case _ ⇒ Apply(Ident(xn), ua)
}
*/
val pat = Apply(Ident(xn), ua)
val cd = CaseDef(pat, g, b)
val cdc =
s"${show(cd)} // generated from ln ${cp.line}, col ${cp.column - 5}"
val cdp = cp.withPoint(0).withSource(new BatchSourceFile(file, cdc), 0)
/*
[error] /path/to/file.scala#refactored-match-M:1: value Xtr0 is not a case class constructor, nor does it have an unapply/unapplySeq method
[error] case Xtr1((a @ _)) => Nil // generated from ln L, col C
*/
(atPos(vdp)(vd), atPos(cdp)(cd))
}
}
}
}
@@ -0,0 +1,23 @@
package acolyte
object MatchPluginSpec extends org.specs2.mutable.Specification {
"Match plugin" title
case class A(e: String) {
lazy val re = e.r
def unapplySeq(target: Any): Option[List[String]] = re.unapplySeq(target)
}
case class B(e: String)
"X" should {
"Y" in {
"x" match {
case ~(B(".*"), a) => Nil
case _ => Nil
}
true must beTrue
}
}
}

0 comments on commit 88aaedd

Please sign in to comment.