diff --git a/README.md b/README.md index bea30b29..b9600d9d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ See also these [talk slides revised for the current syntax](https://github.com/w #### [Comparison of the chemical machine vs. the coroutines / channels approach (CSP)](docs/chymyst_vs_jc.md#comparison-chemical-machine-vs-csp) -#### [Technical documentation for `Chymyst Core`](docs/chymyst-core.md). +#### [Technical documentation for `Chymyst Core`](docs/chymyst-core.md) ## Example: "dining philosophers" diff --git a/core/src/main/scala/io/chymyst/jc/ConjunctiveNormalForm.scala b/core/src/main/scala/io/chymyst/jc/ConjunctiveNormalForm.scala index d7a93a1b..7de19a8c 100644 --- a/core/src/main/scala/io/chymyst/jc/ConjunctiveNormalForm.scala +++ b/core/src/main/scala/io/chymyst/jc/ConjunctiveNormalForm.scala @@ -1,24 +1,81 @@ package io.chymyst.jc +/** Helper functions that perform computations with Boolean formulas while keeping them in the conjunctive normal form. + * + * A Boolean formula in CNF is represented by a list of lists of an arbitrary type `T`. + * For instance, the Boolean formula (a || b) && (c || d || e) is represented as + * {{{ List( List(a, b), List(c, d, e) ) }}} + * + * These helper methods will compute disjunction, conjunction, and negation of Boolean formulas in CNF, outputting the results also in CNF. + * Simplifications are performed only in so far as to remove exact duplicate terms or clauses such as `a || a` or `(a || b) && (a || b)`. + * + * The type `T` represents primitive Boolean terms that cannot be further simplified or factored to CNF. + * These terms could be represented by expression trees or in another way; the CNF computations do not depend on the representation of terms. + * + * Note that negation such as `! a` is considered to be a primitive term. + * Negation of a conjunction or disjunction, such as `! (a || b)`, can be simplified to CNF. + */ object ConjunctiveNormalForm { - def disjunctionOneTerm[T](a: T, b: List[List[T]]): List[List[T]] = b.map(y => (a :: y).distinct).distinct - def disjunctionOneClause[T](a: List[T], b: List[List[T]]): List[List[T]] = b.map(y => (a ++ y).distinct).distinct + type CNF[T] = List[List[T]] - def disjunction[T](a: List[List[T]], b: List[List[T]]): List[List[T]] = a.flatMap(x => disjunctionOneClause(x, b)).distinct + /** Compute `a || b` where `a` is a single Boolean term and `b` is a Boolean formula in CNF. + * + * @param a Primitive Boolean term that cannot be simplified; does not contain disjunctions or conjunctions. + * @param b A Boolean formula in CNF. + * @tparam T Type of primitive Boolean terms. + * @return The resulting Boolean formula in CNF. + */ + def disjunctionOneTerm[T](a: T, b: CNF[T]): CNF[T] = b.map(y => (a :: y).distinct).distinct - def conjunction[T](a: List[List[T]], b: List[List[T]]): List[List[T]] = (a ++ b).distinct + /** Compute `a || b` where `a` is a single "clause", i.e. a disjunction of primitive Boolean terms, and `b` is a Boolean formula in CNF. + * + * @param a A list of primitive Boolean terms. This list represents a single disjunction clause, e.g. `List(a, b, c)` represents `a || b || c`. + * @param b A Boolean formula in CNF. + * @tparam T Type of primitive Boolean terms. + * @return The resulting Boolean formula in CNF. + */ + def disjunctionOneClause[T](a: List[T], b: CNF[T]): CNF[T] = b.map(y => (a ++ y).distinct).distinct - def negation[T](negationOneTerm: T => T)(a: List[List[T]]): List[List[T]] = a match { + /** Compute `a || b` where `a` and `b` are Boolean formulas in CNF. + * + * @param a A Boolean formula in CNF. + * @param b A Boolean formula in CNF. + * @tparam T Type of primitive Boolean terms. + * @return The resulting Boolean formula in CNF. + */ + def disjunction[T](a: CNF[T], b: CNF[T]): CNF[T] = a.flatMap(x => disjunctionOneClause(x, b)).distinct + + /** Compute `a && b` where `a` and `b` are Boolean formulas in CNF. + * + * @param a A Boolean formula in CNF. + * @param b A Boolean formula in CNF. + * @tparam T Type of primitive Boolean terms. + * @return The resulting Boolean formula in CNF. + */ + def conjunction[T](a: CNF[T], b: CNF[T]): CNF[T] = (a ++ b).distinct + + /** Compute `! a` where `a` is a Boolean formula in CNF. + * + * @param a A Boolean formula in CNF. + * @param negateOneTerm A function that describes the transformation of a primitive term under negation. + * For instance, `negateOneTerm(a)` should return `! a` in the term's appropriate representation. + * @tparam T Type of primitive Boolean terms. + * @return The resulting Boolean formula in CNF. + */ + def negation[T](negateOneTerm: T => T)(a: CNF[T]): CNF[T] = a match { case x :: xs => - val nxs = negation(negationOneTerm)(xs) - x.flatMap(t => disjunctionOneTerm(negationOneTerm(t), nxs)) + val nxs = negation(negateOneTerm)(xs) + x.flatMap(t => disjunctionOneTerm(negateOneTerm(t), nxs)) case Nil => List(List()) // negation of true is false } - def trueConstant[T]: List[List[T]] = List() + /** Represents the constant `true` value in CNF. */ + def trueConstant[T]: CNF[T] = List() - def falseConstant[T]: List[List[T]] = List(List()) + /** Represents the constant `false` value in CNF. */ + def falseConstant[T]: CNF[T] = List(List()) - def oneTerm[T](a: T): List[List[T]] = List(List(a)) + /** Injects a single primitive term into a CNF. */ + def oneTerm[T](a: T): CNF[T] = List(List(a)) } \ No newline at end of file diff --git a/core/src/main/scala/io/chymyst/jc/Core.scala b/core/src/main/scala/io/chymyst/jc/Core.scala index 5afa21d5..f6db4e03 100644 --- a/core/src/main/scala/io/chymyst/jc/Core.scala +++ b/core/src/main/scala/io/chymyst/jc/Core.scala @@ -3,29 +3,23 @@ package io.chymyst.jc import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.atomic.AtomicLong -//import java.util.function.{Function, BiFunction} - import scala.annotation.tailrec -import scala.collection.mutable import scala.util.{Left, Right} /** Syntax helper for zero-argument molecule emitters. + * This trait has a single method, `getUnit`, which returns a value of type `T`, but the only instance will exist if `T` is `Unit` and will return `()`. * - * @tparam A Type of the molecule value. If this is `Unit`, we will have an implicit value of type `TypeIsUnit[A]`, which will provide extra functionality. + * @tparam A Type of the molecule value. If this is `Unit`, we will have an implicit value of type `TypeIsUnit[A]`, which will define `getUnit` to return `()`. */ sealed trait TypeMustBeUnit[A] { - type UnapplyType - def getUnit: A } /** Syntax helper for molecules with unit values. - * A value of [[TypeMustBeUnit]]`[A]` is available only for `A == Unit`. + * An implicit value of [[TypeMustBeUnit]]`[A]` is available only if `A == Unit`. */ object TypeMustBeUnitValue extends TypeMustBeUnit[Unit] { - override type UnapplyType = Boolean - override def getUnit: Unit = () } @@ -70,6 +64,12 @@ object Core { def =!=(other: A): Boolean = self != other } + /** Compute the difference between sequences, enforcing type equality. + * (The standard `diff` method will allow type mismatch, which has lead to an error.) + * + * @param s Sequence whose elements need to be "subtracted". + * @tparam T Type of sequence elements. + */ implicit final class SafeSeqDiff[T](val s: Seq[T]) extends AnyVal { def difff(t: Seq[T]): Seq[T] = s diff t } @@ -78,24 +78,18 @@ object Core { def difff(t: List[T]): List[T] = s diff t } + /** Provide `.toScalaSymbol` method for `String` values. */ implicit final class StringToSymbol(val s: String) extends AnyVal { def toScalaSymbol: scala.Symbol = scala.Symbol(s) } - /** Type alias for reaction body. - * - */ + /** Type alias for reaction body, which is used often. */ private[jc] type ReactionBody = PartialFunction[ReactionBodyInput, Any] - // for M[T] molecules, the value inside AbsMolValue[T] is of type T; for B[T,R] molecules, the value is of type - // ReplyValue[T,R]. For now, we don't use shapeless to enforce this typing relation. - // private[jc] type MoleculeBag = MutableBag[Molecule, AbsMolValue[_]] - private[jc] type MutableLinearMoleculeBag = mutable.Map[Molecule, AbsMolValue[_]] - + /** For each site-wide molecule, this array holds the values of the molecules actually present at the reaction site. + * The `MutableBag` instance may be of different subclass for each molecule. */ private[jc] type MoleculeBagArray = Array[MutableBag[AbsMolValue[_]]] - // private[jc] def moleculeBagToString(mb: MoleculeBag): String = moleculeBagToString(mb.getMap) - private[jc] def moleculeBagToString(mb: Map[Molecule, Map[AbsMolValue[_], Int]]): String = mb.toSeq .map { case (mol, vs) => (s"$mol${pipelineSuffix(mol)}", vs) } @@ -107,17 +101,24 @@ object Core { } }.sorted.mkString(" + ") - private[jc] def moleculeBagToString(inputs: InputMoleculeList): String = - inputs.map { - case (mol, jmv) => s"$mol${pipelineSuffix(mol)}($jmv)" + private[jc] def moleculeBagToString(reaction: Reaction, inputs: InputMoleculeList): String = + inputs.indices.map { i ⇒ + val jmv = inputs(i) + val mol = reaction.info.inputs(i).molecule + s"$mol${pipelineSuffix(mol)}($jmv)" }.toSeq.sorted.mkString(" + ") + /** The pipeline suffix is printed only in certain debug messages; the molecule's [[Molecule.name]] does not include that suffix. + * The reason for this choice is that typically many molecules are automatically pipelined, + * so output messages would be unnecessarily encumbered with the `/P` suffix. + */ private def pipelineSuffix(mol: Molecule): String = if (mol.isPipelined) "/P" else "" + /** Global log of all error and warning messages ever emitted by any reaction site. */ private[jc] val errorLog = new ConcurrentLinkedQueue[String] private[jc] def reportError(message: String): Unit = { @@ -125,11 +126,10 @@ object Core { () } - // TODO: simplify InputMoleculeList to an array of AbsMolValue, eliminating the tuple - we already have the input list and it's fixed for each reaction at compile time - /** List of molecules used as inputs by a reaction. */ - type InputMoleculeList = Array[(Molecule, AbsMolValue[_])] + /** List of molecules used as inputs by a reaction. The molecules are ordered the same as in the reaction input list. */ + type InputMoleculeList = Array[AbsMolValue[_]] - // Type used as argument for ReactionBody. + /** Type used as argument for [[ReactionBody]]. The `Int` value is the index into the [[InputMoleculeList]] array. */ type ReactionBodyInput = (Int, InputMoleculeList) implicit final class EitherMonad[L, R](val e: Either[L, R]) extends AnyVal { diff --git a/core/src/main/scala/io/chymyst/jc/CrossMoleculeSorting.scala b/core/src/main/scala/io/chymyst/jc/CrossMoleculeSorting.scala index 498b0b58..c1c23088 100644 --- a/core/src/main/scala/io/chymyst/jc/CrossMoleculeSorting.scala +++ b/core/src/main/scala/io/chymyst/jc/CrossMoleculeSorting.scala @@ -110,8 +110,16 @@ private[jc] object CrossMoleculeSorting { } +/** Commands used while searching for molecule values among groups of input molecules that are constrained by cross-molecule guards or conditionals. + * A sequence of these commands (the "SearchDSL program") is computed for each reaction by the reaction site. + * SearchDSL programs are interpreted at run time by [[Reaction.findInputMolecules]]. + */ private[jc] sealed trait SearchDSL +/** Choose a molecule value among the molecules available at the reaction site. + * + * @param i Index of the molecule within the reaction input list (the "input index"). + */ private[jc] final case class ChooseMol(i: Int) extends SearchDSL /** Impose a guard condition on the molecule values found so far. @@ -120,4 +128,8 @@ private[jc] final case class ChooseMol(i: Int) extends SearchDSL */ private[jc] final case class ConstrainGuard(i: Int) extends SearchDSL +/** A group of cross-dependent molecules has been closed. + * At this point, we can select one set of molecule values and stop searching for other molecule values within this group. + * If no molecule values are found that satisfy all constraints for this group, the search for the molecule values can be abandoned (the current reaction cannot run). + */ private[jc] case object CloseGroup extends SearchDSL diff --git a/core/src/main/scala/io/chymyst/jc/Macros.scala b/core/src/main/scala/io/chymyst/jc/Macros.scala index 8aeaf910..e80d74ca 100644 --- a/core/src/main/scala/io/chymyst/jc/Macros.scala +++ b/core/src/main/scala/io/chymyst/jc/Macros.scala @@ -1,6 +1,7 @@ package io.chymyst.jc import Core._ +import io.chymyst.jc.ConjunctiveNormalForm.CNF import scala.language.experimental.macros import scala.reflect.macros.blackbox @@ -109,10 +110,13 @@ class CommonMacros(val c: blackbox.Context) { override def patternSha1(showCode: Tree => String): String = getSha1String(showCode(matcher) + showCode(guard.getOrElse(EmptyTree))) } - /** Describes the pattern matcher for output molecules. + /** Describes the pattern matcher for output molecules. This flag is used only within the macro code and is not exported to compiled code. + * The corresponding value of type [[OutputPatternType]] is exported to compiled code of the [[Reaction]] instance. + * * Possible values: * ConstOutputPatternF(x): a(123) or a(Some(4)), etc. * OtherOutputPatternF: a(x), a(x+y), or any other kind of expression. + * EmptyOutputPatternF: no argument given, i.e. bare molecule emitter value or reply emitter value */ sealed trait OutputPatternFlag { val needTraversal: Boolean = false @@ -126,6 +130,14 @@ class CommonMacros(val c: blackbox.Context) { override val toString: String = "?" } + case object EmptyOutputPatternF extends OutputPatternFlag { + override val needTraversal: Boolean = true + override val patternType: OutputPatternType = OtherOutputPattern + + /** The pattern is empty, and most probably will be used with a value, so we print `f(?)` in error messages. */ + override val toString: String = "?" + } + final case class ConstOutputPatternF(v: Tree) extends OutputPatternFlag { // ignore warning about "outer reference in this type test" override val patternType: OutputPatternType = ConstOutputPattern(v) @@ -190,28 +202,31 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac maybeError("Input guard", "matches on additional input molecules", guardIn.map(_._1.name)) maybeError("Input guard", "emit any output molecules", guardOut.map(_._1.name), "not") maybeError("Input guard", "perform any reply actions", guardReply.map(_._1.name), "not") - maybeError("Input guard", "uses other molecules inside molecule value patterns", wrongMoleculesInGuard) + maybeError("Input guard", "matches on molecules", wrongMoleculesInGuard.map(_.name)) val (bodyIn, bodyOut, bodyReply, wrongMoleculesInBody) = moleculeInfoMaker.from(body) // bodyIn should be empty maybeError("Reaction body", "matches on additional input molecules", bodyIn.map(_._1.name)) - maybeError("Reaction body", "uses other molecules inside molecule value patterns", wrongMoleculesInBody) + maybeError("Reaction body", "matches on molecules", wrongMoleculesInBody.map(_.name)) // Blocking molecules should not be used under nontrivial output environments. val nontrivialEmittedBlockingMoleculeStrings = bodyOut .filter(_._1.typeSignature <:< weakTypeOf[B[_, _]]) .filter(_._3.exists(!_.linear)) - .map { case (molSymbol, flag, _) => s"${molSymbol.name}($flag)" } + .map { case (molSymbol, flag, _) => s"molecule ${molSymbol.name}($flag)" } maybeError("Reaction body", "emit blocking molecules inside function blocks", nontrivialEmittedBlockingMoleculeStrings, "not") // Reply emitters should not be used under nontrivial output environments. val nontrivialEmittedRepliesStrings = bodyReply - .filter(_._3.exists(!_.linear)) - .map { case (molSymbol, flag, _) => s"${molSymbol.name}($flag)" } + .filter{ + case (_, EmptyOutputPatternF, _) ⇒ true + case (_, _, envs) ⇒ envs.exists(!_.linear) + } + .map { case (molSymbol, flag, _) => s"reply emitter ${molSymbol.name}($flag)" } maybeError("Reaction body", "use reply emitters inside function blocks", nontrivialEmittedRepliesStrings, "not") - // TODO: Reply emitters should be used only once. This depends on proper shrinkage. + // TODO: Code should check at compile time that each reply emitter is used only once. This depends on proper shrinkage. - val guardCNF: List[List[Tree]] = convertToCNF(guard) // Conjunctive normal form of the guard condition. In this CNF, `true` is List() and `false` is List(List()). + val guardCNF: CNF[Tree] = convertToCNF(guard) // Conjunctive normal form of the guard condition. In this CNF, `true` is List() and `false` is List(List()). // If any of the CNF clauses is empty, the entire guard is identically `false`. This is an error condition: reactions should not be permanently prohibited. if (guardCNF.exists(_.isEmpty)) { @@ -342,7 +357,7 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac val blockingMolecules = patternIn.filter(_._3.nonEmpty) // It is an error to have blocking molecules that do not match on a simple variable. val wrongBlockingMolecules = blockingMolecules.filter(_._3.get.notReplyValue).map(_._1) - maybeError("Blocking input molecules", "matches a reply emitter with anything else than a simple variable", wrongBlockingMolecules) + maybeError("Blocking input molecules", "matches a reply emitter with a simple variable", wrongBlockingMolecules.map(ms ⇒ s"molecule ${ms.name}"), "contain a pattern that") // If we are here, all reply emitters have correct pattern variables. Now we check that each blocking molecule has one and only one reply. val bodyReplyInfoMacro = bodyReply.map { case (m, p, envs) => (m, p.patternType, envs) } @@ -358,8 +373,8 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac val blockingMoleculesWithoutReply = expectedBlockingReplies difff shrunkGuaranteedReplies val blockingMoleculesWithMultipleReply = shrunkPossibleReplies difff expectedBlockingReplies - maybeError("Blocking molecules", "but no unconditional reply found for", blockingMoleculesWithoutReply, "receive a reply") - maybeError("Blocking molecules", "but possibly multiple replies found for", blockingMoleculesWithMultipleReply, "receive only one reply") + maybeError("Blocking molecules", "but no unconditional reply found for", blockingMoleculesWithoutReply.map(ms ⇒ s"reply emitter $ms"), "receive a reply") + maybeError("Blocking molecules", "but possibly multiple replies found for", blockingMoleculesWithMultipleReply.map(ms ⇒ s"reply emitter $ms"), "receive only one reply") if (patternIn.isEmpty && !isStaticReaction(pattern, guard, body)) // go { case x => ... } reportError(s"Reaction input must be `_` or must contain some input molecules, but is ${showCode(pattern)}") @@ -379,7 +394,7 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac // However, the order of output molecules corresponds to the order in which they might be emitted. val allOutputInfo = bodyOut // Output molecule info comes only from the body since neither the pattern nor the guard can emit output molecules. - val outputMoleculesReactionInfo = allOutputInfo.map { case (m, p, envs) => q"OutputMoleculeInfo(${m.asTerm}, $p, ${envs.reverse})" }.toArray + val outputMoleculesReactionInfo = allOutputInfo.map { case (m, p, envs) => q"OutputMoleculeInfo(${m.asTerm}, ${p.patternType}, ${envs.reverse})" }.toArray val outputMoleculeInfoMacro = allOutputInfo.map { case (m, p, envs) => (m, p.patternType, envs) } @@ -398,7 +413,7 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac // That is, only if all matchers are trivial, and if the guard is absent. // Then it is sufficient to take the shrunk output info list and to see whether enough output molecules are present to cover all input molecules. if (isGuardAbsent && allInputMatchersAreTrivial && inputMoleculesAreSubsetOfOutputMolecules) { - maybeError("Unconditional livelock: Input molecules", "output molecules, with all trivial matchers for", patternIn.map(_._1.asTerm.name.decodedName), "not be a subset of") + maybeError("Unconditional livelock: Input molecules", "output molecules, with all trivial matchers for", patternIn.map(_._1.name), "not be a subset of") } // Compute reaction sha1 from simplified input list. @@ -422,16 +437,19 @@ final class BlackboxMacros(override val c: blackbox.Context) extends ReactionMac object Macros { - /** Return the raw expression tree. This macro is used only for testing. + /** Return the raw expression tree. This macro is used only in tests, to verify that certain expression trees are what the code expects them to be. * * @param x Any scala expression. The expression will not be evaluated. * @return The raw syntax tree object (after typer) corresponding to the expression. */ private[jc] def rawTree(x: Any): String = macro CommonMacros.rawTreeImpl - /** This macro is not actually used. - * It serves only for testing the mechanism by which we detect the name of the enclosing value. - * For example, `val myVal = { 1; 2; 3; getName }` returns the string "myVal". + /** Determine the name of the enclosing value. + * For example, `val myVal = { 1; 2; 3; getName }` returns the string `"myVal"`. + * + * This works only for simple values, but not for pattern-matched values such as `val (x,y) = (getName, getName)`. + * + * This macro is used only in tests, to check the mechanism by which we detect the name of the enclosing value. * * @return The name of the enclosing value as string. */ diff --git a/core/src/main/scala/io/chymyst/jc/Molecules.scala b/core/src/main/scala/io/chymyst/jc/Molecules.scala index cc953484..e5da4541 100644 --- a/core/src/main/scala/io/chymyst/jc/Molecules.scala +++ b/core/src/main/scala/io/chymyst/jc/Molecules.scala @@ -13,7 +13,7 @@ import scala.concurrent.duration.Duration * {{{go { case a(MyCaseClass(x, y)) + b(Some(z)) if x > z => ... } }}} * * The chemical notation should be used only with the left-associative `+` operator grouped to the left. - * Input patterns with a right-associative grouping of the `+` operator, for example `a(x) + ( b(y) + c(z) )`, are refused. + * Input patterns with a right-associative grouping of the `+` operator, for example `a(x) + ( b(y) + c(z) )`, are rejected at compile time. */ object + { def unapply(inputs: ReactionBodyInput): Option[(ReactionBodyInput, ReactionBodyInput)] = { @@ -27,57 +27,54 @@ object + { * @tparam T Type of the molecule value. */ private[jc] sealed trait AbsMolValue[T] { - private[jc] def getValue: T + private[jc] def moleculeValue: T /** The hash code of an [[AbsMolValue]] should not depend on anything but the wrapped value (of type `T`). * However, extending [[PersistentHashCode]] leads to errors! * (See the test "correctly store several molecule copies in a MutableQueueBag" in `ReactionSiteSpec.scala`.) * Therefore, we override the `hashCode` directly here, and make it a `lazy val`. */ - override lazy val hashCode: Int = getValue.hashCode() + override lazy val hashCode: Int = moleculeValue.hashCode() /** String representation of molecule values will omit printing the `Unit` values but print all other types normally. * * @return String representation of molecule value of type T. Unit values are printed as empty strings. */ - override final def toString: String = getValue match { + override final def toString: String = moleculeValue match { case () => "" case v => v.toString } - /** Checks whether the reaction has sent no reply to this molecule. This check is meaningful only after the reaction body has finished evaluating. - * This check does not make sense for non-blocking molecules. - * This method is in the parent trait only because we would like to check for missing replies faster, - * without pattern-matching on blocking vs non-blocking molecules. + /** Checks whether the reaction has sent no reply to this molecule, and also that there was no error and no timeout with reply. + * This check is meaningful only for blocking molecules and only after the reaction body has finished evaluating. * * @return `true` if the reaction has failed to send a reply to this instance of the blocking molecule. - * Will also return `false` if the molecule is not a blocking molecule. + * Will also return `false` if this molecule is not a blocking molecule. */ + // This method is in the parent trait only because we would like to check for missing replies faster, + // without pattern-matching on blocking vs non-blocking molecules. private[jc] def reactionSentNoReply: Boolean = false } /** Container for the value of a non-blocking molecule. * - * @param v The value of type T carried by the molecule. * @tparam T The type of the value. */ -private[jc] final case class MolValue[T](v: T) extends AbsMolValue[T] { - override private[jc] def getValue: T = v -} +private[jc] final case class MolValue[T](private[jc] val moleculeValue: T) extends AbsMolValue[T] /** Container for the value of a blocking molecule. * The `hashCode` of a [[BlockingMolValue]] should depend only on the `hashCode` of the value `v`, * and not on the reply value (which is mutable). This is now implemented in the parent trait [[AbsMolValue]]. * - * @param v The value of type T carried by the molecule. * @param replyValue The wrapper for the reply value, which will ultimately return a value of type R. * @tparam T The type of the value carried by the molecule. * @tparam R The type of the reply value. */ -private[jc] final case class BlockingMolValue[T, R](v: T, replyValue: AbsReplyValue[T, R]) extends AbsMolValue[T] { - override private[jc] def getValue: T = v - - override private[jc] def reactionSentNoReply: Boolean = replyValue.noReplyAttemptedYet // no value, no error, and no timeout +private[jc] final case class BlockingMolValue[T, R]( + private[jc] val moleculeValue: T, + replyValue: AbsReplyEmitter[T, R] +) extends AbsMolValue[T] { + override private[jc] def reactionSentNoReply: Boolean = replyValue.noReplyAttemptedYet // `true` if no value, no error, and no timeout } /** Abstract trait representing a molecule emitter. @@ -97,8 +94,6 @@ sealed trait Molecule extends PersistentHashCode { def index: Int = inputIndex - override def toString: String = (if (name.isEmpty) "" else name) + (if (isBlocking) "/B" else "") - /** This is called by a [[ReactionSite]] when a molecule becomes bound to that reaction site. * * @param rs Reaction site to which the molecule is now bound. @@ -131,6 +126,7 @@ sealed trait Molecule extends PersistentHashCode { else None + // All these variables will be assigned exactly once and will never change thereafter. It's not clear how best to enforce this in Scala. private var valIsPipelined: Boolean = false protected var reactionSiteWrapper: ReactionSiteWrapper[_, _] = ReactionSiteWrapper.noReactionSite(this) @@ -141,13 +137,12 @@ sealed trait Molecule extends PersistentHashCode { protected var hasReactionSite: Boolean = false - /** The set of reactions that can consume this molecule. + /** The list of reactions that can consume this molecule. * - * @return `None` if the molecule emitter is not yet bound to any reaction site. + * Will be empty if the molecule emitter is not yet bound to any reaction site. + * This value is used only for static analysis. */ - final private[jc] def consumingReactions: Option[Array[Reaction]] = if (isBound) - Some(reactionSiteWrapper.consumingReactions) - else None + private[jc] lazy val consumingReactions: Array[Reaction] = reactionSiteWrapper.consumingReactions /** The set of all reactions that *potentially* emit this molecule as output. * Some of these reactions may evaluate a run-time condition to decide whether to emit the molecule; so emission is not guaranteed. @@ -155,13 +150,18 @@ sealed trait Molecule extends PersistentHashCode { * Note that these reactions may be defined in any reaction sites, not necessarily at the site to which this molecule is bound. * The set of these reactions may change at run time if new reaction sites are written that output this molecule. * + * This is used only during static analysis. This cannot be made a `lazy val` since static analysis can proceed before all emitting reactions are known. + * Static analysis may be incomplete if that happens; but we can do little about this, since reaction sites are activated at run time. + * * @return Empty set if the molecule is not yet bound to any reaction site. */ final private[jc] def emittingReactions: Set[Reaction] = emittingReactionsSet.toSet private val emittingReactionsSet: mutable.Set[Reaction] = mutable.Set() - // This is called by the reaction site only during the initial setup. Once the reaction site is activated, the set of emitting reactions will never change. + // This is called by the reaction site only during the initial setup. + // Each reaction site will add emitting reactions to all molecules it emits, including molecules bound to other reaction sites. + // Once all reaction sites are activated, the set of emitting reactions for this molecule will never change. final private[jc] def addEmittingReaction(r: Reaction): Unit = { emittingReactionsSet += r () @@ -175,6 +175,12 @@ sealed trait Molecule extends PersistentHashCode { /** This is `lazy` because we will only know whether this molecule is static after this molecule is bound to a reaction site, at run time. */ lazy val isStatic: Boolean = false + + /** Prints a molecule's displayed name and a `/B` suffix for blocking molecules. + * + * @return A molecule's displayed name as string. + */ + override def toString: String = (if (name.isEmpty) "" else name) + (if (isBlocking) "/B" else "") // This can't be a lazy val because `isBlocking` is overridden in derived classes. } /** Non-blocking molecule class. Instance is mutable until the molecule is bound to a reaction site and until all reactions involving this molecule are declared. @@ -186,7 +192,7 @@ final class M[T](val name: String) extends (T => Unit) with Molecule { def unapply(arg: ReactionBodyInput): Option[T] = { val (index, inputMoleculeList) = arg - inputMoleculeList.lift(index).map(_._2.asInstanceOf[MolValue[T]].v) + inputMoleculeList.lift(index).map(_.asInstanceOf[MolValue[T]].moleculeValue) } /** Emit a non-blocking molecule. @@ -210,16 +216,15 @@ final class M[T](val name: String) extends (T => Unit) with Molecule { else throw new Exception("Molecule c is not bound to any reaction site") private[jc] def assignStaticMolVolatileValue(molValue: AbsMolValue[_]) = - volatileValueContainer = molValue.asInstanceOf[MolValue[T]].getValue + volatileValueContainer = molValue.asInstanceOf[MolValue[T]].moleculeValue @volatile private var volatileValueContainer: T = _ - override lazy val isStatic: Boolean = isBound && - reactionSiteWrapper.staticMolsDeclared.contains(this) + override lazy val isStatic: Boolean = reactionSiteWrapper.staticMolsDeclared.contains(this) override private[jc] def setReactionSiteInfo(rs: ReactionSite, index: Int, valType: Symbol, pipelined: Boolean) = { super.setReactionSiteInfo(rs, index, valType, pipelined) - reactionSiteWrapper = rs.makeWrapper[T, Unit](this) + reactionSiteWrapper = rs.makeWrapper[T, Unit](this) // need to specify types for `makeWrapper`; set `Unit` instead of `R` for non-blocking molecules } } @@ -227,7 +232,7 @@ final class M[T](val name: String) extends (T => Unit) with Molecule { * Initially, the status is [[HaveReply]] with a `null` value. * Reply is successful if the emitting call does not time out. In this case, we have a reply value. * This is represented by [[HaveReply]] with a non-null value. - * If the reply times out, there is still no reply value. This is represented by the AtomicBoolean flag [[AbsReplyValue.hasTimedOut]] set to `true`. + * If the reply times out, there is still no reply value. This is represented by the AtomicBoolean flag [[AbsReplyEmitter.hasTimedOut]] set to `true`. * If the reaction finished but did not reply, it is an error condition. If the reaction finished and replied more than once, it is also an error condition. * After a reaction fails to reply, the emitting closure will put an error message into the status for that molecule. This is represented by [[ErrorNoReply]]. * When a reaction replies more than once, it is too late to put an error message into the status for that molecule. So we do not have a status value for this situation. @@ -248,12 +253,12 @@ private[jc] final case class ErrorNoReply(message: String) extends ReplyStatus */ private[jc] final case class HaveReply[R](result: R) extends ReplyStatus -/** This trait contains the implementations of most methods for [[ReplyValue]] class. +/** This trait contains the implementations of most methods for [[ReplyEmitter]] class. * * @tparam T Type of the value that the molecule carries. * @tparam R Type of the reply value. */ -private[jc] sealed trait AbsReplyValue[T, R] { +private[jc] sealed trait AbsReplyEmitter[T, R] { @volatile private var replyStatus: ReplyStatus = HaveReply[R](null.asInstanceOf[R]) // the `null` and the typecast will not be used because `replyStatus` will be either overwritten or ignored on timeout. This avoids a third case class in ReplyStatus, and the code can now be completely covered by tests. @@ -312,10 +317,10 @@ private[jc] sealed trait AbsReplyValue[T, R] { * The reply value will be received by the process that emitted the blocking molecule, and will unblock that process. * The reply value will not be received if the emitting process timed out on the blocking call, or if the reply was already made (then it is an error to reply again). * - * @param x Value to reply with. + * @param r Value to reply with. * @return `true` if the reply was received normally, `false` if it was not received due to one of the above conditions. */ - final protected def performReplyAction(x: R): Boolean = { + final protected def performReplyAction(r: R): Boolean = { // TODO: simplify this code under the assumption that repeated replies are impossible val replyWasNotRepeated = hasReply.compareAndSet(false, true) @@ -328,7 +333,7 @@ private[jc] sealed trait AbsReplyValue[T, R] { // After acquiring this semaphore, it is safe to read and modify `replyStatus`. // The reply value will be assigned only if there was no timeout and no previous reply action. - replyStatus = HaveReply(x) + replyStatus = HaveReply(r) releaseSemaphoreForEmitter() // Unblock the reaction that emitted this blocking molecule. // That reaction will now set reply status and release semaphoreForReplyStatus again. @@ -340,32 +345,32 @@ private[jc] sealed trait AbsReplyValue[T, R] { } /** This is similar to [[performReplyAction]] except that user did not request the timeout checking, so we have fewer semaphores to deal with. */ - final protected def performReplyActionWithoutTimeoutCheck(x: R): Unit = { + final protected def performReplyActionWithoutTimeoutCheck(r: R): Unit = { val replyWasNotRepeated = hasReply.compareAndSet(false, true) if (replyWasNotRepeated) { // We have not yet tried to reply. - replyStatus = HaveReply(x) + replyStatus = HaveReply(r) releaseSemaphoreForEmitter() // Unblock the reaction that emitted this blocking molecule. } } } -/** Reply-value wrapper for blocking molecules. This is a mutable class. +/** Reply emitter for blocking molecules. This is a mutable class (the mutable parts are inherited from [[AbsReplyEmitter]]). * * @tparam T Type of the value carried by the molecule. * @tparam R Type of the value replied to the caller via the "reply" action. */ -private[jc] final class ReplyValue[T, R] extends (R => Unit) with AbsReplyValue[T, R] { +private[jc] final class ReplyEmitter[T, R] extends (R => Unit) with AbsReplyEmitter[T, R] { /** Perform a reply action for a blocking molecule without checking the timeout status (this is slightly faster). * For each blocking molecule consumed by a reaction, exactly one reply action should be performed within the reaction body. * If a timeout occurred after the reaction body started evaluating but before the reply action was performed, the reply value will not be actually sent anywhere. * This method will not fail in that case, but since it returns `Unit`, the user will not know whether the reply succeeded. * - * @param x Value to reply with. + * @param r Value to reply with. * @return Unit value, regardless of whether the reply succeeded before timeout. */ - def apply(x: R): Unit = performReplyActionWithoutTimeoutCheck(x) + def apply(r: R): Unit = performReplyActionWithoutTimeoutCheck(r) /** Same but for molecules with type `R = Unit`. */ def apply()(implicit arg: TypeMustBeUnit[R]): Unit = apply(arg.getUnit) @@ -375,10 +380,10 @@ private[jc] final class ReplyValue[T, R] extends (R => Unit) with AbsReplyValue[ * If a timeout occurred after the reaction body started evaluating but before the reply action was performed, the reply value will not be actually sent anywhere. * This method will return `false` in that case. * - * @param x Value to reply with. + * @param r Value to reply with. * @return `true` if the reply was successful, `false` if the blocking molecule timed out, or if a reply action was already performed. */ - def checkTimeout(x: R): Boolean = performReplyAction(x) + def checkTimeout(r: R): Boolean = performReplyAction(r) /** Same as [[checkTimeout]] above but for molecules with type `R = Unit`, with shorter syntax. */ def checkTimeout()(implicit arg: TypeMustBeUnit[R]): Boolean = checkTimeout(arg.getUnit) @@ -391,7 +396,6 @@ private[jc] final class ReplyValue[T, R] extends (R => Unit) with AbsReplyValue[ * @tparam R Type of the value replied to the caller via the "reply" action. */ final class B[T, R](val name: String) extends (T => R) with Molecule { - override val isBlocking = true /** Emit a blocking molecule and receive a value when the reply action is performed, unless a timeout is reached. @@ -401,9 +405,9 @@ final class B[T, R](val name: String) extends (T => R) with Molecule { * @return Non-empty option if the reply was received; None on timeout. */ def timeout(v: T)(duration: Duration): Option[R] = reactionSiteWrapper.asInstanceOf[ReactionSiteWrapper[T, R]] - .emitAndAwaitReplyWithTimeout(duration.toNanos, this, v, new ReplyValue[T, R]) + .emitAndAwaitReplyWithTimeout(duration.toNanos, this, v, new ReplyEmitter[T, R]) - /** Same but for molecules with type `T = Unit`, with shorter syntax. */ + /** Same but for molecules with type `T = Unit`; enables shorter syntax `b().timeout(1.second)`. */ def timeout()(duration: Duration)(implicit arg: TypeMustBeUnit[T]): Option[R] = timeout(arg.getUnit)(duration) /** Perform the unapply matching and return a wrapped ReplyValue on success. @@ -411,11 +415,11 @@ final class B[T, R](val name: String) extends (T => R) with Molecule { * @param arg The input molecule list, which should be a one-element list. * @return None if there was no match; Some(...) if the reaction inputs matched. */ - def unapply(arg: ReactionBodyInput): Option[(T, ReplyValue[T, R])] = { + def unapply(arg: ReactionBodyInput): Option[(T, ReplyEmitter[T, R])] = { val (index, inputMoleculeList) = arg inputMoleculeList.lift(index) - .map(_._2.asInstanceOf[BlockingMolValue[T, R]]) - .map { bmv => (bmv.v, bmv.replyValue.asInstanceOf[ReplyValue[T, R]]) } + .map(_.asInstanceOf[BlockingMolValue[T, R]]) + .map { bmv => (bmv.moleculeValue, bmv.replyValue.asInstanceOf[ReplyEmitter[T, R]]) } } /** Emit a blocking molecule and receive a value when the reply action is performed. @@ -424,14 +428,14 @@ final class B[T, R](val name: String) extends (T => R) with Molecule { * @return The "reply" value. */ def apply(v: T): R = reactionSiteWrapper.asInstanceOf[ReactionSiteWrapper[T, R]] - .emitAndAwaitReply(this, v, new ReplyValue[T, R]) + .emitAndAwaitReply(this, v, new ReplyEmitter[T, R]) - /** This enables the short syntax `b()` and will only work when `T == Unit`. */ + /** This enables the short syntax `b()` instead of `b(())`, and will only work when `T == Unit`. */ def apply()(implicit arg: TypeMustBeUnit[T]): R = apply(arg.getUnit) override private[jc] def setReactionSiteInfo(rs: ReactionSite, index: Int, valType: Symbol, pipelined: Boolean) = { super.setReactionSiteInfo(rs, index, valType, pipelined) - reactionSiteWrapper = rs.makeWrapper[T, R](this) + reactionSiteWrapper = rs.makeWrapper[T, R](this) // need to specify types for `makeWrapper` } } diff --git a/core/src/main/scala/io/chymyst/jc/Pool.scala b/core/src/main/scala/io/chymyst/jc/Pool.scala index 1556af3d..246b022b 100644 --- a/core/src/main/scala/io/chymyst/jc/Pool.scala +++ b/core/src/main/scala/io/chymyst/jc/Pool.scala @@ -55,20 +55,3 @@ private[jc] class PoolExecutor(threads: Int = 8, execFactory: Int => (ExecutorSe override def runRunnable(runnable: Runnable): Unit = execService.execute(runnable) } - -/** Create a pool from a Handler interface. The pool will submit tasks using a Handler.post() method. - * - * This is useful for Android and JavaFX environments. Not yet tested. Behavior with static molecules will be probably wrong. - * - * Note: the structural type for `handler` should not be used. Here it is used only as an illustration. - */ -/* -class HandlerPool(handler: { def post(r: Runnable): Unit }) extends Pool { - override def shutdownNow(): Unit = () - - override def runClosure(closure: => Unit, info: ReactionInfo): Unit = - handler.post(new RunnableWithInfo(closure, info)) - - override def isInactive: Boolean = false -} -*/ diff --git a/core/src/main/scala/io/chymyst/jc/Reaction.scala b/core/src/main/scala/io/chymyst/jc/Reaction.scala index ac62a2ff..d8a96ad6 100644 --- a/core/src/main/scala/io/chymyst/jc/Reaction.scala +++ b/core/src/main/scala/io/chymyst/jc/Reaction.scala @@ -151,7 +151,7 @@ private[jc] object OutputEnvironment { private[jc] type OutputItem[T] = (T, OutputPatternType, List[OutputEnvironment]) private[jc] type OutputList[T] = List[OutputItem[T]] - private[jc] def shrink[T](outputs: OutputList[T], equals: (Any, Any) => Boolean = (a, b) => a === b): OutputList[T] = { + private[jc] final def shrink[T](outputs: OutputList[T], equals: (Any, Any) => Boolean = (a, b) => a === b): OutputList[T] = { outputs.foldLeft[(OutputList[T], OutputList[T])]((Nil, outputs)) { (accOutputs, outputInfo) => val (outputList, remaining) = accOutputs if (remaining contains outputInfo) { @@ -306,13 +306,13 @@ final case class InputMoleculeInfo(molecule: Molecule, index: Int, flag: InputPa case WildcardInput | SimpleVarInput(_, None) => true case SimpleVarInput(v, Some(cond)) => - cond.isDefinedAt(molValue.getValue) + cond.isDefinedAt(molValue.moleculeValue) case ConstInputPattern(v) => - v === molValue.getValue + v === molValue.moleculeValue case OtherInputPattern(_, _, true) => true case OtherInputPattern(matcher, _, _) => - matcher.isDefinedAt(molValue.getValue) + matcher.isDefinedAt(molValue.moleculeValue) } private[jc] val isSimpleType: Boolean = simpleTypes contains valType @@ -438,7 +438,7 @@ final case class InputMoleculeInfo(molecule: Molecule, index: Int, flag: InputPa v.name case SimpleVarInput(v, Some(_)) => s"${v.name} if ?" - // case ConstInputPattern(()) => "" // We eliminated this case by converting constants of Unit type to Wildcard input flag. + // case ConstInputPattern(()) => "" // This case was eliminated by converting constants of Unit type to Wildcard input flag. case ConstInputPattern(c) => c.toString case OtherInputPattern(_, vars, isIrrefutable) => @@ -529,7 +529,7 @@ final class ReactionInfo( .toArray } - /** Cross-conditionals are repeated input molecules, such that one of them has a conditional or participates in a cross-molecule guard. + /** "Cross-conditionals" are repeated input molecules, such that one of them has a conditional or participates in a cross-molecule guard. * This value holds the set of input indices for all such molecules, for quick access. */ private[jc] val crossConditionalsForRepeatedMols: Set[Int] = repeatedCrossConstrainedMolecules @@ -744,7 +744,7 @@ final case class Reaction( val newValueOpt = if (inputInfo.molecule.isPipelined) molBag.takeOne.filter(inputInfo.admitsValue) // For pipelined molecules, we take the first one; if condition fails, we treat that case as if no molecule is available. - // It is probably useless to try optimizing the selection of a constant value, because 1) values are wrapped and 2) values that are not "simple types" are most likely to be stored in a linear container. + // It is probably useless to try optimizing the selection of a constant value, because 1) values are wrapped and 2) values that are not "simple types" are most likely to be stored in a queue-based molecule bag rather than in a hash map-based molecule bag. else molBag.find(inputInfo.admitsValue) @@ -816,7 +816,7 @@ final case class Reaction( case ConstrainGuard(i) ⇒ val guard = info.crossGuards(i) Some(repeatedMolValuesStream.filter { _ ⇒ - guard.cond.isDefinedAt(guard.indices.map(i ⇒ foundValues(i).getValue).toList) + guard.cond.isDefinedAt(guard.indices.map(i ⇒ foundValues(i).moleculeValue).toList) }) case CloseGroup ⇒ @@ -828,118 +828,9 @@ final case class Reaction( } } if (foundResult) - Some((this, Array.tabulate(foundValues.length)(i ⇒ (info.inputs(i).molecule, foundValues(i))))) + Some((this, foundValues)) else None } - /* - private[jc] def oldfindInputMolecules(moleculesPresent: MoleculeBagArray): Option[(Reaction, InputMoleculeList)] = { - // A simpler, non-flatMap algorithm for the case when there are no cross-dependencies of molecule values. - // For each single (non-repeated) input molecule, select a molecule value that satisfies the conditional. - // For each group of repeated input molecules of the same sort, check whether the bag contains enough molecule values. - // Begin checking with molecules that have more stringent constraints (and thus, are not repeated). - - // This array will be mutated in place as we search for molecule values. - val foundValues = new Array[AbsMolValue[_]](info.inputs.length) - - val foundResult: Boolean = // This will be now computed and will become true (and then `foundValues` has the molecule values) or false (we found no values that match). - if (info.crossGuards.isEmpty && info.crossConditionalsForRepeatedMols.isEmpty) { - // flatFoldLeft is needed only over molecules with refutable matchers; filter them out first; all others don't need a fold since we already checked that present counts are sufficient. - - info.inputsSortedIndependentConditional.forall { inputInfo ⇒ - val newValueOpt = - if (inputInfo.molecule.isPipelined) - moleculesPresent(inputInfo.molecule.index).takeOne.filter(inputInfo.admitsValue) - else - moleculesPresent(inputInfo.molecule.index).find(inputInfo.admitsValue) - - newValueOpt.foreach { newMolValue ⇒ - foundValues(inputInfo.index) = newMolValue - } - newValueOpt.nonEmpty - } && { - info.inputsSortedIndependentIrrefutableGrouped - .foreach { case (siteMolIndex, infos) ⇒ - val molValues = moleculesPresent(siteMolIndex).takeAny(moleculeIndexRequiredCounts(siteMolIndex)) - infos.indices.foreach { idx ⇒ foundValues(infos(idx)) = molValues(idx) } - } - true - } - } else { - type MolVals = Map[Int, AbsMolValue[_]] - - type ValsMap = Map[AbsMolValue[_], Int] - type BagMap = Map[Molecule, ValsMap] - type FoldType = (MolVals, BagMap) - - def removeFromBagMap(relevantMap: BagMap, molecule: Molecule, molValue: AbsMolValue[_]) = { - val valuesMap = relevantMap.getOrElse(molecule, Map()) - val count = valuesMap.getOrElse(molValue, 0) - if (count >= 2) - relevantMap.updated(molecule, valuesMap.updated(molValue, count - 1)) - else { - val newValuesMap = valuesMap.filterKeys(_ != molValue) - if (newValuesMap.isEmpty) - relevantMap.filterKeys(_ != molecule) - else - relevantMap.updated(molecule, newValuesMap) - } - } - - // Map of molecule values for molecules that are inputs to this reaction. - val initRelevantMap: BagMap = inputMoleculesSet - .map(molecule ⇒ (molecule, moleculesPresent(molecule.index).getCountMap))(scala.collection.breakOut) - - val found: Stream[MolVals] = info.inputsSortedByConstraintStrength // We go through all molecules in the order of decreasing strength of conditionals. - .foldLeft[Stream[FoldType]](Stream[FoldType]((Map(), initRelevantMap))) { (prev, inputInfo) => - // In this `foldLeft` closure: - // `prev` contains the molecule value assignments we have found so far (`prevValues`), as well as the map `prevRelevantMap` containing molecule values that would remain in the soup after these previous molecule values were removed. - // `inputInfo` describes the pattern matcher for the input molecule we are currently required to find. - // We need to find all admissible assignments of values for that input molecule, and return them as a stream of pairs (newValues, newRelevantMap). - prev.flatMap { - case (prevValues, prevRelevantMap) => - val valuesMap: ValsMap = prevRelevantMap.getOrElse(inputInfo.molecule, Map()) - val newFound = for { - newMolValue <- - if (inputInfo.molecule.isPipelined) - moleculesPresent(inputInfo.molecule.index).takeOne.toStream - else // Do not eagerly evaluate the list of all possible values. - valuesMap.keysIterator.toStream.filter(inputInfo.admitsValue) - - newRelevantMap = removeFromBagMap(prevRelevantMap, inputInfo.molecule, newMolValue) - newValues = prevValues.updated(inputInfo.index, newMolValue) - } yield (newValues, newRelevantMap) - newFound - } - }.map(_._1) // Get rid of BagMap and tuple. - - // The reaction can run if `found` contains at least one admissible list of input molecules; just one is sufficient. - - // Evaluate all cross-molecule guards: they must all pass on the chosen molecule values. - val filteredAfterCrossGuards = - if (info.crossGuards.nonEmpty) - found.filter { inputValues => - info.crossGuards.forall { - case CrossMoleculeGuard(indices, _, cond) => - cond.isDefinedAt(indices.flatMap(i => inputValues.get(i).map(_.getValue)).toList) - } - } - else - // Here, we don't have any cross-molecule guards, but we do have some cross-molecule conditionals. - // Those are already taken into account by the `flatMap-fold`. So, we don't need to filter the `found` result any further. - found - - // Return result if found something. Assign the found molecule values into the `inputs` array. - val result = filteredAfterCrossGuards.headOption - result.foreach(_.foreach { case (i, molValue) ⇒ foundValues(i) = molValue }) - result.nonEmpty - } - - if (foundResult) - Some((this, Array.tabulate(foundValues.length)(i ⇒ (info.inputs(i).molecule, foundValues(i))))) - else - None - } - */ } diff --git a/core/src/main/scala/io/chymyst/jc/ReactionMacros.scala b/core/src/main/scala/io/chymyst/jc/ReactionMacros.scala index d6074224..3466cdb4 100644 --- a/core/src/main/scala/io/chymyst/jc/ReactionMacros.scala +++ b/core/src/main/scala/io/chymyst/jc/ReactionMacros.scala @@ -442,7 +442,7 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { /** We only support one-argument molecules, so here we only inspect the first element in the list of terms. */ private def getOutputFlag(outputTerms: List[Tree]): OutputPatternFlag = outputTerms match { - case List(t) => + case List(t) => // match a one-element list getConstantTree(t).map(tree => ConstOutputPatternF(tree.asInstanceOf[Tree])).getOrElse(OtherOutputPatternF) case Nil => ConstOutputPatternF(q"()") @@ -466,7 +466,7 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { private def isMolecule(t: Trees#Tree): Boolean = t.asInstanceOf[Tree].tpe <:< typeOf[Molecule] - private def isReplyValue(t: Trees#Tree): Boolean = t.asInstanceOf[Tree].tpe <:< weakTypeOf[AbsReplyValue[_, _]] + private def isReplyEmitter(t: Trees#Tree): Boolean = t.asInstanceOf[Tree].tpe <:< weakTypeOf[AbsReplyEmitter[_, _]] @SuppressWarnings(Array("org.wartremover.warts.Equals")) // For some reason, q"while($cond) $body" triggers wartremover's error about disabled `==` operator. override def traverse(tree: Tree): Unit = { @@ -589,22 +589,29 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { } finishTraverseWithOutputEnv() - // possibly a molecule emission, but could be any function call - case Apply(Select(t@Ident(TermName(name)), TermName(f)), argumentList) + /* The expression is `t.f(argumentList)`. + * This expression could be a molecule emission, but could be any call to `apply`, `checkTimeout`, or `timeout`. + * Other function calls will be analyzed in a later `case` clause. + * + * For example, if `c` is a molecule then `c(123)` is matched as `c.apply(List(123))`. + * Then `t` will be `c` and `f` will be `apply`. + */ + case Apply(Select(t@Ident(TermName(_)), TermName(f)), argumentList) if f === "apply" || f === "checkTimeout" || f === "timeout" => // In the output list, we do not include any molecule emitters defined in the inner scope of the reaction. val includeThisSymbol = !isOwnedBy(t.symbol.owner, reactionBodyOwner) val thisSymbolIsAMolecule = isMolecule(t) - val thisSymbolIsAReply = isReplyValue(t) + val thisSymbolIsAReply = isReplyEmitter(t) val flag1 = getOutputFlag(argumentList) + lazy val funcName = s"${t.symbol.fullName}.$f" if (flag1.needTraversal) { // Traverse the trees of the argument list elements (molecules should only have one argument anyway). if (thisSymbolIsAMolecule || thisSymbolIsAReply) { - argumentList.foreach(traverse) + argumentList.foreach(traverse) // Molecules and replies are a once-only output environment, so no need to push another environment. } else { renewOutputEnvId() - pushEnv(FuncBlock(currentOutputEnvId, name = s"${t.symbol.fullName}.$f")) + pushEnv(FuncBlock(currentOutputEnvId, name = funcName)) argumentList.foreach(traverse) finishTraverseWithOutputEnv() } @@ -612,11 +619,11 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { if (includeThisSymbol) { if (thisSymbolIsAMolecule) { - outputMolecules.append((t.symbol, flag1, outputEnv.toList)) + outputMolecules.append((t.symbol, flag1, outputEnv)) } } if (thisSymbolIsAReply) { - replyActions.append((t.symbol, flag1, outputEnv.toList)) + replyActions.append((t.symbol, flag1, outputEnv)) } // tuple @@ -630,7 +637,8 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { val fullName = f.asInstanceOf[Tree].symbol.fullName if (onceOnlyFunctionCodes.contains(fullName)) // The function is one of the known once-only evaluating functions such as Some(), List(), etc. - // In that case, we don't need to do anything special - just traverse the tree and harvest the molecules normally. + // In that case, we don't need to set a special environment, since a once-only environment is equivalent to no environment. + // We just traverse the tree and harvest the molecules normally. super.traverse(tree) else { @@ -644,16 +652,17 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { // This is an iterator that takes a molecule emitter, in a short syntax, // e.g. `(0 until n).foreach(a)` where `a : M[Int]`. // In that case, the molecule could be emitted zero or more times. - outputMolecules.append((t.asInstanceOf[Tree].symbol, OtherOutputPatternF, outputEnv.toList)) + outputMolecules.append((t.asInstanceOf[Tree].symbol, EmptyOutputPatternF, outputEnv)) } traverse(t.asInstanceOf[Tree]) } finishTraverseWithOutputEnv() } - case Ident(TermName(name)) if isReplyValue(tree) => - // All use of reply emitters must be logged. - replyActions.append((tree.asInstanceOf[Tree].symbol, OtherOutputPatternF, outputEnv.toList)) + // This term is a bare identifier. + case Ident(TermName(_)) if isReplyEmitter(tree) => + // All use of reply emitters must be logged, including just copying an emitter itself. + replyActions.append((tree.asInstanceOf[Tree].symbol, EmptyOutputPatternF, outputEnv)) case _ => super.traverse(tree) } @@ -692,16 +701,9 @@ class ReactionMacros(override val c: blackbox.Context) extends CommonMacros(c) { q"_root_.io.chymyst.jc.WildcardInput" // this case will not be encountered here; we are conflating InputPatternFlag and ReplyInputPatternFlag } - implicit val liftableOutputPatternFlag: Liftable[OutputPatternFlag] = Liftable[OutputPatternFlag] { - case ConstOutputPatternF(tree) => - q"_root_.io.chymyst.jc.ConstOutputPattern($tree)" - case OtherOutputPatternF => - q"_root_.io.chymyst.jc.OtherOutputPattern" - } - implicit val liftableOutputPatternType: Liftable[OutputPatternType] = Liftable[OutputPatternType] { case ConstOutputPattern(v) => - q"_root_.io.chymyst.jc.ConstOutputPattern(${v.asInstanceOf[Tree]})" // When we lift it here, it always has type `Tree`. + q"_root_.io.chymyst.jc.ConstOutputPattern(${v.asInstanceOf[Tree]})" // When we lift it here, `v` always has type `Tree`. case OtherOutputPattern => q"_root_.io.chymyst.jc.OtherOutputPattern" } diff --git a/core/src/main/scala/io/chymyst/jc/ReactionSite.scala b/core/src/main/scala/io/chymyst/jc/ReactionSite.scala index ffc4233e..3d4a0774 100644 --- a/core/src/main/scala/io/chymyst/jc/ReactionSite.scala +++ b/core/src/main/scala/io/chymyst/jc/ReactionSite.scala @@ -87,25 +87,30 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo val relatedMoleculeCounts = Array.tabulate[Int](moleculesPresent.length)(i ⇒ moleculesPresent(i).takeAny(maxRequiredMoleculeCount(i)).size) // This option value will be non-empty if we have a reaction with some input molecules that all have admissible values for that reaction. val found: Option[(Reaction, InputMoleculeList)] = findReaction(mol, relatedMoleculeCounts) - found.foreach(_._2.foreach { case (k, v) => - // This error indicates a bug in this code, which should already manifest itself in failing tests! - if (!removeFromBag(k, v)) reportError(s"Error: In $this: Internal error: Failed to remove molecule $k($v) from its bag; molecule index ${k.index}, bag ${moleculesPresent(k.index)}") - }) + // If we have found a reaction that can be run, remove its input molecule values from their bags. + found.foreach { case (thisReaction, thisInputList) ⇒ + thisInputList.indices.foreach { i ⇒ + val molValue = thisInputList(i) + val mol = thisReaction.info.inputs(i).molecule + // This error (molecule value was not in bag) indicates a bug in this code, which should already manifest itself in failing tests! + if (!removeFromBag(mol, molValue)) reportError(s"Error: In $this: Internal error: Failed to remove molecule $mol($molValue) from its bag; molecule index ${mol.index}, bag ${moleculesPresent(mol.index)}") + } + } found } // End of synchronized block. // We already decided on starting a reaction, so we don't hold the `synchronized` lock on the molecule bag any more. foundReactionAndInputs match { - case Some((reaction, usedInputs)) => + case Some((thisReaction, usedInputs)) => // Build a closure out of the reaction, and run that closure on the reaction's thread pool. - val poolForReaction = reaction.threadPool.getOrElse(reactionPool) + val poolForReaction = thisReaction.threadPool.getOrElse(reactionPool) if (poolForReaction.isInactive) { - reportError(s"In $this: cannot run reaction {${reaction.info}} since reaction pool is not active; input molecules ${Core.moleculeBagToString(usedInputs)} were consumed and not emitted again") + reportError(s"In $this: cannot run reaction {${thisReaction.info}} since reaction pool is not active; input molecules ${Core.moleculeBagToString(thisReaction, usedInputs)} were consumed and not emitted again") // In this case, we do not attempt to schedule a reaction. However, input molecules were consumed and not emitted again. } else { if (!Thread.currentThread().isInterrupted) { - if (logLevel > 1) println(s"Debug: In $this: starting reaction {${reaction.info}} with inputs [${Core.moleculeBagToString(usedInputs)}] on reaction pool $poolForReaction while on site pool $sitePool") + if (logLevel > 1) println(s"Debug: In $this: starting reaction {${thisReaction.info}} with inputs [${Core.moleculeBagToString(thisReaction, usedInputs)}] on reaction pool $poolForReaction while on site pool $sitePool") } if (logLevel > 2) { val moleculesRemainingMessage = @@ -116,7 +121,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo println(moleculesRemainingMessage) } // Schedule the reaction now. Provide reaction info to the thread. - scheduleReaction(reaction, usedInputs, poolForReaction) + scheduleReaction(thisReaction, usedInputs, poolForReaction) decideReactionsForNewMolecule(mol) // Need to try running another reaction with the same molecule, if possible. } case None => @@ -170,16 +175,17 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo /** This closure will be run on the reaction thread pool to start a new reaction. * - * @param reaction Reaction to run. - * @param usedInputs Molecules (with values) that are consumed by the reaction. + * @param thisReaction Reaction to run. + * @param usedInputs Molecules (with values) that are consumed by the reaction. */ - private def runReaction(reaction: Reaction, usedInputs: InputMoleculeList, poolForReaction: Pool): Unit = { - lazy val reactionStartMessage = s"Debug: In $this: reaction {${reaction.info}} started on thread pool $reactionPool with thread id ${Thread.currentThread().getId}" + private def runReaction(thisReaction: Reaction, usedInputs: InputMoleculeList, poolForReaction: Pool): Unit = { + lazy val reactionStartMessage = s"Debug: In $this: reaction {${thisReaction.info}} started on thread pool $reactionPool with thread id ${Thread.currentThread().getId}" if (logLevel > 1) println(reactionStartMessage) + lazy val reactionInputs = Core.moleculeBagToString(thisReaction, usedInputs) val exitStatus: ReactionExitStatus = try { // Here we actually apply the reaction body to its input molecules. - reaction.body.apply((usedInputs.length - 1, usedInputs)) + thisReaction.body.apply((usedInputs.length - 1, usedInputs)) // If we are here, we had no exceptions during evaluation of reaction body. ReactionExitSuccess } catch { @@ -188,7 +194,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo // Running the reaction body produced an exception that is internal to `Chymyst Core`. // We should not try to recover from this; it is either an error on user's part // or a bug in `Chymyst Core`. - val message = s"In $this: Reaction {${reaction.info}} with inputs [${Core.moleculeBagToString(usedInputs)}] produced an exception that is internal to Chymyst Core. Retry run was not scheduled. Message: ${e.getMessage}" + val message = s"In $this: Reaction {${thisReaction.info}} with inputs [$reactionInputs] produced an exception that is internal to Chymyst Core. Retry run was not scheduled. Message: ${e.getMessage}" reportError(message) ReactionExitFailure(message) @@ -196,13 +202,13 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo // Running the reaction body produced an exception. Note that the exception has killed a thread. // We will now re-insert the input molecules (except the blocking ones). Hopefully, no side-effects or output molecules were produced so far. val (status, retryMessage) = - if (reaction.retry) { - scheduleReaction(reaction, usedInputs, poolForReaction) + if (thisReaction.retry) { + scheduleReaction(thisReaction, usedInputs, poolForReaction) (ReactionExitRetryFailure(e.getMessage), " Retry run was scheduled.") } else (ReactionExitFailure(e.getMessage), " Retry run was not scheduled.") - val generalExceptionMessage = s"In $this: Reaction {${reaction.info}} with inputs [${Core.moleculeBagToString(usedInputs)}] produced an exception.$retryMessage Message: ${e.getMessage}" + val generalExceptionMessage = s"In $this: Reaction {${thisReaction.info}} with inputs [$reactionInputs] produced an exception.$retryMessage Message: ${e.getMessage}" reportError(generalExceptionMessage) status @@ -214,15 +220,17 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo // value to unblock the threads. // Compute error messages here in case we will need them later. - val blockingMoleculesWithNoReply = usedInputs - .filter(_._2.reactionSentNoReply) - .map(_._1).toSeq.toOptionSeq.map(_.map(_.toString).sorted.mkString(", ")) + val blockingMoleculesWithNoReply = usedInputs.zipWithIndex + .filter(_._1.reactionSentNoReply) + .map{ case (_, i) ⇒ thisReaction.info.inputs(i).molecule} + .toSeq.toOptionSeq + .map(_.map(_.toString).sorted.mkString(", ")) // Make this non-lazy to improve coverage. val errorMessageFromStatus = exitStatus.getMessage.map(message => s". Reported error: $message").getOrElse("") lazy val messageNoReply = blockingMoleculesWithNoReply.map { s => - s"Error: In $this: Reaction {${reaction.info}} with inputs [${Core.moleculeBagToString(usedInputs)}] finished without replying to $s$errorMessageFromStatus" + s"Error: In $this: Reaction {${thisReaction.info}} with inputs [$reactionInputs] finished without replying to $s$errorMessageFromStatus" } // We will report all errors to each blocking molecule. @@ -233,7 +241,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo // Insert error messages into the reply wrappers and release all semaphores. usedInputs.foreach { - case (_, bm@BlockingMolValue(_, replyValue)) => + case bm@BlockingMolValue(_, replyValue) => if (haveErrorsWithBlockingMolecules && exitStatus.reactionSucceededOrFailedWithoutRetry) { // Do not send error messages to molecules that already got a reply - this is pointless and leads to errors. if (bm.reactionSentNoReply) { @@ -337,6 +345,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo } } else { // For pipelined molecules, check whether their value satisfies at least one of the conditions (if any conditions are present). + // For non-pipelined molecules, `admitsValue` will be `true`. val admitsValue = pipelinedMolecules.get(mol.index).forall(infos ⇒ infos.isEmpty || infos.exists(_.admitsValue(molValue))) if (mol.isStatic) { // Check permission and throw exceptions on errors, but do not add anything to moleculesPresent and do not yet set the volatile value. @@ -409,7 +418,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo * @tparam R Type of the reply value. * @return Reply status for the reply action. */ - private def emitAndAwaitReplyInternal[T, R](timeoutOpt: Option[Long], bm: B[T, R], v: T, replyValueWrapper: AbsReplyValue[T, R]): ReplyStatus = { + private def emitAndAwaitReplyInternal[T, R](timeoutOpt: Option[Long], bm: B[T, R], v: T, replyValueWrapper: AbsReplyEmitter[T, R]): ReplyStatus = { val blockingMolValue = BlockingMolValue(v, replyValueWrapper) emit[T](bm, blockingMolValue) val timedOut: Boolean = !BlockingIdle { @@ -426,7 +435,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo // Adding a blocking molecule may trigger at most one reaction and must return a value of type R. // We must make this a blocking call, so we acquire a semaphore (with or without timeout). - private def emitAndAwaitReply[T, R](bm: B[T, R], v: T, replyValueWrapper: AbsReplyValue[T, R]): R = { + private def emitAndAwaitReply[T, R](bm: B[T, R], v: T, replyValueWrapper: AbsReplyEmitter[T, R]): R = { // check if we had any errors, and that we have a result value emitAndAwaitReplyInternal(timeoutOpt = None, bm, v, replyValueWrapper) match { case ErrorNoReply(message) => @@ -437,7 +446,7 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo } // This is a separate method because it has a different return type than [[emitAndAwaitReply]]. - private def emitAndAwaitReplyWithTimeout[T, R](timeout: Long, bm: B[T, R], v: T, replyValueWrapper: AbsReplyValue[T, R]): + private def emitAndAwaitReplyWithTimeout[T, R](timeout: Long, bm: B[T, R], v: T, replyValueWrapper: AbsReplyEmitter[T, R]): Option[R] = { // check if we had any errors, and that we have a result value emitAndAwaitReplyInternal(timeoutOpt = Some(timeout), bm, v, replyValueWrapper) match { @@ -472,7 +481,8 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo mol.isBoundToAnotherReactionSite(this) match { case Some(otherRS) => throw new ExceptionMoleculeAlreadyBound(s"Molecule $mol cannot be used as input in $this since it is already bound to $otherRS") - case None => mol.setReactionSiteInfo(this, index, valType, pipelinedMolecules contains index) + case None ⇒ + mol.setReactionSiteInfo(this, index, valType, pipelined) } } @@ -500,8 +510,10 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo staticDiagnostics.checkWarningsAndErrors() - // Emit static molecules (note: this is on the same thread as the call to `site`!). - // This must be done without starting any reactions. + // Emit static molecules now. + // This must be done without starting any reactions that might consume these molecules. + // So, we set the flag `emittingStaticMolsNow`, which will prevent other reactions from starting. + // Note: mutable variables are OK since this is on the same thread as the call to `site`, so it's guaranteed to be single-threaded! emittingStaticMolsNow = true staticReactions.foreach { reaction => // It is OK that the argument is `null` because static reactions match on the wildcard: { case _ => ... } @@ -590,8 +602,9 @@ private[jc] final class ReactionSite(reactions: Seq[Reaction], reactionPool: Poo * */ private val pipelinedMolecules: Map[Int, Set[InputMoleculeInfo]] = - moleculeAtIndex - .flatMap { case (index, _) ⇒ infosIfPipelined(index).map(c ⇒ (index, c)) } + moleculeAtIndex.flatMap { + case (index, _) ⇒ infosIfPipelined(index).map(c ⇒ (index, c)) + } private val moleculesPresent: MoleculeBagArray = new Array(knownMolecules.size) @@ -653,8 +666,8 @@ private[jc] final class ReactionSiteWrapper[T, R]( val setLogLevel: Int => Unit, val staticMolsDeclared: List[Molecule], val emit: (Molecule, AbsMolValue[T]) => Unit, - val emitAndAwaitReply: (B[T, R], T, AbsReplyValue[T, R]) => R, - val emitAndAwaitReplyWithTimeout: (Long, B[T, R], T, AbsReplyValue[T, R]) => Option[R], + val emitAndAwaitReply: (B[T, R], T, AbsReplyEmitter[T, R]) => R, + val emitAndAwaitReplyWithTimeout: (Long, B[T, R], T, AbsReplyEmitter[T, R]) => Option[R], val consumingReactions: Array[Reaction], val sameReactionSite: ReactionSite => Boolean ) @@ -667,11 +680,11 @@ private[jc] object ReactionSiteWrapper { toString = "", logSoup = () => exception, setLogLevel = _ => exception, - staticMolsDeclared = null, + staticMolsDeclared = List[Molecule](), emit = (_: Molecule, _: AbsMolValue[T]) => exception, - emitAndAwaitReply = (_: B[T, R], _: T, _: AbsReplyValue[T, R]) => exception, - emitAndAwaitReplyWithTimeout = (_: Long, _: B[T, R], _: T, _: AbsReplyValue[T, R]) => exception, - consumingReactions = null, + emitAndAwaitReply = (_: B[T, R], _: T, _: AbsReplyEmitter[T, R]) => exception, + emitAndAwaitReplyWithTimeout = (_: Long, _: B[T, R], _: T, _: AbsReplyEmitter[T, R]) => exception, + consumingReactions = Array[Reaction](), sameReactionSite = _ => exception ) } diff --git a/core/src/main/scala/io/chymyst/jc/SmartThread.scala b/core/src/main/scala/io/chymyst/jc/SmartThread.scala index 5882b550..e6c06068 100644 --- a/core/src/main/scala/io/chymyst/jc/SmartThread.scala +++ b/core/src/main/scala/io/chymyst/jc/SmartThread.scala @@ -20,7 +20,6 @@ private[jc] final class SmartThread(runnable: Runnable, pool: SmartPool) extends inBlockingCall = false result } - } /** Thread that knows how Chymyst uses it at any time. diff --git a/core/src/main/scala/io/chymyst/jc/StaticAnalysis.scala b/core/src/main/scala/io/chymyst/jc/StaticAnalysis.scala index 19a2ed75..c0830a05 100644 --- a/core/src/main/scala/io/chymyst/jc/StaticAnalysis.scala +++ b/core/src/main/scala/io/chymyst/jc/StaticAnalysis.scala @@ -35,7 +35,7 @@ private[jc] object StaticAnalysis { } private def inputMatchersWeakerThanOutput(isWeaker: (InputMoleculeInfo, OutputMoleculeInfo) => Option[Boolean]) - (input: List[InputMoleculeInfo], output: Array[OutputMoleculeInfo]): Boolean = { + (input: List[InputMoleculeInfo], output: Array[OutputMoleculeInfo]): Boolean = { input.flatFoldLeft(output) { (acc, inputInfo) ⇒ acc .find(outputInfo => isWeaker(inputInfo, outputInfo).getOrElse(false)) @@ -148,9 +148,10 @@ private[jc] object StaticAnalysis { // The chemistry is likely to be a deadlock if at least one the other output molecules are consumed together with the blocking molecule in the same reaction. val likelyDeadlocks = possibleDeadlocks.map { case (info, infos) => - (info, info.molecule.consumingReactions.flatMap( - _.find { r => - // For each reaction that consumes the molecule `info.molecule`, check whether this reaction also consumes any of the molecules from infos.map(_.molecule). If so, it's a likely deadlock. + (info, info.molecule.consumingReactions + .find { r => + // For each reaction that consumes the molecule `info.molecule`, check whether this reaction also consumes + // any of the molecules from infos.map(_.molecule). If so, it's a likely deadlock. val uniqueInputsThatAreAmongOutputs = r.info.inputsSortedByConstraintStrength .filter(infos.map(_.molecule) contains _.molecule) .groupBy(_.molecule).mapValues(_.lastOption).values.toList.flatten // Among repeated input molecules, choose only one molecule with the weakest matcher. @@ -161,7 +162,6 @@ private[jc] object StaticAnalysis { ) } ) - ) } val warningList = likelyDeadlocks @@ -227,14 +227,13 @@ private[jc] object StaticAnalysis { private def checkOutputsForStaticMols(staticMols: Map[Molecule, Int], reactions: Seq[Reaction]): Option[String] = { val errorList = staticMols.flatMap { case (m, _) => - reactions.flatMap { - r => - val outputTimes = r.info.outputs.count(_.molecule === m) - if (outputTimes > 1) - Some(s"static molecule ($m) emitted more than once by reaction ${r.info}") - else if (outputTimes == 1 && !r.inputMoleculesSet.contains(m)) - Some(s"static molecule ($m) emitted but not consumed by reaction ${r.info}") - else None + reactions.flatMap { r => + val outputTimes = r.info.outputs.count(_.molecule === m) + if (outputTimes > 1) + Some(s"static molecule ($m) emitted more than once by reaction ${r.info}") + else if (outputTimes == 1 && !r.inputMoleculesSet.contains(m)) + Some(s"static molecule ($m) emitted but not consumed by reaction ${r.info}") + else None } } @@ -255,7 +254,6 @@ private[jc] object StaticAnalysis { Seq() } - private[jc] def findStaticMolDeclarationErrors(staticReactions: Seq[Reaction]): Seq[String] = { val foundErrors = staticReactions.map(_.info).filterNot(_.guardPresence.noCrossGuards) if (foundErrors.nonEmpty) diff --git a/core/src/main/scala/io/chymyst/jc/package.scala b/core/src/main/scala/io/chymyst/jc/package.scala index 040004b4..e1d96b9d 100644 --- a/core/src/main/scala/io/chymyst/jc/package.scala +++ b/core/src/main/scala/io/chymyst/jc/package.scala @@ -4,8 +4,9 @@ import scala.language.experimental.macros import scala.collection.JavaConverters.asScalaIteratorConverter import scala.util.{Failure, Success, Try} -/** This is a pure interface to other functions to make them visible to users. - * This object does not contain any new code. +/** This object contains code that should be visible to users of `Chymyst Core`. + * It also serves as an interface to macros. + * This allows users to import just one package and use all functionality of `Chymyst Core`. */ package object jc { @@ -33,12 +34,14 @@ package object jc { reactionSite.checkWarningsAndErrors() } + /** `site()` call with a default reaction pool and site pool. */ def site(reactions: Reaction*): WarningsAndErrors = site(defaultReactionPool, defaultSitePool)(reactions: _*) - def site(reactionPool: Pool)(reactions: Reaction*): WarningsAndErrors = site(reactionPool, reactionPool)(reactions: _*) + /** `site()` call with the specified pool serving as both the site pool and the reaction pool. */ + def site(pool: Pool)(reactions: Reaction*): WarningsAndErrors = site(pool, pool)(reactions: _*) /** - * Users will define reactions using this function. + * This is the main method for defining reactions. * Examples: {{{ go { a(_) => ... } }}} * {{{ go { a (_) => ...}.withRetry onThreads threadPool }}} * @@ -47,6 +50,7 @@ package object jc { * @param reactionBody The body of the reaction. This must be a partial function with pattern-matching on molecules. * @return A [[Reaction]] value, containing the reaction body as well as static information about input and output molecules. */ + // IDEA cannot resolve symbol `BlackboxMacros`, but compilation works. def go(reactionBody: Core.ReactionBody): Reaction = macro BlackboxMacros.buildReactionImpl /** @@ -57,7 +61,8 @@ package object jc { * @param x the first emitted molecule * @return An auxiliary class with a `+` operation. */ - implicit final class EmitMultiple(x: Unit) { // Making this `extend AnyVal` crashes JVM in tests! + // Making this `extend AnyVal` crashes JVM in tests! + implicit final class EmitMultiple(x: Unit) { def +(n: Unit): Unit = () } @@ -81,11 +86,37 @@ package object jc { val defaultSitePool = new FixedPool(2) val defaultReactionPool = new FixedPool(4) + /** Access the global error log used by all reaction sites to report runtime errors. + * + * @return An [[Iterable]] representing the complete error log. + */ def globalErrorLog: Iterable[String] = Core.errorLog.iterator().asScala.toIterable + /** Clear the global error log used by all reaction sites to report runtime errors. + * + */ + def clearErrorLog(): Unit = Core.errorLog.clear() + + /** A helper method to run a closure that uses a thread pool, safely closing the pool after use. + * + * @param pool A thread pool value, evaluated lazily - typically `new SmartPool(...)`. + * @param doWork A closure, typically containing a `site(pool)(...)` call. + * @tparam T Type of the value returned by the closure. + * @return The value returned by the closure, wrapped in a `Try`. + */ def withPool[T](pool: => Pool)(doWork: Pool => T): Try[T] = cleanup(pool)(_.shutdownNow())(doWork) - def cleanup[T,R](resource: => T)(cleanup: T => Unit)(doWork: T => R): Try[R] = { + /** Run a closure with a resource that is allocated and safely cleaned up after use. + * Resource will be cleaned up even if the closure throws an exception. + * + * @param resource A value of type `T` that needs to be created for use by `doWork`. + * @param cleanup A closure that will perform the necessary cleanup on the resource. + * @param doWork A closure that will perform useful work, using the resource. + * @tparam T Type of the resource value. + * @tparam R Type of the result of `doWork`. + * @return The value returned by `doWork`, wrapped in a `Try`. + */ + def cleanup[T, R](resource: => T)(cleanup: T => Unit)(doWork: T => R): Try[R] = { try { Success(doWork(resource)) } catch { @@ -102,6 +133,7 @@ package object jc { } } + /** We need to have a single implicit instance of [[TypeMustBeUnit]]. */ implicit val _: TypeMustBeUnit[Unit] = TypeMustBeUnitValue } diff --git a/core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingSpec.scala b/core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingUtest.scala similarity index 99% rename from core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingSpec.scala rename to core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingUtest.scala index 3d9d83df..c4608226 100644 --- a/core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingSpec.scala +++ b/core/src/test/scala/io/chymyst/jc/CrossMoleculeSortingUtest.scala @@ -3,7 +3,7 @@ package io.chymyst.jc import utest._ import CrossMoleculeSorting.{findFirstConnectedGroupSet, groupConnectedSets, sortedConnectedSets, getDSLProgram} -object CrossMoleculeSortingSpec extends TestSuite { +object CrossMoleculeSortingUtest extends TestSuite { val tests = this { val crossGroups1 = Array(Set(0, 1), Set(2, 3), Set(3, 4, 5), Set(0, 6), Set(6, 7)) val crossGroups2 = Array(Set(0, 1), Set(2, 3), Set(3, 4, 5), Set(0, 6), Set(6, 7), Set(7, 1)) diff --git a/core/src/test/scala/io/chymyst/jc/GuardsErrorSpec.scala b/core/src/test/scala/io/chymyst/jc/GuardsErrorsUtest.scala similarity index 97% rename from core/src/test/scala/io/chymyst/jc/GuardsErrorSpec.scala rename to core/src/test/scala/io/chymyst/jc/GuardsErrorsUtest.scala index d8a5b62e..ae052c59 100644 --- a/core/src/test/scala/io/chymyst/jc/GuardsErrorSpec.scala +++ b/core/src/test/scala/io/chymyst/jc/GuardsErrorsUtest.scala @@ -2,7 +2,7 @@ package io.chymyst.jc import utest._ -object GuardsErrorSpec extends TestSuite { +object GuardsErrorsUtest extends TestSuite { val tests = this { "recognize an identically false guard condition" - { val a = m[Int] diff --git a/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsSpec.scala b/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsSpec.scala deleted file mode 100644 index ac242bc1..00000000 --- a/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsSpec.scala +++ /dev/null @@ -1,269 +0,0 @@ -package io.chymyst.jc - -import io.chymyst.jc.Core.ReactionBody -import utest._ -import utest.framework.{Test, Tree} - -import scala.concurrent.duration._ - -object MacroCompileErrorsSpec extends TestSuite { - val tests: Tree[Test] = this { - val a = m[Int] - val c = m[Unit] - val f = b[Int, Int] - val x = 2 - - assert(a.name == "a") - assert(c.name == "c") - assert(f.name == "f") - assert(x == 2) - - "fail to compile molecules with non-unit types emitted as a()" - { - - * - { - compileError( - "val x = a()" - ).check( - """ - | "val x = a()" - | ^ - |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") - } - * - { - compileError( - "val x = f()" - ).check( - """ - | "val x = f()" - | ^ - |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") - } - * - { - compileError( - "val x = f.timeout()(1 second)" - ).check( - """ - | "val x = f.timeout()(1 second)" - | ^ - |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") - } - * - { - compileError( - "val r = go { case f(_, r) => r() } " - ).check( - """ - | "val r = go { case f(_, r) => r() } " - | ^ - |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") - } - * - { - compileError( - "val r = go { case f(_, r) => r.checkTimeout() } " - ).check( - """ - | "val r = go { case f(_, r) => r.checkTimeout() } " - | ^ - |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") - } - } - - "fail to compile a reaction with empty static clause" - { - compileError( - "val r = go { case _ => }" - ).check( - """ - | "val r = go { case _ => }" - | ^ - |""".stripMargin, "Static reaction must emit some output molecules") - } - - "fail to compile a guard that replies to molecules" - { - * - { - compileError( - "val r = go { case f(_, r) if { r(1); x > 0 } => }" - ).check( - """ - | "val r = go { case f(_, r) if { r(1); x > 0 } => }" - | ^ - |""".stripMargin, "Input guard must not perform any reply actions (r)") - } - * - { - compileError( - "val r = go { case f(_, r) if r.checkTimeout(1) && x > 0 => }" - ).check( - """ - | "val r = go { case f(_, r) if r.checkTimeout(1) && x > 0 => }" - | ^ - |""".stripMargin, "Input guard must not perform any reply actions (r)") - - } - } - - "fail to compile a guard that emits molecules" - { - * - { - compileError( - "val r = go { case f(_, r) if f(1) > 0 => r(1) }" - ).check( - """ - | "val r = go { case f(_, r) if f(1) > 0 => r(1) }" - | ^ - |""".stripMargin, "Input guard must not emit any output molecules (f)") - } - - * - { - compileError( - "val r = go { case f(_, r) if f.timeout(1)(1.second).nonEmpty => r(1) }" - ).check( - """ - | "val r = go { case f(_, r) if f.timeout(1)(1.second).nonEmpty => r(1) }" - | ^ - |""".stripMargin, "Input guard must not emit any output molecules (f)") - } - } - - "fail to compile a reaction with two case clauses" - { - * - { - compileError( - "val r = go { case a(x) =>; case c(_) => }" - ).check( - """ - | "val r = go { case a(x) =>; case c(_) => }" - | ^ - |""".stripMargin, "Reactions must contain only one `case` clause") - } - * - { - compileError( - """val result = -go { -case a(x) => c() -case c(_) + a(y) => c() -}""").check( - """ - |go { - | ^ - |""".stripMargin, "Reactions must contain only one `case` clause") - } - } - - "fail to compile a reaction that is not defined inline" - { - val body: ReactionBody = { - case _ => c() - } - assert(body.isInstanceOf[ReactionBody]) - compileError( - "val r = go(body)" - ).check( - """ - | "val r = go(body)" - | ^ - |""".stripMargin, "No `case` clauses found: Reactions must be defined inline with the `go { case ... => ... }` syntax") - } - - "fail to compile reactions with unconditional livelock when all matchers are trivial" - { - val a = m[(Int, Int)] - val bb = m[Int] - val bbb = m[Int] - - assert(a.isInstanceOf[M[(Int, Int)]]) - assert(bb.isInstanceOf[M[Int]]) - assert(bbb.isInstanceOf[M[Int]]) - - * - { - compileError( - "val r = go { case a((x,y)) => a((1,1)) }" - ).check( - """ - | "val r = go { case a((x,y)) => a((1,1)) }" - | ^ - """.stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") - } - * - { - compileError( - "val r = go { case a((_,x)) => a((x,x)) }" - ).check( - """ - | "val r = go { case a((_,x)) => a((x,x)) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") - } - * - { - val r = go { case a((1, _)) => a((1, 1)) } - assert(r.isInstanceOf[Reaction]) - } // cannot detect unconditional livelock here at compile time, since we can't evaluate the binder yet - * - { - val r = go { case bb(y) if y > 0 => bb(1) } - assert(r.isInstanceOf[Reaction]) - } // no unconditional livelock due to guard - * - { - val r = go { case bb(y) => if (y > 0) bb(1) } - assert(r.isInstanceOf[Reaction]) - } // no unconditional livelock due to `if` in reaction - * - { - val r = go { case bb(y) => if (y > 0) bbb(1) else bb(2) } - assert(r.isInstanceOf[Reaction]) - } // no unconditional livelock due to `if` in reaction - * - { - compileError( - "val r = go { case bb(x) => if (x > 0) bb(1) else bb(2) }" - ).check( - """ - | "val r = go { case bb(x) => if (x > 0) bb(1) else bb(2) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bb)") - } // unconditional livelock due to shrinkage of `if` in reaction - * - { - val r = go { case bbb(1) => bbb(2) } - assert(r.isInstanceOf[Reaction]) - // no livelock since constant values are different - } - * - { - compileError( - "val r = go { case bb(x) => bb(1) }" - ).check( - """ - | "val r = go { case bb(x) => bb(1) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bb)") - } // unconditional livelock - * - { - compileError( - "val r = go { case a(_) => a((1,1)) }" // ignore warning "class M expects 2 patterns to hold" - ).check( - """ - | "val r = go { case a(_) => a((1,1)) }" // ignore warning "class M expects 2 patterns to hold" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") - } - // unconditional livelock - * - { - compileError( - "val r = go { case bbb(_) => bbb(0) }" - ).check( - """ - | "val r = go { case bbb(_) => bbb(0) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb)") - } - // unconditional livelock - * - { - compileError( - "val r = go { case bbb(x) => bbb(x + 1) + bb(x) }" - ).check( - """ - | "val r = go { case bbb(x) => bbb(x + 1) + bb(x) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb)") - } - * - { - compileError( - "val r = go { case bbb(x) + bb(y) => bbb(x + 1) + bb(x) + bb(y + 1) }" - ).check( - """ - | "val r = go { case bbb(x) + bb(y) => bbb(x + 1) + bb(x) + bb(y + 1) }" - | ^ - |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb, bb)") - } - } - - } -} \ No newline at end of file diff --git a/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsUtest.scala b/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsUtest.scala new file mode 100644 index 00000000..f010827c --- /dev/null +++ b/core/src/test/scala/io/chymyst/jc/MacroCompileErrorsUtest.scala @@ -0,0 +1,1037 @@ +package io.chymyst.jc + +import io.chymyst.jc.Core.ReactionBody +import utest._ +import utest.framework.{Test, Tree} +import scala.concurrent.duration._ + +object MacroCompileErrorsUtest extends TestSuite { + val tests: Tree[Test] = this { + val a = m[Int] + val c = m[Unit] + val f = b[Int, Int] + val x = 2 + + assert(a.name == "a") + assert(c.name == "c") + assert(f.name == "f") + assert(x == 2) + + "fail to compile molecules with non-unit types emitted as a()" - { + + * - { + compileError( + "val x = a()" + ).check( + """ + | "val x = a()" + | ^ + |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") + } + * - { + compileError( + "val x = f()" + ).check( + """ + | "val x = f()" + | ^ + |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") + } + * - { + compileError( + "val x = f.timeout()(1 second)" + ).check( + """ + | "val x = f.timeout()(1 second)" + | ^ + |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") + } + * - { + compileError( + "val r = go { case f(_, r) => r() } " + ).check( + """ + | "val r = go { case f(_, r) => r() } " + | ^ + |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") + } + * - { + compileError( + "val r = go { case f(_, r) => r.checkTimeout() } " + ).check( + """ + | "val r = go { case f(_, r) => r.checkTimeout() } " + | ^ + |""".stripMargin, "could not find implicit value for parameter arg: io.chymyst.jc.TypeMustBeUnit[Int]") + } + } + + "fail to compile a reaction with empty static clause" - { + compileError( + "val r = go { case _ => }" + ).check( + """ + | "val r = go { case _ => }" + | ^ + |""".stripMargin, "Static reaction must emit some output molecules") + } + + "fail to compile a guard that replies to molecules" - { + * - { + compileError( + "val r = go { case f(_, r) if { r(1); x > 0 } => }" + ).check( + """ + | "val r = go { case f(_, r) if { r(1); x > 0 } => }" + | ^ + |""".stripMargin, "Input guard must not perform any reply actions (r)") + } + * - { + compileError( + "val r = go { case f(_, r) if r.checkTimeout(1) && x > 0 => }" + ).check( + """ + | "val r = go { case f(_, r) if r.checkTimeout(1) && x > 0 => }" + | ^ + |""".stripMargin, "Input guard must not perform any reply actions (r)") + + } + } + + "fail to compile a guard that emits molecules" - { + * - { + compileError( + "val r = go { case f(_, r) if f(1) > 0 => r(1) }" + ).check( + """ + | "val r = go { case f(_, r) if f(1) > 0 => r(1) }" + | ^ + |""".stripMargin, "Input guard must not emit any output molecules (f)") + } + + * - { + compileError( + "val r = go { case f(_, r) if f.timeout(1)(1.second).nonEmpty => r(1) }" + ).check( + """ + | "val r = go { case f(_, r) if f.timeout(1)(1.second).nonEmpty => r(1) }" + | ^ + |""".stripMargin, "Input guard must not emit any output molecules (f)") + } + } + + "fail to compile a reaction with two case clauses" - { + * - { + compileError( + "val r = go { case a(x) =>; case c(_) => }" + ).check( + """ + | "val r = go { case a(x) =>; case c(_) => }" + | ^ + |""".stripMargin, "Reactions must contain only one `case` clause") + } + * - { + compileError( + """val result = +go { +case a(x) => c() +case c(_) + a(y) => c() +}""").check( + """ + |go { + | ^ + |""".stripMargin, "Reactions must contain only one `case` clause") + } + } + + "fail to compile a reaction that is not defined inline" - { + val body: ReactionBody = { + case _ => c() + } + assert(body.isInstanceOf[ReactionBody]) + compileError( + "val r = go(body)" + ).check( + """ + | "val r = go(body)" + | ^ + |""".stripMargin, "No `case` clauses found: Reactions must be defined inline with the `go { case ... => ... }` syntax") + } + + "fail to compile reactions with unconditional livelock when all matchers are trivial" - { + val a = m[(Int, Int)] + val bb = m[Int] + val bbb = m[Int] + + assert(a.isInstanceOf[M[(Int, Int)]]) + assert(bb.isInstanceOf[M[Int]]) + assert(bbb.isInstanceOf[M[Int]]) + + * - { + compileError( + "val r = go { case a((x,y)) => a((1,1)) }" + ).check( + """ + | "val r = go { case a((x,y)) => a((1,1)) }" + | ^ + """.stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") + } + * - { + compileError( + "val r = go { case a((_,x)) => a((x,x)) }" + ).check( + """ + | "val r = go { case a((_,x)) => a((x,x)) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") + } + * - { + val r = go { case a((1, _)) => a((1, 1)) } + assert(r.isInstanceOf[Reaction]) + } // cannot detect unconditional livelock here at compile time, since we can't evaluate the binder yet + * - { + val r = go { case bb(y) if y > 0 => bb(1) } + assert(r.isInstanceOf[Reaction]) + } // no unconditional livelock due to guard + * - { + val r = go { case bb(y) => if (y > 0) bb(1) } + assert(r.isInstanceOf[Reaction]) + } // no unconditional livelock due to `if` in reaction + * - { + val r = go { case bb(y) => if (y > 0) bbb(1) else bb(2) } + assert(r.isInstanceOf[Reaction]) + } // no unconditional livelock due to `if` in reaction + * - { + compileError( + "val r = go { case bb(x) => if (x > 0) bb(1) else bb(2) }" + ).check( + """ + | "val r = go { case bb(x) => if (x > 0) bb(1) else bb(2) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bb)") + } // unconditional livelock due to shrinkage of `if` in reaction + * - { + val r = go { case bbb(1) => bbb(2) } + assert(r.isInstanceOf[Reaction]) + // no livelock since constant values are different + } + * - { + compileError( + "val r = go { case bb(x) => bb(1) }" + ).check( + """ + | "val r = go { case bb(x) => bb(1) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bb)") + } // unconditional livelock + * - { + compileError( + "val r = go { case a(_) => a((1,1)) }" // ignore warning "class M expects 2 patterns to hold" + ).check( + """ + | "val r = go { case a(_) => a((1,1)) }" // ignore warning "class M expects 2 patterns to hold" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (a)") + } + // unconditional livelock + * - { + compileError( + "val r = go { case bbb(_) => bbb(0) }" + ).check( + """ + | "val r = go { case bbb(_) => bbb(0) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb)") + } + // unconditional livelock + * - { + compileError( + "val r = go { case bbb(x) => bbb(x + 1) + bb(x) }" + ).check( + """ + | "val r = go { case bbb(x) => bbb(x + 1) + bb(x) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb)") + } + * - { + compileError( + "val r = go { case bbb(x) + bb(y) => bbb(x + 1) + bb(x) + bb(y + 1) }" + ).check( + """ + | "val r = go { case bbb(x) + bb(y) => bbb(x + 1) + bb(x) + bb(y + 1) }" + | ^ + |""".stripMargin, "Unconditional livelock: Input molecules must not be a subset of output molecules, with all trivial matchers for (bbb, bb)") + } + } + + "fail to compile reactions with incorrect pattern matching" - { + val a = b[Unit, Unit] + val c = b[Unit, Boolean] + val e = m[Unit] + + assert(a.isInstanceOf[B[Unit, Unit]]) + assert(c.isInstanceOf[B[Unit, Boolean]]) + assert(e.isInstanceOf[M[Unit]]) + + // Note: these tests will produce several warnings "expects 2 patterns to hold but crushing into 2-tuple to fit single pattern". + // However, it is precisely this crushing that we are testing here, that actually should not compile with our `go` macro. + // So, these warnings cannot be removed and should be ignored. + * - { + compileError( + "val r = go { case e() => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case e() => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "not enough patterns for class M offering Unit: expected 1, found 0") + } + * - { + compileError( + "val r = go { case e(_,_) => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case e(_,_) => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "too many patterns for class M offering Unit: expected 1, found 2") + } + * - { + compileError( + "val r = go { case e(_,_,_) => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case e(_,_,_) => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "too many patterns for class M offering Unit: expected 1, found 3") + } + // "val r = go { case a() => }" shouldNot compile // no pattern variable for reply in "a" + * - { + compileError( + "val r = go { case a() => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case a() => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "not enough patterns for class B offering (Unit, io.chymyst.jc.ReplyEmitter[Unit,Unit]): expected 2, found 0") + } + // "val r = go { case a(_) => }" shouldNot compile // no pattern variable for reply in "a" + * - { + compileError( + "val r = go { case a(_) => }" // ignore warning "class B expects 2 patterns" + ).check( + """ + | "val r = go { case a(_) => }" // ignore warning "class B expects 2 patterns" + | ^ + |""".stripMargin, "Blocking input molecules must contain a pattern that matches a reply emitter with a simple variable (molecule a)") + } + // "val r = go { case a(_, _) => }" shouldNot compile // no pattern variable for reply in "a" + * - { + compileError( + "val r = go { case a(_, _) => }" + ).check( + """ + | "val r = go { case a(_, _) => }" + | ^ + |""".stripMargin, "Blocking input molecules must contain a pattern that matches a reply emitter with a simple variable (molecule a)") + } + // "val r = go { case a(_, _, _) => }" shouldNot compile // no pattern variable for reply in "a" + * - { + compileError( + "val r = go { case a(_, _, _) => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case a(_, _, _) => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "too many patterns for class B offering (Unit, io.chymyst.jc.ReplyEmitter[Unit,Unit]): expected 2, found 3") + } + // "val r = go { case a(_, r) => }" shouldNot compile // no reply is performed with r + * - { + compileError( + "val r = go { case a(_, r) => }" + ).check( + """ + | "val r = go { case a(_, r) => }" + | ^ + |""".stripMargin, "Blocking molecules must receive a reply but no unconditional reply found for (reply emitter r)") + } + // "val r = go { case a(_, r) + a(_) + c(_) => r() }" shouldNot compile // invalid patterns for "a" and "c" + * - { + compileError( + "val r = go { case a(_, r) + a(_) + c(_) => r() }" // ignore warning "class B expects 2 patterns" + ).check( + """ + | "val r = go { case a(_, r) + a(_) + c(_) => r() }" // ignore warning "class B expects 2 patterns" + | ^ + |""".stripMargin, "Blocking input molecules must contain a pattern that matches a reply emitter with a simple variable (molecule a, molecule c)") + } + // "val r = go { case a(_, r) + a(_) + c(_) => r(); r() }" shouldNot compile // two replies are performed with r, and invalid patterns for "a" and "c" + * - { + compileError( + "val r = go { case a(_, r) + a(_) + c(_) => r(); r() }" // ignore warning "class B expects 2 patterns" + ).check( + """ + | "val r = go { case a(_, r) + a(_) + c(_) => r(); r() }" // ignore warning "class B expects 2 patterns" + | ^ + |""".stripMargin, "Blocking input molecules must contain a pattern that matches a reply emitter with a simple variable (molecule a, molecule c)") + } + // "val r = go { case e(_) if true => c() }" should compile // input guard does not emit molecules + * - { + go { case e(_) if true => c() } // should compile without errors + } + // "val r = go { case e(_) if c() => }" shouldNot compile // input guard emits molecules + * - { + compileError( + "val r = go { case e(_) if c() => }" + ).check( + """ + | "val r = go { case e(_) if c() => }" + | ^ + |""".stripMargin, "Input guard must not emit any output molecules (c)") + } + // "val r = go { case a(_,r) if r() => }" shouldNot compile // input guard performs reply actions + * - { + compileError( + "val r = go { case a(_,r) if r.checkTimeout() => }" + ).check( + """ + | "val r = go { case a(_,r) if r.checkTimeout() => }" + | ^ + |""".stripMargin, "Input guard must not perform any reply actions (r)") + } + // "val r = go { case e(_) => { case e(_) => } }" shouldNot compile // reaction body matches on input molecules + * - { + compileError( + "val r = go { case e(_) => { case e(_) => }: ReactionBody }" + ).check( + """ + | "val r = go { case e(_) => { case e(_) => }: ReactionBody }" + | ^ + |""".stripMargin, "Reaction body must not contain a pattern that matches on molecules (e)") + } + * - { + compileError( + "val r = go { case e(_) if (null match { case e(_) => true }) => }" + ).check( + """ + | "val r = go { case e(_) if (null match { case e(_) => true }) => }" + | ^ + |""".stripMargin, "Input guard must not contain a pattern that matches on additional input molecules (e)") + } + } + + "fail to compile a reaction with regrouped inputs" - { + val a = m[Unit] + assert(a.isInstanceOf[M[Unit]]) + + // "val r = go { case a(_) + (a(_) + a(_)) => }" shouldNot compile + * - { + compileError( + "val r = go { case a(_) + (a(_) + a(_)) => }" + ).check( + """ + | "val r = go { case a(_) + (a(_) + a(_)) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + // "val r = go { case a(_) + (a(_) + a(_)) + a(_) => }" shouldNot compile + * - { + compileError( + "val r = go { case a(_) + (a(_) + a(_)) + a(_) => }" + ).check( + """ + | "val r = go { case a(_) + (a(_) + a(_)) + a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + // "val r = go { case (a(_) + a(_)) + a(_) + a(_) => }" should compile + * - { + go { case (a(_) + a(_)) + a(_) + a(_) => } + } + } + + "miscellaneous compile-time errors" - { + + "fail to compile reactions with no input molecules" - { + val bb = m[Int] + val bbb = m[Int] + + assert(bb.isInstanceOf[M[Int]]) + assert(bbb.isInstanceOf[M[Int]]) + + // "val r = go { case _ => bb(0) }" should compile // declaration of a static molecule + * - { + go { case _ => bb(0) } + } + // "val r = go { case x => x }" shouldNot compile // no input molecules + * - { + compileError( + "val r = go { case x => x }" + ).check( + """ + | "val r = go { case x => x }" + | ^ + |""".stripMargin, "Reaction input must be `_` or must contain some input molecules, but is (x @ _)") + } + // "val r = go { case x => bb(x.asInstanceOf[Int]) }" shouldNot compile // no input molecules + * - { + compileError( + "val r = go { case x => bb(x.asInstanceOf[Int]) }" + ).check( + """ + | "val r = go { case x => bb(x.asInstanceOf[Int]) }" + | ^ + |""".stripMargin, "Reaction input must be `_` or must contain some input molecules, but is (x @ _)") + } + } + + "fail to compile a reaction with grouped pattern variables in inputs" - { + val a = m[Unit] + assert(a.name == "a") + + // "val r = go { case a(_) + x@(a(_) + a(_)) => }" shouldNot compile + * - { + compileError( + "val r = go { case a(_) + x@(a(_) + a(_)) => }" + ).check( + """ + |val r = go { case a(_) + x@(a(_) + a(_)) => } + | ^ + |""".stripMargin, "'=>' expected but '@' found.") + } + // "val r = go { case a(_) + (a(_) + a(_)) + x@a(_) => }" shouldNot compile + * - { + compileError( + "val r = go { case a(_) + (a(_) + a(_)) + x@a(_) => }" + ).check( + """ + |val r = go { case a(_) + (a(_) + a(_)) + x@a(_) => } + | ^ + |""".stripMargin, "'=>' expected but '@' found.") + } + // "val r = go { case x@a(_) + (a(_) + a(_)) + a(_) => }" shouldNot compile + * - { + compileError( + "val r = go { case x@a(_) + (a(_) + a(_)) + a(_) => }" + ).check( + """ + | "val r = go { case x@a(_) + (a(_) + a(_)) + a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + * - { + compileError( + "val r = go { case (x@a(_) + a(_)) + a(_) => }" + ).check( + """ + | "val r = go { case (x@a(_) + a(_)) + a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + * - { + compileError( + "val r = go { case a(_) + (x@a(_) + a(_)) + a(_) => }" + ).check( + """ + | "val r = go { case a(_) + (x@a(_) + a(_)) + a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + // "val r = go { case x@(a(_) + a(_)) + a(_) + a(_) => }" shouldNot compile + * - { + compileError( + "val r = go { case x@(a(_) + a(_)) + a(_) + a(_) => }" + ).check( + """ + | "val r = go { case x@(a(_) + a(_)) + a(_) + a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + // "val r = go { case x@a(_) => }" shouldNot compile + * - { + compileError( + "val r = go { case x@a(_) => }" + ).check( + """ + | "val r = go { case x@a(_) => }" + | ^ + |""".stripMargin, "Reaction's input molecules must be grouped to the left in chemical notation, and have no @-pattern variables") + } + } + + "refuse reactions that match on other molecules in molecule input values" - { + val a = m[Any] + val c = m[Any] + val f = b[Any, Any] + assert(a.name == "a") + assert(c.name == "c") + assert(f.name == "f") + + go { case a(1) => a(a(1)) } // OK + + // "val r = go { case a(a(1)) => }" shouldNot compile + * - { + compileError( + "val r = go { case a(c(1)) => }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case a(c(1)) => }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "Input molecules must not contain a pattern that uses other molecules inside molecule value patterns (c)") + } + // "val r = go { case f(_, 123) => }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, 123) => }" + ).check( + """ + | "val r = go { case f(_, 123) => }" + | ^ + |""".stripMargin, "type mismatch;\n found : Int(123)\n required: io.chymyst.jc.ReplyEmitter[Any,Any]") + } + * - { + compileError( + "val r = go { case f(_, null) => }" + ).check( + """ + | "val r = go { case f(_, null) => }" + | ^ + |""".stripMargin, "Blocking input molecules must contain a pattern that matches a reply emitter with a simple variable (molecule f)") + } + // "val r = go { case f(a(1), r) => r(1) }" shouldNot compile + * - { + compileError( + "val r = go { case f(a(1), r) => r(1) }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case f(a(1), r) => r(1) }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "Input molecules must not contain a pattern that uses other molecules inside molecule value patterns (a)") + } + // "val r = go { case f(f(1, s), r) => r(1) }" shouldNot compile + * - { + compileError( + "val r = go { case f(f(1, s), r) => r(1) }" // ignore warning "non-variable type argument" + ).check( + """ + | "val r = go { case f(f(1, s), r) => r(1) }" // ignore warning "non-variable type argument" + | ^ + |""".stripMargin, "Input molecules must not contain a pattern that uses other molecules inside molecule value patterns (f)") + } + } + } + + "reply checking" - { + "compile a reaction with unconditional reply after shrinking" - { + val f = b[Unit, Int] + assert(f.name == "f") + + // "val r = go { case f(_,r) => if (System.nanoTime() > 0) r(1) else r(2) }" should compile + * - { + go { case f(_, r) => if (System.nanoTime() > 0) r(1) else r(2) } + } + // "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) else r(2) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) else r(2) }" + ).check( + """ + | "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) else r(2) }" + | ^ + |""".stripMargin, "Blocking molecules must receive only one reply but possibly multiple replies found for (reply emitter r)") + } + // "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) }" + ).check( + """ + | "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) }" + | ^ + |""".stripMargin, "Blocking molecules must receive only one reply but possibly multiple replies found for (reply emitter r)") + } + // "val r = go { case f(_,r) => if (System.nanoTime() > 0) r(1) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => if (System.nanoTime() > 0) r(1) }" + ).check( + """ + | "val r = go { case f(_,r) => if (System.nanoTime() > 0) r(1) }" + | ^ + |""".stripMargin, "Blocking molecules must receive a reply but no unconditional reply found for (reply emitter r)") + } + } + + "refuse to compile a reaction with two conditional replies" - { + val f = b[Unit, Int] + assert(f.name == "f") + + // "val r = go { case f(_,r) => val x = System.nanoTime() > 0; if (x) r(1); if (x) r(2) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => val x = System.nanoTime() > 0; if (x) r(1); if (x) r(2) }" + ).check( + """ + | "val r = go { case f(_,r) => val x = System.nanoTime() > 0; if (x) r(1); if (x) r(2) }" + | ^ + |""".stripMargin, "Blocking molecules must receive a reply but no unconditional reply found for (reply emitter r)") + } // reply emitted in only one `if` branch, twice + // "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) }" + ).check( + """ + | "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) }" + | ^ + |""".stripMargin, "Blocking molecules must receive only one reply but possibly multiple replies found for (reply emitter r)") + } // reply emitted once, and then in one `if` branch + // "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) else r(3) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) else r(3) }" + ).check( + """ + | "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) else r(3) }" + | ^ + |""".stripMargin, "Blocking molecules must receive only one reply but possibly multiple replies found for (reply emitter r)") + } // reply emitted once, and then in both `if` branches + } + + "refuse to compile a reaction with no unconditional reply" - { + val f = b[Unit, Unit] + assert(f.name == "f") + + // "val r = go { case f(_,r) => if (System.nanoTime() > 0) r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => if (System.nanoTime() > 0) r() }" + ).check( + """ + | "val r = go { case f(_,r) => if (System.nanoTime() > 0) r() }" + | ^ + |""".stripMargin, "Blocking molecules must receive a reply but no unconditional reply found for (reply emitter r)") + } // reply emitted in only one `if` branch + // "val r = go { case f(_,r) => if (System.nanoTime() > 0) f() else r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => if (System.nanoTime() > 0) f() else r() }" + ).check( + """ + | "val r = go { case f(_,r) => if (System.nanoTime() > 0) f() else r() }" + | ^ + |""".stripMargin, "Blocking molecules must receive a reply but no unconditional reply found for (reply emitter r)") + } // ditto + } + + "refuse to compile a reaction with reply under try/catch" - { + val f = b[Unit, Unit] + assert(f.name == "f") + + // """val r = go { case f(_,r) => try{ throw new Exception(""); r() } catch { case e: Exception => } }""" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => try{ throw new Exception(\"\"); r() } catch { case e: Exception => } }" // ignore warning "dead code following" + ).check( + """ + | "val r = go { case f(_,r) => try{ throw new Exception(\"\"); r() } catch { case e: Exception => } }" // ignore warning "dead code following" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } // reply emitted under try + // """val r = go { case f(_,r) => try{ throw new Exception("") } catch { case e: Exception => r() } }""" shouldNot compile + * - { + compileError( + "val r = go { case f(_,r) => try{ throw new Exception(\"\") } catch { case e: Exception => r() } }" + ).check( + """ + | "val r = go { case f(_,r) => try{ throw new Exception(\"\") } catch { case e: Exception => r() } }" + | ^ + |""".stripMargin + , "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))" + ) + } // reply emitted under try + // """val r = go { case f(_,r) => try{ throw new Exception("") } catch { case e: Exception => } finally { r() } }""" should compile // reply emitted under finally + * - { + go { case f(_, r) => try { + throw new Exception("") + } catch { + case e: Exception => + } finally { + r() + } + } + } + } + + "nonlinear output environments" - { + "refuse emitting blocking molecules" - { + val c = m[Unit] + val f = b[Unit, Unit] + val f2 = b[Int, Unit] + val f3 = b[Unit, Boolean] + val g: Any => Any = x => x + + assert(c.name == "c") + assert(f.name == "f") + assert(f2.name == "f2") + assert(f3.name == "f3") + assert(g(()) == (())) + + go { case c(_) => g(f) } // OK to apply a function to a blocking molecule emitter? + // "val r = go { case c(_) => (0 until 10).flatMap { _ => f(); List() } }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => (0 until 10).flatMap { _ => f(); List() } }" + ).check( + """ + | "val r = go { case c(_) => (0 until 10).flatMap { _ => f(); List() } }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f(()))") + } // reaction body must not emit blocking molecules inside function blocks + // "val r = go { case c(_) => (0 until 10).foreach(i => f2(i)) }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => (0 until 10).foreach(i => f2(i)) }" + ).check( + """ + | "val r = go { case c(_) => (0 until 10).foreach(i => f2(i)) }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f2(?))") + } // same + // "val r = go { case c(_) => (0 until 10).foreach(_ => c()) }" should compile // `c` is a non-blocking molecule, OK to emit it anywhere + * - { + go { case c(_) => (0 until 10).foreach(_ => c()) } + } + // "val r = go { case c(_) => (0 until 10).foreach(f2) }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => (0 until 10).foreach(f2) }" + ).check( + """ + | "val r = go { case c(_) => (0 until 10).foreach(f2) }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f2(?))") + } // same + + // "val r = go { case c(_) => (0 until 10).foreach{_ => g(f); () } }" shouldNot compile + // TODO: for some reason, utest fails to detect the compile error here (but the error does exist) + // for now, this test was moved to MacroErrorSpec.scala + + // * - { + // compileError( + // "val r = go { case c(_) => (0 until 10).foreach{_ => g(f); () } }" + // ).check( + // """ + // | "val r = go { case c(_) => (0 until 10).foreach{_ => g(f); () } }" + // | ^ + // |""".stripMargin, "error message") + // } + // "val r = go { case c(_) => while (true) f() }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => while (true) f() }" + ).check( + """ + | "val r = go { case c(_) => while (true) f() }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f(()))") + } + // "val r = go { case c(_) => while (true) c() }" should compile // `c` is a non-blocking molecule, OK to emit it anywhere + * - { + go { case c(_) => while (true) c() } + } + // "val r = go { case c(_) => while (f3()) { () } }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => while (f3()) { () } }" + ).check( + """ + | "val r = go { case c(_) => while (f3()) { () } }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f3(()))") + } + // "val r = go { case c(_) => do f() while (true) }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => do f() while (true) }" + ).check( + """ + | "val r = go { case c(_) => do f() while (true) }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f(()))") + } + // "val r = go { case c(_) => do c() while (f3()) }" shouldNot compile + * - { + compileError( + "val r = go { case c(_) => do c() while (f3()) }" + ).check( + """ + | "val r = go { case c(_) => do c() while (f3()) }" + | ^ + |""".stripMargin, "Reaction body must not emit blocking molecules inside function blocks (molecule f3(()))") + } + } + + "refuse putting a reply emitter on another molecule" - { + val f = b[Unit, Unit] + val d = m[ReplyEmitter[Unit, Unit]] + + assert(f.name == "f") + assert(d.name == "d") + + // "val r = go { case f(_, r) => d(r); r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => d(r); r() }" + ).check( + """ + | "val r = go { case f(_, r) => d(r); r() }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } // TODO: error message should be "can't put the reply emitter onto a molecule" + * - { + compileError( + "val r = go { case f(_, r) => d(r) }" + ).check( + """ + | "val r = go { case f(_, r) => d(r) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } + * - { + val g = b[ReplyEmitter[Unit, Unit], Unit] + assert(g.isBlocking) + + compileError( + "val r = go { case f(_, r) => g(r) }" + ).check( + """ + | "val r = go { case f(_, r) => g(r) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } + } + + "refuse calling a function on a reply emitter" - { + val f = b[Unit, Unit] + val g: Any => Any = x => x + + assert(f.name == "f") + assert(g(()) == (())) + + // "val r = go { case f(_, r) => g(r); r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => g(r); r() }" + ).check( + """ + | "val r = go { case f(_, r) => g(r); r() }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } // can't call a function on a reply emitter + // "val r = go { case f(_, r) => val x = r; g(x); r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => val x = r; g(x); r() }" + ).check( + """ + | "val r = go { case f(_, r) => val x = r; g(x); r() }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?), reply emitter x(?))") + } // can't call a function on an alias for reply emitter + } + + "refuse emitting blocking molecule replies" - { + val f = b[Unit, Unit] + val f2 = b[Unit, Int] + val g: Any => Any = x => x + + assert(f.name == "f") + assert(f2.name == "f2") + assert(g(()) == (())) + + // "val r = go { case f(_, r) => (0 until 10).flatMap { _ => r(); List() } }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => (0 until 10).flatMap { _ => r(); List() } }" + ).check( + """ + | "val r = go { case f(_, r) => (0 until 10).flatMap { _ => r(); List() } }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } // reaction body must not emit blocking molecule replies inside function blocks + // "val r = go { case f2(_, r) => (0 until 10).foreach(i => r(i)) }" shouldNot compile + * - { + compileError( + "val r = go { case f2(_, r) => (0 until 10).foreach(i => r(i)) }" + ).check( + """ + | "val r = go { case f2(_, r) => (0 until 10).foreach(i => r(i)) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } // same + // "val r = go { case f2(_, r) => (0 until 10).foreach(r) }" shouldNot compile + * - { + compileError( + "val r = go { case f2(_, r) => (0 until 10).foreach(r) }" + ).check( + """ + | "val r = go { case f2(_, r) => (0 until 10).foreach(r) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } // same + // "val r = go { case f2(_, r) => (0 until 10).foreach(_ => g(r)); r(0) }" shouldNot compile + * - { + compileError( + "val r = go { case f2(_, r) => (0 until 10).foreach(_ => g(r)); r(0) }" + ).check( + """ + | "val r = go { case f2(_, r) => (0 until 10).foreach(_ => g(r)); r(0) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(?))") + } + // "val r = go { case f(_, r) => while (true) r() }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => while (true) r() }" + ).check( + """ + | "val r = go { case f(_, r) => while (true) r() }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } + // "val r = go { case f(_, r) => while (r.checkTimeout()) { () } }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => while (r.checkTimeout()) { () } }" + ).check( + """ + | "val r = go { case f(_, r) => while (r.checkTimeout()) { () } }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } + // "val r = go { case f(_, r) => do r() while (true) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => do r() while (true) }" + ).check( + """ + | "val r = go { case f(_, r) => do r() while (true) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } + // "val r = go { case f(_, r) => do () while (r.checkTimeout()) }" shouldNot compile + * - { + compileError( + "val r = go { case f(_, r) => do () while (r.checkTimeout()) }" + ).check( + """ + | "val r = go { case f(_, r) => do () while (r.checkTimeout()) }" + | ^ + |""".stripMargin, "Reaction body must not use reply emitters inside function blocks (reply emitter r(()))") + } + } + + } + } + // End of tests. + } +} \ No newline at end of file diff --git a/core/src/test/scala/io/chymyst/jc/MacroErrorSpec.scala b/core/src/test/scala/io/chymyst/jc/MacroErrorSpec.scala index ce889f53..a03234b3 100644 --- a/core/src/test/scala/io/chymyst/jc/MacroErrorSpec.scala +++ b/core/src/test/scala/io/chymyst/jc/MacroErrorSpec.scala @@ -6,19 +6,6 @@ import org.scalatest.{FlatSpec, Matchers} class MacroErrorSpec extends FlatSpec with Matchers { - behavior of "miscellaneous compile-time errors" - - it should "fail to compile molecules with non-unit types emitted as a()" in { - val c = m[Unit] - - // These should compile. - def emitThem() = { // ignore warning "local method ... is never used" - c(()) - c() - c(123) // ignore warnings "discarded non-Unit value" and "a pure expression does nothing" - } - } - it should "support concise syntax for Unit-typed molecules" in { val a = new M[Unit]("a") val f = new B[Unit, Unit]("f") @@ -50,186 +37,20 @@ class MacroErrorSpec extends FlatSpec with Matchers { ) shouldEqual WarningsAndErrors(List(), List(), "Site{d → ...}") } - it should "fail to compile reactions with incorrect pattern matching" in { - val a = b[Unit, Unit] - val c = b[Unit, Unit] - val e = m[Unit] - - a.isInstanceOf[B[Unit, Unit]] shouldEqual true - c.isInstanceOf[B[Unit, Unit]] shouldEqual true - e.isInstanceOf[M[Unit]] shouldEqual true - - // Note: these tests will produce several warnings "expects 2 patterns to hold but crushing into 2-tuple to fit single pattern". - // However, it is precisely this crushing that we are testing here, that actually should not compile with our `go` macro. - // So, these warnings cannot be removed here and should be ignored. - "val r = go { case e() => }" shouldNot compile // no pattern variable in a non-blocking molecule "e" - "val r = go { case e(_,_) => }" shouldNot compile // two pattern variables in a non-blocking molecule "e" - "val r = go { case e(_,_,_) => }" shouldNot compile // two pattern variables in a non-blocking molecule "e" - - "val r = go { case a() => }" shouldNot compile // no pattern variable for reply in "a" - "val r = go { case a(_) => }" shouldNot compile // no pattern variable for reply in "a" - "val r = go { case a(_, _) => }" shouldNot compile // no pattern variable for reply in "a" - "val r = go { case a(_, _, _) => }" shouldNot compile // no pattern variable for reply in "a" - "val r = go { case a(_, r) => }" shouldNot compile // no reply is performed with r - "val r = go { case a(_, r) + a(_) + c(_) => r() }" shouldNot compile // invalid patterns for "a" and "c" - "val r = go { case a(_, r) + a(_) + c(_) => r(); r() }" shouldNot compile // two replies are performed with r, and invalid patterns for "a" and "c" - - "val r = go { case e(_) if true => c() }" should compile // input guard does not emit molecules - "val r = go { case e(_) if c() => }" shouldNot compile // input guard emits molecules - "val r = go { case a(_,r) if r() => }" shouldNot compile // input guard performs reply actions - - "val r = go { case e(_) => { case e(_) => } }" shouldNot compile // reaction body matches on input molecules - } - - it should "fail to compile reactions with no input molecules" in { - val bb = m[Int] - val bbb = m[Int] - - bb.isInstanceOf[M[Int]] shouldEqual true - bbb.isInstanceOf[M[Int]] shouldEqual true - - "val r = go { case _ => bb(0) }" should compile // declaration of a static molecule - "val r = go { case x => bb(x.asInstanceOf[Int]) }" shouldNot compile // no input molecules - "val r = go { case x => x }" shouldNot compile // no input molecules - } - - it should "fail to compile a reaction with regrouped inputs" in { - val a = m[Unit] - a.isInstanceOf[M[Unit]] shouldEqual true - - "val r = go { case a(_) + (a(_) + a(_)) => }" shouldNot compile - "val r = go { case a(_) + (a(_) + a(_)) + a(_) => }" shouldNot compile - "val r = go { case (a(_) + a(_)) + a(_) + a(_) => }" should compile - } - - it should "fail to compile a reaction with grouped pattern variables in inputs" in { - val a = m[Unit] - a.name shouldEqual "a" - - "val r = go { case a(_) + x@(a(_) + a(_)) => }" shouldNot compile - "val r = go { case a(_) + (a(_) + a(_)) + x@a(_) => }" shouldNot compile - "val r = go { case x@a(_) + (a(_) + a(_)) + a(_) => }" shouldNot compile - "val r = go { case x@(a(_) + a(_)) + a(_) + a(_) => }" shouldNot compile - "val r = go { case x@a(_) => }" shouldNot compile - } - - it should "refuse reactions that match on other molecules in molecule input values" in { - val a = m[Any] - val f = b[Any, Any] - a.name shouldEqual "a" - f.name shouldEqual "f" - - go { case a(1) => a(a(1)) } // OK - - "val r = go { case a(a(1)) => }" shouldNot compile - "val r = go { case f(_, 123) => }" shouldNot compile - "val r = go { case f(a(1), r) => r(1) }" shouldNot compile - "val r = go { case f(f(1,s), r) => r(1) }" shouldNot compile - } - - behavior of "reply check" - - it should "compile a reaction with unconditional reply after shrinking" in { - val f = b[Unit, Int] - f.name shouldEqual "f" - - "val r = go { case f(_,r) => if (System.nanoTime() > 0) r(1) else r(2) }" should compile - "val r = go { case f(_,r) => r(0); if (System.nanoTime() > 0) r(1) else r(2) }" shouldNot compile - } - - it should "refuse to compile a reaction with two conditional replies" in { - val f = b[Unit, Int] - f.name shouldEqual "f" - - "val r = go { case f(_,r) => val x = System.nanoTime() > 0; if (x) r(1); if (x) r(2) }" shouldNot compile // reply emitted in only one `if` branch, twice - "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) }" shouldNot compile // reply emitted once, and then in one `if` branch - "val r = go { case f(_,r) => val x = System.nanoTime() > 0; r(1); if (x) r(2) else r(3) }" shouldNot compile // reply emitted once, and then in both `if` branches - } - - it should "refuse to compile a reaction with no unconditional reply" in { - val f = b[Unit, Unit] - f.name shouldEqual "f" - - "val r = go { case f(_,r) => if (System.nanoTime() > 0) r() }" shouldNot compile // reply emitted in only one `if` branch - "val r = go { case f(_,r) => if (System.nanoTime() > 0) f() else r() }" shouldNot compile // ditto - } - - it should "refuse to compile a reaction with reply under try/catch" in { - val f = b[Unit, Unit] - f.name shouldEqual "f" - - """val r = go { case f(_,r) => try{ throw new Exception(""); r() } catch { case e: Exception => } }""" shouldNot compile // reply emitted under try - """val r = go { case f(_,r) => try{ throw new Exception("") } catch { case e: Exception => r() } }""" shouldNot compile // reply emitted under try - """val r = go { case f(_,r) => try{ throw new Exception("") } catch { case e: Exception => } finally { r() } }""" should compile // reply emitted under finally - } - - behavior of "nonlinear output environments" + behavior of "output environments" it should "refuse emitting blocking molecules" in { val c = m[Unit] val f = b[Unit, Unit] - val f2 = b[Int, Unit] - val f3 = b[Unit, Boolean] val g: Any => Any = x => x - c.name shouldEqual "c" - f.name shouldEqual "f" - f2.name shouldEqual "f2" - f3.name shouldEqual "f3" - g(()) shouldEqual (()) + assert(c.name == "c") + assert(f.name == "f") + assert(g(()) == (())) - go { case c(_) => g(f) } // OK to apply a function to a blocking molecule emitter? - "val r = go { case c(_) => (0 until 10).flatMap { _ => f(); List() } }" shouldNot compile // reaction body must not emit blocking molecules inside function blocks - "val r = go { case c(_) => (0 until 10).foreach(i => f2(i)) }" shouldNot compile // same - "val r = go { case c(_) => (0 until 10).foreach(_ => c()) }" should compile // `c` is a non-blocking molecule, OK to emit it anywhere - "val r = go { case c(_) => (0 until 10).foreach(f2) }" shouldNot compile // same + // For some reason, utest cannot get the compiler error message for this case. + // So we have to move this test back to scalatest, even though here we can't check the error message. "val r = go { case c(_) => (0 until 10).foreach{_ => g(f); () } }" shouldNot compile - "val r = go { case c(_) => while (true) f() }" shouldNot compile - "val r = go { case c(_) => while (true) c() }" should compile // `c` is a non-blocking molecule, OK to emit it anywhere - "val r = go { case c(_) => while (f3()) { () } }" shouldNot compile - "val r = go { case c(_) => do f() while (true) }" shouldNot compile - "val r = go { case c(_) => do c() while (f3()) }" shouldNot compile - } - - it should "refuse putting a reply emitter on another molecule" in { - val f = b[Unit, Unit] - val d = m[ReplyValue[Unit, Unit]] - - f.name shouldEqual "f" - d.name shouldEqual "d" - - "val r = go { case f(_, r) => d(r); r() }" shouldNot compile // can't put the reply emitter onto a molecule - - } - - it should "refuse calling a function on a reply emitter" in { - val f = b[Unit, Unit] - val g: Any => Any = x => x - - f.name shouldEqual "f" - g(()) shouldEqual (()) - - "val r = go { case f(_, r) => g(r); r() }" shouldNot compile // can't call a function on a reply emitter - "val r = go { case f(_, r) => val x = r; g(x); r() }" shouldNot compile // can't call a function on an alias for reply emitter - } - - it should "refuse emitting blocking molecule replies" in { - val f = b[Unit, Unit] - val f2 = b[Unit, Int] - val g: Any => Any = x => x - - f.name shouldEqual "f" - f2.name shouldEqual "f2" - g(()) shouldEqual (()) - - "val r = go { case f(_, r) => (0 until 10).flatMap { _ => r(); List() } }" shouldNot compile // reaction body must not emit blocking molecule replies inside function blocks - "val r = go { case f2(_, r) => (0 until 10).foreach(i => r(i)) }" shouldNot compile // same - "val r = go { case f2(_, r) => (0 until 10).foreach(r) }" shouldNot compile // same - "val r = go { case f2(_, r) => (0 until 10).foreach(_ => g(r)); r(0) }" shouldNot compile - "val r = go { case f(_, r) => while (true) r() }" shouldNot compile - "val r = go { case f(_, r) => while (r.checkTimeout()) { () } }" shouldNot compile - "val r = go { case f(_, r) => do r() while (true) }" shouldNot compile - "val r = go { case f(_, r) => do () while (r.checkTimeout()) }" shouldNot compile } behavior of "compile-time errors due to chemistry" @@ -254,9 +75,11 @@ class MacroErrorSpec extends FlatSpec with Matchers { val bb = m[(Int, Option[Int])] val result = go { // ignore warning about "non-variable type argument Int" + // This generates a compiler warning "class M expects 2 patterns to hold (Int, Option[Int]) but crushing into 2-tuple to fit single pattern (SI-6675)". // However, this "crushing" is precisely what this test focuses on, and we cannot tell scalac to ignore this warning. - case bb(_) + bb(z) if (z match { // ignore warning about "class M expects 2 patterns to hold" + case bb(_) + bb(z) if (z match // ignore warning about "class M expects 2 patterns to hold" + { case (1, Some(x)) if x > 0 => true; case _ => false }) => @@ -277,4 +100,4 @@ class MacroErrorSpec extends FlatSpec with Matchers { } -} \ No newline at end of file +} diff --git a/core/src/test/scala/io/chymyst/jc/MacrosSpec.scala b/core/src/test/scala/io/chymyst/jc/MacrosSpec.scala index 8b27b13e..4edcb004 100644 --- a/core/src/test/scala/io/chymyst/jc/MacrosSpec.scala +++ b/core/src/test/scala/io/chymyst/jc/MacrosSpec.scala @@ -75,10 +75,10 @@ behavior of "reaction sha1" b.emittingReactions.size shouldEqual 1 b.emittingReactions.map(_.toString) shouldEqual Set(expectedReaction) c.emittingReactions shouldEqual Set() - a.consumingReactions.get.size shouldEqual 1 - a.consumingReactions.get.head.toString shouldEqual expectedReaction - b.consumingReactions shouldEqual None - c.consumingReactions.get shouldEqual a.consumingReactions.get + a.consumingReactions.length shouldEqual 1 + a.consumingReactions.head.toString shouldEqual expectedReaction + b.consumingReactions shouldEqual Array() + c.consumingReactions shouldEqual a.consumingReactions } behavior of "macros for defining new molecule emitters" @@ -853,7 +853,7 @@ behavior of "reaction sha1" site( go { case a(1) => a(1) } ) - a.consumingReactions.get.map(_.info.outputs) shouldEqual Set(List(OutputMoleculeInfo(a, ConstOutputPattern(1), List()))) + a.consumingReactions.map(_.info.outputs) shouldEqual Set(List(OutputMoleculeInfo(a, ConstOutputPattern(1), List()))) } thrown.getMessage shouldEqual "In Site{a → ...}: Unavoidable livelock: reaction {a(1) → a(1)}" } @@ -869,10 +869,10 @@ behavior of "reaction sha1" a(2) } ) - a.consumingReactions.get.length shouldEqual 1 a.emittingReactions.size shouldEqual 1 - a.consumingReactions.get.map(_.info.outputs).head shouldEqual List(OutputMoleculeInfo(a, ConstOutputPattern(2), List())) - a.consumingReactions.get.map(_.info.inputs).head shouldEqual List(InputMoleculeInfo(a, 0, ConstInputPattern(1), constantOneSha1, 'Int)) + a.consumingReactions.length shouldEqual 1 + a.consumingReactions.map(_.info.outputs).head shouldEqual List(OutputMoleculeInfo(a, ConstOutputPattern(2), List())) + a.consumingReactions.map(_.info.inputs).head shouldEqual List(InputMoleculeInfo(a, 0, ConstInputPattern(1), constantOneSha1, 'Int)) a.emittingReactions.map(_.info.outputs).head shouldEqual List(OutputMoleculeInfo(a, ConstOutputPattern(2), List())) a.emittingReactions.map(_.info.inputs).head shouldEqual List(InputMoleculeInfo(a, 0, ConstInputPattern(1), constantOneSha1, Symbol("Int"))) } @@ -899,8 +899,8 @@ behavior of "reaction sha1" site( go { case a(2) => b(2); a(1); b(1) } ) - a.consumingReactions.get.length shouldEqual 1 - a.consumingReactions.get.map(_.info.outputs).head shouldEqual List( + a.consumingReactions.length shouldEqual 1 + a.consumingReactions.map(_.info.outputs).head shouldEqual List( OutputMoleculeInfo(b, ConstOutputPattern(2), List()), OutputMoleculeInfo(a, ConstOutputPattern(1), List()), OutputMoleculeInfo(b, ConstOutputPattern(1), List()) @@ -923,7 +923,7 @@ behavior of "reaction sha1" a.isBound shouldEqual true c.isBound shouldEqual false - val reaction = a.consumingReactions.get.head + val reaction = a.consumingReactions.head c.emittingReactions.head shouldEqual reaction a.emittingReactions.head shouldEqual reaction @@ -950,11 +950,11 @@ behavior of "reaction sha1" c.isBound shouldEqual false d.isBound shouldEqual true - val reaction1 = d.consumingReactions.get.head + val reaction1 = d.consumingReactions.head a.emittingReactions.head shouldEqual reaction1 c.emittingReactions.head shouldEqual reaction1 - val reaction2 = a.consumingReactions.get.head + val reaction2 = a.consumingReactions.head d.emittingReactions.head shouldEqual reaction2 reaction1.info.inputs shouldEqual List(InputMoleculeInfo(d, 0, WildcardInput, wildcardSha1, 'Unit)) @@ -1030,8 +1030,8 @@ behavior of "reaction sha1" ) val inputs = new InputMoleculeList(2) - inputs(0) = (dIncorrectStaticMol, MolValue(())) - inputs(1) = (e, MolValue(())) + inputs(0) = MolValue(()) + inputs(1) = MolValue(()) val thrown = intercept[Exception] { r1.body.apply((inputs.length - 1, inputs)) shouldEqual 123 // Reaction ran on a non-reaction thread (i.e. on this thread) and attempted to emit the static molecule. } diff --git a/core/src/test/scala/io/chymyst/test/MoleculesSpec.scala b/core/src/test/scala/io/chymyst/test/MoleculesSpec.scala index 92f92b6a..8c0e035c 100644 --- a/core/src/test/scala/io/chymyst/test/MoleculesSpec.scala +++ b/core/src/test/scala/io/chymyst/test/MoleculesSpec.scala @@ -16,6 +16,7 @@ class MoleculesSpec extends FlatSpec with Matchers with TimeLimitedTests with Be implicit val patienceConfig = PatienceConfig(timeout = Span(500, Millis)) override def beforeEach(): Unit = { + clearErrorLog() tp0 = new FixedPool(4) } @@ -289,12 +290,15 @@ class MoleculesSpec extends FlatSpec with Matchers with TimeLimitedTests with Be Thread.sleep(20) r } - println(results.groupBy(identity).mapValues(_.size)) + println(s"results for test 1: ${results.groupBy(identity).mapValues(_.size)}") globalErrorLog.toList should contain("In Site{p → ...}: Reaction {p(s) → } with inputs [p/P(c)] produced an exception that is internal to Chymyst Core. Retry run was not scheduled. Message: Molecule c is not bound to any reaction site") results should contain(123) results should contain(0) } + // This test verifies that unbound molecule emitters will cause an exception when used in a nested reaction site. + // The way to avoid this problem is to define nested reaction sites *before* the new emitters are used. + // This test intentionally defines the reaction site defining the {e -> } reaction *after* the `e` emitter is passed to the `a` reaction. it should "start reactions and throw exception when molecule emitters are passed to nested reactions slightly before they are bound" in { val results = (1 to 100).map { i => val a = m[M[Int]] @@ -323,10 +327,10 @@ class MoleculesSpec extends FlatSpec with Matchers with TimeLimitedTests with Be } ) begin() - Thread.sleep(20) + Thread.sleep(scala.util.Random.nextInt(40).toLong) r } - println(results.groupBy(identity).mapValues(_.size)) + println(s"results for test 2: ${results.groupBy(identity).mapValues(_.size)}") results should contain(246) globalErrorLog.toList should contain("In Site{a → ...}: Reaction {a(s) → } with inputs [a/P(e)] produced an exception that is internal to Chymyst Core. Retry run was not scheduled. Message: Molecule e is not bound to any reaction site") results should contain(0) @@ -355,7 +359,7 @@ class MoleculesSpec extends FlatSpec with Matchers with TimeLimitedTests with Be begin() f() } - println(results.groupBy(identity).mapValues(_.size)) + println(s"results for test 3: ${results.groupBy(identity).mapValues(_.size)}") globalErrorLog.toList should not contain "In Site{q → ...}: Reaction {q(s) → } with inputs [q/P(e)] produced an exception that is internal to Chymyst Core. Retry run was not scheduled. Message: Molecule e is not bound to any reaction site" results should contain(246) results should not contain 0 @@ -548,7 +552,7 @@ class MoleculesSpec extends FlatSpec with Matchers with TimeLimitedTests with Be c(n) (1 to n).foreach { _ => if (d.timeout()(1500 millis).isEmpty) { - println(globalErrorLog.take(50).toList) // this should not happen, but will be helpful for debugging + println(s"first 50 items from global log for test 4:\n${globalErrorLog.take(50).toList}") // this should not happen, but will be helpful for debugging } } diff --git a/core/src/test/scala/io/chymyst/test/StaticAnalysisSpec.scala b/core/src/test/scala/io/chymyst/test/StaticAnalysisSpec.scala index 34392688..6406a9e5 100644 --- a/core/src/test/scala/io/chymyst/test/StaticAnalysisSpec.scala +++ b/core/src/test/scala/io/chymyst/test/StaticAnalysisSpec.scala @@ -442,6 +442,45 @@ class StaticAnalysisSpec extends FlatSpec with Matchers with TimeLimitedTests { warnings shouldEqual WarningsAndErrors(List(), List(), "Site{a + a → ...}") } + behavior of "livelock with static molecules" + + it should "give warning in a simple reaction with possible livelock" in { + withPool(new FixedPool(2)) { tp ⇒ + val a = m[Int] + val warnings = site(tp)( + go { case _ ⇒ a(1) }, + go { case a(1) ⇒ val x = 1; a(x) } + ) + warnings shouldEqual WarningsAndErrors(List("Possible livelock: reaction {a(1) → a(?)}"), List(), "Site{a → ...}") + }.get + } + + // TODO: rewrite this test when static molecules are property analyzed for emitting code environment + it should "in a reaction with static molecule emitted conditionally" in { + withPool(new FixedPool(2)) { tp ⇒ + val a = m[Int] + val warnings = site(tp)( + go { case _ ⇒ a(1) }, + go { case a(1) ⇒ val x = 1; if (x > 0) a(x) } + ) + warnings shouldEqual WarningsAndErrors(List("Possible livelock: reaction {a(1) → a(?)}"), List(), "Site{a → ...}") + }.get + } + + // TODO: rewrite this test when static molecules are property analyzed for emitting code environment + it should "in a reaction with static molecule emitted conditionally with two branches" in { + withPool(new FixedPool(2)) { tp ⇒ + val a = m[Int] + the[Exception] thrownBy { + val warnings = site(tp)( + go { case _ ⇒ a(1) }, + go { case a(1) ⇒ val x = 1; if (x > 0) a(x) else a(-x) } + ) + warnings shouldEqual WarningsAndErrors(List("Possible livelock: reaction {a(1) → a(?)}"), List(), "Site{a → ...}") + } should have message "In Site{a → ...}: Incorrect static molecule declaration: static molecule (a) emitted more than once by reaction a(1) → a(?) + a(?)" + }.get + } + behavior of "deadlock detection" it should "not warn about likely deadlock for a reaction that emits molecules for itself in the right order" in { diff --git a/docs/README.md b/docs/README.md index 08e9a69d..a9985bb0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,7 +30,7 @@ See also these [talk slides revised for the current syntax](https://github.com/w #### [Comparison of the chemical machine vs. the coroutines / channels approach (CSP)](docs/chymyst_vs_jc.md#comparison-chemical-machine-vs-csp) -#### [Technical documentation for `Chymyst Core`](docs/chymyst-core.md). +#### [Technical documentation for `Chymyst Core`](docs/chymyst-core.md) #### [Source code repository for `Chymyst Core`](https://github.com/Chymyst/chymyst-core) diff --git a/docs/roadmap.md b/docs/roadmap.md index e7acceaf..9aecf6d3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,15 +2,15 @@ # Version history -- 0.1.8 "Singleton" molecules and reactions are now called "static", which is more accurate. Added more examples, including a fully concurrent Game of Life. Some optimizations in the reaction scheduler. Support for pipelined molecules (an automatic optimization). +- 0.1.8 "Singleton" molecules and reactions are now called "static", which is more accurate. Added more tutorial examples, including fork/join and a fully concurrent Game of Life. Some code cleanups and optimizations in the reaction scheduler, especially for reactions with repeated input molecules and cross-molecule conditions. Support for pipelined molecules (an automatic optimization) makes molecule selection faster. - 0.1.7 New compile-time restrictions, towards guaranteeing single reply for blocking molecules. It is now not allowed to call blocking molecules inside loops, or to emit replies in any non-linear code context (such as, under a closure or in a loop). Change of artifact package from `code.chymyst` to `io.chymyst`. This version is the first one published on Sonatype Maven repository. -- 0.1.6 A different mechanism now implements the syntax `a()` for emitting molecules with `Unit` values; no more auxiliary classes `E`, `BE`, `EB`, `EE`, which simplifies code and eliminates the need for whitebox macros. Breaking change: `timeout(value)(duraction)` instead of `timeout(duraction)(value)` as before. An optimization for the reaction scheduler now makes simple reactions start faster. The project build has been revamped: now there is a single JAR artifact and a single SBT project for `Chymyst`, rather than 3 as before. A skeleton "hello-world" project is available in a separate repository. `Chymyst` has been moved to a separate repository as well. Various improvements in the compile-time analysis of reactions: livelock detection now understands that molecules emitted under `if/else` constructions are not always emitted. +- 0.1.6 A different type-safe mechanism now implements the syntax `a()` for emitting molecules with `Unit` values; no more auxiliary classes `E`, `BE`, `EB`, `EE`, which simplified code and eliminated the need for whitebox macros. Breaking API change: `timeout(value)(duraction)` instead of `timeout(duraction)(value)` used previously. An optimization for the reaction scheduler now makes simple reactions start faster. The project build has been revamped: now there is a single JAR artifact and a single SBT project for `Chymyst`, rather than 3 as before. A skeleton "hello-world" project is available in a separate repository. `Chymyst` has been moved to a separate repository as well. Various improvements in the compile-time analysis of reactions: livelock detection now understands that molecules emitted under `if/else` constructions are not always emitted. - 0.1.5 Bug fix for a rare race condition with time-out on blocking molecules. New `checkTimeout` API to make a clean distinction between replies that need to check the timeout status and replies that don't. Documentation was improved. Code cleanups resulted in 100% test coverage. Revamped reaction site code now supports nonlinear input patterns. -- 0.1.4 Simplify API: now users need only one package import. Many more tutorial examples of chemical machine concurrency. Test code coverage is 97%. More compiler warnings enabled (including deprecation warnings). There are now more intelligent "whitebox" macros that generate different subclasses of `M[T]` and `B[T,R]` when `T` or `R` are the `Unit` type, to avoid deprecation warnings with the syntax `f()`. +- 0.1.4 Simplify API: now users need only one package import. Many more tutorial examples of chemical machine concurrency. Test code coverage is at 97%. More compiler warnings enabled (including deprecation warnings). There are now more intelligent "whitebox" macros that generate different subclasses of `M[T]` and `B[T,R]` when `T` or `R` are the `Unit` type, to avoid deprecation warnings with the syntax `f()`. - 0.1.3 Major changes in the API ("site", "go" instead of "join", "run") and in the terminology used in the tutorial and in the code: we now use the chemical machine paradigm more consistently, and avoid using the vague term "join". The build system now uses the "wartremover" SBT plugin to check for more possible errors. Test code coverage is at 96%. @@ -41,7 +41,7 @@ In particular, do not lock the entire molecule bag - only lock some groups of mo This will allow us to implement interesting features such as: - start many reactions at once when possible, even at one and the same reaction site -- allow nonlinear input patterns and arbitrary guards (done in 0.1.5) +- allow nonlinear input patterns and arbitrary guards (done in 0.1.5, optimized in 0.1.8) - automatic pipelining (i.e. strict ordering of consumed molecules) should give a speedup (done in 0.1.8) Version 0.3: Investigate interoperability with streaming frameworks such as Scala Streams, Scalaz Streams, FS2, Akka Streaming, Kafka, Heron. Define and use "pipelined" molecules that are optimized for streaming usage. @@ -58,7 +58,15 @@ Version 0.7: Static optimizations: use advanced macros and code transformations value * difficulty - description - 2 * 3 - detect livelock due to static molecule emission (at the moment, they are not considered as present inputs?) + 2 * 3 - detect static molecule emission in not-exactly-once code environments (see tests with TODO in StaticAnalysisSpec.scala) + + 1 * 1 - static molecules cannot have reactions with only one input (?) + + 3 * 2 - figure out why pipelining does not enforce pairing order in the "pair up to dance" example + + 4 * 5 - allow several reactions to be scheduled *truly simultaneously* out of the same reaction site, when this is possible. Avoid locking the entire bag? - perhaps partition it and lock only some partitions, based on reaction site information gleaned using a macro. + + 4 * 5 - do not schedule reactions if queues are full. At the moment, RejectedExecutionException is thrown. It's best to avoid this. Molecules should be accumulated in the bag, to be inspected at a later time (e.g. when some tasks are finished). Insert a call at the end of each reaction, to re-inspect the bag. 2 * 2 - Detect this condition at the reaction site time: A cycle of input molecules being subset of output molecules, possibly spanning several reaction sites (a->b+..., b->c+..., c-> a+...). This is a warning if there are nontrivial matchers and an error otherwise. - This depends on better detection of output environments. @@ -68,10 +76,6 @@ Version 0.7: Static optimizations: use advanced macros and code transformations 2 * 2 - perhaps use separate molecule bags for molecules with unit value and with non-unit value? for Booleans? for blocking and non-blocking? for constants? for statics / pipelined? - 4 * 5 - allow several reactions to be scheduled *truly simultaneously* out of the same reaction site, when this is possible. Avoid locking the entire bag? - perhaps partition it and lock only some partitions, based on reaction site information gleaned using a macro. - - 4 * 5 - do not schedule reactions if queues are full. At the moment, RejectedExecutionException is thrown. It's best to avoid this. Molecules should be accumulated in the bag, to be inspected at a later time (e.g. when some tasks are finished). Insert a call at the end of each reaction, to re-inspect the bag. - 3 * 3 - add logging of reactions currently in progress at a given RS. (Need a custom thread class, or a registry of reactions?) 3 * 4 - use `java.monitoring` to get statistics over how much time is spent running reactions, waiting while BlockingIdle(), etc. @@ -82,14 +86,14 @@ Version 0.7: Static optimizations: use advanced macros and code transformations 3 * 4 - implement "thread fusion" like in iOS/Android: 1) when a blocking molecule is emitted from a thread T and the corresponding reaction site runs on the same thread T, do not schedule a task but simply run the reaction site synchronously (non-blocking molecules still require a scheduled task? not sure); 2) when a reaction is scheduled from a reaction site that runs on thread T and the reaction is configured to run on the same thread, do not schedule a task but simply run the reaction synchronously. - 3 * 5 - implement automatic thread fusion for static molecules? + 3 * 5 - implement automatic thread fusion for static molecules? -- not sure how that would work. + + 2 * 3 - when attaching molecules to futures or futures to molecules, we can perhaps schedule the new futures on the same thread pool as the reaction site to which the molecule is bound? This requires having access to that thread pool. Maybe that access would be handy to users anyway? 5 * 5 - is it possible to implement distributed execution by sharing the site pool with another machine (but running the reaction sites only on the master node)? Use Paxos, Raft, or other consensus algorithm to ensure consistency? 3 * 4 - LAZY values on molecules? By default? What about pattern-matching then? Probably need to refactor SyncMol and AsyncMol into non-case classes and change some other logic. — Will not do now. Not sure that lazy values on molecules are important as a primitive. We can always simulate them using closures. - 3 * 5 - Can we implement Chymyst Core using Future / Promise and remove all blocking and all semaphores? - 3 * 2 - add per-molecule logging; log to file or to logger function 5 * 5 - implement "progress and safety" assertions so that we could prevent deadlock in more cases @@ -107,9 +111,9 @@ Version 0.7: Static optimizations: use advanced macros and code transformations 3 * 5 - consider whether we would like to prohibit emitting molecules from non-reaction code. Maybe with a construct such as `withMolecule{ ... }` where the special molecule will be emitted by the system? Can we rewrite tests so that everything happens only inside reactions? 3 * 3 - perhaps prohibit using explicit thread pools? It's error-prone because the user can forget to stop a pool. Perhaps only expose an API such as `withFixedPool(4){ implicit tp => ...}`? Investigate using implicit values for pools. - Maybe remove default pools altogether? It seems that every pool needs to be stopped. + Maybe remove default pools altogether? It seems that every pool needs to be stopped. -- However, this would prevent sharing thread pools across scopes. Maybe that is not particularly useful? - 3 * 3 - implement "one-off" or "perishable" molecules that are emitted once (like static, from the reaction site itself) and may be emitted only if first consumed (but not necessarily emitted) + 3 * 3 - implement "one-off" or "perishable" molecules that are emitted once (like static, from the reaction site itself) and may be emitted only if first consumed (but not necessarily emitted at start of the reaction site) 2 * 2 - If a blocking molecule was emitted without a timeout, we don't need the second semaphore, and checkTimeout() should return `true` @@ -121,6 +125,8 @@ Version 0.7: Static optimizations: use advanced macros and code transformations 3 * 3 - `SmartThread` should keep information about which RS and which reaction is now running. This can be used both for monitoring and for automatic assignment of thread pools for reactions defined in the scope of another reaction. + 3 * 3 - Write a tutorial section about timers and time-outs: cancellable recurring jobs, cancellable subscriptions, time-outs on receiving replies from non-blocking molecules (?) + ## Will not do for now 2 * 3 - investigate using wait/notify instead of semaphore; does it give better performance? - So far, attempts to do this failed. @@ -135,3 +141,5 @@ Version 0.7: Static optimizations: use advanced macros and code transformations 3 * 3 - Can we use macros to rewrite f() into f(_) inside reactions for Unit types? Otherwise it seems impossible to implement short syntax `case a() + b() => ` in the input patterns. — No, we can't because { case a() => } doesn't get past the Scala typer, and so macros don't see it at all. + 3 * 5 - Can we implement Chymyst Core using Future / Promise and remove all blocking and all semaphores? -- No. Automatic concurrent execution of reactions when multiple molecules are available cannot be implemented using promises / futures. +