Skip to content

Commit

Permalink
SC-727 REPL with imports (#3458)
Browse files Browse the repository at this point in the history
* Fix typo

* SC-727 REPL imports

* Improve and test error message for lets restriction
  • Loading branch information
xrtm000 authored Apr 28, 2021
1 parent f719835 commit f87d95d
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 41 deletions.
5 changes: 4 additions & 1 deletion lang/js/src/main/scala/JsAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,10 @@ object JsAPI {
def nodeVersion(): js.Dynamic = js.Dynamic.literal("version" -> Version.VersionString)

@JSExportTopLevel("repl")
def repl(settings: UndefOr[NodeConnectionSettings]): js.Dynamic = asJs(Repl(settings.toOption))
def repl(
settings: UndefOr[NodeConnectionSettings],
libraries: js.Array[String] = js.Array()
): js.Dynamic = asJs(Repl(settings.toOption, libraries.toList))

private def asJs(repl: Repl): js.Dynamic =
jObj(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ case class EvaluationContext[C[_[_]], F[_]](
typeDefs : Map[String, FINAL],
letDefs : Map[String, LazyVal[F]],
functions: Map[FunctionHeader, BaseFunction[C]]
)
) {
def mapK[G[_] : Monad](f: F ~> G): EvaluationContext[C, G] =
EvaluationContext(
environment.asInstanceOf[C[G]],
typeDefs,
letDefs.view.mapValues(_.mapK(f)).toMap,
functions
)
}

case class LoggedEvaluationContext[C[_[_]], F[_]](l: LetLogCallback[F], ec: EvaluationContext[C, F]) {
val loggedLets: util.IdentityHashMap[LET, Unit] = new util.IdentityHashMap()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.wavesplatform.lang.v1.repl

import cats.{Functor, Monoid}
import cats.arrow.FunctionK
import cats.implicits._
import cats.{Functor, Id, Monoid}
import com.wavesplatform.lang.v1.compiler.CompilerContext
import com.wavesplatform.lang.v1.evaluator.ctx.EvaluationContext
import com.wavesplatform.lang.v1.repl.node.ErrorMessageEnvironment
import com.wavesplatform.lang.v1.repl.node.http.NodeConnectionSettings
import com.wavesplatform.lang.v1.traits.Environment
import monix.execution.atomic.Atomic
Expand All @@ -13,27 +15,30 @@ import scala.concurrent.Future

case class Repl(
settings: Option[NodeConnectionSettings] = None,
lastСontext: (CompilerContext, EvaluationContext[Environment, Future]) =
libraries: List[String] = Nil,
lastContext: (CompilerContext, EvaluationContext[Environment, Future]) =
(CompilerContext.empty, Monoid[EvaluationContext[Environment, Future]].empty)
) {
private val environment = buildEnvironment(settings)
private val initialState = state(
(
lastСontext._1 |+| initialCtx.compilerContext,
lastСontext._2 |+| initialCtx.evaluationContext(environment)
lastContext._1 |+| initialCtx.compilerContext,
lastContext._2 |+| initialCtx.evaluationContext(environment)
),
view
)
private val currentState = Atomic(initialState)
private val engine = new ReplEngine[Future]()

if (libraries.nonEmpty) initLibraries()

private def state[S, V](s: S, view: S => V): (S, V) = (s, view(s))
private def view(ctx: (CompilerContext, EvaluationContext[Environment, Future])) = StateView(ctx._1)
private def view(ctx: (CompilerContext, Any)) = StateView(ctx._1)

def clear(): Unit = currentState.set(initialState)

def reconfigure(settings: NodeConnectionSettings): Repl =
Repl(Some(settings), currentState.get()._1)
Repl(Some(settings), libraries, currentState.get()._1)

def info(str: String): String = currentState.get()._2.declMap(str)

Expand All @@ -50,6 +55,31 @@ case class Repl(
}: Future[(Either[String, String], (CompilerContext, EvaluationContext[Environment, Future]))]
)

def initLibraries(): Unit = {
val libraryState = state(
(
initialCtx.compilerContext,
initialCtx.evaluationContext(ErrorMessageEnvironment[Id]("Blockchain interaction using lets from libraries is prohibited, use functions instead"))
),
view
)
perform[Id, (CompilerContext, EvaluationContext[Environment, Id]), Either[String, String], StateView](
Atomic(libraryState),
view,
oldCtx =>
new ReplEngine[Id]()
.eval(libraries.mkString("\n"), oldCtx._1, oldCtx._2)
.fold(
e => throw new RuntimeException(e), {
case (r, ctx @ (compilerCtx, evaluationCtx)) =>
val mappedCtx = evaluationCtx.mapK(λ[FunctionK[Id, Future]](Future.successful(_))) |+| initialCtx.evaluationContext(environment)
currentState.set(state((compilerCtx, mappedCtx), view))
(Right(r), ctx)
}
)
)
}

private def perform[F[_] : Functor, S, R, V](
value: Atomic[(S, V)],
view: S => V,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
package com.wavesplatform.lang.v1.repl.node

import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.lang.script.Script
import com.wavesplatform.lang.v1.compiler.Terms.EVALUATED
import com.wavesplatform.lang.v1.traits.Environment.InputEntity
import com.wavesplatform.lang.v1.traits.domain.{BlockInfo, Recipient, ScriptAssetInfo, Tx}
import com.wavesplatform.lang.v1.traits.domain.Recipient.Address
import com.wavesplatform.lang.v1.traits.domain.{BlockInfo, Recipient, ScriptAssetInfo, Tx}
import com.wavesplatform.lang.v1.traits.{DataType, Environment}
import com.wavesplatform.lang.v1.compiler.Terms.EVALUATED
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.lang.script.Script
import monix.eval.Coeval

import scala.concurrent.Future

object ErrorMessageEnvironment extends Environment[Future] {
lazy val unavailable = throw new BlockchainUnavailableException()
override def chainId: Byte = 0
override def height: Future[Long] = unavailable
override def inputEntity: InputEntity = unavailable
override def tthis: Environment.Tthis = unavailable
override def transactionById(id: Array[Byte]): Future[Option[Tx]] = unavailable
override def transferTransactionById(id: Array[Byte]): Future[Option[Tx.Transfer]] = unavailable
override def transactionHeightById(id: Array[Byte]): Future[Option[Long]] = unavailable
override def assetInfoById(d: Array[Byte]): Future[Option[ScriptAssetInfo]] = unavailable
override def lastBlockOpt(): Future[Option[BlockInfo]] = unavailable
override def blockInfoByHeight(height: Int): Future[Option[BlockInfo]] = unavailable
override def data(addressOrAlias: Recipient, key: String, dataType: DataType): Future[Option[Any]] = unavailable
override def hasData(addressOrAlias: Recipient): Future[Boolean] = unavailable
override def resolveAlias(name: String): Future[Either[String, Recipient.Address]] = unavailable
override def accountBalanceOf(addressOrAlias: Recipient, assetId: Option[Array[Byte]]): Future[Either[String, Long]] = unavailable
override def accountWavesBalanceOf(addressOrAlias: Recipient): Future[Either[String, Environment.BalanceDetails]] = unavailable
override def multiPaymentAllowed: Boolean = unavailable
override def txId: ByteStr = unavailable
override def transferTransactionFromProto(b: Array[Byte]): Future[Option[Tx.Transfer]] = unavailable
override def addressFromString(address: String): Either[String, Recipient.Address] = unavailable
override def accountScript(addressOrAlias: Recipient): Future[Option[Script]] = unavailable
override def callScript(dApp: Address, func: String, args: List[EVALUATED], payments: Seq[(Option[Array[Byte]], Long)], availableComplexity: Int): Coeval[Future[(Either[ValidationError, EVALUATED], Int)]] = unavailable
case class ErrorMessageEnvironment[F[_]](message: String) extends Environment[F] {
lazy val unavailable = throw new BlockchainUnavailableException(message)
override def chainId: Byte = 0
override def height: F[Long] = unavailable
override def inputEntity: InputEntity = unavailable
override def tthis: Environment.Tthis = unavailable
override def transactionById(id: Array[Byte]): F[Option[Tx]] = unavailable
override def transferTransactionById(id: Array[Byte]): F[Option[Tx.Transfer]] = unavailable
override def transactionHeightById(id: Array[Byte]): F[Option[Long]] = unavailable
override def assetInfoById(d: Array[Byte]): F[Option[ScriptAssetInfo]] = unavailable
override def lastBlockOpt(): F[Option[BlockInfo]] = unavailable
override def blockInfoByHeight(height: Int): F[Option[BlockInfo]] = unavailable
override def data(addressOrAlias: Recipient, key: String, dataType: DataType): F[Option[Any]] = unavailable
override def hasData(addressOrAlias: Recipient): F[Boolean] = unavailable
override def resolveAlias(name: String): F[Either[String, Recipient.Address]] = unavailable
override def accountBalanceOf(addressOrAlias: Recipient, assetId: Option[Array[Byte]]): F[Either[String, Long]] = unavailable
override def accountWavesBalanceOf(addressOrAlias: Recipient): F[Either[String, Environment.BalanceDetails]] = unavailable
override def multiPaymentAllowed: Boolean = unavailable
override def txId: ByteStr = unavailable
override def transferTransactionFromProto(b: Array[Byte]): F[Option[Tx.Transfer]] = unavailable
override def addressFromString(address: String): Either[String, Recipient.Address] = unavailable
override def accountScript(addressOrAlias: Recipient): F[Option[Script]] = unavailable
override def callScript(
dApp: Address,
func: String,
args: List[EVALUATED],
payments: Seq[(Option[Array[Byte]], Long)],
availableComplexity: Int
): Coeval[F[(Either[ValidationError, EVALUATED], Int)]] = unavailable
}

class BlockchainUnavailableException extends RuntimeException {
override def toString: String = "Blockchain state is unavailable from REPL"
case class BlockchainUnavailableException(message: String) extends RuntimeException {
override def toString: String = message
}

Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ package object repl {
WavesContext.build(global, directives)

def buildEnvironment(settings: Option[NodeConnectionSettings]): Environment[Future] =
settings.fold(ErrorMessageEnvironment: Environment[Future])(WebEnvironment)
settings.fold(ErrorMessageEnvironment[Future]("Blockchain state is unavailable from REPL"): Environment[Future])(WebEnvironment)
}
38 changes: 38 additions & 0 deletions lang/tests/src/test/scala/com/wavesplatform/lang/v1/ReplTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wavesplatform.lang.v1

import com.wavesplatform.lang.Common.{NoShrink, produce}
import com.wavesplatform.lang.v1.repl.Repl
import com.wavesplatform.lang.v1.repl.node.BlockchainUnavailableException
import com.wavesplatform.lang.v1.repl.node.http.NodeConnectionSettings
import com.wavesplatform.lang.v1.testing.ScriptGen
import org.scalatest.{Matchers, PropSpec}
Expand Down Expand Up @@ -206,4 +207,41 @@ class ReplTest extends PropSpec with ScriptGen with Matchers with NoShrink {
await(repl.execute("FOLD<1>()")) shouldBe Left("Can't parse 'FOLD<1>()'")
await(repl.execute("getInteger(")) shouldBe Left("Can't parse 'getInteger('")
}

property("libraries") {
val address = "3MpLKVSnWSY53bSNTECuGvESExzhV9ppcun"
val settings = NodeConnectionSettings("testnodes.wavesnodes.com", 'T'.toByte, address)
val repl = Repl(
Some(settings),
libraries = List(
"""
|func f1(a: Int, b: Int) = a * a + b * b
|func f2(a: Int, b: Int) = a * a - b * b
|
|let a = 12345
""".stripMargin,
"""
|let b = 678
|
|func g1() = this.bytes.toBase58String()
""".stripMargin
)
)
await(repl.execute("f1(4, 3)")) shouldBe Right("res1: Int = 25")
await(repl.execute("f2(4, 3)")) shouldBe Right("res2: Int = 7")
await(repl.execute(""" g1() """)) shouldBe Right(s"""res3: String = "$address"""")
await(repl.execute("a")) shouldBe Right("res4: Int = 12345")
await(repl.execute("b")) shouldBe Right("res5: Int = 678")
}

property("blockchain interaction using lets from libraries is prohibited") {
val address = "3MpLKVSnWSY53bSNTECuGvESExzhV9ppcun"
val settings = NodeConnectionSettings("testnodes.wavesnodes.com", 'T'.toByte, address)
val repl = Repl(
Some(settings),
libraries = List("let a = this")
)
(the[BlockchainUnavailableException] thrownBy await(repl.execute("a"))).toString shouldBe
"Blockchain interaction using lets from libraries is prohibited, use functions instead"
}
}

0 comments on commit f87d95d

Please sign in to comment.