Skip to content

Commit

Permalink
Merge pull request #1017 from ScorexFoundation/i1006
Browse files Browse the repository at this point in the history
[6.0] New Numeric methods
  • Loading branch information
kushti authored Sep 20, 2024
2 parents d284f79 + 5f0c5c5 commit 3d88fc2
Show file tree
Hide file tree
Showing 20 changed files with 2,250 additions and 256 deletions.
20 changes: 17 additions & 3 deletions core/shared/src/main/scala/sigma/SigmaDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import java.math.BigInteger
import sigma.data._

/**
* All `modQ` operations assume that Q is a global constant (an order of the only one cryptographically strong group
* which is used for all cryptographic operations).
* So it is globally and implicitly used in all methods.
* Functions defined for 256-bit signed integers
* */
trait BigInt {
/** Convert this BigInt value to Byte.
Expand Down Expand Up @@ -154,6 +152,22 @@ trait BigInt {
*/
def or(that: BigInt): BigInt
def |(that: BigInt): BigInt = or(that)

/**
* @return a big integer whose value is `this xor that`.
* This method returns a negative BigInteger if and only if exactly one of this and val are negative.
*/
def xor(that: BigInt): BigInt

/**
* @return a 256-bit signed integer whose value is (this << n). `n` should be in 0..255 range (inclusive).
*/
def shiftLeft(n: Int): BigInt

/**
* @return a 256-bit signed integer whose value is (this >> n). `n` should be in 0..255 range (inclusive).
*/
def shiftRight(n: Int): BigInt
}

/** Base class for points on elliptic curves. */
Expand Down
55 changes: 38 additions & 17 deletions core/shared/src/main/scala/sigma/ast/SType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,48 @@ object SType {
* typeId this map contains a companion object which can be used to access the list of
* corresponding methods.
*
* NOTE: in the current implementation only monomorphic methods are supported (without
* type parameters)
* @note starting from v6.0 methods with type parameters are also supported.
*
* NOTE2: in v3.x SNumericType.typeId is silently shadowed by SGlobal.typeId as part of
* `toMap` operation. As a result, the methods collected into SByte.methods cannot be
* @note on versioning:
* In v3.x-5.x SNumericType.typeId is silently shadowed by SGlobal.typeId as part of
* `toMap` operation. As a result, SNumericTypeMethods container cannot be resolved by
* typeId = 106, because SNumericType was being silently removed when `_types` map is
* constructed. See `property("SNumericType.typeId resolves to SGlobal")`.
* In addition, the methods associated with the concrete numeric types cannot be
* resolved (using SMethod.fromIds()) for all numeric types (SByte, SShort, SInt,
* SLong, SBigInt). See the corresponding regression `property("MethodCall on numerics")`.
* SLong) because these types are not registered in the `_types` map.
* See the corresponding property("MethodCall on numerics")`.
* However, this "shadowing" is not a problem since all casting methods are implemented
* via Downcast, Upcast opcodes and the remaining `toBytes`, `toBits` methods are not
* implemented at all.
* In order to allow MethodCalls on numeric types in future versions the SNumericType.typeId
* should be changed and SGlobal.typeId should be preserved. The regression tests in
* `property("MethodCall Codes")` should pass.
* via lowering to Downcast, Upcast opcodes and the remaining `toBytes`, `toBits`
* methods are not implemented at all.
*
* Starting from v6.0 the SNumericType.typeId is demoted as a receiver object of
* method calls and:
* 1) numeric type SByte, SShort, SInt, SLong are promoted as receivers and added to
* the _types map.
* 2) all methods from SNumericTypeMethods are copied to all the concrete numeric types
* (SByte, SShort, SInt, SLong, SBigInt) and the generic tNum type parameter is
* specialized accordingly.
*
* This difference in behaviour is tested by `property("MethodCall on numerics")`.
*
* The regression tests in `property("MethodCall Codes")` should pass.
*/
// TODO v6.0: should contain all numeric types (including also SNumericType)
// to support method calls like 10.toByte which encoded as MethodCall with typeId = 4, methodId = 1
// see https://github.com/ScorexFoundation/sigmastate-interpreter/issues/667
lazy val types: Map[Byte, STypeCompanion] = Seq(
SBoolean, SNumericType, SString, STuple, SGroupElement, SSigmaProp, SContext, SGlobal, SHeader, SPreHeader,
private val v5Types = Seq(
SBoolean, SString, STuple, SGroupElement, SSigmaProp, SContext, SGlobal, SHeader, SPreHeader,
SAvlTree, SBox, SOption, SCollection, SBigInt
).map { t => (t.typeId, t) }.toMap
)
private val v6Types = v5Types ++ Seq(SByte, SShort, SInt, SLong)

private val v5TypesMap = v5Types.map { t => (t.typeId, t) }.toMap

private val v6TypesMap = v6Types.map { t => (t.typeId, t) }.toMap

def types: Map[Byte, STypeCompanion] = if (VersionContext.current.isV6SoftForkActivated) {
v6TypesMap
} else {
v5TypesMap
}

/** Checks that the type of the value corresponds to the descriptor `tpe`.
* If the value has complex structure only root type constructor is checked.
Expand Down Expand Up @@ -452,7 +473,7 @@ case object SLong extends SPrimType with SEmbeddable with SNumericType with SMon
}
}

/** Type of 256 bit integet values. Implemented using [[java.math.BigInteger]]. */
/** Type of 256 bit integer values. Implemented using [[java.math.BigInteger]]. */
case object SBigInt extends SPrimType with SEmbeddable with SNumericType with SMonoType {
override type WrappedType = BigInt
override val typeCode: TypeCode = 6: Byte
Expand Down
6 changes: 6 additions & 0 deletions core/shared/src/main/scala/sigma/ast/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ package object ast {

def asNumType: SNumericType = tpe.asInstanceOf[SNumericType]

/** Cast this type to numeric type or else throws the given error. */
def asNumTypeOrElse(error: => Exception): SNumericType = tpe match {
case nt: SNumericType => nt
case _ => throw error
}

def asFunc: SFunc = tpe.asInstanceOf[SFunc]

def asProduct: SProduct = tpe.asInstanceOf[SProduct]
Expand Down
6 changes: 6 additions & 0 deletions core/shared/src/main/scala/sigma/data/CBigInt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,10 @@ case class CBigInt(override val wrappedValue: BigInteger) extends BigInt with Wr
override def and(that: BigInt): BigInt = CBigInt(wrappedValue.and(that.asInstanceOf[CBigInt].wrappedValue))

override def or(that: BigInt): BigInt = CBigInt(wrappedValue.or(that.asInstanceOf[CBigInt].wrappedValue))

override def xor(that: BigInt): BigInt = CBigInt(wrappedValue.xor(that.asInstanceOf[CBigInt].wrappedValue))

override def shiftLeft(n: Int): BigInt = CBigInt(wrappedValue.shiftLeft(n).to256BitValueExact)

override def shiftRight(n: Int): BigInt = CBigInt(wrappedValue.shiftRight(n).to256BitValueExact)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import debox.cfor
import sigma.VersionContext
import sigma.ast.SCollectionType.{CollectionTypeCode, NestedCollectionTypeCode}
import sigma.ast._
import sigma.serialization.{CoreByteReader, CoreByteWriter, InvalidTypePrefix}
import sigma.util.safeNewArray
import sigma.validation.ValidationRules.{CheckPrimitiveTypeCode, CheckTypeCode}

Expand Down Expand Up @@ -215,7 +214,6 @@ class TypeSerializer {
STypeParam(ident.asInstanceOf[STypeVar])
}
SFunc(tDom, tRange, tpeParams)
// todo: serialize tParams
case _ =>
// todo: 6.0: replace 1008 check with identical behavior but other opcode, to activate
// ReplacedRule(1008 -> new opcode) during 6.0 activation
Expand Down
184 changes: 160 additions & 24 deletions data/shared/src/main/scala/sigma/ast/methods.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import sigma.ast.SCollection.{SBooleanArray, SBoxArray, SByteArray, SByteArray2,
import sigma.ast.SMethod.{MethodCallIrBuilder, MethodCostFunc, javaMethodOf}
import sigma.ast.SType.TypeCode
import sigma.ast.syntax.{SValue, ValueOps}
import sigma.data.ExactIntegral.{ByteIsExactIntegral, IntIsExactIntegral, LongIsExactIntegral, ShortIsExactIntegral}
import sigma.data.NumericOps.BigIntIsExactIntegral
import sigma.data.OverloadHack.Overloaded1
import sigma.data.{DataValueComparer, KeyValueColl, Nullable, RType, SigmaConstants}
import sigma.eval.{CostDetails, ErgoTreeEvaluator, TracedCost}
Expand Down Expand Up @@ -156,14 +158,34 @@ trait MonoTypeMethods extends MethodsContainer {

trait SNumericTypeMethods extends MonoTypeMethods {
import SNumericTypeMethods.tNum

private val subst = Map(tNum -> this.ownerType)

val v5Methods = {
SNumericTypeMethods.v5Methods.map { m =>
m.copy(stype = applySubst(m.stype, subst).asFunc)
}
}

val v6Methods = {
SNumericTypeMethods.v6Methods.map { m =>
m.copy(
objType = this, // associate the method with the concrete numeric type
stype = applySubst(m.stype, subst).asFunc
)}
}

protected override def getMethods(): Seq[SMethod] = {
super.getMethods() ++ SNumericTypeMethods.methods.map {
m => m.copy(stype = applySubst(m.stype, Map(tNum -> this.ownerType)).asFunc)
if (VersionContext.current.isV6SoftForkActivated) {
super.getMethods() ++ v6Methods
} else {
super.getMethods() ++ v5Methods
}
}
}

object SNumericTypeMethods extends MethodsContainer {

/** Type for which this container defines methods. */
override def ownerType: STypeCompanion = SNumericType

Expand Down Expand Up @@ -216,6 +238,15 @@ object SNumericTypeMethods extends MethodsContainer {
val ToBytesMethod: SMethod = SMethod(
this, "toBytes", SFunc(tNum, SByteArray), 6, ToBytes_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, _: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.toBigEndianBytes(obj.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.toBigEndianBytes(obj.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.toBigEndianBytes(obj.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.toBigEndianBytes(obj.asInstanceOf[Long])
case SBigIntMethods => obj.asInstanceOf[BigInt].toBytes
}
})
.withInfo(PropertyCall,
""" Returns a big-endian representation of this numeric value in a collection of bytes.
| For example, the \lst{Int} value \lst{0x12131415} would yield the
Expand All @@ -229,12 +260,124 @@ object SNumericTypeMethods extends MethodsContainer {
val ToBitsMethod: SMethod = SMethod(
this, "toBits", SFunc(tNum, SBooleanArray), 7, ToBits_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, _: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.toBits(obj.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.toBits(obj.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.toBits(obj.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.toBits(obj.asInstanceOf[Long])
case SBigIntMethods => BigIntIsExactIntegral.toBits(obj.asInstanceOf[BigInt])
}
})
.withInfo(PropertyCall,
""" Returns a big-endian representation of this numeric in a collection of Booleans.
| Each boolean corresponds to one bit.
""".stripMargin)

protected override def getMethods(): Seq[SMethod] = Array(
/** Cost of inverting bits of a number. */
val BitwiseOp_CostKind = FixedCost(JitCost(5))

val BitwiseInverseMethod: SMethod = SMethod(
this, "bitwiseInverse", SFunc(tNum, tNum), 8, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, _: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.bitwiseInverse(obj.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.bitwiseInverse(obj.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.bitwiseInverse(obj.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.bitwiseInverse(obj.asInstanceOf[Long])
case SBigIntMethods => BigIntIsExactIntegral.bitwiseInverse(obj.asInstanceOf[BigInt])
}
})
.withInfo(PropertyCall, desc = "Returns bitwise inverse of this numeric. ")

val BitwiseOrMethod: SMethod = SMethod(
this, "bitwiseOr", SFunc(Array(tNum, tNum), tNum), 9, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, other: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.bitwiseOr(obj.asInstanceOf[Byte], other.head.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.bitwiseOr(obj.asInstanceOf[Short], other.head.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.bitwiseOr(obj.asInstanceOf[Int], other.head.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.bitwiseOr(obj.asInstanceOf[Long], other.head.asInstanceOf[Long])
case SBigIntMethods => BigIntIsExactIntegral.bitwiseOr(obj.asInstanceOf[BigInt], other.head.asInstanceOf[BigInt])
}
})
.withInfo(MethodCall,
""" Returns bitwise or of this numeric and provided one. """.stripMargin,
ArgInfo("that", "A numeric value to calculate or with."))

val BitwiseAndMethod: SMethod = SMethod(
this, "bitwiseAnd", SFunc(Array(tNum, tNum), tNum), 10, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, other: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.bitwiseAnd(obj.asInstanceOf[Byte], other.head.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.bitwiseAnd(obj.asInstanceOf[Short], other.head.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.bitwiseAnd(obj.asInstanceOf[Int], other.head.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.bitwiseAnd(obj.asInstanceOf[Long], other.head.asInstanceOf[Long])
case SBigIntMethods => BigIntIsExactIntegral.bitwiseAnd(obj.asInstanceOf[BigInt], other.head.asInstanceOf[BigInt])
}
})
.withInfo(MethodCall,
""" Returns bitwise and of this numeric and provided one. """.stripMargin,
ArgInfo("that", "A numeric value to calculate and with."))

val BitwiseXorMethod: SMethod = SMethod(
this, "bitwiseXor", SFunc(Array(tNum, tNum), tNum), 11, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, other: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.bitwiseXor(obj.asInstanceOf[Byte], other.head.asInstanceOf[Byte])
case SShortMethods => ShortIsExactIntegral.bitwiseXor(obj.asInstanceOf[Short], other.head.asInstanceOf[Short])
case SIntMethods => IntIsExactIntegral.bitwiseXor(obj.asInstanceOf[Int], other.head.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.bitwiseXor(obj.asInstanceOf[Long], other.head.asInstanceOf[Long])
case SBigIntMethods => BigIntIsExactIntegral.bitwiseXor(obj.asInstanceOf[BigInt], other.head.asInstanceOf[BigInt])
}
})
.withInfo(MethodCall,
""" Returns bitwise xor of this numeric and provided one. """.stripMargin,
ArgInfo("that", "A numeric value to calculate xor with."))

val ShiftLeftMethod: SMethod = SMethod(
this, "shiftLeft", SFunc(Array(tNum, SInt), tNum), 12, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, other: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.shiftLeft(obj.asInstanceOf[Byte], other.head.asInstanceOf[Int])
case SShortMethods => ShortIsExactIntegral.shiftLeft(obj.asInstanceOf[Short], other.head.asInstanceOf[Int])
case SIntMethods => IntIsExactIntegral.shiftLeft(obj.asInstanceOf[Int], other.head.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.shiftLeft(obj.asInstanceOf[Long], other.head.asInstanceOf[Int])
case SBigIntMethods => BigIntIsExactIntegral.shiftLeft(obj.asInstanceOf[BigInt], other.head.asInstanceOf[Int])
}
})
.withInfo(MethodCall,
""" Returns a big-endian representation of this numeric in a collection of Booleans.
| Each boolean corresponds to one bit.
""".stripMargin,
ArgInfo("bits", "Number of bit to shift to the left. Note, that bits value must be non-negative and less than " +
"the size of the number in bits (e.g. 64 for Long, 256 for BigInt)"))

val ShiftRightMethod: SMethod = SMethod(
this, "shiftRight", SFunc(Array(tNum, SInt), tNum), 13, BitwiseOp_CostKind)
.withIRInfo(MethodCallIrBuilder)
.withUserDefinedInvoke({ (m: SMethod, obj: Any, other: Array[Any]) =>
m.objType match {
case SByteMethods => ByteIsExactIntegral.shiftRight(obj.asInstanceOf[Byte], other.head.asInstanceOf[Int])
case SShortMethods => ShortIsExactIntegral.shiftRight(obj.asInstanceOf[Short], other.head.asInstanceOf[Int])
case SIntMethods => IntIsExactIntegral.shiftRight(obj.asInstanceOf[Int], other.head.asInstanceOf[Int])
case SLongMethods => LongIsExactIntegral.shiftRight(obj.asInstanceOf[Long], other.head.asInstanceOf[Int])
case SBigIntMethods => BigIntIsExactIntegral.shiftRight(obj.asInstanceOf[BigInt], other.head.asInstanceOf[Int])
}
})
.withInfo(MethodCall,
""" Returns a big-endian representation of this numeric in a collection of Booleans.
| Each boolean corresponds to one bit.
""".stripMargin,
ArgInfo("bits", "Number of bit to shift to the right. Note, that bits value must be non-negative and less than " +
"the size of the number in bits (e.g. 64 for Long, 256 for BigInt)"))

lazy val v5Methods = Array(
ToByteMethod, // see Downcast
ToShortMethod, // see Downcast
ToIntMethod, // see Downcast
Expand All @@ -244,7 +387,21 @@ object SNumericTypeMethods extends MethodsContainer {
ToBitsMethod
)

lazy val v6Methods = v5Methods ++ Array(
BitwiseInverseMethod,
BitwiseOrMethod,
BitwiseAndMethod,
BitwiseXorMethod,
ShiftLeftMethod,
ShiftRightMethod
)

protected override def getMethods(): Seq[SMethod] = {
throw new Exception("SNumericTypeMethods.getMethods shouldn't ever be called")
}

/** Collection of names of numeric casting methods (like `toByte`, `toInt`, etc). */
// todo: add unsigned big int
val castMethods: Array[String] =
Array(ToByteMethod, ToShortMethod, ToIntMethod, ToLongMethod, ToBigIntMethod)
.map(_.name)
Expand Down Expand Up @@ -309,21 +466,6 @@ case object SBigIntMethods extends SNumericTypeMethods {
/** Type for which this container defines methods. */
override def ownerType: SMonoType = SBigInt

/** The following `modQ` methods are not fully implemented in v4.x and this descriptors.
* This descritors are remain here in the code and are waiting for full implementation
* is upcoming soft-forks at which point the cost parameters should be calculated and
* changed.
*/
val ModQMethod = SMethod(this, "modQ", SFunc(this.ownerType, SBigInt), 1, FixedCost(JitCost(1)))
.withInfo(ModQ, "Returns this \\lst{mod} Q, i.e. remainder of division by Q, where Q is an order of the cryprographic group.")
val PlusModQMethod = SMethod(this, "plusModQ", SFunc(IndexedSeq(this.ownerType, SBigInt), SBigInt), 2, FixedCost(JitCost(1)))
.withInfo(ModQArithOp.PlusModQ, "Adds this number with \\lst{other} by module Q.", ArgInfo("other", "Number to add to this."))
val MinusModQMethod = SMethod(this, "minusModQ", SFunc(IndexedSeq(this.ownerType, SBigInt), SBigInt), 3, FixedCost(JitCost(1)))
.withInfo(ModQArithOp.MinusModQ, "Subtracts \\lst{other} number from this by module Q.", ArgInfo("other", "Number to subtract from this."))
val MultModQMethod = SMethod(this, "multModQ", SFunc(IndexedSeq(this.ownerType, SBigInt), SBigInt), 4, FixedCost(JitCost(1)))
.withIRInfo(MethodCallIrBuilder)
.withInfo(MethodCall, "Multiply this number with \\lst{other} by module Q.", ArgInfo("other", "Number to multiply with this."))

protected override def getMethods(): Seq[SMethod] = {
if (VersionContext.current.isV6SoftForkActivated) {
super.getMethods()
Expand All @@ -337,12 +479,6 @@ case object SBigIntMethods extends SNumericTypeMethods {
}
}

/**
*
*/
def nbits_eval(mc: MethodCall, bi: sigma.BigInt)(implicit E: ErgoTreeEvaluator): Long = {
???
}

}

Expand Down
Loading

0 comments on commit 3d88fc2

Please sign in to comment.