Topology refactoring (#100)

* Topology refactoring

* TransformSpec fixed & improved

* Fixes #98

* Better Par handling

* Introduced Cursor class

* Better exit process for par branch

* Force move to target peer when exiting from a par branch
This commit is contained in:
Dmitry Kurinskiy 2021-04-29 14:16:25 +03:00 committed by GitHub
parent 27f2912c5f
commit 1fc5557ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 428 additions and 238 deletions

View File

@ -13,6 +13,8 @@ service Test("test"):
getUserList: -> []User
doSomething: -> bool
check: bool -> ()
log: string -> ()
getString: string -> string
func initAfterJoin(me: User) -> []User:
allUsers <- Test.getUserList()
@ -42,3 +44,18 @@ func checkStreams(ch: []string) -> []bool:
stream <- Peer.is_connected(b)
handleArr(stream)
<- stream
func test(user: string, relay_id: string) -> string:
on relay_id:
Peer.is_connected("on relay_id")
on user:
Peer.is_connected("on user")
par Peer.is_connected("par on init peer")
<- ""
-- should fail, as using str2 in parallel
func topologyTest(friend: string, friendRelay: string) -> string:
on friend via friendRelay:
str2 <- Test.getString("friends string via")
par Test.log(str2)
<- "finish"

View File

@ -14,15 +14,18 @@ case class TypescriptFile(script: ScriptModel) {
object TypescriptFile {
val Header: String =
"""/**
| *
| * This file is auto-generated. Do not edit manually: changes may be erased.
| * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
| * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
| *
| */
|import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence';
|import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable';
|""".stripMargin
s"""/**
| *
| * This file is auto-generated. Do not edit manually: changes may be erased.
| * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
| * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
| * Aqua version: ${Option(getClass.getPackage.getImplementationVersion)
.filter(_.nonEmpty)
.getOrElse("Unknown")}
| *
| */
|import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence';
|import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable';
|""".stripMargin
}

View File

@ -1,18 +1,18 @@
val dottyVersion = "2.13.5"
//val dottyVersion = "3.0.0-RC3"
scalaVersion := dottyVersion
//val dottyVersion = "3.0.0-RC2"
val baseAquaVersion = settingKey[String]("base aqua version")
val catsV = "2.5.0"
val catsV = "2.6.0"
val catsParseV = "0.3.2"
val monocleV = "3.0.0-M4"
val scalaTestV = "3.2.7"
val fs2V = "3.0.0"
val catsEffectV = "3.0.2"
val declineV = "2.0.0-RC1"
val monocleV = "3.0.0-M5"
val scalaTestV = "3.2.7" // TODO update version for scala 3-RC3
val fs2V = "3.0.2"
val catsEffectV = "3.1.0"
val declineV = "2.0.0-RC1" // Scala3 issue: https://github.com/bkirwi/decline/issues/260
name := "aqua-hll"

View File

@ -17,6 +17,7 @@ object Model {
case (l: ScriptModel, r: ScriptModel) =>
ScriptModel.SMMonoid.combine(l, r)
case (l: EmptyModel, r: EmptyModel) => EmptyModel(l.log + " |+| " + r.log)
case (_: EmptyModel, r) => r
case (l, _: EmptyModel) => l
@ -24,6 +25,7 @@ object Model {
ScriptModel.toScriptPart(l).fold(r)(ScriptModel.SMMonoid.combine(_, r))
case (l: ScriptModel, r) =>
ScriptModel.toScriptPart(r).fold(l)(ScriptModel.SMMonoid.combine(l, _))
case (l, r) =>
ScriptModel
.toScriptPart(l)
@ -31,8 +33,6 @@ object Model {
ScriptModel.toScriptPart(r).fold(l)(rs => ScriptModel.SMMonoid.combine(ls, rs))
)
case (l: EmptyModel, r: EmptyModel) => EmptyModel(l.log + " |+| " + r.log)
}
}
}

View File

@ -1,12 +1,20 @@
package aqua.model
import aqua.types.Type
import cats.Eq
import cats.data.Chain
sealed trait ValueModel {
def resolveWith(map: Map[String, ValueModel]): ValueModel = this
}
object ValueModel {
implicit object ValueModelEq extends Eq[ValueModel] {
override def eqv(x: ValueModel, y: ValueModel): Boolean = x == y
}
}
case class LiteralModel(value: String) extends ValueModel
object LiteralModel {

View File

@ -27,15 +27,19 @@ sealed trait OpTag {
}
}
sealed trait GroupTag extends OpTag
sealed trait SeqGroupTag extends GroupTag
case object SeqTag extends OpTag
case object ParTag extends OpTag
case object XorTag extends OpTag
case object SeqTag extends SeqGroupTag
case object ParTag extends GroupTag
case object XorTag extends GroupTag
case class XorParTag(xor: FuncOp, par: FuncOp) extends OpTag
case class OnTag(peerId: ValueModel, via: Chain[ValueModel]) extends OpTag
case class OnTag(peerId: ValueModel, via: Chain[ValueModel]) extends SeqGroupTag
case class NextTag(item: String) extends OpTag
case class MatchMismatchTag(left: ValueModel, right: ValueModel, shouldMatch: Boolean) extends OpTag
case class ForTag(item: String, iterable: ValueModel) extends OpTag
case class MatchMismatchTag(left: ValueModel, right: ValueModel, shouldMatch: Boolean)
extends SeqGroupTag
case class ForTag(item: String, iterable: ValueModel) extends SeqGroupTag
case class CallArrowTag(
funcName: String,

View File

@ -0,0 +1,51 @@
package aqua.model.topology
import cats.data.Chain
import cats.free.Cofree
case class ChainZipper[T](prev: Chain[T], current: T, next: Chain[T]) {
def chain: Chain[T] = (prev :+ current) ++ next
def moveLeft: Option[ChainZipper[T]] =
prev.initLast.map { case (init, last) =>
ChainZipper(init, last, current +: next)
}
def moveRight: Option[ChainZipper[T]] =
next.uncons.map { case (head, tail) =>
ChainZipper(prev :+ current, head, tail)
}
def replaceInjecting(cz: ChainZipper[T]): ChainZipper[T] =
copy(prev ++ cz.prev, cz.current, cz.next ++ next)
}
object ChainZipper {
def one[T](el: T): ChainZipper[T] = ChainZipper(Chain.empty, el, Chain.empty)
def fromChain[T](chain: Chain[T], prev: Chain[T] = Chain.empty): Chain[ChainZipper[T]] =
chain.uncons.fold(Chain.empty[ChainZipper[T]]) { case (t, next) =>
ChainZipper(prev, t, next) +: fromChain(next, prev :+ t)
}
def fromChainMap[T](chain: Chain[T], prev: Chain[T] = Chain.empty)(
f: ChainZipper[T] => Option[T]
): Chain[T] =
chain.uncons.fold(Chain.empty[T]) { case (t, next) =>
f(ChainZipper(prev, t, next)).fold(fromChainMap(next, prev)(f))(r =>
r +: fromChainMap(next, prev :+ r)(f)
)
}
object Matchers {
object `current` {
def unapply[T](cz: ChainZipper[T]): Option[T] = Some(cz.current)
}
object `head` {
def unapply[F[_], T](cz: ChainZipper[Cofree[F, T]]): Option[T] = Some(cz.current.head)
}
}
}

View File

@ -0,0 +1,81 @@
package aqua.model.topology
import Topology.Tree
import aqua.model.func.body.{OnTag, OpTag, ParTag, SeqTag}
import cats.Eval
import cats.data.Chain
import cats.free.Cofree
case class Cursor(point: ChainZipper[Tree], loc: Location) {
def downLoc(tree: Tree): Location =
loc.down(point.copy(current = tree))
def prevOnTags: Chain[OnTag] =
Chain
.fromSeq(
point.prev.lastOption
.orElse(loc.lastLeftSeq.map(_._1.current))
.toList
.flatMap(Cursor.rightBoundary)
.takeWhile {
case ParTag => false
case _ => true
}
)
.collect { case o: OnTag =>
o
}
def nextOnTags: Chain[OnTag] =
Chain
.fromSeq(
loc.lastRightSeq
.map(_._1.current)
.toList
.flatMap(Cursor.leftBoundary)
.takeWhile {
case ParTag => false
case _ => true
}
)
.collect { case o: OnTag =>
o
}
}
object Cursor {
def rightBoundary(root: Tree): LazyList[OpTag] =
root.head #:: LazyList.unfold(root.tail)(_.value.lastOption.map(lo => lo.head -> lo.tail))
def leftBoundary(root: Tree): LazyList[OpTag] =
root.head #:: LazyList.unfold(root.tail)(_.value.headOption.map(lo => lo.head -> lo.tail))
def transform(root: Tree)(f: Cursor => List[Tree]): Option[Tree] = {
def step(cursor: Cursor): Option[Tree] =
f(cursor) match {
case Nil => None
case h :: Nil =>
val np = cursor.downLoc(h)
Some(h.copy(tail = h.tail.map(ChainZipper.fromChainMap(_)(cz => step(Cursor(cz, np))))))
case hs =>
ChainZipper
.fromChain(Chain.fromSeq(hs))
.map(cursor.point.replaceInjecting)
.map { cfh =>
val np = cursor.loc.down(cfh)
cfh.current.copy(tail =
cfh.current.tail.map(ChainZipper.fromChainMap(_)(cz => step(Cursor(cz, np))))
)
}
.uncons
.map {
case (h, t) if t.isEmpty => h
case (h, t) => Cofree(SeqTag, Eval.later(h +: t))
}
}
step(Cursor(ChainZipper.one(root), Location()))
}
}

View File

@ -0,0 +1,63 @@
package aqua.model.topology
import aqua.model.ValueModel
import aqua.model.func.body.{OnTag, SeqGroupTag}
import cats.data.Chain
import cats.free.Cofree
case class Location(path: List[ChainZipper[Topology.Tree]] = Nil) {
def down(h: ChainZipper[Topology.Tree]): Location = copy(h :: path)
def lastOn: Option[OnTag] = path.map(_.current.head).collectFirst { case o: OnTag =>
o
}
def pathOn: List[OnTag] = path.map(_.current.head).collect { case o: OnTag =>
o
}
def pathViaChain: Chain[ValueModel] = Chain.fromSeq(
path
.map(_.current.head)
.collectFirst { case t: OnTag =>
t.via.toList
}
.toList
.flatten
)
def lastLeftSeq: Option[(ChainZipper[Topology.Tree], Location)] =
path match {
case (cz @ ChainZipper(prev, Cofree(_: SeqGroupTag, _), _)) :: tail if prev.nonEmpty =>
cz.moveLeft.map(_ -> Location(tail))
case _ :: tail => Location(tail).lastLeftSeq
case Nil => None
}
def lastRightSeq: Option[(ChainZipper[Topology.Tree], Location)] =
path match {
case (cz @ ChainZipper(_, Cofree(_: SeqGroupTag, _), next)) :: tail if next.nonEmpty =>
cz.moveRight.map(_ -> Location(tail))
case _ :: tail => Location(tail).lastRightSeq
case Nil => None
}
path.collectFirst {
case ChainZipper(prev, Cofree(_: SeqGroupTag, _), _) if prev.nonEmpty => prev.lastOption
}.flatten
}
object Location {
object Matchers {
object /: {
def unapply(loc: Location): Option[(ChainZipper[Topology.Tree], Location)] =
loc.path match {
case h :: tail => Some(h -> Location(tail))
case _ => None
}
}
}
}

View File

@ -0,0 +1,96 @@
package aqua.model.topology
import aqua.model.ValueModel
import aqua.model.func.body._
import cats.Eval
import cats.data.Chain
import cats.free.Cofree
import ChainZipper.Matchers._
import Location.Matchers._
object Topology {
type Tree = Cofree[Chain, OpTag]
// Walks through peer IDs, doing a noop function on each
// If same IDs are found in a row, does noop only once
// if there's a chain like a -> b -> c -> ... -> b -> g, remove everything between b and b
def through(peerIds: Chain[ValueModel]): Chain[Tree] =
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
}
.map(FuncOps.noop)
.map(_.tree)
def mapTag(tag: OpTag, loc: Location): OpTag = tag match {
case c: CallServiceTag if c.peerId.isEmpty =>
c.copy(peerId = loc.lastOn.map(_.peerId))
case t => t
}
def resolve(op: Tree): Tree =
Cofree
.cata[Chain, OpTag, Tree](resolveOnMoves(op)) {
case (SeqTag | _: OnTag, children) =>
Eval.later(
Cofree(
SeqTag,
Eval.now(children.flatMap {
case Cofree(SeqTag, ch) => ch.value
case cf => Chain.one(cf)
})
)
)
case (head, children) => Eval.later(Cofree(head, Eval.now(children)))
}
.value
def resolveOnMoves(op: Tree): Tree =
Cursor
.transform(op) {
case c @ Cursor(
cz @ `current`(cf),
loc @ `head`(parent: GroupTag) /: _
) =>
val cfu = cf.copy(mapTag(cf.head, loc))
val getThere = (cfu.head, loc.pathOn) match {
case (OnTag(pid, _), h :: _) if h.peerId == pid => Chain.empty[ValueModel]
case (OnTag(_, via), h :: _) =>
h.via.reverse ++ via
case (_, _) => Chain.empty[ValueModel]
}
val prevOn = c.prevOnTags
val prevPath = prevOn.map { case OnTag(_, v) =>
v.reverse
}
.flatMap(identity)
val nextOn = parent match {
case ParTag | XorTag => c.nextOnTags
case _ => Chain.empty[OnTag]
}
val nextPath = (if (nextOn.nonEmpty) getThere.reverse else Chain.empty) ++ nextOn.map {
case OnTag(_, v) =>
v.reverse
}
.flatMap(identity) ++ Chain.fromOption(
// Dirty fix for join behaviour
nextOn.lastOption.filter(_ => parent == ParTag).map(_.peerId)
)
if (prevOn.isEmpty && getThere.isEmpty) cfu :: Nil
else
(through(prevPath ++ loc.pathViaChain ++ getThere)
.append(cfu) ++ through(nextPath)).toList
case Cursor(ChainZipper(_, cf, _), loc) =>
cf.copy(mapTag(cf.head, loc)) :: Nil
}
.getOrElse(op)
}

View File

@ -1,106 +0,0 @@
package aqua.model.transform
import aqua.model.ValueModel
import aqua.model.func.body.{CallServiceTag, FuncOps, OnTag, OpTag, ParTag, SeqTag}
import cats.Eval
import cats.data.Chain
import cats.free.Cofree
object Topology {
type Tree = Cofree[Chain, OpTag]
def rightBoundary(root: Tree): List[OpTag] =
root.head :: root.tailForced.lastOption.fold(List.empty[OpTag])(rightBoundary)
// Walks through peer IDs, doing a noop function on each
// If same IDs are found in a row, does noop only once
// TODO: if there's a chain like a -> b -> c -> ... -> b -> g, remove everything between b and b
def through(peerIds: Chain[ValueModel]): Chain[Tree] =
peerIds
.foldLeft(Chain.empty[ValueModel]) {
case (acc, p) if acc.lastOption.contains(p) => acc
case (acc, p) => acc :+ p
}
.map(FuncOps.noop)
.map(_.tree)
// TODO: after topology is resolved, OnTag should be eliminated
def resolve(op: Tree): Tree =
transformWithPath(op) {
case (path, c: CallServiceTag, children) if c.peerId.isEmpty =>
Cofree[Chain, OpTag](
c.copy(peerId = path.collectFirst { case OnTag(peerId, _) =>
peerId
}),
children
)
case (path, tag @ OnTag(pid, via), children) =>
// Drop seq/par/xor from path
val pathOn = path.collect { case ot: OnTag =>
ot
}
pathOn match {
// If we are on the right node, do nothing
case Nil =>
Cofree[Chain, OpTag](tag, children)
case h :: _ if h.peerId == pid =>
Cofree[Chain, OpTag](tag, children)
case h :: _ =>
Cofree[Chain, OpTag](
tag,
// TODO: merge children, if possible
children.map(through(h.via.reverse ++ via) ++ _)
)
}
case (path, SeqTag, children) =>
// TODO if we have OnTag, and then something else, need to get back
// AND keep in mind that we will handle all the children with OnTag processor!
val pathViaChain = Chain.fromSeq(path.collectFirst { case t: OnTag =>
t.via.toList
}.toList.flatten)
def modifyChildrenList(list: List[Tree], prev: Option[Tree]): Chain[Tree] = list match {
case Nil => Chain.empty
case op :: tail =>
// TODO further improve
val prevPath = Chain
.fromSeq(prev.toList.flatMap(rightBoundary).takeWhile {
case ParTag => false
case _ => true
})
.collect { case OnTag(_, v) =>
v.reverse
}
if (prevPath.isEmpty) op +: modifyChildrenList(tail, Some(op))
else
through(prevPath.flatMap(identity) ++ pathViaChain).append(op) ++ modifyChildrenList(
tail,
Some(op)
)
case o :: ops => o +: modifyChildrenList(ops, Some(o))
}
Cofree[Chain, OpTag](
SeqTag,
children.map(_.toList).map(modifyChildrenList(_, None))
)
case (_, t, children) =>
Cofree[Chain, OpTag](t, children)
}
def transformWithPath(cf: Tree, path: List[OpTag] = Nil)(
f: (List[OpTag], OpTag, Eval[Chain[Tree]]) => Tree
): Tree = {
val newCf = f(path, cf.head, cf.tail)
Cofree[Chain, OpTag](
newCf.head,
newCf.tail.map(_.map(transformWithPath(_, newCf.head :: path)(f)))
)
}
}

View File

@ -3,6 +3,7 @@ package aqua.model.transform
import aqua.model.func.body._
import aqua.model.func.FuncCallable
import aqua.model.VarModel
import aqua.model.topology.Topology
import aqua.types.ScalarType
import cats.data.Chain
import cats.free.Cofree

View File

@ -0,0 +1,23 @@
package aqua.model.topology
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import Location.Matchers._
import ChainZipper.Matchers._
import aqua.model.func.body.SeqTag
import cats.Eval
import cats.data.Chain
import cats.free.Cofree
class LocationSpec extends AnyFlatSpec with Matchers {
"matchers" should "unapply correctly" in {
val loc =
Location(ChainZipper.one(Cofree(SeqTag, Eval.later(Chain.empty[Topology.Tree]))) :: Nil)
Option(loc).collect { case `head`(SeqTag) /: _ =>
true
} should be('defined)
}
}

View File

@ -1,4 +1,4 @@
package aqua.model.transform
package aqua.model.topology
import aqua.model.Node
import org.scalatest.flatspec.AnyFlatSpec
@ -20,14 +20,11 @@ class TopologySpec extends AnyFlatSpec with Matchers {
val proc: Node = Topology.resolve(init)
val expected = on(
initPeer,
relay :: Nil,
val expected =
seq(
call(1, initPeer),
call(2, initPeer)
)
)
proc should be(expected)
@ -50,19 +47,8 @@ class TopologySpec extends AnyFlatSpec with Matchers {
val proc: Node = Topology.resolve(init)
val expected = on(
initPeer,
relay :: Nil,
on(
otherPeer,
Nil,
through(relay),
seq(
call(1, otherPeer),
call(2, otherPeer)
)
)
)
val expected =
seq(through(relay), call(1, otherPeer), call(2, otherPeer))
proc should be(expected)
}
@ -84,20 +70,13 @@ class TopologySpec extends AnyFlatSpec with Matchers {
val proc: Node = Topology.resolve(init)
val expected = on(
initPeer,
relay :: Nil,
on(
otherPeer,
otherRelay :: Nil,
val expected =
seq(
through(relay),
through(otherRelay),
seq(
call(1, otherPeer),
call(2, otherPeer)
)
call(1, otherPeer),
call(2, otherPeer)
)
)
proc should be(expected)
}
@ -119,22 +98,15 @@ class TopologySpec extends AnyFlatSpec with Matchers {
val proc: Node = Topology.resolve(init)
val expected = on(
initPeer,
relay :: Nil,
val expected =
seq(
on(
otherPeer,
otherRelay :: Nil,
through(relay),
through(otherRelay),
call(1, otherPeer)
),
through(relay),
through(otherRelay),
call(1, otherPeer),
through(otherRelay),
through(relay),
call(2, initPeer)
)
)
// println(Console.BLUE + init)
// println(Console.YELLOW + proc)
@ -175,38 +147,25 @@ class TopologySpec extends AnyFlatSpec with Matchers {
val proc: Node = Topology.resolve(init)
val expected = on(
initPeer,
relay :: Nil,
val expected =
seq(
on(
through(relay),
through(otherRelay),
call(0, otherPeer),
through(otherRelay),
call(1, otherPeer2),
_match(
otherPeer,
otherRelay :: Nil,
through(relay),
through(otherRelay),
call(0, otherPeer),
on(
otherPeer2,
otherRelay :: Nil,
otherRelay,
seq(
through(otherRelay),
call(1, otherPeer2),
_match(
otherPeer,
otherRelay,
on(
otherPeer,
otherRelay :: Nil,
through(otherRelay),
call(2, otherPeer)
)
)
call(2, otherPeer)
)
),
through(otherRelay),
through(relay),
call(3, initPeer)
)
)
// println(Console.BLUE + init)
// println(Console.YELLOW + proc)

View File

@ -32,17 +32,17 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val expectedFC =
xor(
on(
initPeer,
relayV :: Nil,
seq(
dataCall(bc, "relay", initPeer),
on(otherPeer, Nil, through(relayV), call(1, otherPeer)),
through(relayV),
on(initPeer, relayV :: Nil, respCall(bc, ret, initPeer))
)
seq(
dataCall(bc, "relay", initPeer),
through(relayV),
call(1, otherPeer),
through(relayV),
respCall(bc, ret, initPeer)
),
on(initPeer, relayV :: Nil, xorErrorCall(bc, initPeer))
seq(
through(relayV),
xorErrorCall(bc, initPeer)
)
)
procFC.equalsOrPrintDiff(expectedFC) should be(true)
@ -70,20 +70,18 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val expectedFC =
xor(
on(
initPeer,
relayV :: Nil,
seq(
dataCall(bc, "relay", initPeer),
seq(
call(0, initPeer),
on(otherPeer, Nil, through(relayV), call(1, otherPeer))
),
through(relayV),
on(initPeer, relayV :: Nil, respCall(bc, ret, initPeer))
)
seq(
dataCall(bc, "relay", initPeer),
call(0, initPeer),
through(relayV),
call(1, otherPeer),
through(relayV),
respCall(bc, ret, initPeer)
),
on(initPeer, relayV :: Nil, xorErrorCall(bc, initPeer))
seq(
through(relayV),
xorErrorCall(bc, initPeer)
)
)
procFC.equalsOrPrintDiff(expectedFC) should be(true)
@ -137,25 +135,17 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val res = Transform.forClient(f2, bc): Node
res.equalsOrPrintDiff(
on(
initPeer,
relayV :: Nil,
seq(
dataCall(bc, "relay", initPeer),
Node(
CallServiceTag(
LiteralModel("\"srv1\""),
"foo",
Call(Nil, Some(Call.Export("v", ScalarType.string))),
Some(initPeer)
)
),
on(
initPeer,
relayV :: Nil,
respCall(bc, VarModel("v", ScalarType.string), initPeer)
seq(
dataCall(bc, "relay", initPeer),
Node(
CallServiceTag(
LiteralModel("\"srv1\""),
"foo",
Call(Nil, Some(Call.Export("v", ScalarType.string))),
Some(initPeer)
)
)
),
respCall(bc, VarModel("v", ScalarType.string), initPeer)
)
) should be(true)
}

View File

@ -1 +1 @@
sbt.version=1.5.0
sbt.version=1.5.1