summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2017-10-26 08:53:49 -0700
committerLi Haoyi <haoyi.sg@gmail.com>2017-10-26 09:08:58 -0700
commit845a11f8ec04a08dfaba94c0aff2d41c02c2f736 (patch)
treef548c500085d581f7d3757b55031d7b45ea9250e
parentc8cafe5b835f4bfbcb83f1e04553406e32ac0b05 (diff)
downloadmill-845a11f8ec04a08dfaba94c0aff2d41c02c2f736.tar.gz
mill-845a11f8ec04a08dfaba94c0aff2d41c02c2f736.tar.bz2
mill-845a11f8ec04a08dfaba94c0aff2d41c02c2f736.zip
Serialization using play-json now mostly in place
"pure" targets which depend only on their inputs are modeled but their evaluation semantics aren't fleshed out
-rw-r--r--build.sbt3
-rw-r--r--src/main/scala/forge/DefCtx.scala8
-rw-r--r--src/main/scala/forge/Evaluator.scala37
-rw-r--r--src/main/scala/forge/Target.scala53
-rw-r--r--src/main/scala/forge/package.scala46
-rw-r--r--src/test/scala/forge/ForgeTests.scala28
6 files changed, 129 insertions, 46 deletions
diff --git a/build.sbt b/build.sbt
index 30b81658..72289a15 100644
--- a/build.sbt
+++ b/build.sbt
@@ -15,7 +15,8 @@ libraryDependencies ++= Seq(
"org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided",
"com.lihaoyi" %% "sourcecode" % "0.1.4",
"com.lihaoyi" %% "pprint" % "0.5.3",
- "com.lihaoyi" %% "ammonite-ops" % "1.0.2"
+ "com.lihaoyi" %% "ammonite-ops" % "1.0.2",
+ "com.typesafe.play" %% "play-json" % "2.6.6"
)
sourceGenerators in Compile += Def.task {
diff --git a/src/main/scala/forge/DefCtx.scala b/src/main/scala/forge/DefCtx.scala
index 97f9bf3f..3439f755 100644
--- a/src/main/scala/forge/DefCtx.scala
+++ b/src/main/scala/forge/DefCtx.scala
@@ -6,7 +6,9 @@ import scala.language.experimental.macros
import scala.reflect.macros.blackbox._
-final case class DefCtx(label: String)
+final case class DefCtx(baseLabel: String, anonId: Option[Int]){
+ def label = baseLabel + anonId.getOrElse("")
+}
object DefCtx{
@compileTimeOnly("A DefCtx can only be provided directly within a T{} macro")
implicit def dummy: DefCtx with Int = ???
@@ -22,7 +24,7 @@ object T{
override def transform(tree: c.Tree): c.Tree = {
if (tree.toString.startsWith("forge.") && tree.toString.endsWith(".DefCtx.dummy")) {
count += 1
- c.typecheck(q"forge.DefCtx(sourcecode.Enclosing() + $count)")
+ c.typecheck(q"forge.DefCtx(sourcecode.Enclosing(), Some($count))")
}else tree match{
case Apply(fun, args) =>
val extendedParams = fun.tpe.paramLists.head.padTo(
@@ -61,7 +63,7 @@ object T{
val newArgs = for(x <- args) yield {
if (x.toString.startsWith("forge.") && x.toString.endsWith(".DefCtx.dummy")) {
isTransformed = true
- c.typecheck(q"forge.DefCtx(sourcecode.Enclosing())")
+ c.typecheck(q"forge.DefCtx(sourcecode.Enclosing(), None)")
}else transformer.transform(x)
}
diff --git a/src/main/scala/forge/Evaluator.scala b/src/main/scala/forge/Evaluator.scala
index 43b4f353..1bff722b 100644
--- a/src/main/scala/forge/Evaluator.scala
+++ b/src/main/scala/forge/Evaluator.scala
@@ -2,6 +2,7 @@ package forge
import java.nio.{file => jnio}
+import play.api.libs.json.Json
import sourcecode.Enclosing
import scala.collection.mutable
@@ -9,8 +10,7 @@ import scala.collection.mutable
class Evaluator(workspacePath: jnio.Path,
enclosingBase: DefCtx){
- val resultCache = mutable.Map.empty[String, (Int, Any)]
-
+ val resultCache = mutable.Map.empty[String, (Int, String)]
def evaluate(targets: Seq[Target[_]]): Evaluator.Results = {
jnio.Files.createDirectories(workspacePath)
@@ -20,26 +20,29 @@ class Evaluator(workspacePath: jnio.Path,
for (target <- sortedTargets){
val inputResults = target.inputs.map(results).toIndexedSeq
- val targetDestPath = {
- val enclosingStr = target.defCtx.label
- val targetDestPath = workspacePath.resolve(
- jnio.Paths.get(enclosingStr.stripSuffix(enclosingBase.label))
- )
- deleteRec(targetDestPath)
- targetDestPath
-
- }
+ val enclosingStr = target.defCtx.label
+ val targetDestPath = workspacePath.resolve(
+ jnio.Paths.get(enclosingStr.stripSuffix(enclosingBase.label))
+ )
+ deleteRec(targetDestPath)
val inputsHash = inputResults.hashCode
- resultCache.get(target.defCtx.label) match{
- case Some((hash, res)) if hash == inputsHash && !target.dirty =>
- results(target) = res
+ (target.dirty, resultCache.get(target.defCtx.label)) match{
+ case (Some(dirtyCheck), Some((hash, res)))
+ if hash == inputsHash && !dirtyCheck() =>
+ results(target) = target.formatter.reads(Json.parse(res)).get
+
case _ =>
evaluated.append(target)
- val res = target.evaluate(new Args(inputResults, targetDestPath))
+ if (target.defCtx.anonId.isDefined && target.dirty.isEmpty) {
+ val res = target.evaluate(new Args(inputResults, targetDestPath))
+ results(target) = res
+ }else{
+ val (res, serialized) = target.evaluateAndWrite(new Args(inputResults, targetDestPath))
+ resultCache(target.defCtx.label) = (inputsHash, serialized)
+ results(target) = res
+ }
- resultCache(target.defCtx.label) = (inputsHash, res)
- results(target) = res
}
}
diff --git a/src/main/scala/forge/Target.scala b/src/main/scala/forge/Target.scala
index d8536c60..93fa022e 100644
--- a/src/main/scala/forge/Target.scala
+++ b/src/main/scala/forge/Target.scala
@@ -2,7 +2,8 @@ package forge
import java.nio.{file => jnio}
-trait Target[T] extends Target.Ops[T]{
+import play.api.libs.json.{Format, Json}
+abstract class Target[T](implicit formatter: Format[T]) extends Target.Ops[T]{
/**
* Where in the Scala codebase was this target defined?
*/
@@ -19,62 +20,78 @@ trait Target[T] extends Target.Ops[T]{
/**
* Even if this target's inputs did not change, does it need to re-evaluate
- * anyway? e.g. if the files this target represents on disk changed
+ * anyway?
+ *
+ * - None means it never needs to re-evaluate unless its inputs do
+ * - Some(f) contains a function that returns whether or not it should re-evaluate,
+ * e.g. if the files this target represents on disk changed
*/
- def dirty: Boolean = false
+ val dirty: Option[() => Boolean] = Some(() => false)
}
object Target{
- trait Ops[T]{ this: Target[T] =>
+ abstract class Ops[T](implicit val formatter: Format[T]){ this: Target[T] =>
+ def evaluateAndWrite(args: Args): (T, String) = {
+ val res = evaluate(args)
+ val str = formatter.writes(res)
+ (res, Json.stringify(str))
+ }
val defCtx: DefCtx
- def map[V](f: T => V)(implicit defCtx: DefCtx) = {
+ def map[V: Format](f: T => V)(implicit defCtx: DefCtx) = {
new Target.Mapped(this, f, defCtx)
}
- def zip[V](other: Target[V])(implicit defCtx: DefCtx) = {
+ def zip[V: Format](other: Target[V])(implicit defCtx: DefCtx) = {
new Target.Zipped(this, other, defCtx)
}
- def ~[V, R](other: Target[V])
+ def ~[V: Format, R: Format](other: Target[V])
(implicit s: Implicits.Sequencer[T, V, R], defCtx: DefCtx): Target[R] = {
this.zip(other).map(s.apply _ tupled)
}
override def toString = this.getClass.getName + "@" + defCtx.label
}
- def test(inputs: Target[Int]*)(implicit defCtx: DefCtx) = new Test(inputs, defCtx)
+ def test(inputs: Target[Int]*)(implicit defCtx: DefCtx) = {
+ new Test(inputs, defCtx, pure = false)
+ }
+ def testPure(inputs: Target[Int]*)(implicit defCtx: DefCtx) = {
+ new Test(inputs, defCtx, pure = true)
+ }
/**
* A dummy target that takes any number of inputs, and whose output can be
* controlled externally, so you can construct arbitrary dataflow graphs and
* test how changes propagate.
*/
- class Test(val inputs: Seq[Target[Int]], val defCtx: DefCtx) extends Target[Int]{
+ class Test(val inputs: Seq[Target[Int]],
+ val defCtx: DefCtx,
+ val pure: Boolean) extends Target[Int]{
var counter = 0
var lastCounter = counter
def evaluate(args: Args) = {
lastCounter = counter
- counter + args.args.map(_.asInstanceOf[Int]).sum
+ counter + args.args.map(_.asInstanceOf[Int]).sum
}
- override def dirty = lastCounter != counter
+ override val dirty = if (pure) None else Some(() => lastCounter != counter)
}
- def traverse[T](source: Seq[Target[T]])(implicit defCtx: DefCtx) = {
+ def traverse[T: Format](source: Seq[Target[T]])(implicit defCtx: DefCtx) = {
new Traverse[T](source, defCtx)
}
- class Traverse[T](val inputs: Seq[Target[T]], val defCtx: DefCtx) extends Target[Seq[T]]{
+ class Traverse[T: Format](val inputs: Seq[Target[T]], val defCtx: DefCtx) extends Target[Seq[T]]{
def evaluate(args: Args) = {
for (i <- 0 until args.length)
yield args(i).asInstanceOf[T]
}
}
- class Mapped[T, V](source: Target[T], f: T => V,
- val defCtx: DefCtx) extends Target[V]{
+ class Mapped[T, V: Format](source: Target[T], f: T => V,
+ val defCtx: DefCtx) extends Target[V]{
def evaluate(args: Args) = f(args(0))
val inputs = List(source)
}
- class Zipped[T, V](source1: Target[T],
- source2: Target[V],
- val defCtx: DefCtx) extends Target[(T, V)]{
+ class Zipped[T: Format, V: Format](source1: Target[T],
+ source2: Target[V],
+ val defCtx: DefCtx) extends Target[(T, V)]{
def evaluate(args: Args) = (args(0), args(0))
val inputs = List(source1, source1)
}
diff --git a/src/main/scala/forge/package.scala b/src/main/scala/forge/package.scala
new file mode 100644
index 00000000..8145acd3
--- /dev/null
+++ b/src/main/scala/forge/package.scala
@@ -0,0 +1,46 @@
+import play.api.libs.json._
+import java.nio.{file => jnio}
+
+import ammonite.ops.Bytes
+
+package object forge {
+
+ implicit object pathFormat extends Format[jnio.Path]{
+ def reads(json: JsValue) = json match{
+ case JsString(v) => JsSuccess(jnio.Paths.get(v))
+ case _ => JsError("Paths must be a String")
+ }
+ def writes(o: jnio.Path) = JsString(o.toAbsolutePath.toString)
+ }
+
+ implicit object bytesFormat extends Format[Bytes]{
+ def reads(json: JsValue) = json match{
+ case JsString(v) => JsSuccess(
+ new Bytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(v))
+ )
+ case _ => JsError("Bytes must be a String")
+ }
+ def writes(o: Bytes) = {
+ JsString(javax.xml.bind.DatatypeConverter.printBase64Binary(o.array))
+ }
+ }
+
+ implicit def EitherFormat[T: Format, V: Format] = new Format[Either[T, V]]{
+ def reads(json: JsValue) = json match{
+ case JsObject(struct) =>
+ (struct.get("type"), struct.get("value")) match{
+ case (Some(JsString("Left")), Some(v)) => implicitly[Reads[T]].reads(v).map(Left(_))
+ case (Some(JsString("Right")), Some(v)) => implicitly[Reads[V]].reads(v).map(Right(_))
+ case _ => JsError("Either object layout is unknown")
+ }
+ case _ => JsError("Either must be an Object")
+ }
+ def writes(o: Either[T, V]) = o match{
+ case Left(v) => Json.obj("type" -> "Left", "value" -> implicitly[Writes[T]].writes(v))
+ case Right(v) => Json.obj("type" -> "Right", "value" -> implicitly[Writes[V]].writes(v))
+ }
+ }
+
+ implicit val crFormat = Json.format[ammonite.ops.CommandResult]
+ implicit val tsFormat = Json.format[Target.Subprocess.Result]
+}
diff --git a/src/test/scala/forge/ForgeTests.scala b/src/test/scala/forge/ForgeTests.scala
index 51df4c5f..6c588d4f 100644
--- a/src/test/scala/forge/ForgeTests.scala
+++ b/src/test/scala/forge/ForgeTests.scala
@@ -1,13 +1,13 @@
package forge
import utest._
-import Target.test
+import Target.{test, testPure}
import java.nio.{file => jnio}
object ForgeTests extends TestSuite{
val tests = Tests{
- val baseCtx = DefCtx("forge.ForgeTests.tests ")
- val evaluator = new Evaluator(jnio.Paths.get("target/workspace"), baseCtx)
+ val baseCtx = DefCtx("forge.ForgeTests.tests ", None)
+
object Singleton {
val single = T{ test() }
}
@@ -31,6 +31,11 @@ object ForgeTests extends TestSuite{
val down = T{ test(test(up), test(up)) }
}
+ object AnonImpureDiamond{
+ val up = T{ test() }
+ val down = T{ test(testPure(up), test(up)) }
+ }
+
'syntaxLimits - {
// Make sure that we properly prohibit cases where a `test()` target can
@@ -161,9 +166,9 @@ object ForgeTests extends TestSuite{
}
'evaluateSingle - {
- def check(target: Target[_],
- expValue: Any,
- expEvaled: Seq[Target[_]]) = {
+ val evaluator = new Evaluator(jnio.Paths.get("target/workspace"), baseCtx)
+
+ def check(target: Target[_], expValue: Any, expEvaled: Seq[Target[_]]) = {
val Evaluator.Results(returnedValues, returnedEvaluated) = evaluator.evaluate(Seq(target))
assert(
returnedValues == Seq(expValue),
@@ -228,7 +233,7 @@ object ForgeTests extends TestSuite{
right.counter += 1
check(down, expValue = 5, expEvaled = Seq(right, down))
}
- 'anoniamond - {
+ 'anonDiamond - {
import AnonDiamond._
val left = down.inputs(0).asInstanceOf[Target.Test]
val right = down.inputs(1).asInstanceOf[Target.Test]
@@ -247,6 +252,15 @@ object ForgeTests extends TestSuite{
right.counter += 1
check(down, expValue = 5, expEvaled = Seq(right, down))
}
+// 'anonImpureDiamond - {
+// import AnonImpureDiamond._
+// val left = down.inputs(0).asInstanceOf[Target.Test]
+// val right = down.inputs(1).asInstanceOf[Target.Test]
+// check(down, expValue = 0, expEvaled = Seq(up, left, right, down))
+//
+// down.counter += 1
+// check(down, expValue = 1, expEvaled = Seq(left, down))
+// }
}