From f45571654804a63bc97ba61211e127c71cb4007e Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 14 Jul 2021 16:09:10 +0300 Subject: [PATCH] Bug fix (#198) --- aqua-src/demo.aqua | 34 ----- aqua-src/test.aqua | 11 ++ cli/src/test/scala/Test1.scala | 62 --------- .../aqua/model/func/raw/PathFinder.scala | 33 +++-- .../scala/aqua/model/func/raw/RawCursor.scala | 37 +++-- .../aqua/model/func/resolved/MakeRes.scala | 17 +-- .../scala/aqua/model/topology/Topology.scala | 126 +++++++++--------- .../aqua/model/transform/ResolveFunc.scala | 9 +- .../aqua/model/topology/TopologySpec.scala | 1 - .../main/scala/aqua/parser/lexer/Token.scala | 3 +- .../main/scala/aqua/parser/lexer/Value.scala | 8 +- .../scala/aqua/parser/ArrowTypeExprSpec.scala | 7 +- .../aqua/parser/lexer/TypeTokenSpec.scala | 11 +- 13 files changed, 149 insertions(+), 210 deletions(-) delete mode 100644 aqua-src/demo.aqua create mode 100644 aqua-src/test.aqua delete mode 100644 cli/src/test/scala/Test1.scala diff --git a/aqua-src/demo.aqua b/aqua-src/demo.aqua deleted file mode 100644 index 7db88ad6..00000000 --- a/aqua-src/demo.aqua +++ /dev/null @@ -1,34 +0,0 @@ -service OpH("op"): - puk(s: string) -> string - pek(s: string, -- trgtr - c: string) -> string - pyk: []u32 -> string - pok: []bool -> string - identity: ⊤ -> ⊥ - -func a( -- ferkjn - b: string, -- fr - c: string, -- asdf - g: string - ) -> string: -- rgtr - - try: - f = "world" - OpH "planet" - OpH.pek("TRY THIS", -- gtrg - c) - catch err: - OpH.puk(err.msg) - <- f - -- hello - -func z(t: ?string, b: []u32, c: *bool): - OpH.puk(t!) - OpH.pyk(b) - OpH.pok(c) - c <- OpH.identity(true) - c <- OpH.identity("string") - - -func k(): - z(nil, nil, nil) \ No newline at end of file diff --git a/aqua-src/test.aqua b/aqua-src/test.aqua new file mode 100644 index 00000000..f7bf3354 --- /dev/null +++ b/aqua-src/test.aqua @@ -0,0 +1,11 @@ +service OpH("op"): + get_str(s: string) -> string + get_arr() -> []string + identity: ⊤ -> ⊥ + +func registerKeyPutValue(node_id: string) -> []string: + nodes <- OpH.get_arr() + for n <- nodes par: + on n: + OpH.get_str(node_id) + <- nodes diff --git a/cli/src/test/scala/Test1.scala b/cli/src/test/scala/Test1.scala deleted file mode 100644 index dfd1a4cc..00000000 --- a/cli/src/test/scala/Test1.scala +++ /dev/null @@ -1,62 +0,0 @@ -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -class Test1 extends AnyFlatSpec with Matchers { - "test" should "run" in { - - import cats.parse.{Parser0 ⇒ P0, Parser => P, Numbers} - /* - -def getTime(peer: PeerId, reply: i64 -> string) -> string = - on peer: - t <- Peer.timestamp() - reply(t) - -Scope(Var("peer"), List( - Fetch( - Var("t"), - AbilityCall( - CustomType("Peer"), - FnName("timestamp"), - Nil - ) - ) -)), -Unscoped( - FuncCall("reply", List(Var("t")) -) - -(seq - (call %init_peer_id% ("load" "peer") [] peer) - (seq - (call peer ("peer" "timestamp") [] t) - (call %init_peer_id% ("callback" "reply") [t]) ;; if used in fetch, then put return value - ) -) -;; register peer value -;; register reply callback - -code block: - free variables - free functions - new variables - scope? - - THEN name, types -- maybe bind free to types - - -line variants: -func(...) -x <- func(...) -Ability.func(...) -x <- Ability.func(...) - -par line -xor line - -on peer: - indented lines* - - */ - } -} diff --git a/model/src/main/scala/aqua/model/func/raw/PathFinder.scala b/model/src/main/scala/aqua/model/func/raw/PathFinder.scala index 79b58bcb..b81e9478 100644 --- a/model/src/main/scala/aqua/model/func/raw/PathFinder.scala +++ b/model/src/main/scala/aqua/model/func/raw/PathFinder.scala @@ -32,8 +32,8 @@ object PathFinder extends LogSupport { findPath( fromOn, toOn, - Chain.fromOption(from.currentPeerId), - Chain.fromOption(to.currentPeerId) + from.currentPeerId, + to.currentPeerId ) } } @@ -49,6 +49,10 @@ object PathFinder extends LogSupport { case (acc, p) if acc.contains(p) => acc.takeWhile(_ != p) :+ p case (acc, p) => acc :+ p } + trace(s"PEER IDS: $optimized") + trace(s"PREFIX: $prefix") + trace(s"SUFFIX: $suffix") + trace(s"OPTIMIZED WITH PREFIX AND SUFFIX: $optimized") val noPrefix = skipPrefix(optimized, prefix, optimized) skipSuffix(noPrefix, suffix, noPrefix) } @@ -56,19 +60,30 @@ object PathFinder extends LogSupport { def findPath( fromOn: Chain[OnTag], toOn: Chain[OnTag], - fromPeer: Chain[ValueModel], - toPeer: Chain[ValueModel] + fromPeer: Option[ValueModel], + toPeer: Option[ValueModel] ): Chain[ValueModel] = { + trace(s"FROM ON: $fromOn") + trace(s"TO ON: $toOn") + val (from, to) = skipCommonPrefix(fromOn, toOn) val fromFix = if (from.isEmpty && fromPeer != toPeer) Chain.fromOption(fromOn.lastOption) else from val toFix = if (to.isEmpty && fromPeer != toPeer) Chain.fromOption(toOn.lastOption) else to - val fromTo = fromFix.reverse.flatMap(_.via.reverse) ++ toFix.flatMap(_.via) - val optimized = optimizePath(fromPeer ++ fromTo ++ toPeer, fromPeer, toPeer) - trace("FIND PATH " + fromFix) - trace(" -> " + toFix) - trace(s"$fromPeer $toPeer") + trace("FIND PATH FROM | " + fromFix) + trace(" TO | " + toFix) + + val fromTo = fromFix.reverse.flatMap(_.via.reverse) ++ toFix.flatMap(_.via) + trace(s"FROM TO: $fromTo") + + val fromPeerCh = Chain.fromOption(fromPeer) + val toPeerCh = Chain.fromOption(toPeer) + val optimized = optimizePath(fromPeerCh ++ fromTo ++ toPeerCh, fromPeerCh, toPeerCh) + + trace( + s"FROM PEER '${fromPeer.map(_.toString).getOrElse("None")}' TO PEER '${toPeer.map(_.toString).getOrElse("None")}'" + ) trace(" Optimized: " + optimized) optimized } diff --git a/model/src/main/scala/aqua/model/func/raw/RawCursor.scala b/model/src/main/scala/aqua/model/func/raw/RawCursor.scala index 61e20993..b2cd927f 100644 --- a/model/src/main/scala/aqua/model/func/raw/RawCursor.scala +++ b/model/src/main/scala/aqua/model/func/raw/RawCursor.scala @@ -127,27 +127,26 @@ case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) } def cata[A](wrap: ChainZipper[Cofree[Chain, A]] => Chain[Cofree[Chain, A]])( - f: RawCursor => OptionT[Eval, ChainZipper[Cofree[Chain, A]]] + folder: RawCursor => OptionT[Eval, ChainZipper[Cofree[Chain, A]]] ): Eval[Chain[Cofree[Chain, A]]] = - f(this).map { case ChainZipper(prev, curr, next) => - val inner = curr.copy(tail = - Eval - .later( - Chain.fromSeq( - toFirstChild - .map(fc => - LazyList - .unfold(fc) { _.toNextSibling.map(c => c -> c) } - .prepended(fc) - ) - .getOrElse(LazyList.empty) + folder(this).map { case ChainZipper(prev, curr, next) => + val tail = Eval.later { + Chain.fromSeq( + toFirstChild + .map(folderCursor => + LazyList + .unfold(folderCursor) { _.toNextSibling.map(cursor => cursor -> cursor) } + .prepended(folderCursor) ) - ) - // TODO: this can be parallelized - .flatMap(_.traverse(_.cata(wrap)(f))) - .map(_.flatMap(identity)) - .flatMap(addition => curr.tail.map(_ ++ addition)) - ) + .getOrElse(LazyList.empty) + ) + } + // TODO: this can be parallelized + .flatMap(_.traverse(_.cata(wrap)(folder))) + .map(_.flatMap(identity)) + .flatMap(addition => curr.tail.map(_ ++ addition)) + + val inner = curr.copy(tail = tail) wrap(ChainZipper(prev, inner, next)) }.getOrElse(Chain.empty).memoize diff --git a/model/src/main/scala/aqua/model/func/resolved/MakeRes.scala b/model/src/main/scala/aqua/model/func/resolved/MakeRes.scala index 1b6b38f5..007255b1 100644 --- a/model/src/main/scala/aqua/model/func/resolved/MakeRes.scala +++ b/model/src/main/scala/aqua/model/func/resolved/MakeRes.scala @@ -1,6 +1,7 @@ package aqua.model.func.resolved import aqua.model.func.Call +import aqua.model.topology.Topology.Res import aqua.model.{LiteralModel, ValueModel} import cats.Eval import cats.data.Chain @@ -8,24 +9,24 @@ import cats.free.Cofree object MakeRes { val nilTail: Eval[Chain[Cofree[Chain, ResolvedOp]]] = Eval.now(Chain.empty) - type Cof = Cofree[Chain, ResolvedOp] - def leaf(op: ResolvedOp): Cof = Cofree[Chain, ResolvedOp](op, nilTail) - def next(item: String): Cof = + def leaf(op: ResolvedOp): Res = Cofree[Chain, ResolvedOp](op, nilTail) + + def next(item: String): Res = leaf(NextRes(item)) - def seq(first: Cof, second: Cof, more: Cof*): Cof = + def seq(first: Res, second: Res, more: Res*): Res = Cofree[Chain, ResolvedOp](SeqRes, Eval.later(first +: second +: Chain.fromSeq(more))) - def par(first: Cof, second: Cof, more: Cof*): Cof = + def par(first: Res, second: Res, more: Res*): Res = Cofree[Chain, ResolvedOp](ParRes, Eval.later(first +: second +: Chain.fromSeq(more))) - def xor(first: Cof, second: Cof): Cof = + def xor(first: Res, second: Res): Res = Cofree[Chain, ResolvedOp](XorRes, Eval.later(Chain(first, second))) - def fold(item: String, iter: ValueModel, body: Cof): Cof = + def fold(item: String, iter: ValueModel, body: Res): Res = Cofree[Chain, ResolvedOp](FoldRes(item, iter), Eval.now(Chain.one(body))) - def noop(onPeer: ValueModel): Cof = + def noop(onPeer: ValueModel): Res = leaf(CallServiceRes(LiteralModel.quote("op"), "noop", Call(Nil, None), onPeer)) } diff --git a/model/src/main/scala/aqua/model/topology/Topology.scala b/model/src/main/scala/aqua/model/topology/Topology.scala index e06b2d87..b8c3eade 100644 --- a/model/src/main/scala/aqua/model/topology/Topology.scala +++ b/model/src/main/scala/aqua/model/topology/Topology.scala @@ -1,26 +1,27 @@ package aqua.model.topology -import aqua.model.{LiteralModel, ValueModel, VarModel} -import aqua.model.func.raw._ -import cats.Eval -import cats.data.{Chain, NonEmptyChain, NonEmptyList, OptionT} -import cats.data.Chain.nil -import cats.free.Cofree import aqua.model.cursor.ChainZipper +import aqua.model.func.raw._ import aqua.model.func.resolved._ +import aqua.model.{LiteralModel, ValueModel, VarModel} import aqua.types.{BoxType, ScalarType} -import wvlet.log.LogSupport +import cats.Eval +import cats.data.Chain.nil +import cats.data.{Chain, NonEmptyChain, NonEmptyList, OptionT} +import cats.free.Cofree import cats.syntax.traverse._ +import wvlet.log.LogSupport object Topology extends LogSupport { type Tree = Cofree[Chain, RawTag] type Res = Cofree[Chain, ResolvedOp] - def resolve(op: Tree): Res = + def resolve(op: Tree): Res = { + val resolved = resolveOnMoves(op).value Cofree - .cata[Chain, ResolvedOp, Res](resolveOnMoves(op).value) { + .cata[Chain, ResolvedOp, Res](resolved) { case (SeqRes, children) => - Eval.later( + Eval.later { children.uncons .filter(_._2.isEmpty) .map(_._1) @@ -33,10 +34,11 @@ object Topology extends LogSupport { }) ) ) - ) + } case (head, children) => Eval.later(Cofree(head, Eval.now(children))) } .value + } def wrap(cz: ChainZipper[Res]): Chain[Res] = Chain.one( @@ -44,60 +46,64 @@ object Topology extends LogSupport { else cz.current ) - def resolveOnMoves(op: Tree): Eval[Res] = - RawCursor(NonEmptyList.one(ChainZipper.one(op))) + private def rawToResolved( + currentPeerId: Option[ValueModel] + ): PartialFunction[RawTag, ResolvedOp] = { + case SeqTag => SeqRes + case _: OnTag => SeqRes + case MatchMismatchTag(a, b, s) => MatchMismatchRes(a, b, s) + case ForTag(item, iter) => FoldRes(item, iter) + case ParTag | ParTag.Detach => ParRes + case XorTag | XorTag.LeftBiased => XorRes + case NextTag(item) => NextRes(item) + case CallServiceTag(serviceId, funcName, call) => + CallServiceRes( + serviceId, + funcName, + call, + currentPeerId + .getOrElse(LiteralModel.initPeerId) + ) + } + + def resolveOnMoves(op: Tree): Eval[Res] = { + val cursor = RawCursor(NonEmptyList.one(ChainZipper.one(op))) + val resolvedCofree = cursor .cata(wrap) { rc => debug(s"<:> $rc") - OptionT[Eval, ChainZipper[Res]]( - ({ - case SeqTag => SeqRes - case _: OnTag => SeqRes - case MatchMismatchTag(a, b, s) => MatchMismatchRes(a, b, s) - case ForTag(item, iter) => FoldRes(item, iter) - case ParTag | ParTag.Detach => ParRes - case XorTag | XorTag.LeftBiased => XorRes - case NextTag(item) => NextRes(item) - case CallServiceTag(serviceId, funcName, call) => - CallServiceRes( - serviceId, - funcName, - call, - rc.currentPeerId - .getOrElse(LiteralModel.initPeerId) - ) - }: PartialFunction[RawTag, ResolvedOp]).lift - .apply(rc.tag) - .map(MakeRes.leaf) - .traverse(c => - Eval.later { - val cz = ChainZipper( - through(rc.pathFromPrev), - c, - through(rc.pathToNext) - ) - if (cz.next.nonEmpty || cz.prev.nonEmpty) { - debug(s"Resolved $rc -> $c") - if (cz.prev.nonEmpty) - trace("From prev: " + cz.prev.map(_.head).toList.mkString(" -> ")) - if (cz.next.nonEmpty) - trace("To next: " + cz.next.map(_.head).toList.mkString(" -> ")) - } else debug(s"EMPTY $rc -> $c") - cz - } + val resolved = rawToResolved(rc.currentPeerId).lift + .apply(rc.tag) + .map(MakeRes.leaf) + val chainZipperEv = resolved.traverse(cofree => + Eval.later { + val cz = ChainZipper( + through(rc.pathFromPrev), + cofree, + through(rc.pathToNext) ) + if (cz.next.nonEmpty || cz.prev.nonEmpty) { + debug(s"Resolved $rc -> $cofree") + if (cz.prev.nonEmpty) + trace("From prev: " + cz.prev.map(_.head).toList.mkString(" -> ")) + if (cz.next.nonEmpty) + trace("To next: " + cz.next.map(_.head).toList.mkString(" -> ")) + } else debug(s"EMPTY $rc -> $cofree") + cz + } ) + OptionT[Eval, ChainZipper[Res]](chainZipperEv) + } - } - .map(NonEmptyChain.fromChain(_).map(_.uncons)) - .map { - case None => - error("Topology emitted nothing") - Cofree(SeqRes, MakeRes.nilTail) - case Some((el, `nil`)) => el - case Some((el, tail)) => - warn("Topology emitted many nodes, that's unusual") - Cofree(SeqRes, Eval.now(el +: tail)) - } + resolvedCofree.map(NonEmptyChain.fromChain(_).map(_.uncons)).map { + case None => + error("Topology emitted nothing") + Cofree(SeqRes, MakeRes.nilTail) + case Some((el, `nil`)) => el + case Some((el, tail)) => + warn("Topology emitted many nodes, that's unusual") + Cofree(SeqRes, Eval.now(el +: tail)) + } + } // Walks through peer IDs, doing a noop function on each // If same IDs are found in a row, does noop only once diff --git a/model/src/main/scala/aqua/model/transform/ResolveFunc.scala b/model/src/main/scala/aqua/model/transform/ResolveFunc.scala index 4d50bafb..4d8289b1 100644 --- a/model/src/main/scala/aqua/model/transform/ResolveFunc.scala +++ b/model/src/main/scala/aqua/model/transform/ResolveFunc.scala @@ -1,13 +1,11 @@ package aqua.model.transform -import aqua.model.{ValueModel, VarModel} -import aqua.model.func.{ArgDef, ArgsCall, ArgsDef, Call, FuncCallable} import aqua.model.func.raw.{FuncOp, FuncOps} +import aqua.model.func._ +import aqua.model.{ValueModel, VarModel} import aqua.types.ArrowType import cats.Eval import cats.syntax.apply._ -import cats.syntax.functor._ -import wvlet.log.Logger case class ResolveFunc( transform: FuncOp => FuncOp, @@ -17,9 +15,6 @@ case class ResolveFunc( arrowCallbackPrefix: String = "init_peer_callable_" ) { - private val logger = Logger.of[ResolveFunc] - import logger._ - def returnCallback(retModel: ValueModel): FuncOp = callback( respFuncName, diff --git a/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala b/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala index d0710d42..ff0696b6 100644 --- a/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala +++ b/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala @@ -323,7 +323,6 @@ class TopologySpec extends AnyFlatSpec with Matchers { } "topology resolver" should "get back to init peer after a long chain" in { - val init = on( initPeer, relay :: Nil, diff --git a/parser/src/main/scala/aqua/parser/lexer/Token.scala b/parser/src/main/scala/aqua/parser/lexer/Token.scala index f8c74af6..b3324ac7 100644 --- a/parser/src/main/scala/aqua/parser/lexer/Token.scala +++ b/parser/src/main/scala/aqua/parser/lexer/Token.scala @@ -45,6 +45,7 @@ object Token { val `co`: P[Unit] = P.string("co") val `:` : P[Unit] = P.char(':') val ` : ` : P[Unit] = P.char(':').surroundedBy(` `.?) + val `anum_*` : P[Unit] = P.charsWhile(anum_).void val `name`: P[String] = (P.charIn(az) ~ P.charsWhile(anum_).?).string @@ -55,7 +56,7 @@ object Token { val `--` : P[Unit] = ` `.?.with1 *> P.string("--") <* ` `.? val ` \n` : P[Unit] = - (` `.?.void *> (`--` *> P.charsWhile(_ != '\n')).?.void).with1 *> `\n` + (` `.?.void *> (`--` *> P.charsWhile0(_ != '\n')).?.void).with1 *> `\n` val ` \n+` : P[Unit] = P.repAs[Unit, Unit](` \n`.backtrack, 1)(Accumulator0.unitAccumulator0) val ` : \n+` : P[Unit] = ` `.?.with1 *> `:` *> ` \n+` diff --git a/parser/src/main/scala/aqua/parser/lexer/Value.scala b/parser/src/main/scala/aqua/parser/lexer/Value.scala index c5da95b5..f383914e 100644 --- a/parser/src/main/scala/aqua/parser/lexer/Value.scala +++ b/parser/src/main/scala/aqua/parser/lexer/Value.scala @@ -4,10 +4,10 @@ import aqua.parser.lexer.Token._ import aqua.parser.lift.LiftParser import aqua.parser.lift.LiftParser._ import aqua.types.LiteralType -import cats.{Comonad, Functor} import cats.parse.{Numbers, Parser => P} -import cats.syntax.functor._ import cats.syntax.comonad._ +import cats.syntax.functor._ +import cats.{Comonad, Functor} sealed trait Value[F[_]] extends Token[F] @@ -32,7 +32,7 @@ object Value { P.oneOf( ("true" :: "false" :: Nil) .map(t ⇒ P.string(t).lift.map(fu => Literal(fu.as(t), LiteralType.bool))) - ) + ) <* P.not(`anum_*`) def initPeerId[F[_]: LiftParser: Comonad]: P[Literal[F]] = `%init_peer_id%`.string.lift.map(Literal(_, LiteralType.string)) @@ -57,7 +57,7 @@ object Value { .map(Literal(_, LiteralType.string)) def literal[F[_]: LiftParser: Comonad]: P[Literal[F]] = - P.oneOf(bool :: float.backtrack :: num :: string :: Nil) + P.oneOf(bool.backtrack :: float.backtrack :: num.backtrack :: string :: Nil) def `value`[F[_]: LiftParser: Comonad]: P[Value[F]] = P.oneOf(literal.backtrack :: initPeerId.backtrack :: varLambda :: Nil) diff --git a/parser/src/test/scala/aqua/parser/ArrowTypeExprSpec.scala b/parser/src/test/scala/aqua/parser/ArrowTypeExprSpec.scala index e8d90193..1094722c 100644 --- a/parser/src/test/scala/aqua/parser/ArrowTypeExprSpec.scala +++ b/parser/src/test/scala/aqua/parser/ArrowTypeExprSpec.scala @@ -2,8 +2,7 @@ package aqua.parser import aqua.AquaSpec import aqua.parser.expr.ArrowTypeExpr -import aqua.types.ScalarType.string -import aqua.types.ScalarType.u32 +import aqua.types.ScalarType.{string, u32} import cats.Id import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -21,6 +20,10 @@ class ArrowTypeExprSpec extends AnyFlatSpec with Matchers with AquaSpec { ArrowTypeExpr[Id]("onIn", toArrowType(List("Custom"), Some("Custom2"))) ) + parseArrow("onIn(a: Custom, b: Custom2)") should be( + ArrowTypeExpr[Id]("onIn", toArrowType(List("Custom", "Custom2"), None)) + ) + parseArrow("onIn: Custom, string, u32, Custom3 -> Custom2") should be( ArrowTypeExpr[Id]( "onIn", diff --git a/parser/src/test/scala/aqua/parser/lexer/TypeTokenSpec.scala b/parser/src/test/scala/aqua/parser/lexer/TypeTokenSpec.scala index 2bb4496f..8931ca21 100644 --- a/parser/src/test/scala/aqua/parser/lexer/TypeTokenSpec.scala +++ b/parser/src/test/scala/aqua/parser/lexer/TypeTokenSpec.scala @@ -1,12 +1,12 @@ package aqua.parser.lexer -import org.scalatest.EitherValues -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers import aqua.parser.lift.LiftParser.Implicits.idLiftParser import aqua.types.ScalarType import aqua.types.ScalarType.u32 import cats.Id +import org.scalatest.EitherValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers import scala.language.implicitConversions @@ -27,6 +27,11 @@ class TypeTokenSpec extends AnyFlatSpec with Matchers with EitherValues { ArrowTypeToken.`arrowdef`.parseAll("A -> B").right.value should be( ArrowTypeToken[Id]((), CustomTypeToken[Id]("A") :: Nil, Some(CustomTypeToken[Id]("B"))) ) + + ArrowTypeToken.`arrowWithNames`.parseAll("(a: A) -> B").right.value should be( + ArrowTypeToken[Id]((), CustomTypeToken[Id]("A") :: Nil, Some(CustomTypeToken[Id]("B"))) + ) + ArrowTypeToken.`arrowdef`.parseAll("u32 -> Boo").right.value should be( ArrowTypeToken[Id]((), (u32: BasicTypeToken[Id]) :: Nil, Some(CustomTypeToken[Id]("Boo"))) )