From e3d9a1ff13c9319f6f7a51308dcceb98474f6485 Mon Sep 17 00:00:00 2001 From: Jonas Spenger Date: Tue, 14 Apr 2026 09:13:03 +0200 Subject: [PATCH] Unify Spore factory methods - Replace existing Spore factory methods: `Spore.apply(body)`, `Spore.applyWithEnv(env)(body)`, `Spore.applyWithCtx(env)(body)` with new uniform method: `Spore.apply(captures)(body)`, where `captures` is either an explicit list of the captured variables, or the symbol `*` for capturing all variables - Example: `val y = 12; val z = 13; Spore(y, z) { (x: Int) => x + y + z }` - Example: `Spore(*) { (x: Int) => x + y + z }` - Update all test cases and samples to use new factory method(s) - Update macros and refactoring - Add inlined factory-alias tests --- .../spores/sample/AutoCaptureExample.scala | 14 +- .../spores/sample/ForComprehension.scala | 6 +- .../src-jvm/spores/sample/SporeExample.scala | 16 +- sample/src-jvm/spores/sample/Workflow.scala | 6 +- .../src-jvm/spores/SporeObjectCompanion.scala | 117 ++---- .../spores/SporeObjectCompanion0.scala | 61 +-- spores3/src/spores/Duplicable.scala | 2 +- spores3/src/spores/Macros.scala | 359 ++++++++++++++---- spores3/src/spores/ReadWriters.scala | 14 +- spores3/src/spores/Spore0.scala | 3 + spores3/src/spores/default.scala | 5 +- spores3/src/spores/jvm/SporeJVM.scala | 32 -- spores3/src/spores/jvm/SporeJVM0.scala | 78 +--- .../spores/jvm/AutoCaptureErrorTests.scala | 56 +-- .../src-jvm/spores/jvm/AutoCaptureTests.scala | 38 +- .../spores/jvm/SporeLambdaErrorTests.scala | 125 +++++- .../src-jvm/spores/jvm/SporeLambdaTests.scala | 201 +++++++++- .../jvm/SporeSerializationJvmTests.scala | 28 +- .../test/src-jvm/spores/test/SporeTests.scala | 66 ++-- 19 files changed, 770 insertions(+), 457 deletions(-) delete mode 100644 spores3/src/spores/jvm/SporeJVM.scala diff --git a/sample/src-jvm/spores/sample/AutoCaptureExample.scala b/sample/src-jvm/spores/sample/AutoCaptureExample.scala index 2b3ca27..2e3bde4 100644 --- a/sample/src-jvm/spores/sample/AutoCaptureExample.scala +++ b/sample/src-jvm/spores/sample/AutoCaptureExample.scala @@ -9,7 +9,7 @@ import spores.conversions.given object AutoCaptureExample { - // The `Spore.auto` method does the following: + // The `Spore(*)` method does the following: // // 1. Lifts all captured symbols to parameters // 2. Find the implicit readwriters for each captured symbol @@ -20,7 +20,7 @@ object AutoCaptureExample { // // {{{ // def foo(x: Int, y: Int) = { - // Spore.auto{ (i: Int) => x + y + i } + // Spore(*){ (i: Int) => x + y + i } // } // }}} // @@ -43,7 +43,7 @@ object AutoCaptureExample { // A factory for a serialized function that checks if a number is between the // numbers `x` and `y`. def isBetween(x: Int, y: Int): Spore[Int => Boolean] = { - Spore.auto { (i: Int) => x <= i && i < y } + Spore(*) { (i: Int) => x <= i && i < y } } @@ -58,9 +58,9 @@ object AutoCaptureExample { // Now we can create a similar factory but by capturing a `Range` object. def isInRange(range: Range): Spore[Int => Boolean] = { - // The `Spore.auto` method will automatically pack the `Range` object and + // The `Spore(*)` method will automatically pack the `Range` object and // its readwriter. - Spore.auto{ (i: Int) => range.x <= i && i < range.y } + Spore(*){ (i: Int) => range.x <= i && i < range.y } } @@ -69,7 +69,7 @@ object AutoCaptureExample { // // no implicit values were found that match type spores.Spore[upickle.default.ReadWriter[spores.experimental.example.AutoCaptureExample.Range2]] // case class Range2(x: Int, y: Int) // def isInRange2(range: Range2): Spore[Int => Boolean] = { - // Spore.auto{ (i: Int) => range.x <= i && i < range.y } + // Spore(*){ (i: Int) => range.x <= i && i < range.y } // } @@ -88,7 +88,7 @@ object AutoCaptureExample { assert(btwn1020.get().apply(25) == false) println(btwn1020.apply(25)) - val filter = Spore.auto { (l: List[Int]) => l.filter(btwn1020.get()) } + val filter = Spore(*) { (l: List[Int]) => l.filter(btwn1020.get()) } println(filter) // result: PackedWithEnv(PackedLambda(spores.experimental.example.AutoCaptureExample$Lambda$3),PackedEnv({"$type":"spores.Packed.PackedWithEnv","packed":{"$type":"spores.Packed.PackedWithEnv","packed":{"$type":"spores.Packed.PackedLambda","className":"spores.experimental.example.AutoCaptureExample$Lambda$1"},"packedEnv":{"$type":"spores.Packed.PackedEnv","env":"10","rw":{"$type":"spores.Packed.PackedObject","className":"spores.ReadWriters$IntRW$"}}},"packedEnv":{"$type":"spores.Packed.PackedEnv","env":"20","rw":{"$type":"spores.Packed.PackedObject","className":"spores.ReadWriters$IntRW$"}}},PackedObject(spores.ReadWriters$SporeRW$))) diff --git a/sample/src-jvm/spores/sample/ForComprehension.scala b/sample/src-jvm/spores/sample/ForComprehension.scala index 4aadfd3..d1ec402 100644 --- a/sample/src-jvm/spores/sample/ForComprehension.scala +++ b/sample/src-jvm/spores/sample/ForComprehension.scala @@ -9,11 +9,11 @@ object ForComprehension { private[Container] val sporeOpt: Option[Spore[A]] inline def map[B](inline f: A => B): Container[B] = { - Container.create(this.sporeOpt.map(x => Spore.auto(f).withEnv2(x))) + Container.create(this.sporeOpt.map(x => Spore(*)(f).withEnv2(x))) } inline def flatMap[B](inline f: A => Container[B]): Container[B] = { - this.sporeOpt.map(x => Spore.auto(f).withEnv2(x).get()).getOrElse(Container.empty) + this.sporeOpt.map(x => Spore(*)(f).withEnv2(x).get()).getOrElse(Container.empty) } def withFilter(f: A => Boolean): Container[A] = { @@ -29,7 +29,7 @@ object ForComprehension { private[Container] override val sporeOpt: Option[Spore[A]] = so } - inline def apply[A](inline a: A): Container[A] = create(Some(Spore.auto(a))) + inline def apply[A](inline a: A): Container[A] = create(Some(Spore(*)(a))) def empty[A]: Container[A] = create(None) } diff --git a/sample/src-jvm/spores/sample/SporeExample.scala b/sample/src-jvm/spores/sample/SporeExample.scala index 58b63a2..bd0d731 100644 --- a/sample/src-jvm/spores/sample/SporeExample.scala +++ b/sample/src-jvm/spores/sample/SporeExample.scala @@ -7,26 +7,26 @@ import spores.sample.platform.* object SporeExample { - val Lambda1 = Spore.apply[Int => String] { x => x.toString.reverse } + val Lambda1 = Spore.apply[Int => String]() { x => x.toString.reverse } - val Lambda2 = Spore.applyWithEnv[Int, Int => String](12) { env => x => (env + x).toString.reverse } + val Lambda2 = Spore.apply[Int => Int => String]() { env => x => (env + x).toString.reverse }.withEnv(12) - val Lambda3 = Spore.apply[Option[Int] => Int] { x => x.map { _ + 1 }.getOrElse(0) } + val Lambda3 = Spore.apply[Option[Int] => Int]() { x => x.map { _ + 1 }.getOrElse(0) } - val Lambda4 = Spore.applyWithCtx[Int, Int](14) { summon[Int] } + val Lambda4 = Spore.apply[Int ?=> Int]() { summon[Int] }.withCtx(14) // // Should cause compile error // object ShouldFail: - // Spore.apply[Int => Int] { x => - // Spore.apply[Int => Int] { y => - // // Invalid capture of variable `x`. Use the first parameter of a spore's body to refer to the spore's environment. + // Spore.apply[Int => Int]() { x => + // Spore.apply[Int => Int]() { y => + // // Invalid capture of variable `x`. Add it to the capture list or use `*` to capture all by default. // x + y // }.get().apply(x) // } // // Should cause compile error // import upickle.default.* - // def SporeFactoryFail[T: ReadWriter] = Spore.apply { summon[ReadWriter[T]] } // Invalid capture of variable `evidence$1`. Use the first parameter of a spore's body to refer to the spore's environment. + // def SporeFactoryFail[T: ReadWriter] = Spore.apply() { summon[ReadWriter[T]] } // Invalid capture of variable `evidence$1`. Add it to the capture list or use `*` to capture all by default. def main(args: Array[String]): Unit = { diff --git a/sample/src-jvm/spores/sample/Workflow.scala b/sample/src-jvm/spores/sample/Workflow.scala index bfe59fe..ea113dc 100644 --- a/sample/src-jvm/spores/sample/Workflow.scala +++ b/sample/src-jvm/spores/sample/Workflow.scala @@ -14,7 +14,7 @@ object WorkflowMain { // Custom error type for our workflow case class Error(message: String) derives ReadWriter // Custom Spore[ReadWriter[Error]] for our error type - given Spore[ReadWriter[Error]] = Spore.auto(summon) + given Spore[ReadWriter[Error]] = Spore(*)(summon) def main(args: Array[String]): Unit = { @@ -89,7 +89,7 @@ sealed trait Workflow[-Inp, +Out, Err] { import Workflow.* inline def map[OutB](inline f: Out => Either[Err, OutB])(using rw: Spore[ReadWriter[Either[Err, OutB]]]): Workflow[Inp, OutB, Err] = { - val spore = Spore.auto(f) + val spore = Spore(*)(f) Map(this, spore, rw) } } @@ -125,7 +125,7 @@ object Workflow { /** Create a workflow starting from the initial `value`. */ def apply[Out, Err](value: Either[Err, Out])(using Spore[ReadWriter[Out]], Spore[ReadWriter[Err]]): Workflow[Nothing, Out, Err] = { - val spore = Spore.auto(value) + val spore = Spore(*)(value) Value(spore) } diff --git a/spores3/src-jvm/spores/SporeObjectCompanion.scala b/spores3/src-jvm/spores/SporeObjectCompanion.scala index c809a30..686b3a3 100644 --- a/spores3/src-jvm/spores/SporeObjectCompanion.scala +++ b/spores3/src-jvm/spores/SporeObjectCompanion.scala @@ -1,6 +1,5 @@ package spores -import scala.annotation.targetName import upickle.default.ReadWriter import spores.* @@ -13,112 +12,46 @@ private[spores] trait SporeObjectCompanion { // This is a hack for having platform-specific operations in the companion // object. - /** Create a Spore from the provided closure `body`. - * - * The created Spore is safe to serialize and deserialize. The closure must - * not capture any variables, otherwise it will cause a compile error. - * - * To capture variables, use [[applyWithEnv]], [[applyWithCtx]], or - * [[auto]] instead. - * - * @example - * {{{ - * val mySpore = Spore.apply[Int => String] { x => x.toString.reverse } - * }}} - * - * @param body - * The closure. - * @tparam T - * The type of the closure. - * @return - * A new `Spore[T]` with the packed closure `body`. - */ - inline def apply[T](inline body: T): Spore[T] = { - SporeJVM.apply(body) - } - /** Create a Spore from the provided closure `body` with an environment - * variable `env` as the first parameter of the closure. + /** Create a Spore from the provided closure `body` and parameter list of + * captured variables `captures`. * - * The created Spore is safe to serialize and deserialize. The closure must - * not capture any variables, otherwise it will cause a compile error. + * Captured variables in `body` must either be added to the `captures` + * parameter list, or the "capture all" mode `*` must be used. Mentioned + * variables in the `captures` list must be used in the `body`. For each + * capture of type `E` an implicit/given `Spore[ReadWriter[E]]` must be in + * contextual scope. Deviations will cause a compiler error. The created + * Spore is safe to serialize and deserialize. * - * @example + * @example A Spore with no captures * {{{ - * val mySpore = Spore.applyWithEnv[Int, String](11) { env => (env + 12).toString.reverse } + * Spore.apply() { (x: Int) => x + 1 } * }}} - * - * @param env - * The environment variable applied to the closure. - * @param body - * The closure. - * @param ev - * The implicit `Spore[ReadWriter[E]]` used for packing the `env`. - * @tparam E - * The type of the environment variable. - * @tparam T - * The return type of the closure. - * @return - * A new `Spore[T]` with the packed closure `body` applied to the `env`. - */ - inline def applyWithEnv[E, T](inline env: E)(inline body: E => T)(using ev: Spore[ReadWriter[E]]): Spore[T] = { - SporeJVM.applyWithEnv(env)(body) - } - - /** Create a Spore from the provided closure `body` with an environment - * variable `env` as the first **implicit** parameter of the closure. - * - * The created Spore is safe to serialize and deserialize. The closure must - * not capture any variables, otherwise it will cause a compile error. - * - * @example + * @example A Spore with explicit captures * {{{ - * val mySpore = Spore.applyWithCtx[Int, String](11) { env ?=> (env + 12).toString.reverse } + * val y = 12 + * val z = 13 + * Spore.apply(y, z) { (x: Int) => x + y + z + 1 } * }}} - * - * @param env - * The context environment variable applied to the closure. - * @param body - * The closure. - * @param ev - * The implicit `Spore[ReadWriter[E]]` used for packing the `env`. - * @tparam E - * The type of the context environment variable. - * @tparam T - * The return type of the closure. - * @return - * A new `Spore[T]` with the packed closure `body` using the implicit `env`. - */ - inline def applyWithCtx[E, T](inline env: E)(inline body: E ?=> T)(using ev: Spore[ReadWriter[E]]): Spore[T] = { - SporeJVM.applyWithCtx(env)(body) - } - - /** Create a Spore from `f`. Automatically captures variables and checks - * captured variables. Captured variables in `f` must have an implicit - * `Spore[ReadWriter[T]]` in scope, where `T` is the type of the captured - * variable. - * - * The created Spore is safe to serialize and deserialize. If a captured - * variable does not have an implicit `Spore[ReadWriter[T]]` in scope then it - * will cause a compile error. - * - * @example + * @example A Spore with `*` capture all mode * {{{ - * def isBetween(x: Int , y: Int): Spore[Int => Boolean] = { - * // `x` and `y` are captured variables of type `Int`, so we need to - * // provide an implicit `Spore[ReadWriter[Int]]` in scope. - * Spore.auto { (i: Int) => x <= i && i < y } - * } + * val y = 12 + * val z = 13 + * Spore.apply(*) { (x: Int) => x + y + z + 1 } * }}} * + * @param captures + * The captured variables used in the `body`, or `*` to capture all by + * default. * @param body * The closure. * @tparam T * The type of the closure. * @return - * A new `Spore[F]` with the packed closure `f`. + * A new `Spore[T]`. */ - inline def auto[T](inline body: T): Spore[T] = { - SporeJVM.auto(body) + inline def apply[T](inline captures: Any*)(inline body: T): Spore[T] = { + SporeJVM0.apply[ReadWriter, T](captures*)(body) } + } diff --git a/spores3/src-jvm/spores/SporeObjectCompanion0.scala b/spores3/src-jvm/spores/SporeObjectCompanion0.scala index cc4a58d..a26a2cc 100644 --- a/spores3/src-jvm/spores/SporeObjectCompanion0.scala +++ b/spores3/src-jvm/spores/SporeObjectCompanion0.scala @@ -6,49 +6,52 @@ import spores.jvm.* /** Internal API. Extended from by the [[spores.Spore0]] companion object. */ private[spores] trait SporeObjectCompanion0 { + // This is a hack for having platform-specific operations in the companion // object. - inline def apply[F[_], T](inline body: T): Spore0[F, T] = { - SporeJVM0.apply(body) - } - - inline def applyWithEnv[F[_], E, T](inline env: E)(inline body: E => T)(using ev: Spore0[F, F[E]]): Spore0[F, T] = { - apply[F, E => T](body).withEnv(env)(using ev) - } - inline def applyWithCtx[F[_], E, T](inline env: E)(inline body: E ?=> T)(using ev: Spore0[F, F[E]]): Spore0[F, T] = { - apply[F, E ?=> T](body).withCtx(env)(using ev) - } - - /** Create a Spore0 from `body`. Automatically captures variables and checks - * captured variables. Captured variables in `body` must have an implicit - * `Spore[F[E]]` in scope, where `E` is the type of the captured environment - * variable and `F[_]` is the type of the captured evidence. + /** Create a Spore0 from the provided closure `body` and parameter list of + * captured variables `captures` with evidence type `F[_]`. * - * If a captured - * variable does not have an implicit `Spore[F[E]]` in scope then it - * will cause a compile error. + * Captured variables in `body` must either be added to the `captures` + * parameter list, or the "capture all" mode `*` must be used. Mentioned + * variables in the `captures` list must be used in the `body`. For each + * capture of type `E` an implicit/given `Spore0[F, F[E]]` must be in + * contextual scope. Deviations will cause a compiler error. * - * @example + * @example A Spore with no captures * {{{ - * def isBetween(x: Int , y: Int): Spore0[F, Int => Boolean] = { - * // `x` and `y` are captured variables of type `Int`, so we need to - * // provide an implicit `Spore[F[Int]]` in scope. - * Spore0.auto { (i: Int) => x <= i && i < y } - * } + * Spore0.apply[F, Int => Int]() { (x: Int) => x + 1 } + * }}} + * @example A Spore with explicit captures + * {{{ + * val y = 12 + * val z = 13 + * Spore0.apply[F, Int => Int](y, z) { (x: Int) => x + y + z + 1 } + * }}} + * @example A Spore with `*` capture all mode + * {{{ + * val y = 12 + * val z = 13 + * Spore0.apply[F, Int => Int](*) { (x: Int) => x + y + z + 1 } * }}} * + * @param captures + * The captured variables used in the `body`, or `*` to capture all by + * default. * @param body * The closure. + * @tparam F + * The evidence type for the captures. Each capture of type `E` requires an + * implicit/given `Spore0[F, F[E]]` in contextual scope. * @tparam T * The type of the closure. - * @tparam F - * The type of the captured evidence. * @return - * A new `Spore[F]` with the packed closure `f`. + * A new `Spore0[F, T]`. */ - inline def auto[F[_], T](inline body: T): Spore0[F, T] = { - SporeJVM0.auto(body) + inline def apply[F[_], T](inline captures: Any*)(inline body: T): Spore0[F, T] = { + SporeJVM0.apply[F, T](captures*)(body) } + } diff --git a/spores3/src/spores/Duplicable.scala b/spores3/src/spores/Duplicable.scala index 17b2ff8..f9d7a95 100644 --- a/spores3/src/spores/Duplicable.scala +++ b/spores3/src/spores/Duplicable.scala @@ -11,7 +11,7 @@ object Duplicable { duplicable.duplicate(value) inline def apply[T](inline body: T): Spore0[Duplicable, T] = { - spores.jvm.SporeJVM0.apply(body) + spores.jvm.SporeJVM0.apply()(body) } inline def applyWithEnv[E, T](env: E)(inline body: E => T)(using ev: Spore0[Duplicable, Duplicable[E]]): Spore0[Duplicable, T] = { diff --git a/spores3/src/spores/Macros.scala b/spores3/src/spores/Macros.scala index f0afb5b..1d27e0b 100644 --- a/spores3/src/spores/Macros.scala +++ b/spores3/src/spores/Macros.scala @@ -2,8 +2,116 @@ package spores import scala.quoted.* + private[spores] object Macros { + /** Welcome to the internals of the Spores library! I salute your curiosity. + * How do Spores work? Let's dive deep into closures/functions and the Spore + * factory transformation. + * + * What is a closure? A "closure [is comprised of a] lambda-expression + * [(anonymous function)] and the environment relative to which it was + * evaluated." (P. J. Landin: The Mechanical Evaluation of Expressions. + * Comput. J. 6(4): 308-320 (1964)) + * A lambda-expression may contain free identifiers (variables). An + * environment associates these identifiers to values. The evaluation of a + * lambda-expression "captures" the values of the identifiers from the + * environment. This is illustrated by the following Scala 3 example. + * {{{ + * val y = 12 + * val f = (x: Int) => x + y + * }}} + * The environment associates `y` to the value `12`. The lambda-expression + * contains `y` as a free identifier. When the lambda-expression is evaluated + * and assigned to `f`, the value `12` of the free identifier `y` is + * captured. Evaluating `f(13)` yields `25`. + * + * It is problematic to seralize an arbitrary closure because its free + * variables may not be serializable. Moreover, the programmer has no means + * to access the free variables and their types. And so, it is difficult to + * detect if a closure is serializable. + * The Spores library addresses this problem by transforming a closure into a + * representation with an explicit (accessible) body and its captured + * variables. + * In precise terms, the Spore factory transforms a closure with free + * variables into a closure with no free variables by lifting all free + * variables into function parameters. + * + * The factory method takes as arguments the closure `body` and the + * sequence of captured variables `captures`. It performs the following + * sequence of checks and transformations: + * We'll use the following shorthand notation . Let `b` denote the body or + * closure, and `\seq(c_id)` = fv(b)` the sequence of free variables in `b` + * where `c_id` corresponds to an identifier of a captured variable. The + * runtime value of a closure is created by substituting its free variables + * with their corresponding runtime values: `b[c_id -> c_val]`. + * + * (1) Well-formedness check on `captures`. The provided `captures` is in one + * of three valid forms: + * - `*`: capture all by default + * - `\empty`: no capture + * - `(c_id1, c_id2, ...)`: explicit capture list + * + * (2) Find captured identifiers in `body`. + * - `bodyCaptures = fv(body)` + * + * It should be noted that identifiers that reference static (top-level) + * fields or methods are not considered captured identifiers. + * + * (3) Check that `captures` is consistent with `bodyCaptures`. + * - If `captures` is `*` then OK + * - If `captures` is `()` then if `bodyCaptures` is empty then OK else ERROR + * - If `captures` is `(c_id1, c_id2, ...)` and `bodyCaptures` is `(bc_id1, + * ...)`: then if for each `c_id` in `captures`: if `c_id` is in + * `bodyCaptures`: and if for each `bc_id` in `bodyCaptures`: if `bc_id` + * is in `captures`: then OK else ERROR. + * + * (4) Lift `bodyCaptures` into parameters: + * - Transforms a `b` of shape `{ b }` with `(c_id1, c_id2, ...) = + * bodyCaptures = fv(b)` into `bNew`: + * `bNew = { (c_id1, c_id2, ...) => { b } }` + * + * (5) Create Spore by storing the transformed body `bNew` together with the + * runtime values of the captured variables: + * - `Spore0 = { body = bNew; captures = (c_val1, c_val2, ...) }` + * + * (6) A runtime value of the Spore's closure is created by application of + * the Spore's `body` to the Spore's `captures`. + */ + + + private[spores] def capturesWellFormednessCheck(capturesExpr: Expr[Seq[Any]])(using Quotes): (Int, List[quotes.reflect.Tree]) = { + import quotes.reflect.* + + def expandInlined(term: Term): Term = term match { + case Inlined(_, _, expansion) => expandInlined(expansion) + case _ => term + } + + capturesExpr match { + case Varargs(Seq(star)) if star.asTerm.tpe <:< TypeRepr.of[Spore0.CaptureAllMode] => { + (1, List()) + } + case Varargs(captures) if captures.isEmpty => { + (2, captures.map(_.asTerm).toList) + } + case Varargs(captures) => { + val expanded = captures.map(c => expandInlined(c.asTerm)).toList + if (expanded.exists(_.tpe <:< TypeRepr.of[Spore0.CaptureAllMode])) { + report.error("Invalid capture list.") + (-1, List()) + } else { + (3, expanded) + } + } + case _ => { + report.error("Invalid capture list.") + (-1, List()) + } + } + } + + private[spores] def findCapturedIds(using Quotes)(tree: quotes.reflect.Term): List[quotes.reflect.Tree] = { import quotes.reflect.* @@ -117,21 +225,186 @@ private[spores] object Macros { } - private[spores] def checkBodyExpr[T](bodyExpr: Expr[T])(using Quotes): Unit = { + private[spores] def checkCaptures(using Quotes)(captures: List[quotes.reflect.Tree], bodyCaptures: List[quotes.reflect.Tree]): Unit = { + import quotes.reflect.* + + val capturesFullNameSet = captures.map(_.symbol.fullName).toSet + val bodyCapturesFullNameSet = bodyCaptures.map(_.symbol.fullName).toSet + + captures.foreach { + capture => { + if (!bodyCapturesFullNameSet.contains(capture.symbol.fullName)) { + report.error(s"`${capture.symbol.name}` is not captured by the spore body. Remove it from the capture list. It is a top-level variable or not used in the body.", capture.pos) + } + } + } + + bodyCaptures.foreach { + bodyCapture => { + if (!capturesFullNameSet.contains(bodyCapture.symbol.fullName)) { + bodyCapture match { + case This(Some(qual)) => { + report.error(s"Invalid capture of `this` from class ${qual}. Add it to the capture list or use `*` to capture all by default.", bodyCapture.pos) + } + case This(None) => { + report.error(s"Invalid capture of `this` from outer class. Add it to the capture list or use `*` to capture all by default.", bodyCapture.pos) + } + case _ => { + report.error(s"Invalid capture of variable `${bodyCapture.symbol.name}`. Add it to the capture list or use `*` to capture all by default.", bodyCapture.pos) + } + } + } + } + } + + () + } + + + private[spores] def findEvidence[F[_]](using Quotes)(captures: List[quotes.reflect.Tree])(using Type[F]): List[quotes.reflect.Tree] = { import quotes.reflect.* - val foundIds = findCapturedIds(bodyExpr.asTerm) - - foundIds.foreach { tree => - tree match { - case This(Some(qual)) => - report.error(s"Invalid capture of `this` from class ${qual}.", tree.pos) - case This(None) => - report.error(s"Invalid capture of `this` from outer class.", tree.pos) - case _ => - val sym = tree.symbol - report.error(s"Invalid capture of variable `${sym.name}`. Use the first parameter of a spore's body to refer to the spore's environment.", tree.pos) + captures.flatMap { captured => + val capturedTpe = captured match { + case x@ This(_) => captured.symbol.typeRef + case _ => captured.symbol.termRef.widen + } + val evidenceTpe = TypeRepr.of[[T] =>> Spore0[F, F[T]]].appliedTo(capturedTpe) + + Implicits.search(evidenceTpe) match { + case succ: ImplicitSearchSuccess => { + List(succ.tree) + } + + case fail: ImplicitSearchFailure => { + // Do not use `evidenceTpe.show` here as it throws a MatchError on Scala 3.4.3 for some cases + val msg = s"Missing implicit for captured variable `${captured.symbol.name}`.\n\n" + fail.explanation + report.error(msg, captured.pos) + List() + } + } + } + } + + + private[spores] def liftBody(using Quotes)(owner: quotes.reflect.Symbol, bodyCaptures: List[quotes.reflect.Tree], body: quotes.reflect.Term): quotes.reflect.Term = { + import quotes.reflect.* + + def substituteSymbols(subst: Map[Symbol, Term], body: Term): Term = { + val treeMap = new TreeMap { + override def transformTerm(t: Term)(o: Symbol): Term = { + t match + case x@ Ident(_) => + subst.getOrElse(x.symbol, super.transformTerm(t)(o)) + case x@ This(_) => + subst.getOrElse(x.symbol, super.transformTerm(t)(o)) + case _ => + super.transformTerm(t)(o) + } + } + treeMap.transformTerm(body)(owner) + } + + def liftSymbolGroup(symGroup: List[Tree], body: Term): Term = { + val captureType = symGroup.head match { + case x@ This(_) => x.symbol.typeRef.asType + case x@ _ => x.symbol.termRef.widen.asType + } + val bodyType = body.tpe.widen.asType + + captureType match { + case '[cType] => { + bodyType match { + case '[bType] => { + '{ + (param: cType) => ${ + val subst = symGroup.map(_.symbol -> ('param).asTerm).toMap + substituteSymbols(subst, body).asExprOf[bType] + } + }.asTerm + } + } + } + } + } + + def liftAllSymbolGroups(syms: List[List[Tree]], body: Term): Term = { + syms match { + case Nil => body + case h :: t => liftSymbolGroup(h, liftAllSymbolGroups(t, body)) + } + } + + def groupIdentifiersByFullName(captures: List[Tree]): List[(String, List[Tree])] = { + captures.groupBy(_.symbol.fullName).toList.sortBy(_._1) + } + + val symbolGroup = groupIdentifiersByFullName(bodyCaptures).map(_._2) + liftAllSymbolGroups(symbolGroup, body) + } + + + private[spores] def constructSpore0[F[_]](using Quotes)(bodyTerm: quotes.reflect.Term)(using Type[F]): Expr[Spore0[F, Any]] = { + '{ + class Lambda extends SporeLambdaBuilder0[F, Any] { + override def body = { ${ bodyTerm.asExpr } } } + (new Lambda()).build() + } + } + + + private[spores] def applySpore0[F[_]](using Quotes)(sporeExpr: Expr[Spore0[F, Any]], captures: List[quotes.reflect.Tree], evidence: List[quotes.reflect.Tree])(using Type[F]): Expr[Spore0[F, Any]] = { + var tmp: Expr[Spore0[F, Any]] = sporeExpr + captures.zip(evidence).foreach { + (capture, evidence) => { + val env = capture.asExpr + val ev = evidence.asExpr + tmp = '{ + $tmp + .asInstanceOf[Spore0[F, Any => Any]] + .withEnv($env)(using $ev.asInstanceOf) + } + } + } + tmp + } + + + private[spores] def spore0ApplyMacro[F[_], T](capturesExpr: Expr[Seq[Any]], bodyExpr: Expr[T])(using Type[F], Type[T], Quotes): Expr[Spore0[F, T]] = { + import quotes.reflect.* + + def deduplicateCaptures(captures: List[Tree]): List[Tree] = { + captures.groupBy(_.symbol.fullName).toList.sortBy(_._1).map(_._2.head) + } + + val res1 = capturesWellFormednessCheck(capturesExpr) + val mode = res1._1 + + val captureTerms = res1._2 + val captureTermsDedup = deduplicateCaptures(captureTerms) + val bodyCaptureTerms = findCapturedIds(bodyExpr.asTerm) + val bodyCaptureTermsDedup = deduplicateCaptures(bodyCaptureTerms) + + val evidence = findEvidence(bodyCaptureTermsDedup) + + // Mode 1: `*`: capture all by default; And mode -1: ERROR. Continue with `*` to display further errors + if (mode == 1 || mode == -1) { + val liftedBody = liftBody(Symbol.spliceOwner, bodyCaptureTermsDedup, bodyExpr.asTerm) + val spore = constructSpore0[F](liftedBody) + applySpore0(spore, bodyCaptureTermsDedup, evidence).asInstanceOf[Expr[Spore0[F, T]]] + } + // `()`: no capture + else if (mode == 2) { + checkCaptures(captureTerms, bodyCaptureTerms) + constructSpore0(bodyExpr.asTerm).asInstanceOf[Expr[Spore0[F, T]]] + } + // `(captures*)`: explicit capture list + else { // (mode == 3) + checkCaptures(captureTerms, bodyCaptureTerms) + val liftedBody = liftBody(Symbol.spliceOwner, bodyCaptureTermsDedup, bodyExpr.asTerm) + val spore = constructSpore0[F](liftedBody) + applySpore0(spore, bodyCaptureTermsDedup, evidence).asInstanceOf[Expr[Spore0[F, T]]] } } @@ -221,66 +494,4 @@ private[spores] object Macros { '{ () } } - - private[spores] def findEvidence[F[_]](using Quotes)(captures: List[quotes.reflect.Tree])(using Type[F]): List[(quotes.reflect.Tree, quotes.reflect.Tree)] = { - import quotes.reflect.* - - captures.flatMap { captured => - val capturedTpe = captured.symbol.termRef.widen - val evidenceTpe = TypeRepr.of[[T] =>> Spore0[F, F[T]]].appliedTo(capturedTpe) - - Implicits.search(evidenceTpe) match { - case succ: ImplicitSearchSuccess => { - List((captured, succ.tree)) - } - - case fail: ImplicitSearchFailure => { - // Do not use `evidenceTpe.show` here as it throws a MatchError on Scala 3.4.3 for some cases - val msg = s"Missing implicit for captured variable `${captured.symbol.name}`.\n\n" + fail.explanation - report.error(msg, captured.pos) - List() - } - } - } - } - - - private[spores] def liftAllSymbolGroups(using Quotes)(owner: quotes.reflect.Symbol, syms: List[List[quotes.reflect.Tree]], body: quotes.reflect.Term): quotes.reflect.Term = { - import quotes.reflect.* - - def liftSymbolGroup(owner: Symbol, sym: List[Symbol], body: Term): Term = { - val mtpe = MethodType(List(sym.head.name))(_ => List(sym.head.termRef), _ => body.tpe) - Lambda( - owner, - mtpe, - { - case (methSym, List(arg1: Term)) => { - val subst = sym.map(s => (s -> arg1)).toMap - val treeMap = new TreeMap { - override def transformTerm(t: Term)(o: Symbol): Term = t match { - case id: Ident => { - subst.getOrElse(id.symbol, super.transformTerm(t)(o)) - } - case _ => { - super.transformTerm(t)(o) - } - } - } - treeMap.transformTerm(body)(methSym).changeOwner(methSym) - } - case _ => { - ??? // TODO: throw sensible error - } - } - ) - } - - var newBody = body - var newOwner = owner - for (sym <- syms) do { - newBody = liftSymbolGroup(newOwner, sym.map(_.symbol), newBody) - newOwner = newBody.symbol - } - newBody - } } diff --git a/spores3/src/spores/ReadWriters.scala b/spores3/src/spores/ReadWriters.scala index 6259375..8b7732f 100644 --- a/spores3/src/spores/ReadWriters.scala +++ b/spores3/src/spores/ReadWriters.scala @@ -22,7 +22,7 @@ object ReadWriters { // ReadWriter[Spore[T]] ////////////////////////////////////////////////////////////////////////////// - given [T]: ReadWriter[Spore[T]] = { + given sporeRW[T]: ReadWriter[Spore[T]] = { summon[ReadWriter[ujson.Value]].bimap[Spore[T]]( sp => { sp match { @@ -35,14 +35,14 @@ object ReadWriters { case Spore0.AST.Value(ev, value) => ujson.Obj( "tag" -> ujson.Str("Val"), - "ev" -> writeJs(ev), + "ev" -> writeJs(ev)(using sporeRW), "value" -> writeJs(value)(using ev.get()), ) case Spore0.AST.WithEnv(fun, env) => ujson.Obj( "tag" -> ujson.Str("WithEnv"), - "fun" -> writeJs(fun), - "env" -> writeJs(env), + "fun" -> writeJs(fun)(using sporeRW), + "env" -> writeJs(env)(using sporeRW), ) } }, @@ -58,12 +58,12 @@ object ReadWriters { } Spore0.AST.Body(className, kind, body) case "Val" => - val ev = read[Spore[ReadWriter[T]]](js("ev")) + val ev = read[Spore[ReadWriter[T]]](js("ev"))(using sporeRW) val value = read[T](js("value"))(using ev.get()) Spore0.AST.Value(ev, value) case "WithEnv" => - val fun = read[Spore[Any => T]](js("fun")) - val env = read[Spore[Any]](js("env")) + val fun = read[Spore[Any => T]](js("fun"))(using sporeRW) + val env = read[Spore[Any]](js("env"))(using sporeRW) Spore0.AST.WithEnv(fun, env) } }, diff --git a/spores3/src/spores/Spore0.scala b/spores3/src/spores/Spore0.scala index f8a51bb..a90fe76 100644 --- a/spores3/src/spores/Spore0.scala +++ b/spores3/src/spores/Spore0.scala @@ -150,6 +150,9 @@ sealed trait Spore0[F[_], +T] { self: Spore0[F, T] => */ object Spore0 extends SporeObjectCompanion0 { + opaque type CaptureAllMode = Unit + final val `*`: CaptureAllMode = () + /** Pack a value of type `T` as a `Spore0[F, T]` using an implicit * `Spore0[F, F[T]]` instance. * diff --git a/spores3/src/spores/default.scala b/spores3/src/spores/default.scala index 00951ce..a9724c7 100644 --- a/spores3/src/spores/default.scala +++ b/spores3/src/spores/default.scala @@ -3,9 +3,12 @@ package spores package object default { export spores.Spore - export spores.Spore0 export spores.SporeBuilder export spores.SporeClassBuilder + export spores.Spore0 + export spores.Spore0.`*` + export spores.SporeBuilder0 + export spores.SporeClassBuilder0 export spores.ReadWriters.given export spores.Duplicable export spores.Duplicable.given diff --git a/spores3/src/spores/jvm/SporeJVM.scala b/spores3/src/spores/jvm/SporeJVM.scala deleted file mode 100644 index a65c565..0000000 --- a/spores3/src/spores/jvm/SporeJVM.scala +++ /dev/null @@ -1,32 +0,0 @@ -package spores.jvm - -import scala.annotation.targetName -import upickle.default.ReadWriter - -import spores.* - - -/** Internal API. */ -private[spores] object SporeJVM { - - // The Spore factory only works on the JVM. The generated class here is not a - // top-level class. For this reason, it cannot be reflectively instantiated on - // ScalaJS or ScalaNative. For more information, see: - // https://github.com/portable-scala/portable-scala-reflect. - - inline def apply[T](inline body: T): Spore[T] = { - SporeJVM0.apply(body) - } - - inline def applyWithEnv[E, T](inline env: E)(inline body: E => T)(using rw: Spore[ReadWriter[E]]): Spore[T] = { - apply[E => T](body).withEnv(env) - } - - inline def applyWithCtx[E, T](inline env: E)(inline body: E ?=> T)(using rw: Spore[ReadWriter[E]]): Spore[T] = { - apply[E ?=> T](body).withCtx(env) - } - - inline def auto[T](inline body: T): Spore[T] = { - SporeJVM0.auto(body) - } -} diff --git a/spores3/src/spores/jvm/SporeJVM0.scala b/spores3/src/spores/jvm/SporeJVM0.scala index 5b92ed9..bcb06eb 100644 --- a/spores3/src/spores/jvm/SporeJVM0.scala +++ b/spores3/src/spores/jvm/SporeJVM0.scala @@ -1,6 +1,7 @@ package spores.jvm import spores.* +import scala.quoted.* /** Internal API. */ @@ -11,80 +12,11 @@ private[spores] object SporeJVM0 { // ScalaJS or ScalaNative. For more information, see: // https://github.com/portable-scala/portable-scala-reflect. - inline def apply[F[_], T](inline body: T): Spore0[F, T] = { - ${ applyMacro('body) } + inline def apply[F[_], T](inline captures: Any*)(inline body: T): Spore0[F, T] = { + ${ applyMacro[F, T]('captures, 'body) } } - inline def applyWithEnv[F[_], E, T](inline env: E)(inline body: E => T)(using ev: Spore0[F, F[E]]): Spore0[F, T] = { - apply[F, E => T](body).withEnv(env)(using ev) - } - - inline def applyWithCtx[F[_], E, T](inline env: E)(inline body: E ?=> T)(using ev: Spore0[F, F[E]]): Spore0[F, T] = { - apply[F, E ?=> T](body).withCtx(env)(using ev) - } - - import scala.quoted.* - - private def applyMacro[F[_], T](bodyExpr: Expr[T])(using Type[F], Type[T], Quotes): Expr[Spore0[F, T]] = { - Macros.checkBodyExpr(bodyExpr) - '{ - class Lambda extends SporeLambdaBuilder0[F, T] { - override def body: T = { $bodyExpr } - } - (new Lambda()).build() - } - } - - inline def auto[F[_], T](inline body: T): Spore0[F, T] = { - ${ autoMacro('body) } - } - - import scala.quoted.* - - private def autoMacro[F[_], T](bodyExpr: Expr[T])(using Type[F], Type[T], Quotes): Expr[Spore0[F, T]] = { - import quotes.reflect.* - - // 1. Find all captured symbols - val foundIds = Macros.findCapturedIds(bodyExpr.asTerm) - - // 2. Find all evidence for each captured symbol - // Throws an error if any captured symbol [E] is missing an evidence F[E] in - // implicit scope - val evidence: List[(Tree, Tree)] = Macros.findEvidence[F](foundIds) - - // Group by the captured identifier's full name - val captures: List[List[(Tree, Tree)]] = evidence.groupBy(_._1.symbol.fullName).toList.sortBy(_._1).map(_._2) - - // 3. Lift captured variables into parameters - val liftedExpr: Term = Macros.liftAllSymbolGroups(Symbol.spliceOwner, captures.map(_.map(_._1)).reverse, bodyExpr.asTerm) - - // 4. Construct Spore0 from lifted body - val packed: Expr[Spore0[F, Any]] = '{ - val lambda = { - // TODO: Use type of liftedExpr instead of `Any` here. - class Lambda extends SporeLambdaBuilder0[F, Any] { - override def body = { ${liftedExpr.asExpr} } - } - (new Lambda()) - } - lambda.build() - } - - // 5. Apply captured environment variables - var tmp = packed - for capevi <- captures do { - val cap = capevi.head._1.symbol - val evi = capevi.head._2.asExpr - val env: Expr[Any] = Ref(cap).asExpr - tmp = '{ - $tmp - .asInstanceOf[Spore0[F, Any => Any]] - .withEnv($env)(using $evi.asInstanceOf) - } - } - - '{ - $tmp.asInstanceOf[Spore0[F, T]] - } + private def applyMacro[F[_], T](capturesExpr: Expr[Seq[Any]], bodyExpr: Expr[T])(using Type[F], Type[T], Quotes): Expr[Spore0[F, T]] = { + Macros.spore0ApplyMacro[F, T](capturesExpr, bodyExpr) } } diff --git a/spores3/test/src-jvm/spores/jvm/AutoCaptureErrorTests.scala b/spores3/test/src-jvm/spores/jvm/AutoCaptureErrorTests.scala index 3c19824..ec343c5 100644 --- a/spores3/test/src-jvm/spores/jvm/AutoCaptureErrorTests.scala +++ b/spores3/test/src-jvm/spores/jvm/AutoCaptureErrorTests.scala @@ -26,7 +26,7 @@ object AutoCaptureErrorTestsDefs { // class Outer { outer => // val y = 12 // class Inner { - // def foo = Spore.auto { (x: Int) => x + outer.y } + // def foo = Spore(*) { (x: Int) => x + outer.y } // } // } } @@ -41,7 +41,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ val foo = Foo(12, 13) - Spore.auto { foo } + Spore(*) { foo } """ .exists: _.matches: @@ -53,7 +53,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ val foo = Foo(12, 13) - Spore.auto { (x: Int) => x + foo.x + foo.y } + Spore(*) { (x: Int) => x + foo.x + foo.y } """ .exists: _.matches: @@ -65,7 +65,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ val foo = Foo(12, 13) - Spore.auto { def bar(x: Int): Int = { x + foo.x } } + Spore(*) { def bar(x: Int): Int = { x + foo.x } } """ .exists: _.matches: @@ -79,7 +79,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class A(val a: Int) - Spore.auto { new A(12) } + Spore(*) { new A(12) } """ .exists: _.matches: @@ -91,7 +91,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class A(val a: Int) - Spore.auto { (x: Int) => x + new A(12).a } + Spore(*) { (x: Int) => x + new A(12).a } """ .exists: _.matches: @@ -105,7 +105,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ def captureMeIfYouCan(): Int = 12 - Spore.auto { (x: Int) => x + captureMeIfYouCan() } + Spore(*) { (x: Int) => x + captureMeIfYouCan() } """ .exists: _.matches: @@ -119,7 +119,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Outer { - Spore.auto { this } + Spore(*) { this } } """ .exists: @@ -133,7 +133,7 @@ object AutoCaptureErrorTests extends TestSuite { """ class Outer { val captureThisXIfYouCan = 99 - Spore.auto { this.captureThisXIfYouCan } + Spore(*) { this.captureThisXIfYouCan } } """ .exists: @@ -147,7 +147,7 @@ object AutoCaptureErrorTests extends TestSuite { """ class Outer { val captureThisXIfYouCan = 99 - Spore.auto { (x: Int) => x + captureThisXIfYouCan } + Spore(*) { (x: Int) => x + captureThisXIfYouCan } } """ .exists: @@ -163,7 +163,7 @@ object AutoCaptureErrorTests extends TestSuite { """ case class Bar(x: Int, y: Int) given ReadWriter[Bar] = macroRW[Bar] - given Spore[ReadWriter[Bar]] = Spore.auto { summon[ReadWriter[Bar]] } + given Spore[ReadWriter[Bar]] = Spore(*) { summon[ReadWriter[Bar]] } """ .exists: _.matches: @@ -177,7 +177,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ val opaqueInt = OpaqueInt(12) - Spore.auto { (x: Int) => x + OpaqueInt.unwrap(opaqueInt) } + Spore(*) { (x: Int) => x + OpaqueInt.unwrap(opaqueInt) } """ .exists: _.matches: @@ -193,7 +193,7 @@ object AutoCaptureErrorTests extends TestSuite { class Outer { class Inner { val y = 12 - def foo = Spore.auto { (x: Int) => x + y } + def foo = Spore(*) { (x: Int) => x + y } } } """ @@ -209,7 +209,7 @@ object AutoCaptureErrorTests extends TestSuite { class Outer { val y = 12 class Inner { - def foo = Spore.auto { (x: Int) => x + y } + def foo = Spore(*) { (x: Int) => x + y } } } """ @@ -225,7 +225,7 @@ object AutoCaptureErrorTests extends TestSuite { class Outer { val y = 12 class Inner { - def foo = Spore.auto { (x: Int) => x + Outer.this.y } + def foo = Spore(*) { (x: Int) => x + Outer.this.y } } } """ @@ -241,7 +241,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ val foo = Foo(12, 13) - Spore.auto { + Spore(*) { class Bar { def bar = foo.x + 12 } @@ -259,7 +259,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar(x: Int, y: Int) - Spore.auto { (x: Int) => + Spore(*) { (x: Int) => new Bar(12, 14) } """ @@ -273,7 +273,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar[T](x: T, y: T) - Spore.auto { (x: Int) => + Spore(*) { (x: Int) => new Bar[Int](12, 14) } """ @@ -289,7 +289,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar0 - Spore.auto { + Spore(*) { class FooBar extends Bar0 }.get() """ @@ -303,7 +303,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar1(x: Int, y: Int) - Spore.auto { + Spore(*) { class FooBar extends Bar1(12, 13) } """ @@ -317,7 +317,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar2[T](x: T, y: T) - Spore.auto { + Spore(*) { class FooBar extends Bar2[Int](12, 13) }.get() """ @@ -332,7 +332,7 @@ object AutoCaptureErrorTests extends TestSuite { """ val x = 12 trait Bar3 { def bar: Int = x } - Spore.auto { + Spore(*) { class FooBar extends Foo(12, 13) with Bar3 } """ @@ -346,7 +346,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ trait Bar4[T] { def bar: Int = x } - Spore.auto { + Spore(*) { class FooBar extends Foo(12, 13) with Bar4[Int] }.get() """ @@ -362,7 +362,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ trait Bar - Spore.auto { + Spore(*) { trait FooBar extends Bar }.get() """ @@ -378,7 +378,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ trait Bar - Spore.auto { + Spore(*) { object FooBar extends Bar }.get() """ @@ -394,7 +394,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ enum Bar { case Baz } - Spore.auto { Bar.Baz } + Spore(*) { Bar.Baz } """ .exists: _.matches: @@ -409,7 +409,7 @@ object AutoCaptureErrorTests extends TestSuite { """ sealed trait Bar case class Baz(x: Int, y: Int) extends Bar - Spore.auto { (x: Bar) => x match { + Spore(*) { (x: Bar) => x match { case Baz(a, b) => a + b } } @@ -426,7 +426,7 @@ object AutoCaptureErrorTests extends TestSuite { typeCheckErrorMessages: """ class Bar extends Foo(12, 13) { - def bar = Spore.auto { (x: Int) => x.toString() + super.toString() } + def bar = Spore(*) { (x: Int) => x.toString() + super.toString() } } """ .exists: diff --git a/spores3/test/src-jvm/spores/jvm/AutoCaptureTests.scala b/spores3/test/src-jvm/spores/jvm/AutoCaptureTests.scala index 3015706..f920306 100644 --- a/spores3/test/src-jvm/spores/jvm/AutoCaptureTests.scala +++ b/spores3/test/src-jvm/spores/jvm/AutoCaptureTests.scala @@ -32,10 +32,10 @@ object AutoCaptureTestsDefs { } object FunctionsToReadFromJSON { - def fun0() = Spore.auto { (x: String) => x } - def fun1(y1: String) = Spore.auto { (x: String) => x + y1 } - def fun2(y1: String, y2: String) = Spore.auto { (x: String) => x + y1 + y2 } - def fun3(y1: String, y2: String, y3: List[String]) = Spore.auto { (x: String) => x + y1 + y2 + y3.mkString(",") } + def fun0() = Spore(*) { (x: String) => x } + def fun1(y1: String) = Spore(*) { (x: String) => x + y1 } + def fun2(y1: String, y2: String) = Spore(*) { (x: String) => x + y1 + y2 } + def fun3(y1: String, y2: String, y3: List[String]) = Spore(*) { (x: String) => x + y1 + y2 + y3.mkString(",") } } sealed trait Tree[T] derives ReadWriter @@ -73,7 +73,7 @@ object AutoCaptureTests extends TestSuite { val tests = Tests { test("testCaptureNothing") { - val fun = Spore.auto {} + val fun = Spore(*) {} val unwrapped = writeReadUnwrap(fun) assert(() == unwrapped) } @@ -84,11 +84,11 @@ object AutoCaptureTests extends TestSuite { val a3 = "0123456789-3" val a4 = "0123456789-4" - val fun0 = Spore.auto { (x: String) => x } - val fun1 = Spore.auto { (x: String) => x + a1 } - val fun2 = Spore.auto { (x: String) => x + a1 + a2 } - val fun3 = Spore.auto { (x: String) => x + a1 + a2 + a3 } - val fun4 = Spore.auto { (x: String) => x + a1 + a2 + a3 + a4 } + val fun0 = Spore(*) { (x: String) => x } + val fun1 = Spore(*) { (x: String) => x + a1 } + val fun2 = Spore(*) { (x: String) => x + a1 + a2 } + val fun3 = Spore(*) { (x: String) => x + a1 + a2 + a3 } + val fun4 = Spore(*) { (x: String) => x + a1 + a2 + a3 + a4 } val unwrapped0 = writeReadUnwrap(fun0) val unwrapped1 = writeReadUnwrap(fun1) @@ -116,7 +116,7 @@ object AutoCaptureTests extends TestSuite { test("testCapturedIdentExactlyOnce") { val c = "0123456789" - val fun = Spore.auto { (x: String) => + val fun = Spore(*) { (x: String) => val y = { val z = { x + c @@ -153,13 +153,13 @@ object AutoCaptureTests extends TestSuite { test("testCaptureTree") { val tree = Node(Leaf(1), Node(Leaf(2), Leaf(3))) - val fun = Spore.auto { reduce(tree, (x, y) => x + y) } + val fun = Spore(*) { reduce(tree, (x, y) => x + y) } val unwrapped = writeReadUnwrap(fun) assert(6 == unwrapped) } test("testValDefNoCapture") { - val fun = Spore.auto { (x: Int) => + val fun = Spore(*) { (x: Int) => val y = 12 x + y } @@ -168,7 +168,7 @@ object AutoCaptureTests extends TestSuite { } test("testTypeDefNoCapture") { - val fun = Spore.auto { (y: Int) => + val fun = Spore(*) { (y: Int) => type T = TopLevel new T { override def x = y } } @@ -178,7 +178,7 @@ object AutoCaptureTests extends TestSuite { test("testAsInstanceOfNoCapture") { class Bar(val value: Int) - val fun = Spore.auto { (x: List[Any]) => + val fun = Spore(*) { (x: List[Any]) => x.asInstanceOf[List[Bar]].tail.head.value + x.head.asInstanceOf[Bar].value } val unwrapped = writeReadUnwrap(fun) @@ -187,7 +187,7 @@ object AutoCaptureTests extends TestSuite { test("testMethodTypeParamNoCapture") { class Bar(val value: Int) - val fun = Spore.auto { (x: Any) => + val fun = Spore(*) { (x: Any) => from[Bar](x.asInstanceOf[Bar]).head.value } val unwrapped = writeReadUnwrap(fun) @@ -196,7 +196,7 @@ object AutoCaptureTests extends TestSuite { test("testParameterTypeNoCapture") { class Bar(val value: Int) - val fun = Spore.auto { + val fun = Spore(*) { (l: List[Bar]) => (x: Bar) => val foo: Bar = null @@ -207,7 +207,7 @@ object AutoCaptureTests extends TestSuite { } test("testClassExtendsTopLevelNoCapture") { - val fun = Spore.auto { + val fun = Spore(*) { class FooBar extends Foo(12, 13) { def foo: Foo = this } @@ -225,7 +225,7 @@ object AutoCaptureTests extends TestSuite { val C30 = 0; val C31 = 1; val C32 = 2; val C33 = 3; val C34 = 4; val C35 = 5; val C36 = 6; val C37 = 7; val C38 = 8; val C39 = 9; val C40 = 0; val C41 = 1; val C42 = 2; val C43 = 3; val C44 = 4; val C45 = 5; val C46 = 6; val C47 = 7; val C48 = 8; val C49 = 9; - val fun = Spore.auto { + val fun = Spore(*) { (x00: Int) => (x01: Int) => (x02: Int) => (x03: Int) => (x04: Int) => (x05: Int) => (x06: Int) => (x07: Int) => (x08: Int) => (x09: Int) => (x10: Int) => (x11: Int) => (x12: Int) => (x13: Int) => (x14: Int) => (x15: Int) => (x16: Int) => (x17: Int) => (x18: Int) => (x19: Int) => (x20: Int) => (x21: Int) => (x22: Int) => (x23: Int) => (x24: Int) => (x25: Int) => (x26: Int) => (x27: Int) => (x28: Int) => (x29: Int) => diff --git a/spores3/test/src-jvm/spores/jvm/SporeLambdaErrorTests.scala b/spores3/test/src-jvm/spores/jvm/SporeLambdaErrorTests.scala index f2d93a4..633714c 100644 --- a/spores3/test/src-jvm/spores/jvm/SporeLambdaErrorTests.scala +++ b/spores3/test/src-jvm/spores/jvm/SporeLambdaErrorTests.scala @@ -7,12 +7,17 @@ import spores.default.* import spores.TestUtils.* // // The following code should produce a compile error: -// // Invalid capture of variable `x`. Use the first parameter of a spore's body to refer to the spore's environment.bloop +// // Invalid capture of variable `x`. Add it to the capture list or use `*` to capture all by default.bloop // // ... but reproducing it with the typeCheckErrorMessages macro is not possible as the object needs to be non-nested top-level. // object Issue001: -// def foo(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean] { y => y > x } +// def foo(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean]() { y => y > x } + +object SporeLambdaErrorTestsDefs { + val foo = 12 +} object SporeLambdaErrorTests extends TestSuite { + import SporeLambdaErrorTestsDefs.* val tests = Tests { test("testInvalidCaptureIdent") { @@ -20,21 +25,21 @@ object SporeLambdaErrorTests extends TestSuite { typeCheckErrorMessages: """ val y = 12 - Spore.apply[Int => Int] { x => x + y } + Spore.apply[Int => Int]() { x => x + y } """ .contains: """ - Invalid capture of variable `y`. Use the first parameter of a spore's body to refer to the spore's environment. + Invalid capture of variable `y`. Add it to the capture list or use `*` to capture all by default. """.trim() assert: typeCheckErrorMessages: """ - Spore.apply[Int => Int] { x => Spore.apply[Int => Int] { y => x + y }.get().apply(x) } + Spore.apply[Int => Int]() { x => Spore.apply[Int => Int]() { y => x + y }.get().apply(x) } """ .contains: """ - Invalid capture of variable `x`. Use the first parameter of a spore's body to refer to the spore's environment. + Invalid capture of variable `x`. Add it to the capture list or use `*` to capture all by default. """.trim() } @@ -42,22 +47,22 @@ object SporeLambdaErrorTests extends TestSuite { assert: typeCheckErrorMessages: """ - def fun(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean] { y => y > x } + def fun(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean]() { y => y > x } """ .contains: """ - Invalid capture of variable `x`. Use the first parameter of a spore's body to refer to the spore's environment. + Invalid capture of variable `x`. Add it to the capture list or use `*` to capture all by default. """.trim() assert: typeCheckErrorMessages: """ object ShouldFail: - def fun(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean] { y => y > x } + def fun(x: Int): Spore[Int => Boolean] = Spore.apply[Int => Boolean]() { y => y > x } """ .contains: """ - Invalid capture of variable `x`. Use the first parameter of a spore's body to refer to the spore's environment. + Invalid capture of variable `x`. Add it to the capture list or use `*` to capture all by default. """.trim() } @@ -66,13 +71,13 @@ object SporeLambdaErrorTests extends TestSuite { typeCheckErrorMessages: """ class TestClass { - Spore.apply { () => this.toString() }.get() + Spore.apply() { () => this.toString() }.get() } (new TestClass()) """ .contains: """ - Invalid capture of `this` from outer class. + Invalid capture of `this` from outer class. Add it to the capture list or use `*` to capture all by default. """.trim() assert: @@ -80,12 +85,104 @@ object SporeLambdaErrorTests extends TestSuite { """ class Outer: val x = 12 - Spore.apply { () => 42 * x }.get() + Spore.apply() { () => 42 * x }.get() (new Outer()) """ .contains: """ - Invalid capture of `this` from class Outer. + Invalid capture of `this` from class Outer. Add it to the capture list or use `*` to capture all by default. + """.trim() + } + + test("testInvalidCaptureOfTopLevelMember") { + assert: + typeCheckErrorMessages: + """ + Spore.apply(foo) { (x: Int) => x + foo + 1 }.get() + """ + .contains: + """ + `foo` is not captured by the spore body. Remove it from the capture list. It is a top-level variable or not used in the body. + """.trim() + } + + test("testInvalidCaptureOfUnusedMember") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + Spore.apply(y) { (x: Int) => x + 1 }.get() + """ + .contains: + """ + `y` is not captured by the spore body. Remove it from the capture list. It is a top-level variable or not used in the body. + """.trim() + } + + test("testInvalidCaptureList") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + val z = 13 + Spore.apply(*, y, z) { (x: Int) => x + 1 }.get() + """ + .contains: + """ + Invalid capture list. + """.trim() + } + + test("testInvalidCaptureIdentInlineApply0") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + SporeLambdaTestsDefs.inlineApply0() { (x: Int) => x + y } + """ + .contains: + """ + Invalid capture of variable `y`. Add it to the capture list or use `*` to capture all by default. + """.trim() + } + + test("testInvalidCaptureIdentInlineApplySeq") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + SporeLambdaTestsDefs.inlineApplySeq() { (x: Int) => x + y } + """ + .contains: + """ + Invalid capture of variable `y`. Add it to the capture list or use `*` to capture all by default. + """.trim() + } + + test("testInvalidCaptureListInlineApplySeq") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + val z = 13 + SporeLambdaTestsDefs.inlineApplySeq(y, *, z) { (x: Int) => x + 1 }.get() + """ + .contains: + """ + Invalid capture list. + """.trim() + } + + test("testInvalidCaptureOfUnusedMemberInlineApply1") { + assert: + typeCheckErrorMessages: + """ + val y = 12 + SporeLambdaTestsDefs.inlineApply1(y) { (x: Int) => x + 1 } + """ + .contains: + """ + `y` is not captured by the spore body. Remove it from the capture list. It is a top-level variable or not used in the body. """.trim() } } diff --git a/spores3/test/src-jvm/spores/jvm/SporeLambdaTests.scala b/spores3/test/src-jvm/spores/jvm/SporeLambdaTests.scala index 175a2b5..5139eac 100644 --- a/spores3/test/src-jvm/spores/jvm/SporeLambdaTests.scala +++ b/spores3/test/src-jvm/spores/jvm/SporeLambdaTests.scala @@ -1,6 +1,7 @@ package spores.jvm -import utest._ +import utest.* +import upickle.default.* import spores.default.given import spores.default.* @@ -9,28 +10,61 @@ import spores.TestUtils.* object SporeLambdaTestsDefs { - val lambda = Spore.apply[Int => Boolean] { x => x > 10 } + val lambda = Spore.apply[Int => Boolean]() { x => x > 10 } - val lambdaWithEnv = Spore.applyWithEnv(11) { x => x > 10 } + val lambdaWithEnv = Spore.apply[Int => Boolean]() { x => x > 10 }.withEnv(11) object NestedLambda: - val lambda = Spore.apply[Int => Boolean] { x => x > 10 } + val lambda = Spore.apply[Int => Boolean]() { x => x > 10 } def methodLambda(): Spore[Int => Boolean] = - Spore.apply[Int => Boolean] { x => x > 10 } + Spore.apply[Int => Boolean]() { x => x > 10 } def methodLambdaWithUnnusedArg(x: Int): Spore[Int => Boolean] = - Spore.apply[Int => Boolean] { y => y > 10 } + Spore.apply[Int => Boolean]() { y => y > 10 } inline def inlinedMethodLambda(): Spore[Int => Boolean] = - Spore.apply[Int => Boolean] { x => x > 10 } + Spore.apply[Int => Boolean]() { x => x > 10 } inline def inlinedMethodLambdaWithArg(x: Int): Spore[Int => Boolean] = - Spore.apply[Int => Boolean] { y => y > x } + Spore.apply[Int => Boolean]() { y => y > x } - class ClassWithLambda(): - val lambda = Spore.apply[Int => Boolean] { x => x > 10 } - def methodLambda() = Spore.apply[Int => Boolean] { x => x > 10 } + class ClassWithLambda() { + val lambda = Spore.apply[Int => Boolean]() { x => x > 10 } + def methodLambda() = Spore.apply[Int => Boolean]() { x => x > 10 } + } + + case class Foo(val y: Int) { + import Foo.given + def lambda1 = Spore.apply[Int => Boolean](this) { x => x > this.y } + def lambda2 = Spore.apply[Int => Boolean](y) { x => x > y } + def lambda3 = Spore.apply[Int => Boolean](*) { x => x > this.y } + def lambda4 = Spore.apply[Int => Boolean](*) { x => x > y } + } + object Foo { + given ReadWriter[Foo] = macroRW + given Spore[ReadWriter[Foo]] = Spore(*)(summon) + } + + inline def inlineApply0[T]()(inline body: T): Spore[T] = { + Spore.apply()(body) + } + + inline def inlineApply1[E1, T](inline captures1: E1)(inline body: T)(using ev1: Spore[ReadWriter[E1]]): Spore[T] = { + Spore.apply(captures1)(body) + } + + inline def inlineApply2[E1, E2, T](inline captures1: E1, captures2: E2)(inline body: T)(using ev1: Spore[ReadWriter[E1]], ev2: Spore[ReadWriter[E2]]): Spore[T] = { + Spore.apply(captures1, captures2)(body) + } + + inline def inlineApplySeq[T](inline captures: Any*)(inline body: T): Spore[T] = { + Spore.apply(captures*)(body) + } + + inline def `inlineApply*`[T](inline `*`: Spore0.CaptureAllMode)(inline body: T): Spore[T] = { + Spore.apply(*)(body) + } } object SporeLambdaTests extends TestSuite { @@ -46,21 +80,21 @@ object SporeLambdaTests extends TestSuite { } test("testLambdaWithEnv") { - val predicate9 = Spore.applyWithEnv(9) { x => x > 10 } - val predicate11 = Spore.applyWithEnv(11) { x => x > 10 } + val predicate9 = Spore.apply() { (x: Int) => x > 10 }.withEnv(9) + val predicate11 = Spore.apply() { (x: Int) => x > 10 }.withEnv(11) assert(!predicate9.get()) assert(predicate11.get()) } test("testLambdaWithCtx") { - val predicate9 = Spore.applyWithCtx(9) { summon[Int] > 10 } - val predicate11 = Spore.applyWithCtx(11) { summon[Int] > 10 } + val predicate9 = Spore.apply[Int ?=> Boolean]() { summon[Int] > 10 }.withCtx(9) + val predicate11 = Spore.apply[Int ?=> Boolean]() { summon[Int] > 10 }.withCtx(11) assert(!predicate9.get()) assert(predicate11.get()) } test("testPackBuildHigherOrderLambda") { - val higherLevelFilter = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]] { env => x => if env.get().apply(x) then Some(x) else None } + val higherLevelFilter = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]]() { env => x => if env.get().apply(x) then Some(x) else None } val filter = higherLevelFilter.withEnv(lambda) assert(Some(11) == filter(11)) assert(None == filter(9)) @@ -120,13 +154,13 @@ object SporeLambdaTests extends TestSuite { } test("testLambdaWithOptionEnvironment") { - val packed = Spore.applyWithEnv(Some(11)) { x => x.getOrElse(0) } + val packed = Spore.apply() { (x: Option[Int]) => x.getOrElse(0) }.withEnv(Some(11)) val fun = packed.get() assert(11 == fun) } test("testLambdaWithListEnvironment") { - val packed = Spore.applyWithEnv(List(1, 2, 3)) { x => x.sum } + val packed = Spore.apply() { (x: List[Int]) => x.sum }.withEnv(List(1, 2, 3)) val fun = packed.get() assert(6 == fun) } @@ -172,7 +206,136 @@ object SporeLambdaTests extends TestSuite { assert(fun(11)) assert(!fun(9)) } - + + test("testLambdaCaptureThis") { + val foo10 = Foo(10) + assert(foo10.lambda1.get()(11)) + assert(!foo10.lambda1.get()(9)) + assert(foo10.lambda2.get()(11)) + assert(!foo10.lambda2.get()(9)) + assert(foo10.lambda3.get()(11)) + assert(!foo10.lambda3.get()(9)) + assert(foo10.lambda4.get()(11)) + assert(!foo10.lambda4.get()(9)) + } + + test("testNestedExplicitCapture") { + val c01 = 1; val c02 = 2; val c03 = 3; val c04 = 4; val c05 = 5 + val c06 = 6; val c07 = 7; val c08 = 8; val c09 = 9; val c10 = 10 + val c11 = 11; val c12 = 12; val c13 = 13; val c14 = 14; val c15 = 15 + + val s = Spore.apply(c01, c02, c03, c04, c05, c06, c07, c08, c09, c10, c11, c12, c13, c14, c15) { + (a: Int) => + val s2 = Spore.apply(c01, c02, c03, c04, c05, c06, c07, c08, c09, c10) { + (b: Int) => + val s3 = Spore.apply(c01, c02, c03, c04, c05, c06) { + (c: Int) => + val s4 = Spore.apply(c01, c02, c03) { c01 + c02 + c03 } + s4.get() + c04 + c05 + c06 + c + } + s3.get()(b) + c07 + c08 + c09 + c10 + } + s2.get()(a) + c11 + c12 + c13 + c14 + c15 + } + val res = s.get()(100) + assert(res == 220) + } + + test("testNestedStarCapture") { + val c01 = 1; val c02 = 2; val c03 = 3; val c04 = 4; val c05 = 5 + val c06 = 6; val c07 = 7; val c08 = 8; val c09 = 9; val c10 = 10 + val c11 = 11; val c12 = 12; val c13 = 13; val c14 = 14; val c15 = 15 + + val s = Spore(*) { + (a: Int) => + val s2 = Spore(*) { + (b: Int) => + val s3 = Spore(*) { + (c: Int) => + val s4 = Spore(*) { c01 + c02 + c03 } + s4.get() + c04 + c05 + c06 + c + } + s3.get()(b) + c07 + c08 + c09 + c10 + } + s2.get()(a) + c11 + c12 + c13 + c14 + c15 + } + val res = s.get()(100) + assert(res == 220) + } + + test("testNestedMixedExplicitAndStar") { + val c01 = 1; val c02 = 2; val c03 = 3; val c04 = 4; val c05 = 5 + val c06 = 6; val c07 = 7; val c08 = 8; val c09 = 9; val c10 = 10 + val c11 = 11; val c12 = 12; val c13 = 13; val c14 = 14; val c15 = 15 + + val s = Spore.apply(c01, c02, c03, c04, c05, c06, c07, c08, c09, c10, c11, c12, c13, c14, c15) { + (a: Int) => + val s2 = Spore(*) { + (b: Int) => + val s3 = Spore.apply(c01, c02, c03, c04, c05, c06) { + (c: Int) => + val s4 = Spore(*) { c01 + c02 + c03 } + s4.get() + c04 + c05 + c06 + c + } + s3.get()(b) + c07 + c08 + c09 + c10 + } + s2.get()(a) + c11 + c12 + c13 + c14 + c15 + } + val res = s.get()(100) + assert(res == 220) + } + + test("testInlineApply0") { + val spore = inlineApply0() { (x: Int) => x + 1 } + val fun = spore.get() + assert(fun.apply(12) == 13) + assert(fun.apply(3) == 4) + } + + test("testInlineApply1") { + val y = 12 + val spore = inlineApply1(y) { (x: Int) => x > y } + val fun = spore.get() + assert(fun.apply(13) == true) + assert(fun.apply(11) == false) + } + + test("testInlineApply2") { + val y = 12 + val z = 13 + val spore = inlineApply2(y, z) { (x: Int) => x + y + z } + val fun = spore.get() + assert(fun.apply(11) == 36) + assert(fun.apply(12) == 37) + } + + test("testInlineApplySeq") { + val y = 12 + val z = 13 + val spore = inlineApplySeq(y, z) { (x: Int) => x + y + z } + val fun = spore.get() + assert(fun.apply(11) == 36) + assert(fun.apply(12) == 37) + } + + test("testInlineApplySeq*") { + val y = 12 + val z = 13 + val spore = inlineApplySeq(*) { (x: Int) => x + y + z } + val fun = spore.get() + assert(fun.apply(11) == 36) + assert(fun.apply(12) == 37) + } + + test("testInlineApply*") { + val y = 12 + val z = 13 + val spore = `inlineApply*`(*) { (x: Int) => x + y + z } + val fun = spore.get() + assert(fun.apply(11) == 36) + assert(fun.apply(12) == 37) + } + // Deprecated // test("testSporeApplyWithEnvAlias") { // val spore = Spore.apply[Int, Int => Boolean](12) { env => x => x > env } diff --git a/spores3/test/src-jvm/spores/jvm/SporeSerializationJvmTests.scala b/spores3/test/src-jvm/spores/jvm/SporeSerializationJvmTests.scala index 92f8bfb..56ab758 100644 --- a/spores3/test/src-jvm/spores/jvm/SporeSerializationJvmTests.scala +++ b/spores3/test/src-jvm/spores/jvm/SporeSerializationJvmTests.scala @@ -8,27 +8,27 @@ import spores.default.given object SporeSerializationJvmTestsDefs { - val lambda = Spore.apply[Int => String] { x => x.toString } - val lambdaWithEnv = Spore.applyWithEnv(12) { (env: Int) => (x: Int) => (env + x).toString } - val lambdaWithCtx = Spore.applyWithCtx(12) { summon[Int].toString } - val curriedLambda = Spore.apply[Int => Int => String] { x => y => (x + y).toString() } - val higherOrderLambda = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]] { env => x => + val lambda = Spore.apply[Int => String]() { x => x.toString } + val lambdaWithEnv = Spore.apply() { (env: Int) => (x: Int) => (env + x).toString }.withEnv(12) + val lambdaWithCtx = Spore.apply[Int ?=> String]() { summon[Int].toString }.withCtx(12) + val curriedLambda = Spore.apply[Int => Int => String]() { x => y => (x + y).toString() } + val higherOrderLambda = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]]() { env => x => if env.get().apply(x) then Some(x) else None } - val intPredicate = Spore.apply[Int => Boolean] { x => x > 10 } - val lambdaReturningSpore = Spore.apply[Int => Spore[String]] { x => Spore.value(x.toString()) } + val intPredicate = Spore.apply[Int => Boolean]() { x => x > 10 } + val lambdaReturningSpore = Spore.apply[Int => Spore[String]]() { x => Spore.value(x.toString()) } } object SporeSerializationJvmTestsDeadCode { - val lambda = Spore.apply[Int => String] { x => x.toString } - val lambdaWithEnv = Spore.applyWithEnv(12) { (env: Int) => (x: Int) => (env + x).toString } - val lambdaWithCtx = Spore.applyWithCtx(12) { summon[Int].toString } - val curriedLambda = Spore.apply[Int => Int => String] { x => y => (x + y).toString() } - val higherOrderLambda = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]] { env => x => + val lambda = Spore.apply[Int => String]() { x => x.toString } + val lambdaWithEnv = Spore.apply() { (env: Int) => (x: Int) => (env + x).toString }.withEnv(12) + val lambdaWithCtx = Spore.apply[Int ?=> String]() { summon[Int].toString }.withCtx(12) + val curriedLambda = Spore.apply[Int => Int => String]() { x => y => (x + y).toString() } + val higherOrderLambda = Spore.apply[Spore[Int => Boolean] => Int => Option[Int]]() { env => x => if env.get().apply(x) then Some(x) else None } - val intPredicate = Spore.apply[Int => Boolean] { x => x > 10 } - val lambdaReturningSpore = Spore.apply[Int => Spore[String]] { x => Spore.value(x.toString()) } + val intPredicate = Spore.apply[Int => Boolean]() { x => x > 10 } + val lambdaReturningSpore = Spore.apply[Int => Spore[String]]() { x => Spore.value(x.toString()) } } object SporeSerializationJvmTests extends TestSuite { diff --git a/spores3/test/src-jvm/spores/test/SporeTests.scala b/spores3/test/src-jvm/spores/test/SporeTests.scala index 0a24588..5ec5b32 100644 --- a/spores3/test/src-jvm/spores/test/SporeTests.scala +++ b/spores3/test/src-jvm/spores/test/SporeTests.scala @@ -12,7 +12,7 @@ object SporeTests extends TestSuite { val tests = Tests { test("testWithoutEnv") { - val b = Spore { (x: Int) => x + 2 } + val b = Spore() { (x: Int) => x + 2 } val res = b.get()(3) assert(res == 5) } @@ -20,7 +20,7 @@ object SporeTests extends TestSuite { test("testWithoutEnv2") { def fun(s: Spore[Int => Int]): Unit = {} - val s = Spore((x: Int) => x + 2) + val s = Spore()((x: Int) => x + 2) fun(s) @@ -29,7 +29,7 @@ object SporeTests extends TestSuite { } test("testWithoutEnvWithType") { - val s: Spore[Int => Int] = Spore { + val s: Spore[Int => Int] = Spore() { (x: Int) => x + 2 } val res = s.get()(3) @@ -37,7 +37,7 @@ object SporeTests extends TestSuite { } test("testWithoutEnvWithType1") { - val s: Spore[Int => Int] = Spore { + val s: Spore[Int => Int] = Spore() { x => x + 2 } val res = s.get()(3) @@ -50,13 +50,13 @@ object SporeTests extends TestSuite { [error] | ^ [error] | Found: com.phaller.blocks.Spore[Int, Int]{Env = Int} [error] | Required: com.phaller.blocks.Spore[Int, Int]{Env = Nothing} - [error] 38 | (x: Int) => x + 2 + env + [error] 38 | (x: Int) => x + 2 + y [error] 39 | } */ /*test("testWithoutEnvWithType1") { val y = 5 val s: Spore[Int, Int] { type Env = Nothing } = Spore(y) { - (x: Int) => x + 2 + env + (x: Int) => x + 2 + y } val res = s(3) assert(res == 5) @@ -64,8 +64,8 @@ object SporeTests extends TestSuite { test("testWithEnv") { val y = 5 - val s = Spore.applyWithEnv(y) { - env => (x: Int) => x + env + val s = Spore.apply(y) { + (x: Int) => x + y } val res = s.get()(10) assert(res == 15) @@ -73,15 +73,15 @@ object SporeTests extends TestSuite { /* [error] -- Error: [...]/BlockTests.scala:83:35 - [error] 83 | env => (x: Int) => x + env + z - [error] | ^ - [error] |Invalid capture of variable `z`. Use first parameter of spore's body to refer to the spore's environment. + [error] 83 | (x: Int) => x + y + z + [error] | ^ + [error] |Invalid capture of variable `z`. Add it to the capture list or use `*` to capture all by default. */ /*test("testWithEnvInvalidCapture") { val y = 5 val z = 6 val s = Spore(y) { - env => (x: Int) => x + env + z + (x: Int) => x + y + z } val res = s(10) assert(res == 21) @@ -89,8 +89,8 @@ object SporeTests extends TestSuite { test("testWithEnv2") { val str = "anonymous function" - val s: Spore[Int => Int] = Spore.applyWithEnv(str) { - env => (x: Int) => x + env.length + val s: Spore[Int => Int] = Spore.apply(str) { + (x: Int) => x + str.length } val res = s.get()(10) assert(res == 28) @@ -100,8 +100,8 @@ object SporeTests extends TestSuite { val str = "anonymous function" val i = 5 - val s: Spore[Int => Int] = Spore.applyWithEnv((str, i)) { - case (l, r) => (x: Int) => x + l.length - r + val s: Spore[Int => Int] = Spore.apply(str, i) { + (x: Int) => x + str.length - i } val res = s.get()(10) @@ -112,8 +112,8 @@ object SporeTests extends TestSuite { val str = "anonymous function" val i = 5 - val s = Spore.applyWithEnv((str, i)) { - (l, r) => (x: Int) => x + l.length - r + val s = Spore.apply(str, i) { + (x: Int) => x + str.length - i } val res = s.get()(10) @@ -122,8 +122,8 @@ object SporeTests extends TestSuite { test("testWithEnvWithType") { val y = 5 - val s: Spore[Int => Int] = Spore.applyWithEnv(y) { - env => (x: Int) => x + env + val s: Spore[Int => Int] = Spore.apply(y) { + (x: Int) => x + y } val res = s.get()(11) assert(res == 16) @@ -131,17 +131,17 @@ object SporeTests extends TestSuite { test("testThunk") { val x = 5 - val t = Spore.applyWithEnv(x) { env => () => - env + 7 + val t = Spore.apply(x) { () => + x + 7 } val res = t.get()() assert(res == 12) } test("testNestedWithoutEnv") { - val s = Spore { + val s = Spore() { (x: Int) => - val s2 = Spore { (y: Int) => y - 1 } + val s2 = Spore() { (y: Int) => y - 1 } s2.get()(x) + 2 } val res = s.get()(3) @@ -151,9 +151,9 @@ object SporeTests extends TestSuite { test("testNestedWithEnv1") { val z = 5 - val s = Spore.applyWithEnv(z) { - env => (x: Int) => - val s2 = Spore.applyWithEnv(env) { env => (y: Int) => env + y - 1 } + val s = Spore.apply(z) { + (x: Int) => + val s2 = Spore.apply(z) { (y: Int) => z + y - 1 } s2.get()(x) + 2 } val res = s.get()(3) @@ -164,10 +164,10 @@ object SporeTests extends TestSuite { val z = 5 val w = 6 - val s = Spore.applyWithEnv((w, z)) { - case (l, r) => (x: Int) => - val s2 = Spore.applyWithEnv(r) { env => (y: Int) => env + y - 1 } - s2.get()(x) + 2 - l + val s = Spore.apply(w, z) { + (x: Int) => + val s2 = Spore.apply(z) { (y: Int) => z + y - 1 } + s2.get()(x) + 2 - w } val res = s.get()(3) @@ -177,10 +177,10 @@ object SporeTests extends TestSuite { test("testLocalClasses") { val x = 5 - val s = Spore.applyWithEnv(x) { env => (y: Int) => + val s = Spore.apply(x) { (y: Int) => class Local2 { def m() = y } class Local(p: Int)(using loc: Local2) { - val fld = env + p + val fld = x + p } given l2: Local2 = new Local2