Skip to content

Commit

Permalink
Merge pull request #1008 from ergoplatform/i906
Browse files Browse the repository at this point in the history
[6.0] Lazy default for Option.getOrElse and Coll.getOrElse
  • Loading branch information
kushti authored Oct 21, 2024
2 parents 571e721 + f151e1d commit c21ed63
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 21 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*.fdb_latexmk
*.gz


yarn.lock
*.log
yarn.lock
Expand Down
43 changes: 33 additions & 10 deletions data/shared/src/main/scala/sigma/ast/transformers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import sigma.eval.ErgoTreeEvaluator.DataEnv
import sigma.serialization.CoreByteWriter.ArgInfo
import sigma.serialization.OpCodes
import sigma.serialization.ValueCodes.OpCode
import sigma.{Box, Coll, Evaluation}
import sigma.{Box, Coll, Evaluation, VersionContext}

// TODO refactor: remove this trait as it doesn't have semantic meaning

Expand Down Expand Up @@ -258,10 +258,22 @@ case class ByIndex[V <: SType](input: Value[SCollection[V]],
val indexV = index.evalTo[Int](env)
default match {
case Some(d) =>
val dV = d.evalTo[V#WrappedType](env)
Value.checkType(d, dV) // necessary because cast to V#WrappedType is erased
addCost(ByIndex.costKind)
inputV.getOrElse(indexV, dV)
if (VersionContext.current.isV6SoftForkActivated) {
// lazy evaluation of default in 6.0
addCost(ByIndex.costKind)
if (inputV.isDefinedAt(indexV)) {
inputV.apply(indexV)
} else {
val dV = d.evalTo[V#WrappedType](env)
Value.checkType(d, dV) // necessary because cast to V#WrappedType is erased
inputV.getOrElse(indexV, dV)
}
} else {
val dV = d.evalTo[V#WrappedType](env)
Value.checkType(d, dV) // necessary because cast to V#WrappedType is erased
addCost(ByIndex.costKind)
inputV.getOrElse(indexV, dV)
}
case _ =>
addCost(ByIndex.costKind)
inputV.apply(indexV)
Expand Down Expand Up @@ -613,11 +625,22 @@ case class OptionGetOrElse[V <: SType](input: Value[SOption[V]], default: Value[
override val opType = SFunc(IndexedSeq(input.tpe, tpe), tpe)
override def tpe: V = input.tpe.elemType
protected final override def eval(env: DataEnv)(implicit E: ErgoTreeEvaluator): Any = {
val inputV = input.evalTo[Option[V#WrappedType]](env)
val dV = default.evalTo[V#WrappedType](env) // TODO v6.0: execute lazily (see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/906)
Value.checkType(default, dV) // necessary because cast to V#WrappedType is erased
addCost(OptionGetOrElse.costKind)
inputV.getOrElse(dV)
if(VersionContext.current.isV6SoftForkActivated) {
// lazy evaluation of default in 6.0
val inputV = input.evalTo[Option[V#WrappedType]](env)
addCost(OptionGetOrElse.costKind)
inputV.getOrElse {
val dV = default.evalTo[V#WrappedType](env)
Value.checkType(default, dV) // necessary because cast to V#WrappedType is erased
dV
}
} else {
val inputV = input.evalTo[Option[V#WrappedType]](env)
val dV = default.evalTo[V#WrappedType](env)
Value.checkType(default, dV) // necessary because cast to V#WrappedType is erased
addCost(OptionGetOrElse.costKind)
inputV.getOrElse(dV)
}
}
}
object OptionGetOrElse extends ValueCompanion with FixedCostValueCompanion {
Expand Down
19 changes: 11 additions & 8 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8059,6 +8059,7 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite =>
)
)
)
if(!VersionContext.current.isV6SoftForkActivated) {
verifyCases(
// (coll, (index, default))
{
Expand Down Expand Up @@ -8129,7 +8130,7 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite =>
)
)
)
))
))}
}

property("Tuple size method equivalence") {
Expand Down Expand Up @@ -8696,13 +8697,15 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite =>
"{ (x: Option[Long]) => x.isDefined }",
FuncValue(Vector((1, SOption(SLong))), OptionIsDefined(ValUse(1, SOption(SLong))))))

verifyCases(
Seq(
(None -> Expected(Success(1L), 1766, costDetails3, 1766, Seq.fill(4)(2006))),
(Some(10L) -> Expected(Success(10L), 1766, costDetails3, 1766, Seq.fill(4)(2006)))),
existingFeature({ (x: Option[Long]) => x.getOrElse(1L) },
"{ (x: Option[Long]) => x.getOrElse(1L) }",
FuncValue(Vector((1, SOption(SLong))), OptionGetOrElse(ValUse(1, SOption(SLong)), LongConstant(1L)))))
if (!VersionContext.current.isV6SoftForkActivated) {
verifyCases(
Seq(
(None -> Expected(Success(1L), 1766, costDetails3, 1766, Seq.fill(4)(2006))),
(Some(10L) -> Expected(Success(10L), 1766, costDetails3, 1766, Seq.fill(4)(2006)))),
existingFeature({ (x: Option[Long]) => x.getOrElse(1L) },
"{ (x: Option[Long]) => x.getOrElse(1L) }",
FuncValue(Vector((1, SOption(SLong))), OptionGetOrElse(ValUse(1, SOption(SLong)), LongConstant(1L)))))
}

verifyCases(
Seq(
Expand Down
96 changes: 95 additions & 1 deletion sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import sigma.ast.{SInt, _}
import sigma.data.{CBigInt, CBox, CHeader, CSigmaDslBuilder, ExactNumeric, PairOfCols, RType}
import sigma.eval.{CostDetails, SigmaDsl, TracedCost}
import sigma.serialization.ValueCodes.OpCode
import sigma.util.Extensions.{BooleanOps, IntOps}
import sigma.data.{RType}
import sigma.serialization.ValueCodes.OpCode
import sigma.util.Extensions.{BooleanOps, ByteOps, IntOps, LongOps}
import sigmastate.exceptions.MethodNotFound
import sigmastate.utils.Extensions.ByteOpsForSigma
import sigmastate.utils.Helpers
Expand Down Expand Up @@ -1523,6 +1525,98 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite =>
}



property("Option.getOrElse with lazy default") {

val trace = TracedCost(
Array(
FixedCostItem(Apply),
FixedCostItem(FuncValue),
FixedCostItem(GetVar),
FixedCostItem(OptionGet),
FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))),
FixedCostItem(ValUse),
FixedCostItem(OptionGetOrElse)
)
)

verifyCases(
Seq(
Some(2L) -> Expected(Failure(new java.lang.ArithmeticException("/ by zero")), 6, trace, 1793,
newVersionedResults = {
expectedSuccessForAllTreeVersions(2L, 2015, trace)
} ),
None -> Expected(Failure(new java.lang.ArithmeticException("/ by zero")), 6, trace, 1793)
),
changedFeature(
changedInVersion = VersionContext.V6SoftForkVersion,
{ (x: Option[Long]) => val default = 1 / 0L; x.getOrElse(default) },
{ (x: Option[Long]) => if (VersionContext.current.isV6SoftForkActivated) {x.getOrElse(1 / 0L)} else {val default = 1 / 0L; x.getOrElse(default)} },
"{ (x: Option[Long]) => x.getOrElse(1 / 0L) }",
FuncValue(
Array((1, SOption(SLong))),
OptionGetOrElse(
ValUse(1, SOption(SLong)),
ArithOp(LongConstant(1L), LongConstant(0L), OpCode @@ (-99.toByte))
)
),
allowNewToSucceed = true
)
)
}

property("Coll getOrElse with lazy default") {

val trace = TracedCost(
Array(
FixedCostItem(Apply),
FixedCostItem(FuncValue),
FixedCostItem(GetVar),
FixedCostItem(OptionGet),
FixedCostItem(FuncValue.AddToEnvironmentDesc, FixedCost(JitCost(5))),
FixedCostItem(ValUse),
FixedCostItem(Constant),
FixedCostItem(ByIndex)
)
)

def scalaFuncNew(x: Coll[Int]) = {
if (VersionContext.current.isV6SoftForkActivated) {
x.toArray.toIndexedSeq.headOption.getOrElse(1 / 0)
} else scalaFuncOld(x)
}

def scalaFuncOld(x: Coll[Int]) = {
x.getOrElse(0, 1 / 0)
}

verifyCases(
Seq(
Coll(1) -> Expected(Failure(new java.lang.ArithmeticException("/ by zero")), 6, trace, 1793,
newVersionedResults = {
expectedSuccessForAllTreeVersions(1, 2029, trace)
} ),
Coll[Int]() -> Expected(Failure(new java.lang.ArithmeticException("/ by zero")), 6, trace, 1793)
),
changedFeature(
changedInVersion = VersionContext.V6SoftForkVersion,
scalaFuncOld,
scalaFuncNew,
"{ (x: Coll[Int]) => x.getOrElse(0, 1 / 0) }",
FuncValue(
Array((1, SCollectionType(SInt))),
ByIndex(
ValUse(1, SCollectionType(SInt)),
IntConstant(0),
Some(ArithOp(IntConstant(1), IntConstant(0), OpCode @@ (-99.toByte)))
)
),
allowNewToSucceed = true
)
)
}


property("Global - fromBigEndianBytes") {
import sigma.data.OrderingOps.BigIntOrdering

Expand Down
2 changes: 1 addition & 1 deletion sc/shared/src/test/scala/sigma/SigmaDslTesting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ class SigmaDslTesting extends AnyPropSpec
override def checkExpected(input: A, expected: Expected[B]): Unit = {
// check the new implementation with Scala semantic function
val newRes = VersionContext.withVersions(activatedVersionInTests, ergoTreeVersionInTests) {
checkEq(scalaFuncNew)(newF)(input)
checkEq(scalaFuncNew)(newF)(input)
}

if (VersionContext.current.activatedVersion < changedInVersion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,43 @@ class BasicOpsSpecification extends CompilerTestingCommons
}
}

property("Lazy evaluation of default in Option.getOrElse") {
val customExt = Map (
1.toByte -> IntConstant(5)
).toSeq
def optTest() = test("getOrElse", env, customExt,
"""{
| getVar[Int](1).getOrElse(getVar[Int](44).get) > 0
|}
|""".stripMargin,
null
)

if (VersionContext.current.isV6SoftForkActivated) {
optTest()
} else {
assertExceptionThrown(optTest(), _.isInstanceOf[NoSuchElementException])
}
}

property("Lazy evaluation of default in Coll.getOrElse") {
def optTest() = test("getOrElse", env, ext,
"""{
| val c = Coll[Int](1)
| c.getOrElse(0, getVar[Int](44).get) > 0 &&
| c.getOrElse(1, c.getOrElse(0, getVar[Int](44).get)) > 0
|}
|""".stripMargin,
null
)

if(VersionContext.current.isV6SoftForkActivated) {
optTest()
} else {
an[Exception] shouldBe thrownBy(optTest())
}
}

property("Relation operations") {
test("R1", env, ext,
"{ allOf(Coll(getVar[Boolean](trueVar).get, true, true)) }",
Expand Down

0 comments on commit c21ed63

Please sign in to comment.