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

[6.0] Lazy default for Option.getOrElse and Coll.getOrElse #1008

Merged
merged 10 commits into from
Oct 21, 2024
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
146 changes: 76 additions & 70 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationV5.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7957,77 +7957,81 @@ class LanguageSpecificationV5 extends LanguageSpecificationBase { suite =>
)
)
)
verifyCases(
// (coll, (index, default))
{
def success[T](v: T) = Expected(Success(v), 1773, costDetails, 1773)
Seq(
((Coll[Int](), (0, default)), success(default)),
((Coll[Int](), (-1, default)), success(default)),
((Coll[Int](1), (0, default)), success(1)),
((Coll[Int](1), (1, default)), success(default)),
((Coll[Int](1), (-1, default)), success(default)),
((Coll[Int](1, 2), (0, default)), success(1)),
((Coll[Int](1, 2), (1, default)), success(2)),
((Coll[Int](1, 2), (2, default)), success(default)),
((Coll[Int](1, 2), (-1, default)), success(default))
)
},
existingFeature((x: (Coll[Int], (Int, Int))) => x._1.getOrElse(x._2._1, x._2._2),
"{ (x: (Coll[Int], (Int, Int))) => x._1.getOrElse(x._2._1, x._2._2) }",
if (lowerMethodCallsInTests)
FuncValue(
Vector((1, SPair(SCollectionType(SInt), SPair(SInt, SInt)))),
BlockValue(
Vector(
ValDef(
3,
List(),
SelectField.typed[Value[STuple]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
2.toByte

if(!VersionContext.current.isV6SoftForkActivated) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocol change like this can be tested using changeFeature (see other tests) in combination with newVersionedResults (also see code). In this case both old and new behaviour will be tested.

In principle this test case can be moved to LSV6.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changedFeature is not enough here, you need for versioned verifyCases, as in 6.0 costing trace would be different in some cases due to difference in default evaluation (at the same time, it wouldnt provide higher cost, so the change is secure costing-wise).

Seems like *Feature machinery is overly complex, but still not enough to describe protocol changes, thus I would recommend to decide what it is actually testing (or should test), and then follow simpler approaches

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see other examples where changedFeature is used, there are test cases where cost trace depend on the version and you can pass different traces to the constructor of Expected.
The perceived complexity is due to learning curve.

Copy link
Member Author

@kushti kushti Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changedFeature is tied to V4/V5 switch, see checkExpected in ChangedFeature / checkVerify functions in SigmaDslTesting. So it cant be used in V6 testing without modifications it seems. So it is definitely not about learning curve, changedFeature is just not ready for V6

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aslesarenko see #1024 for example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

verifyCases(
// (coll, (index, default))
{
def success[T](v: T) = Expected(Success(v), 1773, costDetails, 1773)

Seq(
((Coll[Int](), (0, default)), success(default)),
((Coll[Int](), (-1, default)), success(default)),
((Coll[Int](1), (0, default)), success(1)),
((Coll[Int](1), (1, default)), success(default)),
((Coll[Int](1), (-1, default)), success(default)),
((Coll[Int](1, 2), (0, default)), success(1)),
((Coll[Int](1, 2), (1, default)), success(2)),
((Coll[Int](1, 2), (2, default)), success(default)),
((Coll[Int](1, 2), (-1, default)), success(default))
)
},
existingFeature((x: (Coll[Int], (Int, Int))) => x._1.getOrElse(x._2._1, x._2._2),
"{ (x: (Coll[Int], (Int, Int))) => x._1.getOrElse(x._2._1, x._2._2) }",
if (lowerMethodCallsInTests)
FuncValue(
Vector((1, SPair(SCollectionType(SInt), SPair(SInt, SInt)))),
BlockValue(
Vector(
ValDef(
3,
List(),
SelectField.typed[Value[STuple]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
2.toByte
)
)
)
),
ByIndex(
SelectField.typed[Value[SCollection[SInt.type]]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
1.toByte
),
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 1.toByte),
Some(SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 2.toByte))
ByIndex(
SelectField.typed[Value[SCollection[SInt.type]]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
1.toByte
),
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 1.toByte),
Some(SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 2.toByte))
)
)
)
)
else
FuncValue(
Array((1, SPair(SCollectionType(SInt), SPair(SInt, SInt)))),
BlockValue(
Array(
ValDef(
3,
List(),
SelectField.typed[Value[STuple]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
2.toByte
else
FuncValue(
Array((1, SPair(SCollectionType(SInt), SPair(SInt, SInt)))),
BlockValue(
Array(
ValDef(
3,
List(),
SelectField.typed[Value[STuple]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
2.toByte
)
)
)
),
MethodCall.typed[Value[SInt.type]](
SelectField.typed[Value[SCollection[SInt.type]]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
1.toByte
),
SCollectionMethods.getMethodByName("getOrElse").withConcreteTypes(Map(STypeVar("IV") -> SInt)),
Vector(
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 1.toByte),
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 2.toByte)
),
Map()
MethodCall.typed[Value[SInt.type]](
SelectField.typed[Value[SCollection[SInt.type]]](
ValUse(1, SPair(SCollectionType(SInt), SPair(SInt, SInt))),
1.toByte
),
SCollectionMethods.getMethodByName("getOrElse").withConcreteTypes(Map(STypeVar("IV") -> SInt)),
Vector(
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 1.toByte),
SelectField.typed[Value[SInt.type]](ValUse(3, SPair(SInt, SInt)), 2.toByte)
),
Map()
)
)
)
)
))
))
}
}

property("Tuple size method equivalence") {
Expand Down Expand Up @@ -8591,13 +8595,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)),
(Some(10L) -> Expected(Success(10L), 1766, costDetails3, 1766))),
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, this if is not necessary when changeFeature + newVersionedResults is used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

verifyCases(
Seq(
(None -> Expected(Success(1L), 1766, costDetails3, 1766)),
(Some(10L) -> Expected(Success(10L), 1766, costDetails3, 1766))),
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
60 changes: 58 additions & 2 deletions sc/shared/src/test/scala/sigma/LanguageSpecificationV6.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package sigma

import sigma.ast.{Apply, Downcast, FixedCost, FixedCostItem, FuncValue, GetVar, JitCost, OptionGet, SBigInt, SByte, SInt, SLong, SShort, ValUse}
import sigma.ast.{Apply, ArithOp, BlockValue, ByIndex, CompanionDesc, Constant, Downcast, FixedCost, FixedCostItem, FuncValue, GetVar, IntConstant, JitCost, LongConstant, MethodCall, OptionGet, OptionGetOrElse, PerItemCost, SBigInt, SByte, SCollection, SCollectionMethods, SCollectionType, SInt, SLong, SOption, SPair, SShort, STuple, STypeVar, SelectField, ValDef, ValUse, Value}
import sigma.data.{CBigInt, ExactNumeric}
import sigma.eval.SigmaDsl
import sigma.eval.{SigmaDsl, TracedCost}
import sigma.serialization.ValueCodes.OpCode
import sigma.util.Extensions.{BooleanOps, ByteOps, IntOps, LongOps}
import sigmastate.exceptions.MethodNotFound

Expand Down Expand Up @@ -168,7 +169,62 @@ class LanguageSpecificationV6 extends LanguageSpecificationBase { suite =>
Seq(compareTo, bitOr, bitAnd).foreach(_.checkEquality(x))
}
}
}

property("Option.getOrElse with lazy default") {
def getOrElse = newFeature(
{ (x: Option[Long]) => x.getOrElse(1 / 0L) },
"{ (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))
)
)
)

if (VersionContext.current.isV6SoftForkActivated) {
forAll { x: Option[Long] =>
Seq(getOrElse).map(_.checkEquality(x))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check equality is not enough, as it doesn't involve all interpreter components.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please specify what is needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyCases will exercise all the components.

}
} else {
forAll { x: Option[Long] =>
if (x.isEmpty) {
Seq(getOrElse).map(_.checkEquality(x))
}
}
}
}

property("Coll getOrElse with lazy default") {
def getOrElse = newFeature(
(x: (Coll[Int], Int)) => x._1.toArray.unapply(x._2).getOrElse(1 / 0),
"{ (x: (Coll[Int], Int)) => x._1.getOrElse(x._2, 1 / 0) }",
FuncValue(
Array((1, SPair(SCollectionType(SInt), SInt))),
ByIndex(
SelectField.typed[Value[SCollection[SInt.type]]](
ValUse(1, SPair(SCollectionType(SInt), SInt)),
1.toByte
),
SelectField.typed[Value[SInt.type]](ValUse(1, SPair(SCollectionType(SInt), SInt)), 2.toByte),
Some(ArithOp(IntConstant(1), IntConstant(0), OpCode @@ (-99.toByte)))
)
)
)

if (VersionContext.current.isV6SoftForkActivated) {
forAll { x: (Coll[Int], Int) =>
Seq(getOrElse).map(_.checkEquality(x))
}
} else {
forAll { x: (Coll[Int], Int) =>
if (x._1.isEmpty) {
Seq(getOrElse).map(_.checkEquality(x))
}
}
}
}

property("BigInt methods equivalence (new features)") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sigmastate.utxo
import org.ergoplatform.ErgoBox.{AdditionalRegisters, R6, R8}
import org.ergoplatform._
import sigma.Extensions.ArrayOps
import sigma.VersionContext
import sigma.ast.SCollection.SByteArray
import sigma.ast.SType.AnyOps
import sigma.data.{AvlTreeData, CAnyValue, CSigmaDslBuilder}
Expand Down Expand Up @@ -157,6 +158,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 {
an[Exception] shouldBe thrownBy(optTest())
kushti marked this conversation as resolved.
Show resolved Hide resolved
}
}

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
Loading