From b7d6b15de8a50e740d7121e16b77ab3e74c27320 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 27 Apr 2017 15:31:05 -0700 Subject: Initial commit --- .gitignore | 1 + build.sbt | 14 +++++ macros/src/main/scala/macro.scala | 117 ++++++++++++++++++++++++++++++++++++++ project/build.properties | 1 + src/main/scala/Main.scala | 58 +++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 .gitignore create mode 100644 build.sbt create mode 100644 macros/src/main/scala/macro.scala create mode 100644 project/build.properties create mode 100644 src/main/scala/Main.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..9065dda --- /dev/null +++ b/build.sbt @@ -0,0 +1,14 @@ +name := "protomacro" + +scalaVersion in ThisBuild := "2.12.2" + +scalacOptions in ThisBuild ++= Seq("-feature", "-deprecation") + +lazy val root = (project in file(".")) + .dependsOn(macros) + .settings(scalacOptions ++= Seq("-Xlog-implicits")) + +lazy val macros = (project in file("macros")) + .settings( + libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value + ) diff --git a/macros/src/main/scala/macro.scala b/macros/src/main/scala/macro.scala new file mode 100644 index 0000000..87e0000 --- /dev/null +++ b/macros/src/main/scala/macro.scala @@ -0,0 +1,117 @@ +package macros + +//import scala.reflect.macros.whitebox.Context +import scala.reflect.macros.blackbox.Context + +import scala.language.experimental.macros +import scala.language.higherKinds + +trait ProductWriters[Writer[_]] { + implicit def productWriter[P]: Writer[P] = macro ProductMapperBundle.productWriterImpl[Writer[_], P] +} + +class ProductMapperBundle(val c: Context) { + import c.universe._ + + /** Summon an implicit value and return the tree representing its + * invocation, or fail if no implicit is available. */ + private def implicitlyOrFail(tpe: Type, message: String): Tree = { + c.typecheck( + q"""{ + import ${c.prefix}._ + implicitly[$tpe] + }""", + silent = true + ) match { + case EmptyTree => c.abort(c.enclosingPosition, message) + case tree => tree + } + } + + private case class TypeClass(baseType: Type, method: MethodSymbol) + + private def writerTypeClass[T: c.WeakTypeTag]: TypeClass = { + val baseType = weakTypeOf[T] + val typeParam: Symbol = baseType.typeSymbol.asType.typeParams.head + + // extract methods that take one type parameter, matching the type classes type parameter + val methods = baseType.decls.collect{ + case m: MethodSymbol if m.isAbstract => m + }.filter{ m => + m.paramLists match { + case (param :: Nil) :: Nil => + param.info.typeSymbol == typeParam + case _ => false + } + }.toList + + methods match { + case head :: Nil => TypeClass(baseType, head) + case list => + val message = list.map(_.name.toString).map(n => "<" + n + ">") match { + case Nil => "" + case list => list.mkString(", ") + } + c.abort( + c.prefix.tree.pos, + s"${weakTypeOf[T]} must have exaclty one abstract method with conforming signature. It currently has the following abstract methods: $message." + ) + } + } + + /** Create a new writer. + * @param tc the base writer type class + * @param genericType the elemnt type of the new writer to create + * @param body a function that generates the writer's body from a given parameter name + */ + private def newWriter(tc: TypeClass, genericType: Type, body: TermName => Tree): Block = { + val parent = appliedType(tc.baseType.typeConstructor, genericType) + + val paramName = TermName("value") + val param = ValDef(Modifiers(Flag.PARAM), paramName, TypeTree(genericType), EmptyTree) + val defn = DefDef(Modifiers(), tc.method.name, Nil, List(List(param)), TypeTree(tc.method.returnType), body(paramName)) + + val tree = Block( + List( + q"final class $$anon extends $parent { $defn }" + ), + Apply(Select(New(Ident(TypeName("$anon"))), termNames.CONSTRUCTOR), List()) + ) + tree + } + + def productWriterImpl[W: c.WeakTypeTag, P: c.WeakTypeTag]: c.Tree = { + val product = weakTypeOf[P] + val writer: TypeClass = writerTypeClass[W] + + if (!(product <:< weakTypeOf[Product])) { + c.abort(c.enclosingPosition, s"Cannot generate product writer for non-product type $product") + } + + val mapType: Type = appliedType(typeOf[Map[_, _]], typeOf[String], writer.method.returnType) + + val mapWriter = implicitlyOrFail(appliedType(writer.baseType, mapType), + s"No implicit decomposer available for ${appliedType(writer.baseType, product)}. Make sure an implicit $mapType is in scope.") + + val fields: List[(TermName, Type)] = product.decls.collect{ + case m: MethodSymbol if m.isCaseAccessor => + m.name -> m.info.resultType + }.toList + val fieldWriters: List[(TermName, Tree)] = fields.map{ case (name, tpe) => + val writerType = appliedType(writer.baseType, tpe) + name -> implicitlyOrFail(writerType, s"Cannot create writer for $product: no implicit writer available for $product.$name of type $tpe") + } + + def mapBody(param: TermName): List[Tree] = fieldWriters.map{ case (name, fieldWriter) => + q"""(${name.toString}, $fieldWriter.${writer.method.name}($param.$name))""" + } + + def data(param: TermName): Tree = q"""{ + val data = scala.collection.immutable.Map(..${mapBody(param)}); $mapWriter.${writer.method.name}(data) + }""" + + val tree = newWriter(writer, product, param => data(param)) + //println(tree) + tree + } +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..5f32afe --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.13 \ No newline at end of file diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala new file mode 100644 index 0000000..c19d3ec --- /dev/null +++ b/src/main/scala/Main.scala @@ -0,0 +1,58 @@ +package macros + +import scala.collection.immutable.Map + +object Test extends App { + + sealed trait JsValue + case class JsObject(fields: Map[String, JsValue]) extends JsValue + case class JsString(str: String) extends JsValue + case class JsNumber(num: Int) extends JsValue + + trait MyWriter[A] { + def write(value: A): JsValue + def other(a: A): Int = 2 + } + trait BaseWriters { + implicit val idWriter = new MyWriter[JsValue] { + def write(value: JsValue) = value + } + implicit val intWriter = new MyWriter[Int] { + def write(value: Int) = JsNumber(value) + } + implicit val stringWriter = new MyWriter[String] { + def write(value: String) = JsString(value) + } + } + trait CompositeWriters { + implicit def mapWriter[A: MyWriter] = new MyWriter[Map[String, A]] { + def write(mp: Map[String, A]) = { + def writeA(v: A) = implicitly[MyWriter[A]].write(v) + JsObject(mp.map { case (k, v) => k -> writeA(v) }) + } + } + } + + object Util { + def save[A: MyWriter](a: A): Unit = { + val x = implicitly[MyWriter[A]].write(a) + println("saving " + x) + } + } + + case class Foo(x: Int) + case class Composite(x: Int, y: String, other: Foo) + + object Protocol + extends BaseWriters + with CompositeWriters + with ProductWriters[MyWriter] + import Protocol._ + + class Bar + Util.save(2) + //Util.save(new Bar) + Util.save(Composite(2, "some string", Foo(3))) + //Util.save((2, "some string", "1") + +} -- cgit v1.2.3