diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main/scala/forge/DefCtx.scala | 8 | ||||
-rw-r--r-- | src/main/scala/forge/Evaluator.scala | 37 | ||||
-rw-r--r-- | src/main/scala/forge/Target.scala | 53 | ||||
-rw-r--r-- | src/main/scala/forge/package.scala | 46 | ||||
-rw-r--r-- | src/test/scala/forge/ForgeTests.scala | 28 |
5 files changed, 127 insertions, 45 deletions
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)) +// } } |