From 22778914ca2e6b58bfba432bc3ffb36f589983a9 Mon Sep 17 00:00:00 2001 From: Dmitry Kurinskiy Date: Wed, 22 Dec 2021 15:21:37 +0300 Subject: [PATCH] Topology refactoring & fixes (#371) * Topology optimization for Folds * cache RawCursor's parent for better performance * Tests fixed * wip * wip * Use the new Topology to find paths * Compile bug fixed * Old tests works * One more fixed test * Move before seq next * One more fixed test * Bugfix * Disabled debug output * maybe fix? * maybe fix? * Topology wip * Maybe fix * Maybe fix * Fold optimization * Root topology * Respect forceExit in endsOn * better caching * better caching * Root afterOn = const * XorGroup endsOn should break * no EndsOn for Root * Maybe better? * Uncycling * Eval * Respect ParTag.Detach * Detach test * Detach test failing * Detach test fixed * Go to relay via relay * Fixes #380 * Increment Aqua version to 0.5.2 * Add image to transform readme, update dependencies * Review fixes * Updated Scala version in the release flow --- .github/workflows/release.yml | 4 +- .github/workflows/test_branch.yml | 2 +- aqua-src/foldJoin.aqua | 11 + aqua-src/nopingback.aqua | 23 + aqua-src/parfold.aqua | 1 - aqua-src/ret.aqua | 12 +- aqua-src/so.aqua | 6 + build.sbt | 21 +- cli/.jvm/src/main/scala/aqua/Test.scala | 2 +- .../scala/aqua/model/func/raw/RawTag.scala | 63 ++- model/test-kit/src/main/scala/aqua/Node.scala | 6 + .../aqua/model/transform/TransformSpec.scala | 11 +- .../transform/topology/RawCursorSpec.scala | 142 +++--- .../transform/topology/TopologySpec.scala | 124 +++++- model/transform/img.png | Bin 0 -> 87152 bytes model/transform/readme.md | 33 ++ .../model/transform/cursor/ChainCursor.scala | 7 +- .../aqua/model/transform/res/MakeRes.scala | 11 +- .../model/transform/topology/PathFinder.scala | 87 ++-- .../model/transform/topology/RawCursor.scala | 144 ++---- .../model/transform/topology/Topology.scala | 416 +++++++++++++++++- 21 files changed, 839 insertions(+), 287 deletions(-) create mode 100644 aqua-src/foldJoin.aqua create mode 100644 aqua-src/nopingback.aqua create mode 100644 aqua-src/so.aqua create mode 100644 model/transform/img.png create mode 100644 model/transform/readme.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index feff4bc1..7b0d7a85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,8 +51,8 @@ jobs: - name: Check .js exists run: | - JS="cli/.js/target/scala-3.0.2/cli-opt/aqua-${{ env.VERSION }}.js" - mv cli/.js/target/scala-3.0.2/cli-opt/main.js "$JS" + JS="cli/.js/target/scala-3.1.0/cli-opt/aqua-${{ env.VERSION }}.js" + mv cli/.js/target/scala-3.1.0/cli-opt/main.js "$JS" stat "$JS" echo "JS=$JS" >> $GITHUB_ENV diff --git a/.github/workflows/test_branch.yml b/.github/workflows/test_branch.yml index be0bcc50..3b5bd52b 100644 --- a/.github/workflows/test_branch.yml +++ b/.github/workflows/test_branch.yml @@ -52,7 +52,7 @@ jobs: git clone https://github.com/fluencelabs/aqua-playground.git sbt "cliJS/fastOptJS" rm -rf aqua-playground/src/compiled/examples/* - mv cli/.js/target/scala-3.0.2/cli-fastopt.js npm/aqua.js + mv cli/.js/target/scala-3.1.0/cli-fastopt.js npm/aqua.js cd npm npm i cd ../aqua-playground diff --git a/aqua-src/foldJoin.aqua b/aqua-src/foldJoin.aqua new file mode 100644 index 00000000..e0939e0c --- /dev/null +++ b/aqua-src/foldJoin.aqua @@ -0,0 +1,11 @@ +service Op2("op"): + identity(s: u64) + +service Peer("peer"): + timestamp_sec: -> u64 + +func getTwoResults(): + on "other node": + co on "x": + z <- Peer.timestamp_sec() + Op2.identity(z) \ No newline at end of file diff --git a/aqua-src/nopingback.aqua b/aqua-src/nopingback.aqua new file mode 100644 index 00000000..f8e4515d --- /dev/null +++ b/aqua-src/nopingback.aqua @@ -0,0 +1,23 @@ +service Kademlia("kad"): + neighborhood: string, ?bool, ?bool -> []string + +service Peer("peer"): + timestamp_sec: -> () + timeout: u32, string -> () + +func ack_peers() -> []string: + acked_peers: *string + + on HOST_PEER_ID: + nodes <- Kademlia.neighborhood(%init_peer_id%, nil, nil) + for n <- nodes par: + status: *string + on n: + Peer.timestamp_sec() + status <<- "acked" + + if status! == "acked": + acked_peers <<- n + + Peer.timeout(15000, "") -- this line's indentation triggers the bug + <- acked_peers \ No newline at end of file diff --git a/aqua-src/parfold.aqua b/aqua-src/parfold.aqua index a3d21e89..8c5404a6 100644 --- a/aqua-src/parfold.aqua +++ b/aqua-src/parfold.aqua @@ -5,7 +5,6 @@ service Moo("tools"): func foo(): ss <- Moo.bla() on HOST_PEER_ID: - Moo.bla() for s <- ss par: on s: Moo.bla() \ No newline at end of file diff --git a/aqua-src/ret.aqua b/aqua-src/ret.aqua index ff8e1dd5..4ca7f726 100644 --- a/aqua-src/ret.aqua +++ b/aqua-src/ret.aqua @@ -1,7 +1,11 @@ module Ret declares * -export someFunc +export getTwoResults -func someFunc(cb: []string -> ()): - ifaces: *string - cb(ifaces) +service Peer("peer"): + timestamp_sec: -> u64 + +func getTwoResults() -> u64: + on "other node": + res <- Peer.timestamp_sec() + <- res \ No newline at end of file diff --git a/aqua-src/so.aqua b/aqua-src/so.aqua new file mode 100644 index 00000000..e0a195de --- /dev/null +++ b/aqua-src/so.aqua @@ -0,0 +1,6 @@ +service TestS("some-id"): + t: string -> string + +func doStuff(c: bool): + if c: + TestS.t("fr") \ No newline at end of file diff --git a/build.sbt b/build.sbt index ba56d375..d71b239d 100644 --- a/build.sbt +++ b/build.sbt @@ -1,28 +1,27 @@ -val dottyVersion = "3.0.2" +val dottyVersion = "3.1.0" scalaVersion := dottyVersion val baseAquaVersion = settingKey[String]("base aqua version") -val catsV = "2.6.1" -val catsParseV = "0.3.5" +val catsV = "2.7.0" +val catsParseV = "0.3.6" val monocleV = "3.0.0-M6" -val scalaTestV = "3.2.9" -val fs2V = "3.1.0" -val catsEffectV = "3.2.1" -val log4catsV = "2.1.1" -val slf4jV = "1.7.30" -val declineV = "2.1.0" +val scalaTestV = "3.2.10" +val fs2V = "3.2.3" +val catsEffectV = "3.3.1" +val declineV = "2.2.0" val circeVersion = "0.14.1" +val scribeV = "3.6.3" name := "aqua-hll" val commons = Seq( - baseAquaVersion := "0.5.1", + baseAquaVersion := "0.5.2", version := baseAquaVersion.value + "-" + sys.env.getOrElse("BUILD_NUMBER", "SNAPSHOT"), scalaVersion := dottyVersion, libraryDependencies ++= Seq( - "com.outr" %%% "scribe" % "3.5.5", + "com.outr" %%% "scribe" % scribeV, "org.scalatest" %%% "scalatest" % scalaTestV % Test ), scalacOptions ++= { diff --git a/cli/.jvm/src/main/scala/aqua/Test.scala b/cli/.jvm/src/main/scala/aqua/Test.scala index fcb54f88..351229e8 100644 --- a/cli/.jvm/src/main/scala/aqua/Test.scala +++ b/cli/.jvm/src/main/scala/aqua/Test.scala @@ -22,7 +22,7 @@ object Test extends IOApp.Simple { start <- IO(System.currentTimeMillis()) _ <- AquaPathCompiler .compileFilesTo[IO]( - Path("./aqua-src/parfold.aqua"), + Path("./aqua-src/nopingback.aqua"), List(Path("./aqua")), Option(Path("./target")), TypeScriptBackend, diff --git a/model/src/main/scala/aqua/model/func/raw/RawTag.scala b/model/src/main/scala/aqua/model/func/raw/RawTag.scala index af2f9ca9..6830c0cf 100644 --- a/model/src/main/scala/aqua/model/func/raw/RawTag.scala +++ b/model/src/main/scala/aqua/model/func/raw/RawTag.scala @@ -1,11 +1,14 @@ package aqua.model.func.raw import aqua.model.ValueModel +import aqua.model.ValueModel.varName import aqua.model.func.{Call, FuncModel} import cats.data.NonEmptyList import cats.data.Chain sealed trait RawTag { + // What variable names this tag uses (children are not respected) + def usesVarNames: Set[String] = Set.empty def mapValues(f: ValueModel => ValueModel): RawTag = this match { case OnTag(peerId, via) => OnTag(f(peerId), via.map(f)) @@ -58,7 +61,9 @@ sealed trait RawTag { sealed trait NoExecTag extends RawTag sealed trait GroupTag extends RawTag + sealed trait SeqGroupTag extends GroupTag + sealed trait ParGroupTag extends GroupTag case object SeqTag extends SeqGroupTag @@ -67,40 +72,69 @@ case object ParTag extends ParGroupTag { case object Detach extends ParGroupTag } -case object XorTag extends SeqGroupTag { - case object LeftBiased extends SeqGroupTag +case object XorTag extends GroupTag { + case object LeftBiased extends GroupTag +} + +case class XorParTag(xor: FuncOp, par: FuncOp) extends RawTag { + // Collect all the used variable names + override def usesVarNames: Set[String] = xor.usesVarNames.value ++ par.usesVarNames.value } -case class XorParTag(xor: FuncOp, par: FuncOp) extends RawTag case class OnTag(peerId: ValueModel, via: Chain[ValueModel]) extends SeqGroupTag { + override def usesVarNames: Set[String] = + ValueModel.varName(peerId).toSet ++ via.iterator.flatMap(ValueModel.varName) + override def toString: String = s"(on $peerId${if (via.nonEmpty) " via " + via.toList.mkString(" via ") else ""})" } -case class NextTag(item: String) extends RawTag -case class RestrictionTag(name: String, isStream: Boolean) extends SeqGroupTag + +case class NextTag(item: String) extends RawTag { + override def usesVarNames: Set[String] = Set(item) +} + +case class RestrictionTag(name: String, isStream: Boolean) extends SeqGroupTag { + override def usesVarNames: Set[String] = Set(name) +} case class MatchMismatchTag(left: ValueModel, right: ValueModel, shouldMatch: Boolean) - extends SeqGroupTag -case class ForTag(item: String, iterable: ValueModel) extends SeqGroupTag + extends SeqGroupTag { + + override def usesVarNames: Set[String] = + ValueModel.varName(left).toSet ++ ValueModel.varName(right).toSet +} + +case class ForTag(item: String, iterable: ValueModel) extends SeqGroupTag { + override def usesVarNames: Set[String] = Set(item) ++ ValueModel.varName(iterable) +} case class CallArrowTag( funcName: String, call: Call -) extends RawTag +) extends RawTag { + override def usesVarNames: Set[String] = call.argVarNames +} case class DeclareStreamTag( value: ValueModel -) extends NoExecTag +) extends NoExecTag { + override def usesVarNames: Set[String] = ValueModel.varName(value).toSet +} case class AssignmentTag( value: ValueModel, assignTo: String -) extends NoExecTag +) extends NoExecTag { + override def usesVarNames: Set[String] = Set(assignTo) ++ ValueModel.varName(value) +} case class ClosureTag( func: FuncModel -) extends NoExecTag +) extends NoExecTag { + // TODO captured names are lost? + override def usesVarNames: Set[String] = Set(func.name) +} case class ReturnTag( values: NonEmptyList[ValueModel] @@ -118,13 +152,20 @@ case class CallServiceTag( funcName: String, call: Call ) extends RawTag { + + override def usesVarNames: Set[String] = ValueModel.varName(serviceId).toSet ++ call.argVarNames + override def toString: String = s"(call _ ($serviceId $funcName) $call)" } case class PushToStreamTag(operand: ValueModel, exportTo: Call.Export) extends RawTag { + override def usesVarNames: Set[String] = ValueModel.varName(operand).toSet + override def toString: String = s"(push $operand $exportTo)" } case class CanonicalizeTag(operand: ValueModel, exportTo: Call.Export) extends RawTag { + override def usesVarNames: Set[String] = ValueModel.varName(operand).toSet + override def toString: String = s"(can $operand $exportTo)" } diff --git a/model/test-kit/src/main/scala/aqua/Node.scala b/model/test-kit/src/main/scala/aqua/Node.scala index e81dafe8..2f03bbe6 100644 --- a/model/test-kit/src/main/scala/aqua/Node.scala +++ b/model/test-kit/src/main/scala/aqua/Node.scala @@ -148,6 +148,12 @@ object Node { ) } + def co(body: Raw*) = + Node( + ParTag.Detach, + body.toList + ) + def on(peer: ValueModel, via: List[ValueModel], body: Raw*) = Node( OnTag(peer, Chain.fromSeq(via)), diff --git a/model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala b/model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala index 51a07a57..64725fa4 100644 --- a/model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala +++ b/model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala @@ -42,16 +42,17 @@ class TransformSpec extends AnyFlatSpec with Matchers { through(relayV), through(otherRelay), MakeRes.xor( - callRes(1, otherPeer), + MakeRes.seq( + callRes(1, otherPeer), + through(otherRelay), + through(relayV) + ), MakeRes.seq( through(otherRelay), through(relayV), - errorCall(bc, 1, initPeer), - through(relayV) + errorCall(bc, 1, initPeer) ) ), - through(otherRelay), - through(relayV), MakeRes.xor( respCall(bc, ret, initPeer), errorCall(bc, 2, initPeer) diff --git a/model/test-kit/src/test/scala/aqua/model/transform/topology/RawCursorSpec.scala b/model/test-kit/src/test/scala/aqua/model/transform/topology/RawCursorSpec.scala index d26ae0c8..3034176d 100644 --- a/model/test-kit/src/test/scala/aqua/model/transform/topology/RawCursorSpec.scala +++ b/model/test-kit/src/test/scala/aqua/model/transform/topology/RawCursorSpec.scala @@ -26,7 +26,7 @@ class RawCursorSpec extends AnyFlatSpec with Matchers { ) ) - raw.firstExecuted shouldBe raw.lastExecuted + //raw.firstExecuted shouldBe raw.lastExecuted } "simple raw cursor with multiple calls" should "move on seqs" in { @@ -47,10 +47,10 @@ class RawCursorSpec extends AnyFlatSpec with Matchers { ) ) - raw.lastExecuted shouldBe raw.firstExecuted.get.seqNext.get.seqNext.get.seqNext - raw.lastExecuted.get.seqPrev shouldBe raw.firstExecuted.get.seqNext.get.seqNext - raw.lastExecuted.get.seqPrev.get.seqPrev shouldBe raw.firstExecuted.get.seqNext - raw.lastExecuted.get.seqPrev shouldBe raw.firstExecuted.get.seqNext.get.seqNext +// raw.lastExecuted shouldBe raw.firstExecuted.get.seqNext.get.seqNext.get.seqNext +// raw.lastExecuted.get.seqPrev shouldBe raw.firstExecuted.get.seqNext.get.seqNext +// raw.lastExecuted.get.seqPrev.get.seqPrev shouldBe raw.firstExecuted.get.seqNext +// raw.lastExecuted.get.seqPrev shouldBe raw.firstExecuted.get.seqNext.get.seqNext } "simple raw cursor on init_peer_id via relay" should "move properly" in { @@ -66,7 +66,7 @@ class RawCursorSpec extends AnyFlatSpec with Matchers { ) ) - raw.firstExecuted shouldBe raw.lastExecuted + //raw.firstExecuted shouldBe raw.lastExecuted } "raw cursor" should "move properly" in { @@ -105,29 +105,29 @@ class RawCursorSpec extends AnyFlatSpec with Matchers { raw.tag should be( OnTag(LiteralModel.initPeerId, Chain.one(VarModel("-relay-", ScalarType.string))) ) - raw.firstExecuted.map(_.tag) should be( - Some( - callService(LiteralModel.quote("calledOutside"), "fn", Call(Nil, Nil)).tree.head - ) - ) - raw.lastExecuted.map(_.tag) should be( - Some( - callService( - LiteralModel.quote("return"), - "fn", - Call(VarModel("export", ScalarType.string) :: Nil, Nil) - ).tree.head - ) - ) - raw.lastExecuted.flatMap(_.seqPrev).flatMap(_.lastExecuted).map(_.tag) should be( - Some( - callService( - LiteralModel.quote("calledInside"), - "fn", - Call(Nil, Call.Export("export", ScalarType.string) :: Nil) - ).tree.head - ) - ) +// raw.firstExecuted.map(_.tag) should be( +// Some( +// callService(LiteralModel.quote("calledOutside"), "fn", Call(Nil, Nil)).tree.head +// ) +// ) +// raw.lastExecuted.map(_.tag) should be( +// Some( +// callService( +// LiteralModel.quote("return"), +// "fn", +// Call(VarModel("export", ScalarType.string) :: Nil, Nil) +// ).tree.head +// ) +// ) +// raw.lastExecuted.flatMap(_.seqPrev).flatMap(_.lastExecuted).map(_.tag) should be( +// Some( +// callService( +// LiteralModel.quote("calledInside"), +// "fn", +// Call(Nil, Call.Export("export", ScalarType.string) :: Nil) +// ).tree.head +// ) +// ) } @@ -172,48 +172,48 @@ class RawCursorSpec extends AnyFlatSpec with Matchers { raw.tag should be( OnTag(LiteralModel.initPeerId, Chain.one(VarModel("-relay-", ScalarType.string))) ) - raw.firstExecuted.map(_.tag) should be( - Some( - callService(LiteralModel.quote("calledOutside"), "fn", Call(Nil, Nil)).tree.head - ) - ) - raw.lastExecuted.map(_.tag) should be( - Some( - callService( - LiteralModel.quote("return"), - "fn", - Call(VarModel("export", ScalarType.string) :: Nil, Nil) - ).tree.head - ) - ) - raw.lastExecuted.flatMap(_.seqPrev).flatMap(_.lastExecuted).map(_.tag) should be( - Some( - callService( - LiteralModel.quote("calledInside"), - "fn", - Call(Nil, Call.Export("export", ScalarType.string) :: Nil) - ).tree.head - ) - ) - raw.lastExecuted.flatMap(_.seqPrev).map(_.pathOn).get should be( - OnTag( - VarModel("-in-fold-", ScalarType.string), - Chain.one(VarModel("-fold-relay-", ScalarType.string)) - ) :: OnTag( - VarModel("-other-", ScalarType.string), - Chain.one(VarModel("-external-", ScalarType.string)) - ) :: OnTag( - LiteralModel.initPeerId, - Chain.one(VarModel("-relay-", ScalarType.string)) - ) :: Nil - ) - raw.lastExecuted.map(_.pathFromPrev).get should be( - Chain( - VarModel("-fold-relay-", ScalarType.string), - VarModel("-external-", ScalarType.string), - VarModel("-relay-", ScalarType.string) - ) - ) +// raw.firstExecuted.map(_.tag) should be( +// Some( +// callService(LiteralModel.quote("calledOutside"), "fn", Call(Nil, Nil)).tree.head +// ) +// ) +// raw.lastExecuted.map(_.tag) should be( +// Some( +// callService( +// LiteralModel.quote("return"), +// "fn", +// Call(VarModel("export", ScalarType.string) :: Nil, Nil) +// ).tree.head +// ) +// ) +// raw.lastExecuted.flatMap(_.seqPrev).flatMap(_.lastExecuted).map(_.tag) should be( +// Some( +// callService( +// LiteralModel.quote("calledInside"), +// "fn", +// Call(Nil, Call.Export("export", ScalarType.string) :: Nil) +// ).tree.head +// ) +// ) +// raw.lastExecuted.flatMap(_.seqPrev).map(_.topology.pathOn).get should be( +// OnTag( +// VarModel("-in-fold-", ScalarType.string), +// Chain.one(VarModel("-fold-relay-", ScalarType.string)) +// ) :: OnTag( +// VarModel("-other-", ScalarType.string), +// Chain.one(VarModel("-external-", ScalarType.string)) +// ) :: OnTag( +// LiteralModel.initPeerId, +// Chain.one(VarModel("-relay-", ScalarType.string)) +// ) :: Nil +// ) +// raw.lastExecuted.map(_.topology.pathBefore).get should be( +// Chain( +// VarModel("-fold-relay-", ScalarType.string), +// VarModel("-external-", ScalarType.string), +// VarModel("-relay-", ScalarType.string) +// ) +// ) } } diff --git a/model/test-kit/src/test/scala/aqua/model/transform/topology/TopologySpec.scala b/model/test-kit/src/test/scala/aqua/model/transform/topology/TopologySpec.scala index 5d2ed07b..8c9bae83 100644 --- a/model/test-kit/src/test/scala/aqua/model/transform/topology/TopologySpec.scala +++ b/model/test-kit/src/test/scala/aqua/model/transform/topology/TopologySpec.scala @@ -4,7 +4,7 @@ import aqua.Node import aqua.model.VarModel import aqua.model.func.Call import aqua.model.func.raw.FuncOps -import aqua.model.transform.res.{MakeRes, ResolvedOp, XorRes} +import aqua.model.transform.res.{MakeRes, ResolvedOp, SeqRes, XorRes} import aqua.types.ScalarType import cats.Eval import cats.data.Chain @@ -13,6 +13,7 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers class TopologySpec extends AnyFlatSpec with Matchers { + import Node._ "topology resolver" should "do nothing on init peer" in { @@ -440,7 +441,7 @@ class TopologySpec extends AnyFlatSpec with Matchers { // this example doesn't create a hop on relay after fold // but the test create it, so there is not a one-on-one simulation // change it or write an integration test - "topology resolver" should "create returning hops on chain of 'on'" ignore { + "topology resolver" should "create returning hops on chain of 'on'" in { val init = on( initPeer, @@ -470,14 +471,16 @@ class TopologySpec extends AnyFlatSpec with Matchers { val expected: Node.Res = MakeRes.seq( - callRes(0, initPeer), - callRes(1, otherRelay) + through(relay), + callRes(0, otherPeer), + MakeRes.fold("i", valueArray, MakeRes.par(callRes(2, otherPeer2), nextRes("i"))), + through(relay), + callRes(3, initPeer) ) proc.equalsOrPrintDiff(expected) should be(true) } - // This behavior is correct, but as two seqs are not flattened, it's a question how to make the matching result structure - "topology resolver" should "create returning hops on nested 'on'" ignore { + "topology resolver" should "create returning hops on nested 'on'" in { val init = on( initPeer, @@ -505,17 +508,16 @@ class TopologySpec extends AnyFlatSpec with Matchers { val expected: Node.Res = MakeRes.seq( callRes(0, initPeer), - callRes(1, otherRelay), + through(relay), + callRes(1, otherPeer), + through(otherRelay2), MakeRes.fold( "i", valueArray, - MakeRes.seq( - through(otherRelay2), - callRes(2, otherPeer2), - through(otherRelay2), - nextRes("i") - ) + callRes(2, otherPeer2), + nextRes("i") ), + through(otherRelay2), through(relay), callRes(3, initPeer) ) @@ -524,7 +526,7 @@ class TopologySpec extends AnyFlatSpec with Matchers { } // https://github.com/fluencelabs/aqua/issues/205 - "topology resolver" should "optimize path over fold" ignore { + "topology resolver" should "optimize path over fold" in { val i = VarModel("i", ScalarType.string) val init = on( @@ -546,12 +548,14 @@ class TopologySpec extends AnyFlatSpec with Matchers { val expected: Node.Res = MakeRes.seq( through(relay), - through(otherRelay), MakeRes.fold( "i", valueArray, MakeRes.seq( - callRes(1, i), + through(otherRelay), + callRes(1, i) + ), + MakeRes.seq( through(otherRelay), nextRes("i") ) @@ -561,4 +565,92 @@ class TopologySpec extends AnyFlatSpec with Matchers { proc.equalsOrPrintDiff(expected) should be(true) } + "topology resolver" should "handle detach" in { + val init = + on( + initPeer, + relay :: Nil, + co(on(otherPeer, Nil, callTag(1, Call.Export(varNode.name, varNode.`type`) :: Nil))), + callTag(2, Nil, varNode :: Nil) + ) + + val proc = Topology.resolve(init) + + val expected: Node.Res = + MakeRes.seq( + through(relay), + MakeRes.par( + MakeRes.seq( + callRes(1, otherPeer, Some(Call.Export(varNode.name, varNode.`type`))), + through(relay), + through(initPeer) // pingback + ) + ), + callRes(2, initPeer, None, varNode :: Nil) + ) + + proc.equalsOrPrintDiff(expected) should be(true) + } + + "topology resolver" should "handle moved detach" in { + val init = + on( + initPeer, + relay :: Nil, + on( + otherPeer2, + Nil, + co(on(otherPeer, Nil, callTag(1, Call.Export(varNode.name, varNode.`type`) :: Nil))), + callTag(2, Nil, varNode :: Nil) + ) + ) + + val proc = Topology.resolve(init) + + val expected: Node.Res = + MakeRes.seq( + through(relay), + MakeRes.par( + MakeRes.seq( + callRes(1, otherPeer, Some(Call.Export(varNode.name, varNode.`type`))), + through(otherPeer2) // pingback + ) + ), + callRes(2, otherPeer2, None, varNode :: Nil) + ) + + proc.equalsOrPrintDiff(expected) should be(true) + } + + "topology resolver" should "handle detach moved to relay" in { + val init = + on( + initPeer, + relay :: Nil, + on( + relay, + Nil, + co(on(otherPeer, Nil, callTag(1, Call.Export(varNode.name, varNode.`type`) :: Nil))) + ), + callTag(2, Nil, varNode :: Nil) + ) + + val proc = Topology.resolve(init) + + val expected: Node.Res = + MakeRes.seq( + through(relay), + MakeRes.par( + MakeRes.seq( + callRes(1, otherPeer, Some(Call.Export(varNode.name, varNode.`type`))), + through(relay), // pingback + through(initPeer) // pingback + ) + ), + callRes(2, initPeer, None, varNode :: Nil) + ) + + proc.equalsOrPrintDiff(expected) should be(true) + } + } diff --git a/model/transform/img.png b/model/transform/img.png new file mode 100644 index 0000000000000000000000000000000000000000..11f4072aab2beed98c2efecdb5a806fff7bc482b GIT binary patch literal 87152 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

NFT?kqR zVP^0**QeX)JJf2h$#9cybN}^^lg{Jh!&ol13()xCPQ8F;I6-(G3O=LI*Z#U>tJros zi?#yYXqAl+8ZFhIS(!?~mFw-4&IhY;m@;oUAA?qMM46@F|( zW3LZ=M@}v2Z~~g6b;}5|sBBas_`>1RB;){Z+b5s&#=5?244k6(A;~X?Eh5D90KS-E z8vkrM^}aEA=kJL!m>(z;lLBvw4nxQlPuVd1>dUk1ymULPThI;*00@E${(2U+Ds1=>^oWtnjw3QnJe+5yIfB0 z$Zg8F_=rQ%e###B!m7PS4Mo1b?W2iISj4D5PZ2T$U}QAUorm5KMg8P>?=!QzS{pxd ztzFB6G7P6o)p))V=?nb2&4_^w{PtV*U<1_*)?17%;u=7ua;nJ$@5SP;@If(mbU}PJ zAt(C4Z&W%QMJdgv>QBeTk_0#iWM`Yd-kw%jJ3`nJMpX6C1RVv%c_H-Q_E<7>uObxX zOC8)GA?nXwB)-dJGNu45qv1I89y$00CuL>^Ig{%0D<=~M^ih`+7pwpRT)TyLYb2-J ze%${1XC{KsFB3;@W6n&@f>Jx(e}Vp;7%|Z#zkOG+u0Dc~xo4EQbJ3v#TvTW(p%tIy zgtRuxjDZ~BtlVq2WvikWyL97ze+8s&RVoqas@)?cZf45t{K=n1#DLqlN{ncFQ8bQv z*)K%Z^(Q^duP5P0AHb0^3(WyY zb#xHy@R+)poq2A4X0&vs?4iD9_}t=bhh>lgh*nY>!lvHvYvD)k6$bMOC6CMW-V&EZ z0EQ9QIByhwX%X5wSFKw+dOkZV90)n`F2<%&&$?hgmiZZIg(#Z3tF@iIRX!ZLyo;%C z;%cMAWlCw)d|>8uEf){5Oe_AjiSDKwlFjE=tgPI$_9gy0&IO*Z(v^O=44~HtJ@GR2h34}O-eQD9|@Prw3}F?Fv3A3vIVJIJ3=W%rLiLD3A8c9?;#&mLQwzV6k>sR48?n|31U( zF$UMGsW8j)Uu^!LOPfl_?RKnSPBVW~nr% zLgog+)as}Cr)PlenDM63Q**GI-T{aGYT&@*8^2~UTcZ<8f(|HVRR;VYmj!V8y>-vl zJ^#nkBJPEC@A^L_Kz#tZV*M%X`j7^JZ{542LyxQf^>0J#g8ZwY`_FDi=)HZGpdPb2 zPg-OxPJ#(wHdwF#K6e=8-yv+ZFgeCR z=8#0Y4ODUeH9-)LngspqAQDJ1u4eQ97!&WVL7d7cx;Wk%J=})9y5_KA_=kti0<(#d zeV9xeeWYBm<|O*3>56pbqUeg&KmFlfKk5(CgZPJfus?C2535radqAUB= z=r*?4gl~V2{C5XSWq0R#zS>OvQP1PQM*8`SSBzv6O-qX#e0DQO{&I9Pq#0yieu*Hp z2yngeJe;*fQ%m#Xn-PABSnjFwSZF~~+K=eI<{wTy0Go!<1oe-r5%a)wk#E@Dly~1vs4Y!$2)fLRQ~Ho01IW7N}#+&S)tuZfWA5Q!jt=R;-pu@6${f z0eabqf;S68%Rx1S;nHG{q^BLw8Rtawg?dK5!`Ji2X^i|99~@ec!GX|OE++pYDNz6B z$NJ2Bao8j#hIHS02*D)il^g*AU%Bt#=NI=E0l^E(aG^Nn`Ur|2!9#ibw6t|v<{^rO zt_f`9Xik4nyr#om@-T@=E}Rja)^2L%7i4iu_Q1j-%*<-fm&r;@0bEebfSya{H&!x) zNtDxVt&It$590vS>?(Kwr9#u&&$^hzto>F}uesN|B{l;!z^=Vj(QP`u>A)Nnq~PVo z5J^OzRx~H<+`1*WljL=g*Mt{mX7?&H{m5 z%WI_U54f&T4Pb&Li&%P5D#Fqo zry+L16AwUaat;A+^iBAP7rwk!_c$;Bz}rmbGL2|W*Hlb3v<2%iKlX0zi^Mr}mvt2w zdNd9ca33uNDfDVAS0duPL92Ggl%Z{=ED*@9Fioq%|^= z81J`m>TRG?4A!*+_q6B^8ZEcZaTYP-Rz)l^%1C3=>dgC*t4-}bSG$jMN7Y!awD=+2 ziC^fl{4?Cz zk?2R7KA{TJW|ioF|Ex?{?hA1_zI-kbqpU2VIk-5WJ9IhTxsRkRy-fm~5l=l%W?W!z zQX-;nSpjKzY#11Vq<&MGCA}N9ItlM-E5Si&P=(9IeT4;biU`uW=gX&wBM2wXSB zl4*H;KR7e-d@I(z<7Upwlzgb;sNb!5xTnfMJ?d~J@$on9-zO%d6^L>d z>R8tgRu}i=mh*`iVae#K6MZ{<>5RHUlw}Y*iST$wV9<0?^Z^I#E$)Z_@d`|;fJWeo zjJMb!n7J|6-V1EAM^x|p9R}G=ho>#TB`@8n`2EcXT3>S_1C5#;^<@C&)a73A*G`7h zUSy%#U51CRj--gA80tpz_CGt{Dz^Xq{xP%}n_>YI)&R_`fE7y31r zJ*yLrLq)icW!X~7LII}~a}Ff4EaQHB{2==lt%Q|g2^isM*Z3eg9^lBbugWkd!d}o9 zugQpaYa#T;^%Fp${(U|GDN~u#-(dLgrFn4a*-0-7`_0UHSt3D%?%pl?tCYU(7ol&Q zjW^=f1Es0X_I{_)^v3TSY9Q`vou!)Bkc3u8ESK!d_T`ye5BFai$=Y9JMU{G2<9|EC z!#8FGJQ4JDEi#@8(U&zY<|7v&%)wFH0`~U)Noe%;jx47*d1X_tai$r$g5m1e)NY9c z5ef9#fSvGZCvBjMJ<9vt+CXIs11LV;hlz<G2zQVl^}^}{x#qd zumE~(HOpJO=2X(-B4ndb1?^8WeO=MO@`xxXhPb_F4lJaA7dlt3g+lfdXmw%#U<4PF zAdkUqFN63RT7?ge@X}zvSnwoK!z=6BT#CtK_AoEU);B#&$g9PJH zJF2>Kt>?1Cg#Z2^z=3`_VFHef$D=zC5D?X?xqZ3g_=~> zqina&ua$z3Un>QxTlv6iX%N04{fO3xfi{L>WENqhmc0g_{_Xb9Kod2Hi0=PRr8f=f z2k6t5F!wb~|Cjf2{_+T?_j!+ zh3lWC;UHc5M~b-mx456O9{i2d7-vwg%o!&Cf!}jv9oYDS8sB8%F>AF!C;ATg>7}9t!F# zqyYMgCX9F^JPs)H|E_rxub6_?41oo@UEvmh4FID80^2_h4WyqexH!Fv5XhJR`|4?6 zQ^bI+yo=EE&iwbI!@2j25>pHM0^1}^U7BCr}0j6=sv)JeUA3VK+$9+*ZtuKIovh!|-1> z7G%R3YAtrsKwL`xzd7Ox7=apL=ZQo%6z+Tff30_L+2$eB3jFU8 zL(C^&19z|Q`~Nn4#CH<@-lG25|M~X_CjQsXyb_=M={3QuX^aBjA${SO)Avsk1Bt^a z@T$WNzvL=^+LjU399d0LQV!$)w=H`>b^|Xwe&3jMQ2gse`GM^6yFyXy6rj;LmoNSm zWc}0P@AMN#gaIp%P6hCtJ2G+o<*X*u$a&T2-{9~- zi#q~+)auZ3&he=fw4C8OBwj3{0w#Q2*bl+ck0U@*0qO3%*l@}&%TxW+FI0pbbm&f} z4K=J1+g6Wtpp(O zv;aFU6+%4?pqfU>xr~XFT?aE$aC>G)Y>6{*R^uS+{wqGvX=$OtXvV1_GPbpe!~%rK zFmplK`qxrjsWtwN@bbHAms4T5EG>UoMgK;usPH)erOq+6pufEWu$g{Ank$-0Re2!Q zNCtv7f)}pl(~q2LPvS+uI*F;~B63&`vQgE!mgjw6s8aT!gWzF|+^TxL>mp zC&HZ9fi!1lTtwH5EU+qK&_|X`X%DH!({HTD@EyKOPd1V5)K7Z&|HvHr5OZ6$>m3iI z1pYf2u*M@bYTUP%&06GZp^$$lMCpRgK!SeRsUh*dNBo)VE%`AS3Hp2qi?e^tpg+%V_w3N5^XA$GCC+mJ`b&(D*VyQ)3M+*NPqY zsBHK(IO%!2{xrQw{3GLEiGtx4Sx5^td+V2KT-vN2ROF3yg3bl5f}M`O=t zKzKU?^w$y4K{V>qX+$cX&M5~(>mrwe+b84K=kByYFSKkRfy{u5RByvP99pB$-MnQC zY~wOxuN`s8jFMc9X{bLwJLfW^eDiZFQ(gPhBcn=nb-p>? zLVx3qz2CQo$SGrAkbB>%B7`q56*jsi#D$@wTsQ}@#YoA&0=?+?U!$jiTpu9UlmzCV z*D%9jB8DGd4qr+3`HoPKJsVHE%;kI=F@^g144|%@-}7Ly_UoEo`sq(2#Y2LYC^O8fcCUbUt(64ZnT5_Eo&|9>UlE=4X zVWU)N9h;xY=?eYY0_6~W0MfCs!yQ<(WuA28T&@e>mLNj|kk%-OH|n6`uK zi}Dxpi;)t=qg%SRw5N@$v9>7Xd^0E5@k1xszrAMgg6ccryWI!5MR#JAxgEzwT?eB5 zH?&*^uH`E(D+A8SxBmU$ImTpVPyDXNdV0s$6XGP453Z-}7A#lThM;@cHYyCgsAmrF zgSygWA`u!<3^k6bvwU+q*$RayuE2%cFK8`p_T{zpcG@csifv)r;ukc;mDlloc-TTJ zzvpwj`L&EEMWn!A^-bfKneWOKu!7$e1-n++t6H-`{Fq1VRu{8s>1W0whCaq(ClbSR zUB-=PFNq;KLHgbwyrN}eA?)QNwGgoz7nxF=}FCz8QP}t7nm0lzF9*xqmie;F* zU$Sh-y(nK-f$bPh*wUK+YmGA;Cyn$WgmPKjY=^fAIVDDIaC&oQKEpf>n~Z%FmPVR+ zcA&19L|Cfc@d}!n?i42y+PCH0>+qU{$S~WuHG&$XJD};h0_B4IOT*p%U~3lzVwcuM zugtlJ;XZ2z81axxnOdnFe%aEt$d%hhmUSk8ai%iGVhSVd(@6Kqz|2KnssBB*@0F=9 z(-EaQI(c@@lJh;S)l|X-I(TVKyqkAbIh`U4UrIEZerycAwXVb6t&}?_z zhzB>IphjZe+^7KH_=)i@VqSNfmuV4Z)aGx-Rb#~w`A>NT_A^MW-R6TszX_aOzIq2` z%{1L1mfM5BQ&^qsw%~KPvnP1uR^Q~km#se8N2XX5JKU%GTgv;4L;Y!A8L(r6Wqz%c zhvNW@=kg%pPO;ukEl_d{Ho*kmqN19rFY?>mHd_hpy1{I$D;+vbiq$Okz$c*dpoJ3J z)H_9^txiL9T#6WC))`r8EvDa|kB+~wXuV{HcYXo)#jD0{17@?_wqAV?4x5$Nx!^gd zf~%IEPJ;~oTcyKX6&2=`!w2~ug|3yPdF6yRr_k9zhd=K)q&QK+1ZE}ve%`RE2VHF& z!C&(Fqw5eRInAc$Y@kC6m)k(ymRT#n50~Z9Q!_%29|jg5QP$q>j??zteS>$Z3IUSj zQ{3(=@aekjS`-`Ac*)^PYh(Xg-_u5Els;`)IK?>z74y)+fswa3n$YO0&U3d8OvC*9^8nmszT34;}BbzKBV z-^FD1sz8FaF_?`wDA55GYn@e$Dy9#8Vyso<<~*lFGp7VDKIbs<~9BY8aGO?YLMqHqjAr<5AeMmTj{SnJp!@`&W*PIYoPcltme}155b}uqx4d zkG;WlMNgaGlAR*8|^tv5hf?CNNbJLFK-2P&6WaZv?iIicGS zIYf7wTqGG26NvD&GVZ7x_))fF&&3Wvd-M`z@t)_x2*;J|lG!lT`vVW%iCqj%A`Co@ z6-S00wK|G|4DSVBXdMu?^Q@M&Ehiqne7cz{Zk2lj?9_)986$ZgI5diU)3^-EHyzoF zRlUZlL3y3yRxkvsk*qf7JIl;R$c&ROhcP{~XG*2sa0&AJQp4@z^JX8FTcvl@%ypBa z6LZ5Fm;4%2UT>UHo|CswNCM`lK?Xh9Px_Z^u@o>*wv*L(TaJ^ryI#_KdE&!T(&ras zU(e)16|C!kWqIT4CHA~DRk0h}`Y%&py%)C4DRhCA$A?q=?vO31Kn+f$2`wZ<`Kj=! zs8OX#RG}mzo8V+j)a<27I!+mGQGYY_pq!zDixDy}UIW}HQA59EQhIO`N`MIJ<{fV0 zPXtMHne%4|I0}>Y&$Gh4zLUM9?n~WDBV;v-!!P`g7$hByw4KfFG0%2RIHp(wEKs$_ zkqwC{c4L>Hj^CNCI+=dUIYHy#N=gh_UvK@#-_xpRu3$&ON71GDSnTlks3hU{2~K9+ z{CVrh%=-5w%f387lkL`9*`;yQW0U@pt-y5(Xf4JiRi^mX^2NAu_N;TbzP8o z2I4YphNpD*eiIP#4L=H;X5DM8=5)D3=jSI!h>nI;_hxEeBxP8&y1mFXn>;f=e)JKU z;z7Y9UtKK!C>D5#lCm@BzSsEhFl*-l8xLE-k!Qo{O#Dq`V%eY2SnK-*N2{T2)}I-5 zpmt@x*~$>q^c46ehop_`KId9lRe+Vnq~5QG+wavOYqZEM{{v+oa`{o{#D{qcOY@`P z#%uY;uIZg^J)zaZZ``S2XWX|wBKzeJd!nsy$9R6Bg*>3n5Av;^gF|w74GLi;LNa7fAi0l=GkYmd{ zvO|9Nm+E=mp6C6&|CaL|pYLbf_jO1~P2Z zUh|{fuA_|%8=cS|Tjjz4OAwo|PNzD^?K36i17h39y@zjUQ+0rEg`M7gX|2*t#1FGS zQNi_@kvr!@b4X?eWyHY1$@e{k(Ge}{usMa#-k1%X%?BB#xeZD6ePtp`EWWdde2*zm zs*EfrL6L}A=Ah+$ipB=aVri=fv_NJX)onm%$n^T)W52d$KyRKRWPu)}mUYz;JaP0a z%BxG2x?~|EV~7axj<4e#{iD8lh1-qac35yxS5B5-Hk0H!`N_SdW1LD3)i9fU;BQ;( zaaknrOrT{a`2wO7RmJ*7)*KkYPa&1O5$dcY2g#NZVug?lFq$2KXxn5lc&hP76s42| z_$I7@OHO`I{uN6(5I9kRWMUfH6W^&Ub{JGQBCL^DAlqzI$AcpGFbk{h)1 z^4<3ZC^)^V)6yYY-`!ILJ)7`Z9y&lA)anS%7Kpsat5Pj!tDlY#FFraYVDy;!^~p_v z61~ax^q7Q2F9N<|NvyEf0x9t?)!q`Qw*P3v#JqfSuz)g1pVS~ogH(Mgp2C-+n8J&~ zlj1ssM^~sS*x$ZduLk*#!?BtP?}=X;F`h{AQ#AN9fTHvFO1;&RX;9OJYcFf_LKAkM zy&J8ho3!tCzY_Fu|8B;)%d#@E0np>k#rv&oL!BF7$6dvSfxgtEG_0&@xtDj7vC_!1 z-jL+w^EJyoa4PMvnVQJaZ_c+444e>AksvcB6{NvJ%Qj2@V`zs#5(KT(%H6cAH(nh% z3$0?+Y9=Kf4pM8Pp9(8eBQuxVKpgMmYv9GXwFy#OM*PM7KApD-BU*!Al~)`GZBzG> zmaGqVyz?2{;9FOw2;iw4Llahcw;e2f$$eSu<$NWw9D-+W&VuvK*}1n2sNDC=_YyMn z9|al}r$IBj1+8d4+h_3{ulFC+zxgL*(Dxz<_7dNelC5FBLIGs~8?jQLQgk9>I%-Kl z{f_a&<4$yFGbVGz#{pzM#$k1*Ze&=)FkOG&Di1TGyb(#&z zX8S>!3;s352ma(6oCn(~T>%@ONYGV~h?ih_*rMR%)LPg$)A}IEwEw!A`?XGp*dvdT zP1;zGxbn@5~EIp zo&=FZMiP=TYO+x9cD*@!^W$3pbXpyIkMi|+#pha1nR>UJQdx^FS3h|Pszy-Vf-?noy8)cl8PWN5Ok>Sf!sAm;9eP# z(gz)eX?_+u5t&F;f|!F$NzJATn_uzGc4o%ZF1-EAyII(V#ivZJfJl9!)nZ z&AO3r1d>KG2T_q8BBkNflyATcQO1vg^i>5-{FKuuQw6CFne+MpIdQZWsr0&2#reHS zcMr41#q?QG*knT-cN>L8@8SYu(K`+nc(%oufY1loV{2nnidc(arCO8g=3}_AhraD6 zvsrWuYDbW_L;|YTPgqql8x808B;}vKQOk|9ww)jRMy_b!mTfe^UVeM7fsIQ>(Jo1) zCMw(9Ms2tvFfC(PM`hRiZhs@Zs9^ImzMz2+50l;RoTFY}2FPR2vJ{_h-yLPL0nz)6 zrLCI)6@DyR8H~^xue2@%Fro|8!&1iOT0`-t1wnG^HhHIDo0nm&<8UPnsTXC$14T^T zaH1p49m5>oKw;{Vuismm5~n|()pOY>4b@Ign?dABt8yA3$u;TK<0I!x3a=UAOZu!` z6LKjzqXn;V>Q1!NJGt+BOqN7CbT0g&P2!J;I&ZwPs#7zQRQFT5G`veX&XI;JCE|fB zvvrO2X5Ox~b!8phf)R;1=;Folfg9%&h(6-2ypfI{k8egv2HY9c#uWRTPd0qi zmFJ`P9Non;a8DJ~0*E#tD{>;2%qpo4BmXE&#hovZVo@=a^(7TklNd_NAvlwBOKBgF zN)RDLxFDe_ti5}4t3jeT_l@6sntro}0beQtFB8yl^perwiG~RfZ$Yr5nA=KgJ5!!l4n0rM z3{i(y2Q3vn@wx@G6o&NqO{5@1;(lOZErizMOVaA&Bi#)(y*$DJ6JLD#8qP&ONcIZx zk+rEkYxuq13A0}#wi83ytLyQ+M`nU$Sw0oQ8C(ell^URte3^w8t~G^O;^D zk}miy0dGjGS+Rk|tUx#Mvf%+kae6o2tZdV+_(|kx3&;&7hJJA7D_=f%WX*Et)uPwn zKziT=$l%=uWo@EIkNOkkBqa6IHJ+o%D-F(3D4Oft8932W${vFn1f(VXQWj$;54|abdsr~i}N7$jV2bzqQ zFZCs^{H!;&4fW0vrZ(tE$tKPm-GwU2BYy!+M$s1ou6^cQ>Dij}&YsUs+#j4#O%N-R zob7Zbj&bEmfi`4M5VAO|dTxA~;JGCXE!ox%ItOjXB#PE%kjJV+Yd6-#k-jxPs`qG1 zuV&&t_)K&a2nPM~-h|n`lyBdC+U+cShZ9&z{m2wIW( zHVIwIhsr9nv(Ar_tz_zyG9p!jaEL#D%X-}`J=z!zf`Z#BLwkGL3$I;jI~FtbQ3jc% z=4kEgxEY9Pgu=A_ZO8gutP3^HN2Af&TwHp0 z{39P8r3o*)Ll_lW41rm+Bk7#dJb8ax(VdSV*7SI`XAU~Z{EQMe z-r4N!W1YHW}iFBRw(X5^y$VFu%{IYnFf=n4iWY#{%D;ft^M4|7Z)a0-<>GrN8 z!FABB>A+cFl!3^gQzZ(aq^${jzKXj+gw{6)9@7VMQf~Vfkb!(gj9Dmcn5BxIx?bW7 z^5LT1YE$tIfi@Lgaeg*ptsd-{wO^2JKrCua)g* zFE+U^4Lly?5X`fIF4>vxWJ*RsQsUsR$Yo2>LgfyJ9DC)LMO%QCGi;$@u{}y(0yh2H zT0Qwg&_Zy3%9_cF9Gk{B7!SL?V)xI|O6Ghq@Sx#Su|i1`6)({es5gaHe-yADR9LM#g$ zo(_;<&Rhj~#@#t$rlm%{ArOMLIQEyly$C(kYaIr9Z(uoXX|OU$PoQp4^ZufpVhM z0vP#_1|BYkc*+lO%pgCB6S@lGJas*c>crL{Jis_m(*SdeFOaET$$etjd#;1eR0(?c zrdm|i8kbY#vzUItK4q+G%S`@A z#e?G}Fr#tv07!pElxn8i5nb6W0nI0LUvLJo&8YbkQk0KadPOjK;Ox3)dL8#vL`dGe z5{C$6i37!ffLBS*-LQl#-0@Nt^uT!0*gUm@qmBOybk}y>gma6y2%q?3^0Bj_gA(m zPirSi=>m(hkjfM7iG;~1L}yc3V&D^+KG(>x0~gn+d{q7(!4w-Kl$DeaEQKb=oJh!Z zIkac>_N5lRXWmJdxUV(&^Gg~Tgh9Qe)5JBZOV)NCBv4F~sNxH?fcSK{kLC2&iuQd* zT&xbR)y#&Z#D|OF(Dc)PdxdRTe(>0Vj_Q3v9e&Wb;2U2`wOc&QOBtLjqt|UrlFEz@ z5kRzc-@?2M;Ry>!;5N;T>$rb!mq+dKKNU)Og_C)tb3QJUdu+7iWXwrQnonm2U7$FV zf!lXDzlhOf*x}YgH`J-+;4|KY2sUyB#~C=vF7v8AN!(t}TU}BUgVK8@ydTq~!`+xKoAYcqy`5|zrbUwrm~+m?VK z3%HdBqSJ3$H0~4Ib|!h|-K?@^?@U0s_xqu@=ahQs)yq$Zvg+a3+FIYEuPVHaGkn^# zSc=>wu=qgVjpnykSrU#^R z`s9l6yvn^Y(Dm!YIAQL`#c(QEl*8s_@niqktR#U+d0PbzQON)+2CJjndsPfiZvPwFQW1fu&6*#>K+Ro6wtT7>*C_9GS~=2fDV-HBAgokXL@8)&1xUNTg3%= z!vgb2i5L{lFzqBF2{E91S9gDrNo!qYws|u%DKzEB@Kk=nfue(_xq;5`UkYk ztJI$hQWtlZ=>F3r*fr?RHN?5(14!X`FWGPc{FsYyN%x&XTK^cUae1#9T0N!=vi5PZF zAUk^=Xv}t#e6D#xMOaYRFT;$cGp$`6#>^O5 zV(n%Mt}zna2#FvMM@@a0l-?5hU8S8QwomBF3<59*4s z{N=hGxY%h3%`y=W_M{SwD{18pa9sVgs_N;6tRlmgdxwG0D@;Y_@#1~x^g3cxN(FJd zb1nskN(Vjn>RA%af(Udd6DhaxCGegb2i3@={FoQY*w0}#o#3*nq77A~tqa^+)V99v z(0v=7P7fIrA|9T{U75cW4IT?sBJ!%d_FwGRRl~&5lqgoK-Y1U5BUlu%` zRLrbfuUgqFk^v>n=v4YWKcz*F=`(R#j@eC3HQ?gBw3G?>IwsL)A&LpmGAtk*r1ev{ z6(#^VM=#sFNc3#qBx`Qw0@EVSa%wkXG*LvI&(2i&EB1yDjaW25OP}AEZq1F8Pt!sF ze#m*@(@Wz}j~}p4Xi~KOoSu2<44%8L^^E?!oGWM#)|d16P=EpIiS5_L#ia^wxm>Y% ze(B4tkTNs)y1yQcw6`2;c?tc&3K|*-e_hcl^4L?*4jS3I-U!aJu|HG)kwyj#$`O3U zkE4Lq-KIVXxheA#=5OWm)zNM!G(fX#w97Eiod2xZ7t2KvfpP}0^NJ|-f_B}L_H6YfY%=xEFvK#Y9Xzi-Uhh)A)rDe`c2w?lo__P|=Wejhr3v^0 zdR=3)YM*eH`L9{N=3k9fb{x}C-!gj02M?D;E)Ts%m&KY8@BOvZ>Ps|i(uX}h#pPgo zx8CA;VZPvr1m4XVP*_r`RA;?ABPOOw+CAhUzHJ5a>+VuI_h}c^BuKkm``kP0n!Oja zSi(w4nn6?E@C=C_u&J6vjP2wp99W8EL#R1TOaG)OxXE) z`wVh#zS=F3e+&-NU!$OwY*Hvrq8Gwk zjgtYfgH<_ga~(<`dO3HvR!}2NKigBye7Q>0TDF7aExlFCm$yDc{k1R7fOvl{XjO;M z@rZAZs`!1E4^%e-_)A-vrF6^q=pQ4BfC|!tJN0!xe+dc0-px_y=TrQjzXZjZQG-J@ z%QNnX#6H{=(gvZ;QLv*|`d`2Gg%JKSesy%Nh6`l#b24^vuSbC^uma96CCpk-69*OXmlC z{w5sK*Q4D^5l*ikLd$Wv9h%zrySTUv&qkLM8KPSqkvJD2dciha#^!MWy1l`Y+tEi= zfZ#N*zcF9)VmwSI9c=kt%;x%7+Pk7(C5AL|MBJN_T3z>yd_jpON&Co zGT$Wku^Izw+ZQm55ztm#Gq*O>4`EVl%^mTJ7aH%&)QAp=y_El&qH6j`DEiMS;1)AC zy%+{%h^=q2#}kA)1xfTEpW;IE)n0%aua|obyH&51edK3ggw7u#OZl%?LUhKhLf>Bb z?y&ug#4TW-WQDI-y>k*xUYOph3%U%Vgyj^#$WTk*z#tW4jSq{PSsh~b9vuI3f-Res z5i3qHGrm{a`bN|!naho_0TRtZhlE#{D@g@a`hmF`EqxYTCgN%!2j7U6mZ%h1ct4#L zNF3vAza?YM^yjz#xx&V|Eq_$@GIT2bytL)}$Xfvtj%yj}6*;ndmpqa9!>HWh^|`d$h|7g1ln1(%RJPrcT&%p7{Lm3{H_(M4BZ1Li-qB-|5z; zThM4#_Oxf`T6)qT;rer6=~0B`rOD0~2Bvj8O@0D?!)d3sSYf?ZP!-mt%{WG(zWExFg{rnI;AT0YH1+#fm??rKXdSBjt&;li7ost+ zeA#b@wt@Qcx7ljTUOOW~8d8f3*<-KQ2K2mDQ+?QhhOatmu%%GFbN)Kjf5@&wcz*1z znDIGKBhXa29sS~*;c%hA=oG6tdEx7)R3$C0pZsX&?558WS&cjyt;&dhmkXZo7RhEZDFUhTiC#RKB`>$Cp%`t(7Ic@`; z?0N5I`kslA=pom1ohcftFTY+J%NH#DhU|W zN?+P@5bd0zT;9N_x%s);@1Woye9f|`&lEfr{`(YrbjT$8TF=6D8WbBi&FMj4HPRn( zH{zo{-jKt$abQ2q0HtVF%WL~?=~r+D-_Zj-_Nrsc>vL4f&cFA9pBsLvkBh782>t`s zQJE}-=71fVeEteydC)F`oob!R*x=xm4~OkE{Z~#kU;qjGa|s>3Jg@JGs6&zHE!N*< z^OVlWaNzz7FxKj_DM&;|>!%8soIU1Yy|d8#R1RVUZ3M~{k5P9)Lu3DScRg)bt}^{G ziJ|ZB!fHCZQ1$fJ`EctqY5~MxZlTE&Z^AjG3wvFM(W|v*iH0uc-v)O%lwWW@nao&roO z%%dZ$eCydIGt!r8UW%257$0}^IlnJ)-XH;v`TsJ7y78cRA3l{&Cu%&(iruda_Mv9{ z57@!r`|8+x=^@JRDuhO*8Xp`&Dv57osJGcCt?_CbUcX z%YjQ6gzV3Kb?SpDDE#Z8YZR}rp=~hMO*}a+iTTh>_~l>s+-|(o=q39k*`}XkQv{0H z!~H(p*6|7&A(z+;9h|gkx(e%!#%kCDc`bDi6Qw=_yp*7rKnA`)LE?$-aq*)``BQuU zeZR`9u*|w$;?{dI8lCvTpaVkAs|td9Uclo!jZ}A4aH^*P0zCSWd1!+nD7PZrIkBM_ z<8=yv>;E!atT5zCCXd@w{Fbu;Bn8qnw(UGy16nwSVL5zsf`z8q#C!EMhEQ+xjBA;cH?7R>yC?=f;1 z+6^48P9t7Kms@e)QO^wxvDLx)!8`7r__Xo%|GPn?E(lCDvuwJvmG0j}rk^0QWp)%h z|A$j~VuCYomrdDJdh6FGU>V$baR~a`jm6t;h-hb49c$42FJ~DC3QpbI$xAr#i$!k_ zhcLaty|${`%|=$L%iVlO63uG<7|HjfbJ!)#ZsZ8PVf9S<^Tf(*(j1UV3IF;j4?q>T zK$E632gRd`Vh`P7jhmG(r5ih)88vnh@0nmTv~lwnchqB}zIN`#xszqH2zv|wsgyGy zT_d;Ai#nK%Z0%`6;=hBfj?qVi;nC6hisw7ap27xn7&aiJZ~q%7qVjf*pnnK-)aNNh zB`QBqP55}Wab52DQoL!yN&@^(hhVeYs&k9wr%lr#{Hd*HEke1OK zwpsMUF}W$V(x=S-<#>hZI+bAh%k1MM(P&ZBmsX&^vF#y{*L9jG@#MB#GCG)rHy^Qo>iS^<)UU>;=gwH)- zkk2(PfvdYiEZY#)U^F}S`D)Ih`x}73qrJsFx9mQDp9tIeX9@bq6U>933S z>B%}DU6J@L2lV>x%h=SH7eTQwI(5Jh925?bkO}S+AZ-Gjs2?1LX1Qc7F!4q#37*r9 zGA(YUp^1`uW8v!s$FxDPNV-4f{~5OGcJR>XcFrmCO0y(9TNepOlN%ZhgAZU%!IKXV z%Uj=+)IcKW;mJ>(S(WW36Wg2De;=2G_#4qaUuqeCJvy{g)#EU$W?!?bNnTzmH#NPb z0F9VUv`UD2Vq2OPwQKWvP$;wJnj{rnk2X^9g5q*?9z>zFXXBj@*#KQ7+U5Vr9%IE) zUgVTr@aUDJQS7!Q~vr zni&JBl0lRoC{r#z z)fkk%pYq+jYNVg9+j)7i3rY(l5XhvMdD}&ZOa+7^? z_N*eeaI$Mf%o)0D+kx5l-g0&~glSfR^;Xgi>8|b=LMyyiWWo-d6}!9~$o>8E|}uR-~V zdcosC_x1f4CWwe~GRU|~L$s^s3PFXekAYoUOchP87tO|aE6Uw(ogyapU1D6E<;)lW z7B$l*+lI9Nm_|skv8e+8u*5@w%&Goi8oQ_3R=n>IRS6}Ht%tztvKIVUaWk}TSqsQ9UqfNMQ`vbyV;KO}g=|RkGv~6qi*Gajd9^rkt zB*S`V=C%H^4?ZA6D?h- zg4Sr|wDx;}Aybl=xh~(wN1}!YgYCSe2b?`DF&Ag<&Ft+iilzFuMI;DW{&MC{YEBaQ z5xrBpPzVB&vj=R*KQM5gvGyjPKT{dr7(#BC8d)TtiC|RDKvPIinM?}@1m$)zb>_ce zT=w7c?LHp&_`;E{>rR8eql$RMqUwUm13+grddH&!AWvTM;>B#@iMQU1(tbv&R2c;1+0=A{tf;T!XJb^pXCOM9Tsi5dV?iQ$iUAH0*V2^ZF!H?H z0R?sYe6MRu*icPOgNn@AfCrH$JT2~RC6Bd&2oh*L_SSaFJ(?)#8=%f9@Vy(#%YpSD z&HIST5DlK@x9^>HAY|IXnK=mM_=26!092`aA>Va54TL$RDWK@u>=D`HkjtkTk}&dq zXaxvGrtV-)Rwu>($KB`()YWSIChux)4=5cf2KDXcx}?BS>_z4Mjt_6lS^Qa^|>`f~D4k+`{=Y^&lx z(I_Uijp$m%Aqs!tdI!Z2HJ+1F1PX=p>aJwY`F9<;^xJ<78d~}$DRipq`m#F|2}3^1 ziZC%tAUL1__VK58K)j!(pf>;IMn_sbvI=;66V4U795^xZBVbwRoV$aidH0*w*D$80_e2$`gKh%AK{tQmE&UbZ_>37kR(HXGk*P*-jyh8Dj7C+|E{dZrFSY!nb~SU@Hb!m}`S z3|ZLXWM_kxi47D8y%)y$AjRNkmva;ip<&|(GBWi?K`-F@k1UCRDes*cOU*j-cOi8j zXQQmSfRMRUj_xFU8|cKWJ(6)6iQCzswst*I?IMsOZY3s8QqXrJ)`_=t@odW8`3r*; zG2r5qFl%`haiD-~#BN?0)CJNs`~zNaQ{M$y$$Z;37^xuA(R=rP2FgwI;6i(!rz1dh zKGKpmy%a2(xz@!HOv4@HSJ1KJ7BILeSw7+Bk(PJZ<74rxb@Q1#I$1XzfT)A`WJNv?IpP%&J^IDKqnXzSqf?O^nTdh)^BV$e>_m5rt8H>5~D;gD+_dvzKFn9 z^tVoKztE5tV2{#okKi*+it*}?x+zN)hc~G4ksATuNCw}pO3d9#rDuXOU zj^M9A31G75MqQ)lwZ^(`oIO$19%VZ#jJ}Z=lsW|06qLR60OGI26VuPG#bXNGFs{*Oy#(r>tHbE-djxB!6eL@^*Bp$?<@ol@|F`koeI)Xew)(arJ zDH3b7LTn|Tg(fFj2Y~7L{z?8o5MjQ$z^a-9N9Q2$wC?2$QHtAF2cC^T_5Rn3k?YZ< z^65S|PDE>is=^G`D^j^Y{}OB~u0i3v;6-e*axs*=&??HKlM-bHLBr(?buTanI}Axl zQ$GlaCy@#g@H^!)6CJ%y$pW8!2YZ^y*AFzP2ZN)JW{!_maB_FBa2vEVrHKi_AO*jA z6Me|`$?nRqPTXs+`OBR+%x29Kza-61%kOQ>f%rJ8k&Jx)#89#-L>e}c=lkrpa=#ZC zg|oS^&A1eK<<^Um5qF#i6J^c{{tTG9Jhs}ZF7x{Y&MKJJJbS!S=`~!-FfT7LtCL6P z$o(J{tBeh{AWeadz=dY?Oa-!Eoc4;mhRcu>&CCgj;6-f1(=^Q-ul%e#7%Llbb8)Sr zts6Lzo3eWL?yr?G!|j-N`2kE)P9g!@Uv+O@iWJy<>!W?QGSQ?1MlgI#!xc~apr%|` zJlR)LC}2jqO(}&bL|TE-4r;q4!Ers#QZk)t4}3mT6vgiMFJB^NmJHH?dw()xDFg9{ zvE)?kwUUVgHDfgc={DtH>nSYlr##I=cbPlr7`EtX5 zAJ=AFX~xgkInV1~e>$`Z`Y6>v+;a@!4mx(|1{2_H6nu(K^Q||$5C?OmdDX`IL#(Zt z9WZn2?naM6;=CIO2~;P>!R$R~9M`1DNLr2Lm)-YsSmm0fQVlYzjW5htRbm3b{JoJz zIo{MeQ{X1k-AEkf+7;V$I2=H1ia7&e2;tg627U1DQ|s-@v;7ilA>FfGMIW{ym=2a2YR#?@_?Ol_ro@AS9h`Uc-ky@iEWV zklpD1!tlsQ7-Akv!wIJ>GcZnFf^JuK1n#5;XYqZF{pd8|`WP@3t;I2t(vi_UX*GS> zBS~1<`YqIloFz(;9kp!E;R=vsqRmbxR4S^2)9f?h@Nc3+1-`FT|9b9J8Ew1*?J_Hy zr!Sn0$(7%HR|Y}{py z!6u_!gZRUC0JDyMZwsYR5--O{_r7I$GWP>T$nzYh+cWPCYI`G!EJp|xn|ev|w<@6T z#R8DlKwhtA`5~Ha;l%X*QPz&kVORj^@~8~e#t|e7Wb=ikzAtAb%eb()4RL2>EO*@KYSv%Hjpx z+r=d6J?P4IK0fuHw)o62m(#APJ0)1JjFRvkOu-5Wa94L^8fZbUW)mJ$O#)3XVn?Dm zL8;JaU_1$|cBS_t73$U~GkK1M1*qG9C)X#aGVa z1yNYgfl=s(myK+oKmj(amIC=zJyGgAHXE29^w+KwF&?J-JO-3{MyS6sK(rTIs=xAuk};_2#iG;wUp}BgslXwt8>?(9)T`k;6eTxYZ~$_~ z{HEnsrbLyb8c5b*a!XYfUmpOusdYf0F>Dt^ zbBN+i9;Svf^wP5GBx~1x2j4wlrs#R|NGw}XdK;<#!SRxz`v*>Np-3l;Q_B|)qYaF4DJ@5CJmUh5zbEQT#9NyK)-|{sX=NlOvLpz)Brc-U| z;I?J#QMKZ6b4RdlF#tr;haZsuuXKKdb8l~mrcg{26+=dL`48{aD8_!2lru-nWXpXS z?Ow03WEVVcMdlD*78IJ4EWJhuKA=>}-2mTP_0B^UYVLvI?#)RKC}YNIxl_ zf(c+wM#P>27z4M*rZdx14z!s2GT^4INF!iE*Dv*G5m$d#q(RqoT9Vqp&L)eh-;M#>%7b5imm2iq9K|JA zmT@F|$g)9?`20bK&u9EK-}hs#HHclvkHv7N1ikzC68h_m$?|t`Dc%IZk-BMBf>;UC zPy+~yOKg>xR)-GYBO|!5ebR&|PLln^T=y8UKp+{xhZ~1ec;09F!Z;iC5dARtVc5f{ zexh<}f1!296|T7wjg>yWburfAx$EjH98&n9*I>DARvaU{W= zgAJ>p=L@1X(Cbh6a4ukdi09jt3n&NcX_C`*pRY?2NNX{4yt>arzm?NICs7(f_VBHPInj~mbFQ*-$a{MhA|g;2&lK~JL!xKJZ4)puDMd2oepCXXsUvY&YV-j~Rv ztwPR`rk6EIYK7m*Out$!JiS%Y7U5=_rb$dVY3(#PlA-0P5nCx{5+q70ZW2~ubi0#_^*8&{DjC7S!dvO)60i~ga9w5 z8rX2vprBNZ1Upzy@S3Rxe>uS~-jiZ;@#X7Uz6)*IgM@*1-!cVz{nTo4mY*hUw6(RO zJ|k9B_X)p7~pgUwL`$P5S}I4&B}uNhh-%6;%!$T%SZVTD@WK ztA4n-ZsSbAt9yyi8NQHYHP^4yq|LxUTwg~-?JDXrU(P&KKq=_LUU)+CK4X6ph?t4w zIIMXIGp$!#8Pg zSp8#`H9+7pJR5Q&tbY-KTg#Ok*#@sw@Wzx=&zDPf4N(z0GMofqx?SrTA)e)AORee_e@<&UEbEhEcCmA8A0z)TH zHakha3qOeA-rntS*Zf{2@0p8H6b?Bb#F~Mq2;TioAT=l86Di1MBNW|7AAkLiAMgcv zs!t;}gTLWQ3Jf`;SevActU@rl2AGS#KM{h){qws1%IwcTGlmBBzb`8@)F#P_+k9ga zW9gKh1R;e`EjPZf>?2~UTAKRn72UiYE8D3(Id!1`Qi)dp4z4`nPS9#`bfnLZw4;6KTs*CGip*184A{TK)TIG)~8A0!lV&C{_`AW{j6}ay<2*q(gR_c zZ|N1gS=37iK%*9skpw|fCune`U+&;E}1xkhVAzO%A>F$#OCcO!T)(1J@S#3}jQa#{w zwt9!*8A8{0TDe}ckZ2%eYC}KTkrYN@4$wJ0U*NrHcRu9oB+(}yX+b49yN;tF9F0Vu z%lt}gK`@fd8@h-bew=`iJ=&GF{Hm^Q^!f%ttq!=3Hr(2IB!5iE6DdgV16{9XVgC&4 zpYyQ}Id;Yt^V@Y_zj^%1S-L(qJvDrWGv(CdBXymy07c%#+y&c<^Itw|cmd>bB8R~< zHBMLY*!ow!fZNs+K5RKitezv!U#+2Xc*Lq!})l%S*7OfAh!nd6M;!0w|!M7NOhiZwnsmUBv+eUQgP^AuDRV zHgL=N@hpdwMUYokC(hFE2^TB%o`S2L&5L8~^Wg;B;#ZbRfGz9)v7SbuDI;*b^=+=( zeaqN%e*`i9z3cg4A)aeg3sinZMVyEkG#av_Kg#S02Dp~@1~Nr_%@1aOG8&u1-|`gb zNL`0W{kAHJwPVavKw;?JwrjTN{04a}B1U+atIR@&`>rYydF+u^Gw6%wdPK z2HyR1q=3KWYep8)^D6c^c{DV*If{cWEM51PuK9WjnFP=0E`X!gzs-((=ne&mL~DZO z2c>4N$G!ARBi?}Qi_R_>Z@CJ3W(V*1W5wHZ4;k0xmW_Qo_LAv8)&!lcM7hId&n%V2 z{sWHOBA#4^p^|Lhft_`4bJoLNeO5D%`p-+HfXG+Fk%`ZF`$+Vt{ys<)e5U{SY)B#6 z{t4L|J1N4GzaLdW^n*M9abeIhYcI9gkdbY7Wt-YhmSXkm;03jCmJUQL8Ulx(#MxZ0 zT+ugD6*F;S#+)R|%m^&=YeM|XwIfrYXQjTvcXqHk8l4jU_bo_+VlMFK7XqMyDKH&s z7cP?J0fUEY)by&Wn>72Ft>}ge%Zj{&q2YfE&A8!{D=kbmUuS4C!#bXcMbMDIDfId$ zIXT3nXnSotA>F$pW-7TlhANqN5C44yQu0+%|J+V~@xED=VaeU4{m8F+Uz&jZFOT;t zN?L0q z11yMU30PUuzY-vde?GVo;(UFN#qktNB1ed;)aTQ?cL~# z6u`cYY{zHGd;TTVePJ`vk?EpV{)B_)S~rug5c$b)@b)n9eLKCwc1vh|DzC{l{la@St- zbDh@*t%f7`fze0j2N5>faH`72a`|%gYLWknz$l5~E!+(tgq5`-CeOQMqv8?XK70M5H1~LCG(hh z;P^k5^w7UD!y3Gu4`D&)LJDx z=gQ*)f6L^L<|34lk$21>r2VNklJ)^&`4NGM8lh)1f7~*JW?r zj!ClQg7fU-kQ z%*(TfrDOg$6#!#(PHof8IvpKNBcaaoKx!ZfIP>=%)HWAlE!LZeLohg$+j~QRFIZlxCSM3=4n!aK zI8-gJL?c4bAj2;*%S7IvF_*$(;)F)&AzKo%)C1C$W=jna>yUz>x1@+gSVKR<*6BCX zk1LAoXQ?TLE7?iyzZ{8!SnYKqQYc$BPm)?VP=n(+J9qEi&tt;$o?wNB5CMCV9@P1B znv9n{^&dlN;@jp+ja4KBUsFJQS3^2aBFJZ94^1h;VtDTis&;N0J)jNX?+9Hz)oqWp zKU$n_{38%OMd)>D5c`Ia%P_vKYoh!2Rk&LeN-@XkY&)S8Q$jTsfo2!KafqOX^zb15 zggNCuim`|+%G!{7P^?7RN~Ld(m~lZu8G`aQ(|&Kb^n-s(Uj^cV`Hd{YEAUumzHNZ^ zXHPH7jd#CJ7e!!FFGn)^Kc)f|w=bd1sYy@eK3a(HKjs&FwIGk}bB^B)>RQ9lKm zC+d2%&LJtqy`dyY$xr*=5x<;RMK4RDp!zR;X?dHa(H>)La?YuN*w91203bGFHcGNT zPBkVJRy8t2qaR3B>Qu#*Kh< z(7{XEU0bL+Al#zbHGsTcZ(MFfsK9V@Cyp#w1>fASnvnp5Jbv~fSgXzf8dc+)OiKQb z*QVM1o_raJ5-1=lW`;v{0m&l0Ybub>2ybCoUb4FGNU9&{!G%D^tjJ}^K8eN=_$8cu%8(}zZ>Z<(e2f?&2e zP=~AAp`U-1$oSxrVhx@L*sXtss)1V0C?Tls?j_z@E)6*&e!=)7k8>#H%@;1MC3MiH^D zg39mX3b=Ck3J*Q--EXeOulMDWcY-U02xwD@#GEV;r~k*KID>k>x5y(s`MuB^Q{W~2 zjsYn5RI_=S`p#*!BZ@O4wtOkx6Z>EL!YGbT`hywnBJ3KqE?IS$Wl=~FX-xKF|MAc$ znEH8h2Sx*0zoqzmVc3x-_#YEpxEVJO+^_rGS@rf=8Ii8@k3zX`&wJ~W4anX>_v1(; z^=dsO-t_ZT9RAM)_Gb9lY`|v8>cEdlO=B)1Uc0}Je%%Pt2ypRAMfffh3IjomRY)bE z{+^gfKeh3R4p4u(hj4l#c~nh*ppt=MR?E;%K0qx@Df?GGX&`);oZ4mJ-BMNZbwy&e z4&XNZ4moXe6`*)HJVx(x(aYgsFg1%}WpF$W@<*6rW zRx6r@mLA--8u6qZDi^4Gb20rCl+%5M$_3Vsz%0nrZhNouDYnzm^2jHJW0A zL{(eKr;ajqZveEos9QZygSrWhQaXA;o)o`h9<25Jyir z<8ee@Giv|Cw{7jVlaA$_$&8|F~z& zrOtcc??du;NEu4ZCU0@*2{63SqLXc`GE<$)Yv6<*0Ha=(6tShUerwh)n|1cdCqrP= zm*(Gs=0b=&z`D@}C=e^}DNtP`w3b0zkUGugQNgJPFUrovri~Vj+%q0F>n}OkQ~Q7A zePvXY?e`{%gbD&8NC<+UNJt4Hc#sxQIwb_81nKS)M5UDy1SF(HPyy)kmMB|Hd+ zDJ^h7H27e}4T4VW8JS zajl66JCWv4vGJ|#!A0>5FwhbRXeRFQ+S{rhThRBq7U6z#O@6*uL2Jn@07N?OzW5CvHR%I z;Q#FFIRI$q4Ng__#>PVlFJQhMTk5!l4Heb@G6jg`MGL#5EK1iR3KM^P94w|vplSN0 zSHJ%L_HSa00aPxA)7m*+Yf+@q{?zXKcXy|8vF=CMt;i+a+%=a<|LIkdN5pSG-O0mm zh5QFC_pTx3aoeYI7z_)y^)S(bITMG(q`W-F0U#qsX!y&tjopT}v}Nc9y#GWHF%t(E z0tPv@2zsse`8crM59nM73O_V$}=>Ki+!Ku zPw(%Q!F!p25Yb85GK=2kcnCGK6h(H3)AY zCc)8)?+){}KHltaC!;dQzn>(}kVZNIhABe}^$3R}wx7FyC;sBapCTdmFlJSmQ~R=$ z4A$J|eB|e31@7&a+Rbzlfs2v|Sewg=;3|aa)j;RFstV$3XxtX1L-&XKL6(HKppqAX z`mnLd#>%su6~}-Iz1UnSTv{Jb@$+s< z*l|!R`~^S7uCCjb9}(Ya#{iPZ4ArbA(44gNU&*{CkuqW)p)PWbj(_M*uegFqS&!{d zQ%xVw;Cxf7V?G~{A^>3t&VUNV+5Fr?IuXHE3u>uhG2Cycy=&rvxDY&s*U|%yQ|K-~ zCN^FO?QqH+*5i9E(@5%1_(dgvvfjmEoh1E2*fyWrAuJI5@e|B8O~~{`^5x9Ksfk z?N!O&a(oRD9ARAAdazju4O>MB_~|JNwE`*7kH2sn{SX8#ZgZrnmD(N3CD7bPzrJrT z3#dKeatvAk8~TLk{{x>m#|ej1P!WF2_6u|j4ZSx<8CsxQPLwV>>6*+7ff0?LZk zhwud+=oy;XIIyoZu5qol-Yfo#2a51TK*hk5WQ z*TAt_5fBSzWf*gorO#t_pw2>e16EG75}|$}$E_-Q4uE51kbv)f`e+hhj!CAoFM#u0 zuh`+QN{GLHvH~1_PoWWIqud9F_6y=VUqH!+#3Q~7Ag&xQTmjpiS#ELzIuJB{pDW!M zQuIH591mE%5=W=OQ^mtVTR(Wcpy*HAT51%6`#nJ%TC@!qjjnvwYv6_&>`OyEy} zJ_;Q0#4nAFE15Kv6(ZSK$QYK%Rvc6I+@I2M>F1C+O0>yiwnA?34fP8<_QeoWTK3;n zG*gmuQ4pq?jj75z94ko5ne(u-kK&-!-|cIBaql)+4-UG9Lo$)KrLrGw1uReP*W^3N z?wcbxTYXT7gvOo$N{$V)9vqh|X!M}vi0~400byDgZUN8>8hx^WvK$t)a^9cJK=|*X zO|pn?*w2Pg@#qZG_8x`!4dF$C?lF$_E@$g~S8>ZPp!Y-K6(NUivm8*@^|4?IP@JMr zj@Z-_@Y-Ov7}9YUcz9IZC@GQF8-Wzu+5E!q%VYt4A-v2W9i8vc;?VEOQmGy>c-{8t&${y_R)dH=p zo8+Jh65nWK?kWuCG!b-ya9?wFBWrP!Ybk^u7~jv6xq>yD$1CvrNt194#awvP?k>~2~WcB1$)Vr?dQ zg+;Hy?S#B14&=PuwG?`5SuebPgtXb&3?r?lC~&&WKcTCav~w@4t6vI^dpz-v3hyfy z-x-85$g(cl_~1)sdLU|v6Hd8M7(evc3}+D9Q|HccK9pw|(ZT*kxtXlyNW%hLE#_AJ zfL9MTI}cd%)3*Ph-&@^W(TQ?QB1VPP21CsZqPVDMP4}TlZKoSx9Y$pjQjQYb{iypm zv&404Sr>#qkJ|}a!ZDx=j&8vIO(8O0ROg6ttDKS#qIrJhmqMFz{b>1GyA(pC6E23F zi@V?|Sbs8Hb9e{I;3;TrhHw||I(2V+fEt8jWC2p*^)v{r4Le3oO!R!@W?hb+FEUAzqPY~3?C{7 zeh5-00z@&pL*1h>Gl=~;KJrMM$%{8lrW^d$?ARttd&UC4ljIkkqK2*s?QrM(*jH0| z-?KsYrT|WHWTt{}y5)zzDKa15HELhvseLQwV`-Qqg{SH_Vsbf?koZxl|R8@cclwga(H(7)$6 z+6t*$y`gUB`_l^I1-QV~_RFL@!fzE&fw{dpO@~?Ya{sJB?Vk@e8CC=StU}xoww?7G zM{l(uER8TbxYDPd>EWZkyKTe{*c_wPZM_G!$iIB715(H-iZ^GIwYhMT?Yi)-kiY*P zE`M=qC($?>64>QB`vrTS0GfJS)LpS0$gLn2vB6sqKkERU;RNwS>^O+e`y)By&>x6u zr=^qVyKeV(Q(&u{!Ce(hZ9i+`DoA8A%%%;GIfMjUpAD{*Q~GE@M3 zFCQkoA|4c?rv_GxAJ3vryj{n^ZtKAp>9S668h|X(XU$=MDcIwiRzGxMFUiDm`{Y(k zX9WEhXY3BGD8Ch;M)Eo4mHgK*2|;&nIsM zTgQkZ^B}PWEZ$fzD2g<08N({#9L>Ft30fzX5e7gVAF6QPErt_EWpl9ckGg62BYwkx z{;@bk8Qx?^i(eH-2wjH+G`2W@ljSz_q!nrPZ;arTp+}~D0?5}-w-Ga_4IcsJ8tyeY zW3#i&L;$Z1hh9;Vg0eA6UJsyX*m~5ukkg<>z+uihMhZPDB@Q@g3^H#kfSz<$yb}Q4 zTH}QlRmbY;MUj;fg4%|o&ZBQmpmZ9-7&#Yn&8<&Zm>{31(V>Nk@$#Z8lte7&kqWmm zGpq2)ARdq336L!q57PIrg?~en+%apm>y@e#e3teOm=EEJj>l25s3o702E`&J#-#&l zmnJI((8DnZisNH0k&`*s0;p{IC787WOgxEZX0AWIrb?a~qOQb|M3cwYZG22xlRF)T^s)V_LQMm>`PkX#k) zxE%MMfY3;Y0i_8-%J)({LlBXD$s$tX8Jt%B!8U#D2eK(?@t4_udr0tx&Eu$ekAr>A zMa|BS#P6385%D@)vdu#~wWPs2bgDFvl8OQ9#NoYAU$iPO$M7e} zZiW5zDe$b0GlqxK?H8@xud6?N>_zlrC84eAgFK^lVXblUp$*NOAJMb9g&tI>%s^Zvj8E$MUhtTjuc)Q6e2%Nx6#5Lq~XZ;R6h=^C^hUjXW)`v zc z%)&d3$?Hcza$qQjpQl&d;Z$&1US1XjcBFCTIA8*?IeHbD3(K$5IC7C3%W24Ef#2rn zil5ckIR;wvHX&6($gx=*^*!4PRVD_*>36eYHo=q#B7TmD!3hAqBJy*wB+YRN$+sfh zM4YRuMs}Ah%#ay~0ogT3!P03hr4r{M*A?mKp`mmltw>PjIyribAATLMYJU=eI52b? zD-?kkX{j5N;JSNt0e*2T{2+dvg{ZS9#8w^~HN_-iM+qNlD~& zQGm5euDfJMC&#w@a|>EVibAf?V2ZqNQgc2{2EIW8APXZn?_`y=1blJ8?pNoa)Y|ic z^7?B;@|ZNSeiu}u*;5x2$0{$DA&k2jsJoAy5Wg=p6qz{Zp%}IwXDf<>_Gdyd4nt0y zt4i-R)m%%Qa>kA&!*|l8AVUtjXIt%`H3{Z7G19ypkDs9@lew&KDQ%1%{Uuv~!+?UB z_BktXm;bfv^0PGMj0imbObQ;NgB+8{G10?s9j?-}@7ech)GW8}g6D1%+P)A6l9COy z!}4KShi3-=g4UNXq;d`EUB|hadt;AFYZ{vWByvK1Ic$X0$qQJ|B>~3nA_`n%`cuXo zs}7^D>drgF6%H-YfI~)GLjd0$ zuj|ork&XePpoy+HJ}9aq4I;nd{;jm%O7}3Z20kgc0ZT)_BPGt%!#qzt6IW>%-viMR z+)4d-H@ldXPv>j#ik3hoXzUpBuFJnR+gn23*)ma=APXP}9j-4l| zZ-1+jt>5*?hmMLIE}NE1RbP`wQ{8sXok!mZWDPF!PFAY~9)$15JrV}5*+dlV6#c9T zjMQY=c9_4Q&ciFFl%d&6Zp4!Cc^j($=xGumFP;eA@$x4itmRx39mIU`*iY$Ltxe;V z{4b$S5Nup#`~cuEKPX!5i&@aa7aU(Eb`QR);osZCj)5H5Py2Z`sg5iAI5os8kfVOU zPO#FTRVvm)dZkwK)6XycGC}O27usy6+JrQ%YDshf{-FzH{?@M&5oV8b0`8jbw7&MU z-)Ms;tGrhwPMjbyhqr^X)<94J`C1nzPDCBUaK8gyfxkl%Tgo#(P0zF_qee}0F- z=D<6kqquySuWBziDwRAx&u8EAia|yCB@GhXDcBw;OH}=NEzx*yJ5kg9{OtG)p3i;} zWZ@DYw8rT=x*woyCZs-^5Avalik9lm`@`j5kH^*ReqO-|YHh$b;cbM%_YfluSiMjS>?L$ z-C}ILs7UrPBxJQ(!+2S#far{@ke7Jv=I}5-k>_GF(s(fuLS9)%(i94FELx|V5nGi*Y2mv((>~7=<{8+M;k+qNiVI(NtP3WasT`aRQ|~QX82$iP?&!@TLdFv7;s!V#8dQErcJ2cp;D>8U8gTr&q^U z=u@R%T~f)?WVD|+C;{AkUslr@<(Eqc&7SkCr0mTSBO?@))`=Jy3zVm?DIO28TgzI#D}Z#+Uf;#RhJ z1;MN?b7ql0ViCfPJw+WTknHYOxY@Ou;m&q#h)=mBTQ_+VA-|@g6q6t6@rZL%ky&(m zFvzzw3;nL1JZK3!`GdA1VA|t(L4gTRd!_-i)17qeteUW=8yD+^1Nt*BmINj9Stb{$ zzSqk3hyg~QlG0qT9hXe^7a)ps{>wg#hIfE!K!pvU>TFip(p+c+UO8hodt1;!2yftn zFOAewcQ}|ez;-h}_8bZ7f4^j!U&A8w1y@9QrCjYk!)Bw)dYJ0t0Cpb>n}-aN@=EM$ zEBkxab1(CIvPgEIUy+9SE7}N%(C#cK>bpO?fk*M+!D zx}|LA2h=oOmuhu^7-Ctn6Crs;ko?wP*(U0da7O|GrxwRiS-`mgQ)DN91@0MlT)(=z zPg2t`gDJaDVNv0`FCYPS_oS-5d59wTA^m*^j@(oxt3uJbR^NDk?*_+2;=b;Fn*8#$ z-bQAXr|B~{r9Zjoi*WVM(h>k?Gr z1F&qChN`V`yMh*Nk<+5i$Ib1Jx+d-qoOGr+x5%c0TTx?EZH!D!Rd4Q@@>@FgU*ml; z|JY@Ly*Zg^FQ6 zbEJzeM5b9MtA`mcYI;?M#xpOMrwvP99Jq}JuI(6Ozf1%y5g1N~sxcJbi-E-a{uK1F z8UW9{Rp z)9_>0ODAqqZ$JN=?0IL2-48DLdW-RCqlA~Hb;X8)c`PDi1?Nj$p2wsWPfR@1t(&033(} z;m5Vy0dWu+>bzt-`F($#XX_0Erqm*YY{B7uB>b3CXi48?Sn_e78bvH)AKAvt!QvBv zFIdHv8U!8!)Msn=+)y{)+7Hz~dd_|B{`mAQ=tFfal`H(n@ss`|_7o~owMm(UTLP0y zH|9h7t#wvz4yaeR)C?rVRb|qkPVMz}KN;5_P*r)ic`z4?-$W%=`q?5xmsuwx?80jW zTpgk}$!!W*#|rKy6G|9MF_+SKJvw8QFx++WDfZDbgoVrZDG zn($M>+_q(TVnqq{0HVs5rEZ1}vWAfN<6T&dS&`9pFaPm6uXts4J}TB`eY&Miz-2W0 zyx|7ec232KLWP`t5ARIIuyfxSHxYkTS;)!qWVYBM?_fSD=bm_AL*AefC!fLH)z4yj zT3mP6Dgx)W0J7Z#buNYxv27a4&1Idr51VhP=Y6C?fQp7R@5{8;ZxY2f_y<2i;=DD&uMn~|o2mNI@l-@^HvR2goI z?p3GNOQI(~OA9QQH&GG=-gv&$_AT?c-e-Nijc->ebNbDDrvlfKtS)oX^y5)I?0#(h zGxxjin}AWRqK$mkH@${QM~1A%xQ*S;mfmZBNlI>(xsu4L-kjC>jWly3(aO}had|2! z*brK-y_>$^jNnTHD2ii!rA~Sh{6i42t2@*No=C(>e)Vhw8WZxQH;h~AuOvEjx#y=Q zURktc70obRy*dXi$?Ii?HU)c>-0TXC>zy+1y%9UNCa83qV7%MveDvI{E%y195Ngx= zZYuewwki4IhGva@L#!#%DV0)06=N9y3Oye_l$iGO!Tdns#Eq2nR?f+Qgfa8GE^a_2(gT8p^(FFcw%YtPYraC>V~JXLw>Yn~BHpZzX`L#0o>3s+|&G|)5&=mpkR z3&U39iNGRd{Zp%xFFEv@uHhRglZW#UdtwI2IG5sgEZGh~H$;8t5})SOm|#WENurn{ zRSyRF^_&Pl?CJ)cFJyr8OrO|Z!KZc=Hxp*eN{&>`h^XW$HC=3ia9N$z7X_+rdn-XH)BS$bi`Cf3K z`iA|%!Du^Uo6mz4eLYZE@p|}1_2AH%bfdDB*aaO&8e=ZfX}F-xYawVeaKS9e>Uwm$ zEx@lH*i1ClmmTaayb`L$7*6FvMPE*+rPYfIjcwa~vM+zOfojzJB=X49F^mIq($D>F z<#@8ALYSOI(n(zA$W?<1(_evbs;)bZG^=)%%jB(q(dTfs*66BhsOXVoa(zFOtisQ{ zCo1ZK^^0=fxseP!n)AFQLnoY4CC)_gc~P*zD7x50?o71$(0Q&agWV=G#VMnWi)`w- zY05H+V~w1*UspIBy*dB)Ri&07x$-jJWOFsOlqp#_oeF9#wqzuDp5suR;++O zGm88PMY*DR$HTU{JhlEsBkl=4s(*R@8TkgB9kUi7JZ@S~S4_>zGb>%|HjH+nx=n7i zx7PDY?sSVnsuIb#{bTG~kse@C#g>kT`H6Js|CHl95w6I0D!E>+C=fimsE@ZeAKG13 z7IO+h(tlDrB-@HP4PkvE(>KCyc`q>F9s6^jKc3PB8Icn-zhvs4fHE364&jEADo;*l zMkuy6E*pgPliZlgRn!;{Q*9iF4td;8pYy)y`roX(e2{6pR=!!wrkO6Z)oMBi!9inT zy|CG_deNmlA1{n*MbAqN`b@C7m5YK8k)qnSWKXtMCiB<##?Bvn0VGp_Z-t?Ndv*h`qj8Fh;lf?of)Q9y>d zA!}dN?Oo>j^Zr$69^yytIGr5Hv~wq`bX`4^`(8r+bps~@6SjQjF!|Em9s7PWRSt4{ zUYEJoK)CbZ_0GT~Q%{1_GGuvd)_-ar@D&3tSUSSV(1bDd&fqAJCS^Im^n4Z=cmzW=^0UL6qItYeZ=0 zgg^5_y}?27sIt=R_xCPhWjQ$JWTp%F$`NhV_>cXCRbJ-$SqKnLJ1G$oUU@?WQJ+J!2qZx^XDgG zxljU-%h9+b*74(DJ-@tAjOj~!AT@uH-N4mWRF0kI>ybieVN^PaMTRRu?U!;o;aF^$ zgqX;(oW$c#COw8gmSym>QUKrxeb>^hZsf@?+PU@GeqFr$T}90hS`}aQe#aeO$jDWe z+G&I*A3M@M%R)}?sYK&s1ZI<5{&P@x^aPKU3c%@RCh*97mc;noPCi0WVzMPMd-}}? z?`oR}^JF(!oZPq9ckWr*`HCMLS&AjriI+&Z)7Yn4dF9(FqfxF4Xvw>eNASk8FV0=g zj82{+s>rhOp}i^<%nvfCvo`blK3RK7!8bYUtnpg`>N6LEucPyaUJW@q-<0vPZ8qzx zL;*}-7LY%goPagve!sm#ss2H(0BO!UZ`Fd${^3u(a^#ahe~FRYGJ*S|w@v^hPgY_F z3)^{k@p{D!&?dg(;Mog2-Lr@SL#jIp1DpDfO~(v0Jk7Kxyw%pD&drJSFgO9Jqha<< zV4YNAz?a>Ml?TI-dWe&Cn>tG^=d%z03SKp4`rphf9Y>w`!s(P(OayKh+fC1Yngjez zes1)5a<3LE3<1b8(=``>h-ZRy+S~AXIOa>)>mxqhRvf4hzCRQLHbt?)MVQ&#PaXqll|Mb>p3`KJ-&rn+1ZBsjFn_DYZ9Ku`dX$G+hnn zL_Ek|01JF)N)>8ps8)V7;??=b2C;`U#qk%feNgSh=Q9_cQ#pyzSLXz-JF`I_HwxO$ zy*pQ@b*D~iQoNjm!bCr^OGHSqJD6|ga*j-hg6KC3`!gVpuk5aYlo0%(>%D~MtZSYU zWj-k#@-gDwBAdng>1W|OMj5_7kTqgu+8gtch8V{cZ3&SbTc3R=;P02Ak;YpLpa~POd^&;&`7sBE@&ZZklP@ia1 zTjd`uKIV<};t46(7Iu_d=tGTxkwzEAdc{N=D_`eE{IiayXwSx%?kZF>%%)4 zj{pOs^ABB(ff0Y|4@t+su>0fOz^gF-zJeGSf>eKqBnHMz#ed82Imt7va>MJAOcL(J zsZ}v1iWg6g!n5cJ#1x;&J(AIWlSu1y8HOjc+y<6sf;k+Y%RDjigL^|fyp=E|NIS7oRfp5+r1axF^!NtEm zan4BkjJjXtV+viLdI4#B9Q`nGaX3&%e;H~vo$}&QpOFA;$4lhn!~knN1UJ=OjyAP) z9MSd>+5$vs2RTr9>ev_b=S_NybL4-=noE3M zH1cK}c%w0Q42-^iNDS;BTqM#Lg^%;OJ@@ndk$@PY^&%(1Zbe&l_o=A=e~}(IKZC;n z3aY0D%%k*)(kK=A0rj;w1J0XN#sHv}=v zfxa@{DDRZRm;G^iio8r50f87k2ju2^e&dai`9b-G(f{$Q2s_>^!iP~_1suD)y{hHg zU8c|?qc>5^pJ_)SrM{DE4pRc_j}tvu!Hq&ZQH?j+X=Z&N3)}P`Mz0@vbMz1GYTjmC zom2SrKYg?UaTa|jQ7=92Vk%oN>%JV?y$|Kh-%T~3JklO?ts&xhm8~YTk$W#STex2> z1{7EhPy+LcFCsk?48VL;y9O@IEcAD1g+OFz;CX=|+e~Bp3_wSH-8JuQ*MB&(e&z0m z_%$_iFtSy)F6H70rWcO*Qf9+d+j`|FA=bYPIEis!+}5t=`cojy{M`eH z%j1&Ya3S|ZjJSXMLd6HCZu_31>g)4>-ptPUew0irV6dqM(d|RL$WZ*8m8eGDWf6;z z)t!;Awu5#%5O_~$CXhag)W7+JR1^WOHdE5?+=aL3 zj$colF8aCd21%mcmKzHLeLMiOx|!2$3iQy)4QqM*_}Fcy{zd*yfg+uSss~eM_wA0? zMNcxgaW3YK$j^d#(QDzcqoJ6^k|AJliHo$3HN%SUn)dGKS&g0bK?m)Qd*jsN31NhJ z?IE<*#zpffZosD`IZm;rHd6q7y41(>fFsTn`kI;(c^ZN9>-=&{m^?fDMYAc#4{ZtDT6%22w>u zX0;6 z<7&m5Mh%=Z!ZwTt)rsF+7D{){Vd^MK{#ygw7PDt@s@dC&UU=j*Cr;kTR(r6!Qt#x2 zOfz~?HmzAwj;Rd7C>#IGjhd&7L(s^`otsm--j|V`7r5u-3S*|@x>(AR^vQSj zNd5X<`oIk}XzrPOYzpsB8LgZ!b?m%&DM?SeTv%X}LN#gS=ecO#n~Pe;a z6~BDPRc)SGd+YYkgdDJcbMt;C9Bae>!*a6qZthx)vHdEUIp>^{n0soz&~%mXa-&1z z;sA8|%>3EKo&Ix%r+iD=q#v>jC07eD_>TOku|5=1P&eB$<(q_LyHHa~tK|oGT&bqJ zn$#87nB8;F;oddymAlKi6*FW`en|f({qQPdbd>;=}^<|3Y(V+n1Ymg7U)UGLqy__R9gc;;3T zR*EO_+*CZJvHor_3i4P6R!`_UB5e4kU*AZ&1w3vjwy<5b7n^1n$X>tQF5NSj+xmpx zQEe$*){RL6Fq#^8@k#W0j;rh|k6+nl#0nCp%KMzQ-M&DAUoVnCY1^@QnaZzUCEVog zK)HVXi-wzLjSWZ}mgo6Uw`tybRj4cX?&JAEzZ89(bC!bkz+4+$S)7OM8n$ zQY`1IM}}VEh7X|C0N1VK@%{82$p&*7!=$D-5C$MZ2jtm(acJCDQ@Z2)+`E)j3&k*s z{H3cd>EWUSykOcz0C@Xd_?r;K(-^o8mGtbW^A3TBA9s3w(jITPTk33= zXFu21kSAAT#AUGmsYmN)PAOyC4_6Iq+~pLr$Z>r2r=xdtvVp zW4IC$u-hSC68|j2f{T}9E$~uSS$mDKXcNCMvFdm4dxzg4>WFWz>EelwdAWTH{*1&} zCa4phWh_)?RW5S1F$X?1ZbQFFM#?P-c@jt&`h^=hJ%_GL4VMv@6+E3G1s)Gix*qtn zP#k{Z^1(!cx{t8*>t)1|@Iv1HFQbNsgrIV1os;Fm0(;R{I!uZB_gK$xY~V_3x)Ipp z-&4n5y-nBfdEh+E_5aXl*-^+lbSb!^3$|~C68~P{ngKePApbXRvN&w#WFiiH3K&yT z{j+pf44AhJo$fpl{DgWIqI{U8U!T6jMlbb`nob_%NyMo{zo+4w>ZN5YD&>t{YHMAX zEIbLE@0Tt@WEeiBv&WBb#FbRMWqgkN*c&eB$5;?NM!u;yI564EQH0CL@|9tn=u9 E0B}tl&Hw-a literal 0 HcmV?d00001 diff --git a/model/transform/readme.md b/model/transform/readme.md new file mode 100644 index 00000000..0c5a7c58 --- /dev/null +++ b/model/transform/readme.md @@ -0,0 +1,33 @@ +# Topology transformations + +Beware cycles! + +![img.png](img.png) + +- Before: where execution happened before entering into this tag +- Begin: where execution is expected to be in the beginning of this tag + +Usually tags handle their beforePaths: inject pathway from location Before to location Begin, to ensure that execution can get there. + +- End: where execution is expected to end when all the children of the tag are executed +- After: where execution flow should go after this tag is handled + +Usually Before == previous End or parent's Begin, and After == next Begin or parent's After. + +- Finally: either After, if tag takes care of getting from its internal scope to the outer scope, or End if it doesn't + +Usually tags do not care about the afterPath and inject nothing. But in some cases this path is super necessary, e.g. returning from the Par branch must be done within that Par branch, as doing it later is too late. + +| Tag | Before | Begin | End | After | Finally | Force Exit | +|------------|-------------------------|--------------------|--------------------|-----------------------------|-----------------------------------|------------------| +| Default | parent.begin OR _path_ | _path_ | **<-** begin | **<-** ends | force ? **<-** after: **<-** ends | _false_ | +| seq | - | - | lastChild.finally | - | - | - | +| seq/* | prev.finally OR default | - | - | next.begin OR parent.after | - | - | +| xor/*:0 | - | - | - | parent.after | - | hasExecLater | +| xor/*:1 | prev.ends | - | - | parent.after | - | hasExecLater | +| xor | - | - | lastChild.finally | - | - | - | +| par/* | - | - | **<-** before | parent.after | - | exportsUsedLater | +| for | - | fc.begins(until i) | - | - | - | - | +| noExec | - | - | **<-** begin | - | - | - | + + diff --git a/model/transform/src/main/scala/aqua/model/transform/cursor/ChainCursor.scala b/model/transform/src/main/scala/aqua/model/transform/cursor/ChainCursor.scala index 8700bfa3..53a28095 100644 --- a/model/transform/src/main/scala/aqua/model/transform/cursor/ChainCursor.scala +++ b/model/transform/src/main/scala/aqua/model/transform/cursor/ChainCursor.scala @@ -12,7 +12,7 @@ abstract class ChainCursor[C <: ChainCursor[C, T], T](make: NonEmptyList[ChainZi val tree: NonEmptyList[ChainZipper[T]] // Parent element, if not at root - def parent: Option[T] = tree.tail.headOption.map(_.current) + def parent: Option[T] = moveUp.map(_.current) // The closest element def current: T = tree.head.current @@ -34,13 +34,14 @@ abstract class ChainCursor[C <: ChainCursor[C, T], T](make: NonEmptyList[ChainZi ) // Path to this position: just drop siblings - def path: NonEmptyList[T] = tree.map(_.current) + lazy val path: NonEmptyList[T] = tree.map(_.current) // Move cursor up + // TODO: ensure this cursor's data is cached properly def moveUp: Option[C] = NonEmptyList.fromList(tree.tail).map(make) // Path to root, in form of Cursors; this is skipped - def pathToRoot: LazyList[C] = LazyList.unfold(this)(_.moveUp.map(c => c -> c)) + val pathToRoot: LazyList[C] = LazyList.unfold(this)(_.moveUp.map(c => c -> c)) // Move down: need a ChainZipper that's below def moveDown(focusOn: ChainZipper[T]): C = make(focusOn :: tree) diff --git a/model/transform/src/main/scala/aqua/model/transform/res/MakeRes.scala b/model/transform/src/main/scala/aqua/model/transform/res/MakeRes.scala index d0a69719..9b8e8aef 100644 --- a/model/transform/src/main/scala/aqua/model/transform/res/MakeRes.scala +++ b/model/transform/src/main/scala/aqua/model/transform/res/MakeRes.scala @@ -24,14 +24,17 @@ object MakeRes { def seq(first: Res, second: Res, more: Res*): Res = Cofree[Chain, ResolvedOp](SeqRes, Eval.later(first +: second +: Chain.fromSeq(more))) - def par(first: Res, second: Res, more: Res*): Res = - Cofree[Chain, ResolvedOp](ParRes, Eval.later(first +: second +: Chain.fromSeq(more))) + def par(first: Res, more: Res*): Res = + Cofree[Chain, ResolvedOp](ParRes, Eval.later(first +: Chain.fromSeq(more))) def xor(first: Res, second: Res): Res = Cofree[Chain, ResolvedOp](XorRes, Eval.later(Chain(first, second))) - def fold(item: String, iter: ValueModel, body: Res): Res = - Cofree[Chain, ResolvedOp](FoldRes(item, iter), Eval.now(Chain.one(body))) + def fold(item: String, iter: ValueModel, body0: Res, body: Res*): Res = + Cofree[Chain, ResolvedOp]( + FoldRes(item, iter), + Eval.now(Chain.one(body0) ++ Chain.fromSeq(body)) + ) def noop(onPeer: ValueModel): Res = leaf(CallServiceRes(LiteralModel.quote("op"), "noop", CallRes(Nil, None), onPeer)) diff --git a/model/transform/src/main/scala/aqua/model/transform/topology/PathFinder.scala b/model/transform/src/main/scala/aqua/model/transform/topology/PathFinder.scala index 119b6ad6..458abbd2 100644 --- a/model/transform/src/main/scala/aqua/model/transform/topology/PathFinder.scala +++ b/model/transform/src/main/scala/aqua/model/transform/topology/PathFinder.scala @@ -10,53 +10,19 @@ import scala.annotation.tailrec object PathFinder extends Logging { - def find(from: RawCursor, to: RawCursor, isExit: Boolean = false): Chain[ValueModel] = { - - val fromOn = Chain.fromSeq(from.pathOn).reverse - val toOn = Chain.fromSeq(to.pathOn).reverse - - val wasHandled = - !isExit && - to.leftSiblings.isEmpty && - to.moveUp.exists(_.pathOn == to.pathOn) && - !to.parentTag.exists(_.isInstanceOf[ParGroupTag]) - - if (wasHandled) { - logger.trace("Was handled") - logger.trace(" :: " + from) - logger.trace(" -> " + to) - Chain.empty - } else { - logger.trace("Find path") - logger.trace(" :: " + from) - logger.trace(" -> " + to) - findPath( - fromOn, - toOn, - from.currentPeerId, - to.currentPeerId - ) - } - } - - def optimizePath( - peerIds: Chain[ValueModel], - prefix: Chain[ValueModel], - suffix: Chain[ValueModel] - ): Chain[ValueModel] = { - val optimized = peerIds - .foldLeft(Chain.empty[ValueModel]) { - case (acc, p) if acc.lastOption.contains(p) => acc - case (acc, p) if acc.contains(p) => acc.takeWhile(_ != p) :+ p - case (acc, p) => acc :+ p - } - logger.trace(s"PEER IDS: $optimized") - logger.trace(s"PREFIX: $prefix") - logger.trace(s"SUFFIX: $suffix") - logger.trace(s"OPTIMIZED WITH PREFIX AND SUFFIX: $optimized") - val noPrefix = skipPrefix(optimized, prefix, optimized) - skipSuffix(noPrefix, suffix, noPrefix) - } + /** + * Finds the path – chain of peers to visit to get from [[fromOn]] to [[toOn]] + * @param fromOn Previous location + * @param toOn Next location + * @return Chain of peers to visit in between + */ + def findPath(fromOn: List[OnTag], toOn: List[OnTag]): Chain[ValueModel] = + findPath( + Chain.fromSeq(fromOn).reverse, + Chain.fromSeq(toOn).reverse, + fromOn.headOption.map(_.peerId), + toOn.headOption.map(_.peerId) + ) def findPath( fromOn: Chain[OnTag], @@ -89,6 +55,33 @@ object PathFinder extends Logging { optimized } + /** + * Removes cycles from the path + * + * @param peerIds peers to walk trough + * @param prefix getting from the previous peer + * @param suffix getting to the next peer + * @return optimal path with no duplicates + */ + def optimizePath( + peerIds: Chain[ValueModel], + prefix: Chain[ValueModel], + suffix: Chain[ValueModel] + ): Chain[ValueModel] = { + val optimized = peerIds + .foldLeft(Chain.empty[ValueModel]) { + case (acc, p) if acc.lastOption.contains(p) => acc + case (acc, p) if acc.contains(p) => acc.takeWhile(_ != p) :+ p + case (acc, p) => acc :+ p + } + logger.trace(s"PEER IDS: $optimized") + logger.trace(s"PREFIX: $prefix") + logger.trace(s"SUFFIX: $suffix") + logger.trace(s"OPTIMIZED WITH PREFIX AND SUFFIX: $optimized") + val noPrefix = skipPrefix(optimized, prefix, optimized) + skipSuffix(noPrefix, suffix, noPrefix) + } + @tailrec def skipPrefix[T](chain: Chain[T], prefix: Chain[T], init: Chain[T]): Chain[T] = (chain, prefix) match { diff --git a/model/transform/src/main/scala/aqua/model/transform/topology/RawCursor.scala b/model/transform/src/main/scala/aqua/model/transform/topology/RawCursor.scala index a25ceb0f..2d38625d 100644 --- a/model/transform/src/main/scala/aqua/model/transform/topology/RawCursor.scala +++ b/model/transform/src/main/scala/aqua/model/transform/topology/RawCursor.scala @@ -2,21 +2,36 @@ package aqua.model.transform.topology import aqua.model.ValueModel import aqua.model.func.raw.* +import aqua.model.func.raw.FuncOp.Tree import cats.Eval import cats.data.{Chain, NonEmptyList, OptionT} -import aqua.model.transform.cursor._ -import cats.syntax.traverse._ +import aqua.model.transform.cursor.* +import cats.syntax.traverse.* import cats.free.Cofree import scribe.Logging // Can be heavily optimized by caching parent cursors, not just list of zippers -case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) - extends ChainCursor[RawCursor, FuncOp.Tree](RawCursor.apply) with Logging { +case class RawCursor( + tree: NonEmptyList[ChainZipper[FuncOp.Tree]], + cachedParent: Option[RawCursor] = None +) extends ChainCursor[RawCursor, FuncOp.Tree](RawCursor.apply(_, None)) with Logging { + + override def moveUp: Option[RawCursor] = cachedParent.orElse(super.moveUp) + + override lazy val toPrevSibling: Option[RawCursor] = + super.toPrevSibling.map(_.copy(cachedParent = cachedParent)) + + override lazy val toNextSibling: Option[RawCursor] = + super.toNextSibling.map(_.copy(cachedParent = cachedParent)) + + override def moveDown(focusOn: ChainZipper[Tree]): RawCursor = + super.moveDown(focusOn).copy(cachedParent = Some(this)) def tag: RawTag = current.head + def parentTag: Option[RawTag] = parent.map(_.head) - def hasChildren: Boolean = + lazy val hasChildren: Boolean = current.tailForced.nonEmpty lazy val toFirstChild: Option[RawCursor] = @@ -25,61 +40,30 @@ case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) lazy val toLastChild: Option[RawCursor] = ChainZipper.last(current.tail.value).map(moveDown) + lazy val children: LazyList[RawCursor] = + LazyList.unfold(toFirstChild)(_.map(c => c -> c.toNextSibling)) + + def findInside(f: RawCursor => Boolean): LazyList[RawCursor] = + children.flatMap(_.findInside(f)).prependedAll(Option.when(f(this))(this)) + + lazy val topology: Topology = Topology.make(this) + lazy val tagsPath: NonEmptyList[RawTag] = path.map(_.head) - lazy val pathOn: List[OnTag] = tagsPath.collect { case o: OnTag => - o - } - - // Assume that the very first tag is `on` tag - lazy val rootOn: Option[RawCursor] = moveUp - .flatMap(_.rootOn) - .orElse(tag match { - case _: OnTag => - Some(this) - case _ => None - }) - - // The closest peerId - lazy val currentPeerId: Option[ValueModel] = - pathOn.headOption.map(_.peerId) - - // Cursor to the last sequentially executed operation, if any - lazy val lastExecuted: Option[RawCursor] = tag match { - case XorTag => toFirstChild.flatMap(_.lastExecuted) - case _: SeqGroupTag => toLastChild.flatMap(_.lastExecuted) - case _: ParGroupTag => - None // ParGroup builds exit path within itself; there's no "lastExecuted", they are many - case _: NoExecTag => moveLeft.flatMap(_.lastExecuted) - case _ => Some(this) - } - - lazy val firstExecuted: Option[RawCursor] = tag match { - case _: SeqGroupTag => toFirstChild.flatMap(_.firstExecuted) - case _: ParGroupTag => - None // As many branches are executed simultaneously, no definition of first - case _: NoExecTag => moveRight.flatMap(_.firstExecuted) - case _ => Some(this) - } - - /** - * Sequentially previous cursor - * @return - */ - lazy val seqPrev: Option[RawCursor] = - parentTag.flatMap { - case p: SeqGroupTag if leftSiblings.nonEmpty => - toPrevSibling.flatMap(c => c.lastExecuted orElse c.seqPrev) - case p => - moveUp.flatMap(_.seqPrev) + // Whether the current branch contains any AIR-executable code or not + lazy val isNoExec: Boolean = + tag match { + case _: NoExecTag => true + case _: GroupTag => children.forall(_.isNoExec) + case _ => false } - lazy val seqNext: Option[RawCursor] = - parentTag.flatMap { - case _: SeqGroupTag if rightSiblings.nonEmpty => - toNextSibling.flatMap(c => c.firstExecuted orElse c.seqNext) - case _ => moveUp.flatMap(_.seqNext) - } + def hasExecLater: Boolean = + !allToRight.forall(_.isNoExec) + + // Whether variables exported from this branch are used later in the code or not + def exportsUsedLater: Boolean = + FuncOp(current).exportsVarNames.map(ns => ns.nonEmpty && checkNamesUsedLater(ns)).value // TODO write a test def checkNamesUsedLater(names: Set[String]): Boolean = @@ -88,50 +72,6 @@ case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) .map(FuncOp(_)) .exists(_.usesVarNames.value.intersect(names).nonEmpty) - lazy val pathFromPrev: Chain[ValueModel] = pathFromPrevD() - - def pathFromPrevD(forExit: Boolean = false): Chain[ValueModel] = - parentTag.fold(Chain.empty[ValueModel]) { - case _: GroupTag => - seqPrev - .orElse(rootOn) - .fold(Chain.empty[ValueModel])(PathFinder.find(_, this, isExit = forExit)) - case _ => - Chain.empty - } - - lazy val pathToNext: Chain[ValueModel] = parentTag.fold(Chain.empty[ValueModel]) { - case _: ParGroupTag => - val exports = FuncOp(current).exportsVarNames.value - if (exports.nonEmpty && checkNamesUsedLater(exports)) - seqNext.fold(Chain.empty[ValueModel])(nxt => - PathFinder.find(this, nxt) ++ - // we need to "wake" the target peer to enable join behaviour - Chain.fromOption(nxt.currentPeerId) - ) - else Chain.empty - case XorTag if leftSiblings.nonEmpty => - lastExecuted - .flatMap(le => - seqNext - .map(nxt => PathFinder.find(le, nxt, isExit = true) -> nxt) - .flatMap { - case (path, nxt) if path.isEmpty && currentPeerId == nxt.currentPeerId => - nxt.pathFromPrevD(true).reverse.initLast.map(_._1) - case (path, nxt) => - path.initLast.map { - case (init, last) - if nxt.pathFromPrevD(forExit = true).headOption.contains(last) => - init - case (init, last) => init :+ last - } - } - ) - .getOrElse(Chain.empty) - case _ => - Chain.empty - } - def cata[A](wrap: ChainZipper[Cofree[Chain, A]] => Chain[Cofree[Chain, A]])( folder: RawCursor => OptionT[Eval, ChainZipper[Cofree[Chain, A]]] ): Eval[Chain[Cofree[Chain, A]]] = @@ -141,7 +81,9 @@ case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) toFirstChild .map(folderCursor => LazyList - .unfold(folderCursor) { _.toNextSibling.map(cursor => cursor -> cursor) } + .unfold(folderCursor) { + _.toNextSibling.map(cursor => cursor -> cursor) + } .prepended(folderCursor) ) .getOrElse(LazyList.empty) diff --git a/model/transform/src/main/scala/aqua/model/transform/topology/Topology.scala b/model/transform/src/main/scala/aqua/model/transform/topology/Topology.scala index 3006d619..431ce1bd 100644 --- a/model/transform/src/main/scala/aqua/model/transform/topology/Topology.scala +++ b/model/transform/src/main/scala/aqua/model/transform/topology/Topology.scala @@ -1,23 +1,401 @@ package aqua.model.transform.topology +import aqua.model.ValueModel.varName import aqua.model.transform.cursor.ChainZipper import aqua.model.func.raw.* import aqua.model.transform.res.* import aqua.model.{LiteralModel, ValueModel, VarModel} import aqua.types.{BoxType, ScalarType} import cats.Eval -import cats.data.Chain.nil +import cats.data.Chain.{==:, nil} import cats.data.{Chain, NonEmptyChain, NonEmptyList, OptionT} import cats.free.Cofree import cats.syntax.traverse.* +import cats.syntax.apply.* import scribe.Logging +/** + * Wraps all the logic for topology reasoning about the tag in the AST represented by the [[cursor]] + * + * @param cursor Pointer to the current place in the AST + * @param before Strategy of calculating where the previous executions happened + * @param begins Strategy of calculating where execution of this tag/node should begin + * @param ends Strategy of calculating where execution of this tag/node happens + * @param after Strategy of calculating where the next execution should happen and whether we need to move there or not + */ +case class Topology private ( + cursor: RawCursor, + before: Topology.Before, + begins: Topology.Begins, + ends: Topology.Ends, + after: Topology.After +) { + + val pathOn: Eval[List[OnTag]] = Eval + .later(cursor.tagsPath.collect { case o: OnTag => + o + }) + .memoize + + lazy val firstExecutesOn: Eval[Option[List[OnTag]]] = + (cursor.tag match { + case _: CallServiceTag => pathOn.map(Some(_)) + case _ => + children + .map(_.firstExecutesOn) + .scanLeft[Eval[Option[List[OnTag]]]](Eval.now(None)) { case (acc, el) => + (acc, el).mapN(_ orElse _) + } + .collectFirst { + case e if e.value.isDefined => e + } + .getOrElse(Eval.now(None)) + }).memoize + + lazy val lastExecutesOn: Eval[Option[List[OnTag]]] = + (cursor.tag match { + case _: CallServiceTag => pathOn.map(Some(_)) + case _ => + children + .map(_.lastExecutesOn) + .scanRight[Eval[Option[List[OnTag]]]](Eval.now(None)) { case (acc, el) => + (acc, el).mapN(_ orElse _) + } + .collectFirst { + case e if e.value.isDefined => e + } + .getOrElse(Eval.now(None)) + }).memoize + + lazy val currentPeerId: Option[ValueModel] = pathOn.value.headOption.map(_.peerId) + + lazy val prevSibling: Option[Topology] = cursor.toPrevSibling.map(_.topology) + + lazy val nextSibling: Option[Topology] = cursor.toNextSibling.map(_.topology) + + lazy val firstChild: Option[Topology] = cursor.toFirstChild.map(_.topology) + + lazy val lastChild: Option[Topology] = cursor.toLastChild.map(_.topology) + + lazy val children: LazyList[Topology] = cursor.children.map(_.topology) + + def findInside(f: Topology => Boolean): LazyList[Topology] = + children.flatMap(_.findInside(f)).prependedAll(Option.when(f(this))(this)) + + val parent: Option[Topology] = cursor.moveUp.map(_.topology) + + val parents: LazyList[Topology] = + LazyList.unfold(parent)(p => p.map(pp => pp -> pp.parent)) + + lazy val forTag: Option[ForTag] = Option(cursor.tag).collect { case ft: ForTag => + ft + } + + lazy val isForTag: Boolean = forTag.isDefined + + // Before the left boundary of this element, what was the scope + lazy val beforeOn: Eval[List[OnTag]] = before.beforeOn(this).memoize + + // Inside the left boundary of this element, what should be the scope + lazy val beginsOn: Eval[List[OnTag]] = begins.beginsOn(this).memoize + + // After this element is done, what is the scope + lazy val endsOn: Eval[List[OnTag]] = ends.endsOn(this).memoize + + // After this element is done, where should it move to prepare for the next one + lazy val afterOn: Eval[List[OnTag]] = after.afterOn(this).memoize + + // Usually we don't care about exiting from where this tag ends into the outer scope + // But for some cases, like par branches, its necessary, so the exit can be forced + lazy val forceExit: Eval[Boolean] = after.forceExit(this).memoize + + // Where we finally are, after exit enforcement is applied + lazy val finallyOn: Eval[List[OnTag]] = after.finallyOn(this).memoize + + lazy val pathBefore: Eval[Chain[ValueModel]] = begins.pathBefore(this).memoize + + lazy val pathAfter: Eval[Chain[ValueModel]] = after.pathAfter(this).memoize +} + object Topology extends Logging { type Tree = Cofree[Chain, RawTag] type Res = Cofree[Chain, ResolvedOp] - def resolve(op: Tree): Res = { - val resolved = resolveOnMoves(op).value + // Returns a peerId to go to in case it equals the last relay: useful when we do execution on the relay + private def findRelayPathEnforcement(bef: List[OnTag], beg: List[OnTag]): Chain[ValueModel] = + Chain.fromOption( + beg.headOption + .map(_.peerId) + .filter(lastPeerId => beg.tail.headOption.exists(_.via.lastOption.contains(lastPeerId))) + .filter(lastPeerId => !bef.headOption.exists(_.peerId == lastPeerId)) + ) + + trait Before { + + def beforeOn(current: Topology): Eval[List[OnTag]] = + // Go to the parent, see where it begins + current.parent.map(_.beginsOn) getOrElse + // This means, we have no parent; then we're where we should be + current.pathOn + } + + trait Begins { + def beginsOn(current: Topology): Eval[List[OnTag]] = current.pathOn + + def pathBefore(current: Topology): Eval[Chain[ValueModel]] = + (current.beforeOn, current.beginsOn).mapN { case (bef, beg) => + (PathFinder.findPath(bef, beg), bef, beg) + }.flatMap { case (pb, bef, beg) => + // Handle the case when we need to go through the relay, but miss the hop as it's the first + // peer where we go, but there's no service calls there + current.firstExecutesOn.map { + case Some(where) if where != beg => + pb ++ findRelayPathEnforcement(bef, beg) + case _ => pb + } + } + } + + trait Ends { + + def endsOn(current: Topology): Eval[List[OnTag]] = + current.beginsOn + + protected def lastChildFinally(current: Topology): Eval[List[OnTag]] = + current.lastChild.map(lc => + lc.forceExit.flatMap { + case true => current.afterOn + case false => lc.endsOn + } + ) getOrElse current.beginsOn + } + + trait After { + def forceExit(current: Topology): Eval[Boolean] = Eval.now(false) + + def afterOn(current: Topology): Eval[List[OnTag]] = current.pathOn + + protected def afterParent(current: Topology): Eval[List[OnTag]] = + current.parent.map( + _.afterOn + ) getOrElse current.pathOn + + // In case exit is performed and pathAfter is inserted, we're actually where + // execution is expected to continue After this node is handled + final def finallyOn(current: Topology): Eval[List[OnTag]] = + current.forceExit.flatMap { + case true => current.afterOn + case false => current.endsOn + } + + // If exit is forced, make a path outside this node + // – from where it ends to where execution is expected to continue + def pathAfter(current: Topology): Eval[Chain[ValueModel]] = + current.forceExit.flatMap { + case true => + (current.endsOn, current.afterOn).mapN(PathFinder.findPath) + case false => + Eval.now(Chain.empty) + } + } + + object Default extends Before with Begins with Ends with After { + override def toString: String = "" + } + + // Parent == Seq, On + object SeqGroupBranch extends Before with After { + override def toString: String = "/*" + + // If parent is seq, then before this node we are where previous node, if any, ends + override def beforeOn(current: Topology): Eval[List[OnTag]] = + current.prevSibling + .map(_.finallyOn) getOrElse super.beforeOn(current) + + override def afterOn(current: Topology): Eval[List[OnTag]] = + current.nextSibling.map(_.beginsOn) getOrElse afterParent(current) + + } + + object SeqGroup extends Ends { + override def toString: String = "" + + override def endsOn(current: Topology): Eval[List[OnTag]] = + lastChildFinally(current) + } + + // Parent == Xor + object XorBranch extends Before with After { + override def toString: String = "/*" + + override def beforeOn(current: Topology): Eval[List[OnTag]] = + current.prevSibling.map(_.endsOn) getOrElse super.beforeOn(current) + + // TODO: if this xor is in par that needs no forceExit, do not exit + override def forceExit(current: Topology): Eval[Boolean] = + Eval.later(current.cursor.moveUp.exists(_.hasExecLater)) + + override def afterOn(current: Topology): Eval[List[OnTag]] = + afterParent(current) + } + + // Parent == Par + object ParGroupBranch extends Ends with After { + override def toString: String = "/*" + + override def forceExit(current: Topology): Eval[Boolean] = + Eval.later(current.cursor.exportsUsedLater) + + override def afterOn(current: Topology): Eval[List[OnTag]] = + afterParent(current) + + override def pathAfter(current: Topology): Eval[Chain[ValueModel]] = + current.forceExit + .flatMap[Chain[ValueModel]] { + case false => Eval.now(Chain.empty[ValueModel]) + case true => + (current.endsOn, current.afterOn, current.lastExecutesOn).mapN { + case (e, a, _) if e == a => Chain.empty[ValueModel] + case (e, a, l) if l.contains(e) => + // Pingback in case no relays involved + Chain.fromOption(a.headOption.map(_.peerId)) + case (e, a, _) => + // We wasn't at e, so need to get through the last peer in case it matches with the relay + findRelayPathEnforcement(a, e) ++ Chain.fromOption(a.headOption.map(_.peerId)) + } + } + .flatMap { appendix => + // Ping the next (join) peer to enforce its data update + super.pathAfter(current).map(_ ++ appendix) + } + + override def endsOn(current: Topology): Eval[List[OnTag]] = current.beforeOn + } + + object XorGroup extends Ends { + override def toString: String = "" + + // Xor tag ends where any child ends; can't get first one as it may lead to recursion + override def endsOn(current: Topology): Eval[List[OnTag]] = + lastChildFinally(current) + + } + + object Root extends Before with Ends with After { + override def toString: String = "" + + override def beforeOn(current: Topology): Eval[List[OnTag]] = current.beginsOn + + override def endsOn(current: Topology): Eval[List[OnTag]] = current.pathOn + + override def afterOn(current: Topology): Eval[List[OnTag]] = current.pathOn + + override def forceExit(current: Topology): Eval[Boolean] = Eval.now(false) + } + + object ParGroup extends Begins with Ends { + override def toString: String = "" + + // Optimization: find the longest common prefix of all the par branches, and move it outside of this par + // When branches will calculate their paths, they will take this move into account. + // So less hops will be produced + override def beginsOn(current: Topology): Eval[List[OnTag]] = + current.children + .map(_.beginsOn.map(_.reverse)) + .reduceLeftOption { case (b1e, b2e) => + (b1e, b2e).mapN { case (b1, b2) => + (b1 zip b2).takeWhile(_ == _).map(_._1) + } + } + .map(_.map(_.reverse)) getOrElse super.beginsOn(current) + + // Par block ends where all the branches end, if they have forced exit (not fire-and-forget) + override def endsOn(current: Topology): Eval[List[OnTag]] = + current.children + .map(_.forceExit) + .reduceLeftOption { case (a, b) => + (a, b).mapN(_ || _) + } + .map(_.flatMap { + case true => current.afterOn + case false => super.endsOn(current) + }) getOrElse super.endsOn(current) + } + + object For extends Begins { + override def toString: String = "" + + // Optimization: get all the path inside the For block out of the block, to avoid repeating + // hops for every For iteration + override def beginsOn(current: Topology): Eval[List[OnTag]] = + (current.forTag zip current.firstChild.map(_.beginsOn)).map { case (f, b) => + // Take path until this for's iterator is used + b.map( + _.reverse + .foldLeft((true, List.empty[OnTag])) { + case ((true, acc), OnTag(_, r)) if r.exists(ValueModel.varName(_).contains(f.item)) => + (false, acc) + case ((true, acc @ (OnTag(_, r @ (r0 ==: _)) :: _)), OnTag(p, _)) + if ValueModel.varName(p).contains(f.item) => + // This is to take the outstanding relay and force moving there + (false, OnTag(r0, r) :: acc) + case ((true, acc), on) => (true, on :: acc) + case ((false, acc), _) => (false, acc) + } + ._2 + ) + } getOrElse super.beginsOn(current) + + } + + object SeqNext extends Begins { + override def toString: String = "/" + + override def beginsOn(current: Topology): Eval[List[OnTag]] = + current.parents.find(_.isForTag).map(_.beginsOn) getOrElse super.beginsOn(current) + } + + def make(cursor: RawCursor): Topology = + Topology( + cursor, + // Before + cursor.parentTag match { + case Some(XorTag) => XorBranch + case Some(_: SeqGroupTag) => SeqGroupBranch + case None => Root + case _ => Default + }, + // Begin + (cursor.parentTag, cursor.tag) match { + case (Some(_: SeqGroupTag), _: NextTag) => + SeqNext + case (_, _: ForTag) => + For + case (_, ParTag | ParTag.Detach) => + ParGroup + case _ => + Default + }, + // End + cursor.tag match { + case _: SeqGroupTag => SeqGroup + case XorTag => XorGroup + case ParTag | ParTag.Detach => ParGroup + case _ if cursor.parentTag.isEmpty => Root + case _ => Default + }, + // After + cursor.parentTag match { + case Some(ParTag | ParTag.Detach) => ParGroupBranch + case Some(XorTag) => XorBranch + case Some(_: SeqGroupTag) => SeqGroupBranch + case None => Root + case _ => Default + } + ) + + def resolve(op: Tree, debug: Boolean = false): Res = { + val resolved = resolveOnMoves(op, debug).value Cofree .cata[Chain, ResolvedOp, Res](resolved) { case (SeqRes, children) => @@ -46,10 +424,11 @@ object Topology extends Logging { else cz.current ) - def resolveOnMoves(op: Tree): Eval[Res] = { - val cursor = RawCursor(NonEmptyList.one(ChainZipper.one(op))) + def resolveOnMoves(op: Tree, debug: Boolean): Eval[Res] = { + val cursor = RawCursor(NonEmptyList.one(ChainZipper.one(op)), None) // TODO: remove var var i = 0 + def nextI = { i = i + 1 i @@ -60,18 +439,37 @@ object Topology extends Logging { logger.debug(s"<:> $rc") val resolved = MakeRes - .resolve(rc.currentPeerId, nextI) + .resolve(rc.topology.currentPeerId, nextI) .lift .apply(rc.tag) logger.trace("Resolved: " + resolved) + if (debug) { + println(Console.BLUE + rc + Console.RESET) + println(rc.topology) + println("Before: " + rc.topology.beforeOn.value) + println("Begin: " + rc.topology.beginsOn.value) + println("PathBefore: " + rc.topology.pathBefore.value) + + println(Console.CYAN + "Parent: " + rc.topology.parent + Console.RESET) + + println("End : " + rc.topology.endsOn.value) + println("After: " + rc.topology.afterOn.value) + println("Exit : " + rc.topology.forceExit.value) + println("PathAfter: " + rc.topology.pathAfter.value) + println(Console.YELLOW + " - - - - -" + Console.RESET) + } + val chainZipperEv = resolved.traverse(cofree => - Eval.later { + ( + rc.topology.pathBefore.map(through(_)), + rc.topology.pathAfter.map(through(_, reversed = true)) + ).mapN { case (pathBefore, pathAfter) => val cz = ChainZipper( - through(rc.pathFromPrev), + pathBefore, cofree, - through(rc.pathToNext) + pathAfter ) if (cz.next.nonEmpty || cz.prev.nonEmpty) { logger.debug(s"Resolved $rc -> $cofree")