Skip to content

Commit

Permalink
NODE-2273: /utils/script/evaluate API route (wavesplatform#3305)
Browse files Browse the repository at this point in the history
  • Loading branch information
Karasiq authored Dec 14, 2020
1 parent e54a1ad commit f4c9fe5
Show file tree
Hide file tree
Showing 20 changed files with 493 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ object JavaAdapter {

def compile(input: String): EXPR = {
ExpressionCompiler
.compile(input, ctx)
.compileBoolean(input, ctx)
.fold(
error => throw new IllegalArgumentException(error),
res => res
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import com.wavesplatform.lang.script.ContractScript.ContractScriptImpl
import com.wavesplatform.lang.script.v1.ExprScript
import com.wavesplatform.lang.script.{ContractScript, Script}
import com.wavesplatform.lang.utils
import com.wavesplatform.lang.v1.BaseGlobal.DAppInfo
import com.wavesplatform.lang.v1.BaseGlobal.ArrayView
import com.wavesplatform.lang.v1.BaseGlobal.{ArrayView, DAppInfo}
import com.wavesplatform.lang.v1.compiler.CompilationError.Generic
import com.wavesplatform.lang.v1.compiler.Terms.EXPR
import com.wavesplatform.lang.v1.compiler.Types.FINAL
Expand Down Expand Up @@ -133,7 +132,7 @@ trait BaseGlobal {
}

val compileExpression =
compile(_, _, _, _, ExpressionCompiler.compile)
compile(_, _, _, _, ExpressionCompiler.compileBoolean)

val compileDecls =
compile(_, _, _, _, ExpressionCompiler.compileDecls)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,47 @@ package com.wavesplatform.lang.v1.compiler
import cats.Monoid
import com.wavesplatform.lang.v1.FunctionHeader
import com.wavesplatform.lang.v1.compiler.CompilerContext._
import com.wavesplatform.lang.v1.compiler.Types.{CASETYPEREF, FINAL}
import com.wavesplatform.lang.v1.compiler.Types._
import com.wavesplatform.lang.v1.evaluator.Contextful.NoContext
import com.wavesplatform.lang.v1.evaluator.ctx.{BaseFunction, FunctionTypeSignature}
import com.wavesplatform.lang.v1.parser.Expressions.Pos
import com.wavesplatform.lang.v1.parser.Expressions.Pos.AnyPos
import shapeless._

case class CompilerContext(predefTypes: Map[String, FINAL], varDefs: VariableTypes, functionDefs: FunctionTypes, tmpArgsIdx: Int = 0) {
case class CompilerContext(
predefTypes: Map[String, FINAL],
varDefs: VariableTypes,
functionDefs: FunctionTypes,
tmpArgsIdx: Int = 0,
arbitraryFunctions: Boolean = false
) {
private lazy val allFuncDefs: FunctionTypes =
predefTypes.collect {
case (_, t @ CASETYPEREF(typeName, fields, false)) =>
case (_, CASETYPEREF(typeName, fields, false)) =>
typeName ->
FunctionInfo(AnyPos, List(FunctionTypeSignature(CASETYPEREF(typeName, fields), fields, FunctionHeader.User(typeName))))
} ++ functionDefs

def functionTypeSignaturesByName(name: String): List[FunctionTypeSignature] = allFuncDefs.getOrElse(name, FunctionInfo(AnyPos, List.empty)).fSigList
private def resolveFunction(name: String): FunctionInfo =
if (arbitraryFunctions) {
val primitives = List(LONG, BYTESTR, BOOLEAN, STRING)
val maybeAllTypes = UNION(
UNION(primitives),
UNION.create(predefTypes.values.toSeq),
LIST(UNION(primitives))
)
def signature(name: String, i: Int) = {
FunctionTypeSignature(maybeAllTypes, Seq.fill(i)(("arg", maybeAllTypes)), FunctionHeader.User(name))
}
allFuncDefs
.withDefault(name => FunctionInfo(AnyPos, (0 to 22).map(i => signature(name, i)).toList))
.apply(name)
} else {
allFuncDefs.getOrElse(name, FunctionInfo(AnyPos, List.empty))
}

def functionTypeSignaturesByName(name: String): List[FunctionTypeSignature] =
resolveFunction(name).fSigList

def getSimpleContext(): Map[String, Pos] = {
(varDefs.map(el => el._1 -> el._2.pos) ++ functionDefs.map(el => el._1 -> el._2.pos))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,23 @@ object ExpressionCompiler {
errors: Iterable[CompilationError] = Iterable.empty
)

def compile(input: String, ctx: CompilerContext): Either[String, EXPR] = {
def compile(input: String, ctx: CompilerContext): Either[String, (EXPR, FINAL)] = {
Parser.parseExpr(input) match {
case fastparse.Parsed.Success(xs, _) =>
ExpressionCompiler(ctx, xs) match {
case Left(err) => Left(err.toString)
case Right((expr, BOOLEAN)) => Right(expr)
case Right((_, _)) => Left("Script should return boolean")
}
case fastparse.Parsed.Success(xs, _) => ExpressionCompiler(ctx, xs)
case f @ fastparse.Parsed.Failure(_, _, _) => Left(f.toString)
}
}

def compileBoolean(input: String, ctx: CompilerContext): Either[String, EXPR] = {
compile(input, ctx).flatMap {
case (expr, BOOLEAN) => Right(expr)
case (_, _) => Left("Script should return boolean")
}
}

def compileUntyped(input: String, ctx: CompilerContext): Either[String, EXPR] = {
Parser.parseExpr(input) match {
case fastparse.Parsed.Success(xs, _) =>
ExpressionCompiler(ctx, xs) match {
case Left(err) => Left(err.toString)
case Right((expr, _)) => Right(expr)
}
case f @ fastparse.Parsed.Failure(_, _, _) => Left(f.toString)
}
compile(input, ctx)
.map { case (expr, _) => expr }
}

def compileWithParseResult(
Expand Down Expand Up @@ -98,10 +93,7 @@ object ExpressionCompiler {

def compileDecls(input: String, ctx: CompilerContext): Either[String, EXPR] = {
val adjustedDecls = s"$input\n${PureContext.unitVarName}"
Parser.parseExpr(adjustedDecls) match {
case fastparse.Parsed.Success(xs, _) => ExpressionCompiler(ctx, xs).map(_._1)
case f @ fastparse.Parsed.Failure(_, _, _) => Left(f.toString)
}
compileUntyped(adjustedDecls, ctx)
}

def compileExpr(expr: Expressions.EXPR): CompileM[(Terms.EXPR, FINAL, Expressions.EXPR)] =
Expand Down Expand Up @@ -224,26 +216,30 @@ object ExpressionCompiler {
): CompileM[CompilationStepResultExpr] =
for {
ctx <- get[Id, CompilerContext, CompilationError]
_ <- {
_ <- {
val types = ctx.predefTypes.keySet
val typeNamedCases =
cases.collect {
case MATCH_CASE(_, Some(PART.VALID(_, name)), _, _, _, _) if types.contains(name) => name
}

Either.cond(
typeNamedCases.isEmpty,
(),
TypeNamedCases(p.start, p.end, typeNamedCases)
).toCompileM
Either
.cond(
typeNamedCases.isEmpty,
(),
TypeNamedCases(p.start, p.end, typeNamedCases)
)
.toCompileM
}
_ <- {
_ <- {
val defaultCasesCount = cases.count(_.caseType.isEmpty)
Either.cond(
defaultCasesCount < 2,
(),
MultipleDefaultCases(p.start, p.end, defaultCasesCount)
).toCompileM
Either
.cond(
defaultCasesCount < 2,
(),
MultipleDefaultCases(p.start, p.end, defaultCasesCount)
)
.toCompileM
}
typedExpr <- compileExprWithCtx(expr, saveExprContext)
exprTypesWithErr <- (typedExpr.t match {
Expand Down Expand Up @@ -295,14 +291,14 @@ object ExpressionCompiler {
(matchTypes, exprTypes)
}
matchedTypesUnion = UNION.create(checktypes._1)
checkWithErr <-
Either.cond(
(cases.last.caseType.isEmpty && (checktypes._2 >= matchedTypesUnion)) || (checktypes._2 equivalent matchedTypesUnion),
(),
MatchNotExhaustive(p.start, p.end, exprTypes.typeList, matchTypes)
)
.toCompileM
.handleError()
checkWithErr <- Either
.cond(
(cases.last.caseType.isEmpty && (checktypes._2 >= matchedTypesUnion)) || (checktypes._2 equivalent matchedTypesUnion),
(),
MatchNotExhaustive(p.start, p.end, exprTypes.typeList, matchTypes)
)
.toCompileM
.handleError()
_ <- set[Id, CompilerContext, CompilationError](ctx.copy(tmpArgsIdx = tmpArgId))

errorList = exprTypesWithErr._2 ++ ifCasesWithErr._2 ++ compiledMatch.errors ++ checkWithErr._2
Expand All @@ -329,7 +325,7 @@ object ExpressionCompiler {
val refIsOverlappedByDecl =
decl.name match {
case PART.VALID(_, name) if name == ref => true
case _ => false
case _ => false
}
if (refIsOverlappedByDecl) false
else {
Expand All @@ -341,7 +337,7 @@ object ExpressionCompiler {
val refIsOverlappedByArg =
args.exists {
case (PART.VALID(_, name), _) if name == ref => true
case _ => false
case _ => false
}
if (!refIsOverlappedByArg) exprContainsRef(expr, ref)
else false
Expand All @@ -350,9 +346,9 @@ object ExpressionCompiler {
}

case Expressions.IF(_, cond, ifTrue, ifFalse, _, _) =>
exprContainsRef(cond, ref) ||
exprContainsRef(ifTrue, ref) ||
exprContainsRef(ifFalse, ref)
exprContainsRef(cond, ref) ||
exprContainsRef(ifTrue, ref) ||
exprContainsRef(ifFalse, ref)

case Expressions.FUNCTION_CALL(_, _, args, _, _) =>
args.exists(exprContainsRef(_, ref))
Expand All @@ -365,13 +361,13 @@ object ExpressionCompiler {

case Expressions.MATCH(_, matchingExpr, cases, _, _) =>
exprContainsRef(matchingExpr, ref) ||
cases.exists {
case MATCH_CASE(_, Some(PART.VALID(_, varName)), _, caseExpr, _, _) if varName != ref =>
exprContainsRef(caseExpr, ref)
case MATCH_CASE(_, None, _, caseExpr, _, _) =>
exprContainsRef(caseExpr, ref)
case _ => false
}
cases.exists {
case MATCH_CASE(_, Some(PART.VALID(_, varName)), _, caseExpr, _, _) if varName != ref =>
exprContainsRef(caseExpr, ref)
case MATCH_CASE(_, None, _, caseExpr, _, _) =>
exprContainsRef(caseExpr, ref)
case _ => false
}

case _ => false
}
Expand Down Expand Up @@ -412,7 +408,7 @@ object ExpressionCompiler {
compiledLet <- compileExprWithCtx(let.value, saveExprContext)
ctx <- get[Id, CompilerContext, CompilationError]

letType = let.types.getOrElse(compiledLet.t)
letType = let.types.getOrElse(compiledLet.t)
errorList = letNameWithErr._2
parseNodeDecl = let.copy(value = compiledLet.parseNodeExpr)

Expand Down Expand Up @@ -444,7 +440,7 @@ object ExpressionCompiler {
.traverse {
case (argName, argType) =>
for {
name <- handlePart(argName)
name <- handlePart(argName)
handledType <- handleCompositeType(p, argType, None, Some(name))
} yield (name, VariableInfo(argName.position, handledType))
}
Expand Down Expand Up @@ -754,25 +750,26 @@ object ExpressionCompiler {
}

private def handleCompositeType(
pos: Pos,
t: Expressions.Type,
expectedType: Option[FINAL],
varName: Option[String]
pos: Pos,
t: Expressions.Type,
expectedType: Option[FINAL],
varName: Option[String]
): CompileM[FINAL] =
t match {
case Expressions.Single(name, parameter) =>
for {
ctx <- get[Id, CompilerContext, CompilationError]
handledName <- handlePart(name)
ctx <- get[Id, CompilerContext, CompilationError]
handledName <- handlePart(name)
handledParameter <- parameter.traverse(handlePart)
expectedTypes = expectedType.fold(ctx.predefTypes.keys.toList)(_.typeList.map(_.name))
parameter <- handledParameter.traverse(handleCompositeType(pos, _, expectedType, varName))
t <- liftEither[Id, CompilerContext, CompilationError, FINAL](parameter.fold(flatSingle(pos, ctx.predefTypes, expectedTypes, varName, handledName).map(v => UNION.reduce(UNION.create(v, None)))) {
p =>
t <- liftEither[Id, CompilerContext, CompilationError, FINAL](
parameter.fold(flatSingle(pos, ctx.predefTypes, expectedTypes, varName, handledName).map(v => UNION.reduce(UNION.create(v, None)))) { p =>
for {
typeConstr <- findGenericType(pos, handledName)
} yield typeConstr(p)
})
}
)
} yield t
case Expressions.Union(types) =>
types.toList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,29 @@ object ContractEvaluator {
caller: Recipient.Address,
callerPk: ByteStr,
payments: AttachedPayments,
dappAddress: ByteStr,
transactionId: ByteStr,
fee: Long,
feeAssetId: Option[ByteStr]
)

def buildSyntheticCall(contract: DApp, call: EXPR): EXPR = {
val callables = contract.callableFuncs.flatMap { cf =>
val argName = cf.annotation.invocationArgName
val invocation = Invocation(
null,
Recipient.Address(ByteStr(new Array[Byte](26))),
ByteStr(new Array[Byte](32)),
AttachedPayments.Single(None),
ByteStr(new Array[Byte](32)),
0L,
None
)
LET(argName, Bindings.buildInvocation(invocation, StdLibVersion.VersionDic.default)) :: cf.u :: Nil
}

foldDeclarations(contract.decs ++ callables, BLOCK(LET("__synthetic_call", TRUE), call))
}

private def buildExprFromInvocation(c: DApp, i: Invocation, version: StdLibVersion): Either[String, EXPR] = {
val functionName = i.funcCall.function.funcName

Expand Down Expand Up @@ -118,15 +135,16 @@ object ContractEvaluator {
case (buildingExpr, (letName, letValue)) =>
BLOCK(LET(letName, letValue.value.value.explicitGet()), buildingExpr)
}
EvaluatorV2.applyLimited(exprWithLets, limit, ctx, version)
EvaluatorV2
.applyLimited(exprWithLets, limit, ctx, version)
.flatMap {
case (expr, unusedComplexity, log) =>
val result =
expr match {
case value: EVALUATED => ScriptResult.fromObj(ctx, transactionId, value, version)
case expr: EXPR => Right(IncompleteResult(expr, unusedComplexity))
}
result.bimap((_, log), (_, log))
result.bimap((_, log), (_, log))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ class ContractIntegrationTest extends PropSpec with PropertyChecks with ScriptGe
Recipient.Address(callerAddress),
callerPublicKey,
AttachedPayments.Single(None),
ByteStr.empty,
transactionId,
fee,
feeAssetId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ class EvaluatorV1V2Test extends PropSpec with PropertyChecks with Matchers with
evalWithLogging(
context.evaluationContext(Common.emptyBlockchainEnvironment()),
ExpressionCompiler
.compile(script, context.compilerContext)
.compileBoolean(script, context.compilerContext)
.explicitGet()
).map {
case (CONST_BOOLEAN(b), log) => (b, log)
Expand Down Expand Up @@ -1011,7 +1011,7 @@ class EvaluatorV1V2Test extends PropSpec with PropertyChecks with Matchers with
evalWithLogging(
context.evaluationContext[Id](Common.emptyBlockchainEnvironment()),
ExpressionCompiler
.compile(script, context.compilerContext)
.compileBoolean(script, context.compilerContext)
.explicitGet()
).map {
case (CONST_BOOLEAN(b), log) => (b, log)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class IntegrationTest extends PropSpec with PropertyChecks with ScriptGen with M
}

def compile(script: String): Either[String, Terms.EXPR] =
ExpressionCompiler.compile(script, CTX.empty.compilerContext)
ExpressionCompiler.compileBoolean(script, CTX.empty.compilerContext)

property("wrong script return type") {
compile("1") should produce("should return boolean")
Expand Down
3 changes: 3 additions & 0 deletions node/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ waves {
transactions-by-address-limit = 1000
distribution-address-limit = 1000

# The limit of complexity of a function that is not Callable (/utils/script/evaluate)
evaluate-script-complexity-limit = 4000

# Max number of time-limited pool threads (script compile/decompile/estimate)
limited-pool-threads = 2
}
Expand Down
Loading

0 comments on commit f4c9fe5

Please sign in to comment.