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-2549 Fixes for global variable in estimator #3800

Merged
merged 29 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f04feb0
Fix global variable overlapping
xrtm000 Dec 30, 2022
e0019a9
Add flag for estimator fix
xrtm000 Dec 30, 2022
d062f0e
Use estimator with the new flag in tests
xrtm000 Dec 30, 2022
c20ae76
Use set instead of list for function args param
xrtm000 Dec 30, 2022
be176fc
Fix excessive overhead in function body
xrtm000 Dec 30, 2022
9fd7d63
Merge remote-tracking branch 'origin/version-1.4.x' into node-2549-es…
xrtm000 Jan 9, 2023
61f4006
Adapt for node package
xrtm000 Jan 9, 2023
ae845f3
Use ScriptEstimatorV3.latest for RIDE API
xrtm000 Jan 9, 2023
fae6063
Adapt test
xrtm000 Jan 9, 2023
18f85ae
Adapted IT tests
xrtm000 Jan 9, 2023
93fa7c5
Adapted SetScriptTransactionGrpcSuite
xrtm000 Jan 9, 2023
eca2a9d
Another adaption
xrtm000 Jan 9, 2023
a2d1ad1
Return original test logic
xrtm000 Jan 9, 2023
d4f12dc
Refactor ScriptEstimatorV3
xrtm000 Jan 10, 2023
86ac7e4
Avoid excessive overhead for function argument
xrtm000 Jan 10, 2023
09133dd
Better naming
xrtm000 Jan 10, 2023
fda596e
Avoid excessive overhead for reference in function body with let overlap
xrtm000 Jan 10, 2023
2748584
More complex test for overlapped function argument
xrtm000 Jan 10, 2023
40527ee
Better naming
xrtm000 Jan 10, 2023
910ce29
Additional estimator tests
xrtm000 Jan 10, 2023
2e71f5d
Restore overhead for expression in blank function argument (as evalua…
xrtm000 Jan 10, 2023
8e5675f
Tests for evaluator
xrtm000 Jan 10, 2023
36c547b
Activation test
xrtm000 Jan 10, 2023
f7aae2b
Merge remote-tracking branch 'origin/version-1.4.x' into node-2549-es…
xrtm000 Feb 20, 2023
bd9a08e
Adapted UtilsRouteEvaluateSpec
xrtm000 Feb 20, 2023
7b48712
Better code
xrtm000 Feb 20, 2023
735c42e
Merge branch 'version-1.5.x' into node-2549-estimator-global-var-overlap
phearnot Nov 30, 2023
ff1dbba
bound activation to another feature
phearnot Nov 30, 2023
f07eb00
Merge branch 'version-1.5.x' into node-2549-estimator-global-var-overlap
phearnot Dec 1, 2023
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
2 changes: 1 addition & 1 deletion lang/jvm/src/main/scala/com/wavesplatform/lang/Lang.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object Lang {

def compileDApp(input: String): DAppWithMeta =
API
.compile(input, ScriptEstimatorV3(fixOverflow = true, overhead = false))
.compile(input, ScriptEstimatorV3.latest)
.flatMap {
case r: CompileResult.DApp =>
val javaMeta = Meta(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.nio.file.{Files, Paths}
import scala.jdk.CollectionConverters.*

object FileCompiler extends App {
private val estimator = ScriptEstimatorV3(true, false)
private val estimator = ScriptEstimatorV3.latest
args
.foreach { path =>
val script = Files.readAllLines(Paths.get(path)).asScala.reduce(_ + "\n" + _)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ trait ScriptEstimator {

object ScriptEstimator {
def all(fixOverflow: Boolean): List[ScriptEstimator] =
List(ScriptEstimatorV1, ScriptEstimatorV2, ScriptEstimatorV3(fixOverflow, overhead = false))
List(ScriptEstimatorV1, ScriptEstimatorV2, ScriptEstimatorV3(fixOverflow, overhead = false, letFixes = true))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import shapeless.{Lens, lens}

private[v3] case class EstimatorContext(
funcs: Map[FunctionHeader, (Coeval[Long], Set[String])],
usedRefs: Set[String] = Set.empty,
usedRefs: Set[String] = Set(),
refsCosts: Map[String, Long] = Map(),
globalFunctionsCosts: Map[String, Long] = Map(), //
globalLetsCosts: Map[String, Long] = Map(), // only for globalDeclarationsMode
globalLetEvals: Map[String, EvalM[Long]] = Map() //
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.wavesplatform.lang.v1.estimator.v3

import cats.implicits.*
import cats.implicits.{toBifunctorOps, toFoldableOps, toTraverseOps}
import cats.{Id, Monad}
import com.wavesplatform.lang.v1.FunctionHeader
import com.wavesplatform.lang.v1.FunctionHeader.User
Expand All @@ -13,7 +13,7 @@ import monix.eval.Coeval

import scala.util.Try

case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean) extends ScriptEstimator {
case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean, letFixes: Boolean) extends ScriptEstimator {
private val overheadCost: Long = if (overhead) 1 else 0

override val version: Int = 3
Expand All @@ -39,55 +39,45 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean) extends Sc
globalDeclarationsMode: Boolean
): (EstimatorContext, Either[EstimationError, Long]) = {
val ctxFuncs = funcs.view.mapValues((_, Set[String]())).toMap
evalExpr(expr, globalDeclarationsMode).run(EstimatorContext(ctxFuncs)).value
evalExpr(expr, Set(), globalDeclarationsMode).run(EstimatorContext(ctxFuncs)).value
}

private def evalExpr(t: EXPR, globalDeclarationsMode: Boolean = false): EvalM[Long] =
private def evalExpr(t: EXPR, activeFuncArgs: Set[String], globalDeclarationsMode: Boolean = false): EvalM[Long] =
if (Thread.currentThread().isInterrupted)
raiseError("Script estimation was interrupted")
else
t match {
case LET_BLOCK(let, inner) => evalLetBlock(let, inner, globalDeclarationsMode)
case BLOCK(let: LET, inner) => evalLetBlock(let, inner, globalDeclarationsMode)
case BLOCK(f: FUNC, inner) => evalFuncBlock(f, inner, globalDeclarationsMode)
case LET_BLOCK(let, inner) => evalLetBlock(let, inner, activeFuncArgs, globalDeclarationsMode)
case BLOCK(let: LET, inner) => evalLetBlock(let, inner, activeFuncArgs, globalDeclarationsMode)
case BLOCK(f: FUNC, inner) => evalFuncBlock(f, inner, activeFuncArgs, globalDeclarationsMode)
case BLOCK(_: FAILED_DEC, _) => zero
case REF(str) => markRef(str)
case REF(str) => evalRef(str, activeFuncArgs)
case _: EVALUATED => const(overheadCost)
case IF(cond, t1, t2) => evalIF(cond, t1, t2)
case GETTER(expr, _) => evalGetter(expr)
case FUNCTION_CALL(header, args) => evalFuncCall(header, args)
case IF(cond, t1, t2) => evalIF(cond, t1, t2, activeFuncArgs)
case GETTER(expr, _) => evalGetter(expr, activeFuncArgs)
case FUNCTION_CALL(header, args) => evalFuncCall(header, args, activeFuncArgs)
case _: FAILED_EXPR => zero
}

private def evalHoldingFuncs(expr: EXPR): EvalM[Long] =
private def evalLetBlock(let: LET, nextExpr: EXPR, activeFuncArgs: Set[String], globalDeclarationsMode: Boolean): EvalM[Long] =
for {
_ <- if (globalDeclarationsMode) saveGlobalLetCost(let, activeFuncArgs) else doNothing
startCtx <- get[Id, EstimatorContext, EstimationError]
cost <- evalExpr(expr)
_ <- update(funcs.set(_)(startCtx.funcs))
} yield cost

private def evalLetBlock(let: LET, inner: EXPR, globalDeclarationsMode: Boolean): EvalM[Long] =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
overlap = startCtx.usedRefs.contains(let.name)
_ <- update(usedRefs.modify(_)(_ - let.name))
letEval = evalHoldingFuncs(let.value)
_ <- if (globalDeclarationsMode) saveGlobalLetCost(let) else doNothing
nextCost <- evalExpr(inner, globalDeclarationsMode)
ctx <- get[Id, EstimatorContext, EstimationError]
letCost <- if (ctx.usedRefs.contains(let.name)) letEval else zero
_ <- update(usedRefs.modify(_)(r => if (overlap) r + let.name else r - let.name))
result <- sum(nextCost, letCost)
letEval = evalHoldingFuncs(let.value, activeFuncArgs)
_ <- beforeNextExprEval(let, letEval)
nextExprCost <- evalExpr(nextExpr, activeFuncArgs, globalDeclarationsMode)
nextExprCtx <- get[Id, EstimatorContext, EstimationError]
_ <- afterNextExprEval(let, startCtx)
letCost <- if (nextExprCtx.usedRefs.contains(let.name)) letEval else const(0L)
result <- sum(nextExprCost, letCost)
} yield result

private def saveGlobalLetCost(let: LET): EvalM[Unit] = {
private def saveGlobalLetCost(let: LET, activeFuncArgs: Set[String]): EvalM[Unit] = {
val costEvaluation =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
bodyCost <- evalExpr(let.value)
bodyEvalCtx <- get[Id, EstimatorContext, EstimationError]
usedRefs = bodyEvalCtx.usedRefs diff startCtx.usedRefs
letCosts <- usedRefs.toSeq.traverse(bodyEvalCtx.globalLetEvals.getOrElse(_, zero))
(bodyCost, usedRefs) <- withUsedRefs(evalExpr(let.value, activeFuncArgs))
ctx <- get[Id, EstimatorContext, EstimationError]
letCosts <- usedRefs.toSeq.traverse(ctx.globalLetEvals.getOrElse(_, zero))
} yield bodyCost + letCosts.sum
for {
cost <- local(costEvaluation)
Expand All @@ -100,26 +90,47 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean) extends Sc
} yield ()
}

private def evalFuncBlock(func: FUNC, inner: EXPR, globalDeclarationsMode: Boolean): EvalM[Long] =
private def beforeNextExprEval(let: LET, eval: EvalM[Long]): EvalM[Unit] =
for {
cost <- local(eval)
_ <- update(ctx =>
usedRefs
.modify(ctx)(_ - let.name)
.copy(refsCosts = ctx.refsCosts + (let.name -> cost))
)
} yield ()

private def afterNextExprEval(let: LET, startCtx: EstimatorContext): EvalM[Unit] =
update(ctx =>
usedRefs
.modify(ctx)(r => if (startCtx.usedRefs.contains(let.name)) r + let.name else r - let.name)
.copy(refsCosts =
if (startCtx.refsCosts.contains(let.name))
ctx.refsCosts + (let.name -> startCtx.refsCosts(let.name))
else
ctx.refsCosts - let.name
)
)

private def evalFuncBlock(func: FUNC, nextExpr: EXPR, activeFuncArgs: Set[String], globalDeclarationsMode: Boolean): EvalM[Long] =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
_ <- checkShadowing(func, startCtx)
funcCost <- evalHoldingFuncs(func.body)
bodyEvalCtx <- get[Id, EstimatorContext, EstimationError]
refsUsedInBody = bodyEvalCtx.usedRefs diff startCtx.usedRefs
_ <- if (globalDeclarationsMode) saveGlobalFuncCost(func.name, funcCost, bodyEvalCtx, refsUsedInBody) else doNothing
_ <- handleUsedRefs(func.name, funcCost, startCtx, refsUsedInBody)
nextCost <- evalExpr(inner, globalDeclarationsMode)
} yield nextCost
startCtx <- get[Id, EstimatorContext, EstimationError]
_ <- checkShadowing(func, startCtx)
(funcCost, refsUsedInBody) <- withUsedRefs(evalHoldingFuncs(func.body, activeFuncArgs ++ func.args))
_ <- if (globalDeclarationsMode) saveGlobalFuncCost(func.name, funcCost, refsUsedInBody) else doNothing
_ <- handleUsedRefs(func.name, funcCost, startCtx, refsUsedInBody)
nextExprCost <- evalExpr(nextExpr, activeFuncArgs, globalDeclarationsMode)
} yield nextExprCost

private def checkShadowing(func: FUNC, startCtx: EstimatorContext): EvalM[Any] =
if (fixOverflow && startCtx.funcs.contains(FunctionHeader.User(func.name)))
raiseError(s"Function '${func.name}${func.args.mkString("(", ", ", ")")}' shadows preceding declaration")
else
doNothing

private def saveGlobalFuncCost(name: String, funcCost: Long, ctx: EstimatorContext, refsUsedInBody: Set[String]): EvalM[Unit] =
private def saveGlobalFuncCost(name: String, funcCost: Long, refsUsedInBody: Set[String]): EvalM[Unit] =
for {
ctx <- get[Id, EstimatorContext, EstimationError]
letCosts <- local(refsUsedInBody.toSeq.traverse(ctx.globalLetEvals.getOrElse(_, zero)))
totalCost = math.max(1, funcCost + letCosts.sum)
_ <- set[Id, EstimatorContext, EstimationError](ctx.copy(globalFunctionsCosts = ctx.globalFunctionsCosts + (name -> totalCost)))
Expand All @@ -135,46 +146,75 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean) extends Sc
}
)

private def evalIF(cond: EXPR, ifTrue: EXPR, ifFalse: EXPR): EvalM[Long] =
private def evalIF(cond: EXPR, ifTrue: EXPR, ifFalse: EXPR, activeFuncArgs: Set[String]): EvalM[Long] =
for {
cond <- evalHoldingFuncs(cond)
right <- evalHoldingFuncs(ifTrue)
left <- evalHoldingFuncs(ifFalse)
cond <- evalHoldingFuncs(cond, activeFuncArgs)
right <- evalHoldingFuncs(ifTrue, activeFuncArgs)
left <- evalHoldingFuncs(ifFalse, activeFuncArgs)
r1 <- sum(cond, Math.max(right, left))
r2 <- sum(r1, overheadCost)
} yield r2

private def markRef(key: String): EvalM[Long] =
update(usedRefs.modify(_)(_ + key)).map(_ => overheadCost)
private def evalRef(key: String, activeFuncArgs: Set[String]): EvalM[Long] =
if (activeFuncArgs.contains(key) && letFixes)
const(overheadCost)
else
update(usedRefs.modify(_)(_ + key)).map(_ => overheadCost)

private def evalGetter(expr: EXPR): EvalM[Long] =
evalExpr(expr).flatMap(sum(_, overheadCost))
private def evalGetter(expr: EXPR, activeFuncArgs: Set[String]): EvalM[Long] =
evalExpr(expr, activeFuncArgs).flatMap(sum(_, overheadCost))

private def evalFuncCall(header: FunctionHeader, args: List[EXPR]): EvalM[Long] =
private def evalFuncCall(header: FunctionHeader, args: List[EXPR], activeFuncArgs: Set[String]): EvalM[Long] =
for {
ctx <- get[Id, EstimatorContext, EstimationError]
(bodyCost, bodyUsedRefs) <- funcs
.get(ctx)
.get(header)
.map(const)
.getOrElse(
raiseError[Id, EstimatorContext, EstimationError, (Coeval[Long], Set[String])](s"function '$header' not found")
)
_ <- update(
(funcs ~ usedRefs).modify(_) { case (funcs, usedRefs) =>
(
funcs + ((header, (bodyCost, Set[String]()))),
usedRefs ++ bodyUsedRefs
)
}
)
argsCosts <- args.traverse(evalHoldingFuncs)
argsCostsSum <- argsCosts.foldM(0L)(sum)
bodyCostV = bodyCost.value()
correctedBodyCost = if (!overhead && bodyCostV == 0) 1 else bodyCostV
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)
bodyCostV = bodyCost.value()
correctedBodyCost =
if (!overhead && !letFixes && bodyCostV == 0) 1
else if (letFixes && bodyCostV == 0 && isBlankFunc(bodyUsedRefs ++ argsUsedRefs, ctx.refsCosts)) 1
else bodyCostV
result <- sum(argsCostsSum, correctedBodyCost)
} yield result

private def setFuncToCtx(header: FunctionHeader, bodyCost: Coeval[Long], bodyUsedRefs: Set[EstimationError]): EvalM[Unit] =
update(
(funcs ~ usedRefs).modify(_) { case (funcs, usedRefs) =>
(
funcs + (header -> (bodyCost, Set())),
usedRefs ++ bodyUsedRefs
)
}
)

private def getFuncCost(header: FunctionHeader, ctx: EstimatorContext): EvalM[(Coeval[Long], Set[EstimationError])] =
funcs
.get(ctx)
.get(header)
.map(const)
.getOrElse(
raiseError[Id, EstimatorContext, EstimationError, (Coeval[Long], Set[EstimationError])](s"function '$header' not found")
)

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] =
for {
startCtx <- get[Id, EstimatorContext, EstimationError]
cost <- evalExpr(expr, activeFuncArgs)
_ <- update(funcs.set(_)(startCtx.funcs))
} yield cost

private def withUsedRefs[A](eval: EvalM[A]): EvalM[(A, Set[String])] =
for {
ctxBefore <- get[Id, EstimatorContext, EstimationError]
result <- eval
ctxAfter <- get[Id, EstimatorContext, EstimationError]
} yield (result, ctxAfter.usedRefs diff ctxBefore.usedRefs)

private def update(f: EstimatorContext => EstimatorContext): EvalM[Unit] =
modify[Id, EstimatorContext, EstimationError](f)

Expand All @@ -192,3 +232,7 @@ case class ScriptEstimatorV3(fixOverflow: Boolean, overhead: Boolean) extends Sc
liftEither(Try(r).toEither.leftMap(_ => "Illegal script"))
}
}

object ScriptEstimatorV3 {
val latest = ScriptEstimatorV3(fixOverflow = true, overhead = false, letFixes = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ class ContractCompilerTest extends PropSpec {
NoLibraries,
dAppV4Ctx,
V4,
ScriptEstimatorV3(fixOverflow = true, overhead = true),
ScriptEstimatorV3(fixOverflow = true, overhead = true, letFixes = true),
needCompaction = false,
removeUnusedCode = false
) should produce("Script is too large: 165187 bytes > 163840 bytes")
Expand Down Expand Up @@ -1042,7 +1042,7 @@ class ContractCompilerTest extends PropSpec {
NoLibraries,
getTestContext(V4).compilerContext,
V4,
ScriptEstimatorV3(fixOverflow = true, overhead = true),
ScriptEstimatorV3(fixOverflow = true, overhead = true, letFixes = true),
needCompaction = false,
removeUnusedCode = false
) shouldBe Symbol("right")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ class ExpressionCompilerV1Test extends PropSpec {
)
.compilerContext

val e = ScriptEstimatorV3(fixOverflow = true, overhead = true)
val e = ScriptEstimatorV3(fixOverflow = true, overhead = true, letFixes = true)
Global.compileExpression(expr, NoLibraries, ctx, V4, Account, e) should produce("Script is too large: 8756 bytes > 8192 bytes")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class FunctionComplexityDocTest extends PropSpec {
val arg = CONST_STRING("throw").explicitGet()
val expr = FUNCTION_CALL(function.header, List.fill(function.args.size)(arg))
val estimatedCost =
ScriptEstimatorV3(fixOverflow = true, overhead = false)(
ScriptEstimatorV3(fixOverflow = true, overhead = false, letFixes = true)(
varNames(ds.stdLibVersion, ds.contentType),
functionCosts(ds.stdLibVersion, ds.contentType),
expr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ class CommonScriptEstimatorTest
extends ScriptEstimatorTestBase(
ScriptEstimatorV1,
ScriptEstimatorV2,
ScriptEstimatorV3(fixOverflow = true, overhead = true),
ScriptEstimatorV3(fixOverflow = true, overhead = false),
ScriptEstimatorV3(fixOverflow = false, overhead = true),
ScriptEstimatorV3(fixOverflow = false, overhead = false),
ScriptEstimatorV3(fixOverflow = true, overhead = true, letFixes = false),
ScriptEstimatorV3(fixOverflow = true, overhead = false, letFixes = false),
ScriptEstimatorV3(fixOverflow = false, overhead = true, letFixes = false),
ScriptEstimatorV3(fixOverflow = false, overhead = false, letFixes = false),
ScriptEstimatorV3(fixOverflow = true, overhead = false, letFixes = true), // <- all fixes
evaluatorV2AsEstimator(overhead = true),
evaluatorV2AsEstimator(overhead = false)
) {
Expand Down
Loading
Loading