Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NODE-2606 Fix scopes processing for script estimation #3883

Merged
merged 8 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wavesplatform.lang.v1.estimator.v3

import cats.implicits.{toBifunctorOps, toFoldableOps, toTraverseOps}
import cats.{Id, Monad}
import cats.syntax.functor.*
import com.wavesplatform.lang.v1.FunctionHeader
import com.wavesplatform.lang.v1.FunctionHeader.User
import com.wavesplatform.lang.v1.compiler.Terms.*
Expand Down Expand Up @@ -63,7 +64,7 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean, letFixes:
for {
_ <- if (globalDeclarationsMode) saveGlobalLetCost(let, activeFuncArgs) else doNothing
startCtx <- get[Id, EstimatorContext, EstimationError]
letEval = evalHoldingFuncs(let.value, activeFuncArgs)
letEval = evalHoldingFuncs(let.value, activeFuncArgs, Some(startCtx.funcs))
_ <- beforeNextExprEval(let, letEval)
nextExprCost <- evalExpr(nextExpr, activeFuncArgs, globalDeclarationsMode)
nextExprCtx <- get[Id, EstimatorContext, EstimationError]
Expand All @@ -75,9 +76,17 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean, letFixes:
private def saveGlobalLetCost(let: LET, activeFuncArgs: Set[String]): EvalM[Unit] = {
val costEvaluation =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
(bodyCost, usedRefs) <- withUsedRefs(evalExpr(let.value, activeFuncArgs))
ctx <- get[Id, EstimatorContext, EstimationError]
letCosts <- usedRefs.toSeq.traverse(ctx.globalLetEvals.getOrElse(_, zero))
letCosts <- usedRefs.toSeq.traverse { ref =>
local {
for {
_ <- update(funcs.set(_)(startCtx.funcs))
cost <- ctx.globalLetEvals.getOrElse(ref, zero)
} yield cost
}
}
} yield bodyCost + letCosts.sum
for {
cost <- local(costEvaluation)
Expand Down Expand Up @@ -166,15 +175,15 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean, letFixes:

private def evalFuncCall(header: FunctionHeader, args: List[EXPR], activeFuncArgs: Set[String]): EvalM[Long] =
for {
ctx <- get[Id, EstimatorContext, EstimationError]
(bodyCost, bodyUsedRefs) <- getFuncCost(header, ctx)
_ <- setFuncToCtx(header, bodyCost, bodyUsedRefs)
(argsCosts, argsUsedRefs) <- withUsedRefs(args.traverse(evalHoldingFuncs(_, activeFuncArgs)))
argsCostsSum <- argsCosts.foldM(0L)(sum)
ctx <- get[Id, EstimatorContext, EstimationError]
(bodyCost, bodyUsedRefs) <- getFuncCost(header, ctx)
_ <- setFuncToCtx(header, bodyCost, bodyUsedRefs)
(argsCosts, _) <- withUsedRefs(args.traverse(evalHoldingFuncs(_, activeFuncArgs)))
argsCostsSum <- argsCosts.foldM(0L)(sum)
bodyCostV = bodyCost.value()
correctedBodyCost =
if (!overhead && !letFixes && bodyCostV == 0) 1
else if (letFixes && bodyCostV == 0 && isBlankFunc(bodyUsedRefs ++ argsUsedRefs, ctx.refsCosts)) 1
else if (letFixes && bodyCostV == 0 && isBlankFunc(bodyUsedRefs, ctx.refsCosts)) 1
else bodyCostV
result <- sum(argsCostsSum, correctedBodyCost)
} yield result
Expand All @@ -201,9 +210,14 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean, letFixes:
private def isBlankFunc(usedRefs: Set[String], refsCosts: Map[String, Long]): Boolean =
!usedRefs.exists(refsCosts.get(_).exists(_ > 0))

private def evalHoldingFuncs(expr: EXPR, activeFuncArgs: Set[String]): EvalM[Long] =
private def evalHoldingFuncs(
expr: EXPR,
activeFuncArgs: Set[String],
ctxFuncsOpt: Option[Map[FunctionHeader, (Coeval[Long], Set[String])]] = None
): EvalM[Long] =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
_ <- ctxFuncsOpt.fold(doNothing.void)(ctxFuncs => update(funcs.set(_)(ctxFuncs)))
cost <- evalExpr(expr, activeFuncArgs)
_ <- update(funcs.set(_)(startCtx.funcs))
} yield cost
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.wavesplatform.lang.compiler
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.lang.API
import com.wavesplatform.lang.CompileAndParseResult.{Contract, Expression}
import com.wavesplatform.lang.CompileResult.DApp
import com.wavesplatform.lang.v1.compiler.CompilationError
import com.wavesplatform.lang.v1.compiler.CompilationError.{DefNotFound, Generic}
import com.wavesplatform.lang.v1.estimator.ScriptEstimator
Expand Down Expand Up @@ -84,6 +85,111 @@ class ApiCompilerTest extends PropSpec {
checkParseAndCompileVerifier(correctVerifier, libWithCompileError, Seq(DefNotFound(85, 108, "abc")))
}

property("compile should correctly process scopes during estimation") {
val estimator: ScriptEstimator = ScriptEstimator.all(true).last

val script1 =
"""
|{-# STDLIB_VERSION 6 #-}
|{-# CONTENT_TYPE DAPP #-}
|{-# SCRIPT_TYPE ACCOUNT #-}
|let a = {
| func bar(i: Int) = i
| bar(1)
|}
|let b = {
| func bar(i: Int) = i
| bar(a)
|}
|""".stripMargin

val script2 =
"""
|{-# STDLIB_VERSION 6 #-}
|{-# CONTENT_TYPE DAPP #-}
|{-# SCRIPT_TYPE ACCOUNT #-}
|
|let a = {
| func bar(i: Int) = i
| bar(1)
|}
|
|let b = {
| let c = {
| let d = {
| func bar(i: Int) = i
| bar(a)
| }
|
| func bar(i: Int) = i
| bar(d)
| }
|
| func bar(i: Int) = i
| bar(c)
|}
|
|let e = {
| func bar(i: Int) = i
| bar(b)
|}
|
|""".stripMargin

val script3 =
"""
|{-# STDLIB_VERSION 6 #-}
|{-# CONTENT_TYPE DAPP #-}
|{-# SCRIPT_TYPE ACCOUNT #-}
|
|let l = []
|let a = {
| func foo(fooAcc: Int, i: Int) = fooAcc + i
| FOLD<1>(l, 0, foo)
|}
|let b = {
| func bar(barAcc: Int, i: Int) = barAcc + i
| FOLD<1>(l, a, bar)
|}
|""".stripMargin

val script4 =
"""
|{-# STDLIB_VERSION 6 #-}
|{-# CONTENT_TYPE DAPP #-}
|{-# SCRIPT_TYPE ACCOUNT #-}
|
|let a = 1 + 1 + 1
|let b = a + 1 + 1
|let c = b + a + 1
|
|@Callable(i)
|func foo() = []
|""".stripMargin

API.compile(script1, estimator, libraries = Map.empty).explicitGet().asInstanceOf[DApp].dAppInfo.globalVariableComplexities shouldBe Map(
"a" -> 1,
"b" -> 2
)
API.compile(script2, estimator, libraries = Map.empty).explicitGet().asInstanceOf[DApp].dAppInfo.globalVariableComplexities shouldBe Map(
"a" -> 1,
"b" -> 4,
"e" -> 5
)

API.compile(script3, estimator, libraries = Map.empty).explicitGet().asInstanceOf[DApp].dAppInfo.globalVariableComplexities shouldBe Map(
"l" -> 0,
"a" -> 8,
"b" -> 16
)

API.compile(script4, estimator, libraries = Map.empty).explicitGet().asInstanceOf[DApp].dAppInfo.globalVariableComplexities shouldBe Map(
"a" -> 2,
"b" -> 4,
"c" -> 6
)
}

private def checkCompile(script: String, lib: String, expectedError: String): Assertion = {
val estimator: ScriptEstimator = ScriptEstimator.all(true).last
API.compile(script, estimator, libraries = Map(libName -> lib)) shouldBe Left(expectedError)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ class EstimatorGlobalVarTest extends ScriptEstimatorTestBase(ScriptEstimatorV3(f
estimateFixed(script) shouldBe Right(2700)
}

property("avoid redundant overhead for function argument referencing to global variable") {
property("leave unchanged adding overhead complexity for function with zero complexity body") {
val script =
"""
| let a = groth16Verify(base58'', base58'', base58'')
| func f(a: Boolean) = a
| f(a)
""".stripMargin
estimate(script) shouldBe Right(2701)
estimateFixed(script) shouldBe Right(2700)
estimateFixed(script) shouldBe Right(2701)
}

property("avoid redundant overhead for single reference in function body with let overlap") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.wavesplatform.state.diffs.ci

import com.wavesplatform.db.WithDomain
import com.wavesplatform.features.BlockchainFeatures.*
import com.wavesplatform.db.WithState.AddrWithBalance
import com.wavesplatform.lang.directives.values.V5
import com.wavesplatform.features.BlockchainFeatures.*
import com.wavesplatform.lang.directives.values.{V5, V6}
import com.wavesplatform.lang.script.Script
import com.wavesplatform.lang.v1.compiler.TestCompiler
import com.wavesplatform.settings.TestFunctionalitySettings
Expand All @@ -12,22 +12,6 @@ import com.wavesplatform.transaction.TxHelpers

class EvaluatorFunctionCallScopeTest extends PropSpec with WithDomain {

private val dAppScript: Script =
TestCompiler(V5).compileContract(
s"""
| @Callable(i)
| func default() = {
| let a = 4
| func g(b: Int) = a
| func f(a: Int) = g(a)
| let r = f(1)
| [
| IntegerEntry("key", r)
| ]
| }
""".stripMargin
)

private val settings =
TestFunctionalitySettings
.withFeatures(BlockV5, SynchronousCalls)
Expand All @@ -38,6 +22,22 @@ class EvaluatorFunctionCallScopeTest extends PropSpec with WithDomain {
val dApp = TxHelpers.signer(1)
val balances = AddrWithBalance.enoughBalances(invoker, dApp)

val dAppScript: Script =
TestCompiler(V5).compileContract(
s"""
| @Callable(i)
| func default() = {
| let a = 4
| func g(b: Int) = a
| func f(a: Int) = g(a)
| let r = f(1)
| [
| IntegerEntry("key", r)
| ]
| }
""".stripMargin
)

val setScript = TxHelpers.setScript(dApp, dAppScript)
val invoke = () => TxHelpers.invoke(dApp.toAddress, func = None, invoker = invoker)

Expand All @@ -51,4 +51,52 @@ class EvaluatorFunctionCallScopeTest extends PropSpec with WithDomain {
d.blockchain.accountData(dApp.toAddress, "key").get.value shouldBe 4
}
}

property("evaluator should correctly process scopes for global declarations") {
val dAppScript =
TestCompiler(V6).compileContract(
s"""
|{-# STDLIB_VERSION 6 #-}
|{-# CONTENT_TYPE DAPP #-}
|{-# SCRIPT_TYPE ACCOUNT #-}
|
|let a = {
| func bar(i: Int) = i
| bar(1)
|}
|let b = {
| let c = {
| let d = {
| func bar(i: Int) = i + 1
| bar(a)
| }
|
| func bar(i: Int) = i * 2
| bar(d)
| }
|
| func bar(i: Int) = i + 1
| bar(c)
|}
|
|@Callable(i)
|func default() = {
| [
| IntegerEntry("key", b)
| ]
|}
|""".stripMargin
)
val invoker = TxHelpers.signer(0)
val dApp = TxHelpers.signer(1)
val balances = AddrWithBalance.enoughBalances(invoker, dApp)
val setScript = TxHelpers.setScript(dApp, dAppScript)
val invoke = () => TxHelpers.invoke(dApp.toAddress, func = None, invoker = invoker)

withDomain(DomainPresets.TransactionStateSnapshot, balances) { d =>
d.appendBlock(setScript)
d.appendBlock(invoke())
d.blockchain.accountData(dApp.toAddress, "key").get.value shouldBe 5
}
}
}