diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/NumericModule.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/NumericModule.scala index 128bb6d4d3bf..3be6f4b8bc0c 100644 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/NumericModule.scala +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/NumericModule.scala @@ -249,7 +249,7 @@ abstract class NumericModule { final def assertFromUnscaledBigDecimal(x: BigDec): Numeric = assertRight(fromUnscaledBigDecimal(x)) - final def toUnscaledString(x: Numeric): String = { + final def toUnscaledString(x: BigDecimal): String = { // Strip the trailing zeros (which BigDecimal keeps if the string // it was created from had them), and use the plain notation rather // than scientific notation. diff --git a/daml-lf/interpreter/BUILD.bazel b/daml-lf/interpreter/BUILD.bazel index 8c705f7e72e4..2f99918c24a1 100644 --- a/daml-lf/interpreter/BUILD.bazel +++ b/daml-lf/interpreter/BUILD.bazel @@ -4,6 +4,7 @@ load( "//bazel_tools:scala.bzl", "da_scala_library", + "da_scala_test", "da_scala_test_suite", "lf_scalacopts", ) @@ -39,10 +40,15 @@ da_scala_library( ], ) +bigNumericTests = "src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinBigNumericTest.scala" + da_scala_test_suite( name = "tests", size = "small", - srcs = glob(["src/test/**/*.scala"]), + srcs = glob( + ["src/test/**/*.scala"], + exclude = [bigNumericTests], + ), scala_deps = [ "@maven//:org_scala_lang_modules_scala_collection_compat", "@maven//:org_scalacheck_scalacheck", @@ -66,6 +72,23 @@ da_scala_test_suite( ], ) +da_scala_test( + name = "test_bignumeric", + srcs = [bigNumericTests], + scala_deps = [ + "@maven//:org_scalatest_scalatest", + "@maven//:org_scalaz_scalaz_core", + ], + scalacopts = lf_scalacopts, + deps = [ + ":interpreter", + "//daml-lf/data", + "//daml-lf/language", + "//daml-lf/parser", + "//daml-lf/transaction", + ], +) + scala_repl( name = "interpreter@repl", deps = [ diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala index 00270262d49e..75f5c19ac468 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala @@ -428,7 +428,7 @@ private[lf] object SBuiltin { case SParty(p) => p case SUnit => s"" case SDate(date) => date.toString - case SBigNumeric(x) => Numeric.toString(x) + case SBigNumeric(x) => Numeric.toUnscaledString(x) case SContractId(_) | SNumeric(_) => crash("litToText: literal not supported") case otherwise => throw SErrorCrash( @@ -923,11 +923,7 @@ private[lf] object SBuiltin { final object SBToBigNumericNumeric extends SBuiltinPure(2) { override private[speedy] def executePure(args: util.ArrayList[SValue]): SBigNumeric = { - // should not fail - rightOrArithmeticError( - "overflow/underflow", - SBigNumeric.fromBigDecimal(getSNumeric(args, 1)), - ) + SBigNumeric.fromNumeric(getSNumeric(args, 1)) } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala index de182483c25b..7a1a77d313c5 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala @@ -242,10 +242,9 @@ object SValue { override def toString: String = s"SBigNumeric($value)" } object SBigNumeric { - // TODO https://github.com/digital-asset/daml/issues/8719 - // Decide what are the actual bound for BigDecimal - val MaxScale = 1 << 10 - val MaxPrecision = MaxScale << 2 + val MaxPrecision = 1 << 16 + val MaxScale = MaxPrecision / 2 + val MinScale = -MaxPrecision / 2 + 1 def unapply(value: SBigNumeric): Some[java.math.BigDecimal] = Some(value.value) @@ -253,12 +252,15 @@ object SValue { def fromBigDecimal(x: java.math.BigDecimal): Either[String, SBigNumeric] = { val norm = x.stripTrailingZeros() Either.cond( - test = norm.scale <= MaxScale && norm.precision + norm.scale < MaxScale, + test = norm.scale <= MaxScale && norm.precision - norm.scale <= MaxScale, right = new SBigNumeric(norm), left = "non valid BigNumeric", ) } + def fromNumeric(x: Numeric) = + new SBigNumeric(x.stripTrailingZeros()) + def assertFromBigDecimal(x: java.math.BigDecimal): SBigNumeric = data.assertRight(fromBigDecimal(x)) diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinBigNumericTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinBigNumericTest.scala new file mode 100644 index 000000000000..7f69b332ac25 --- /dev/null +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinBigNumericTest.scala @@ -0,0 +1,389 @@ +// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf +package speedy + +import com.daml.lf.data._ +import com.daml.lf.language.Ast._ +import com.daml.lf.speedy.SError.SError +import com.daml.lf.speedy.SExpr._ +import com.daml.lf.speedy.SResult.{SResultError, SResultFinalValue} +import com.daml.lf.speedy.SValue.{SValue => _, _} +import com.daml.lf.testing.parser.Implicits._ +import org.scalatest.Inside.inside +import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.matchers.should.Matchers +import org.scalatest.freespec.AnyFreeSpec + +import scala.language.implicitConversions + +class SBuiltinBigNumericTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChecks { + + import SBuiltinBigNumericTest._ + + private implicit def toScale(i: Int): Numeric.Scale = Numeric.Scale.assertFromInt(i) + + private def n(scale: Int, x: BigDecimal): Numeric = Numeric.assertFromBigDecimal(scale, x) + private def n(scale: Int, str: String): Numeric = n(scale, BigDecimal(str)) + private def s(scale: Int, x: BigDecimal): String = Numeric.toString(n(scale, x)) + private def s(scale: Int, str: String): String = s(scale, BigDecimal(str)) + + private def tenPowerOf(i: Int, scale: Int) = + if (i == 0) + "1." + "0" * scale + else if (i > 0) + "1" + "0" * i + "." + "0" * scale + else + "0." + "0" * (-i - 1) + "1" + "0" * (scale + i) + + private val decimals = Table[String]( + "Decimals", + "161803398.87499", + "3.1415926536", + "2.7182818285", + "0.0000000001", + "0.0", + "100.0", + "-0.0000000001", + "-2.7182818285", + "-3.1415926536", + "-161803398.87499", + ) + + "BigNumeric operations" - { + import java.math.BigDecimal + import SValue.SBigNumeric.{MaxScale, MinScale} + + "BigNumeric binary operations compute proper results" in { + import scala.math.{BigDecimal => BigDec} + import SBigNumeric.assertFromBigDecimal + + val testCases = Table[String, (BigDecimal, BigDecimal) => Option[SValue]]( + ("builtin", "reference"), + ("ADD_BIGNUMERIC", (a, b) => Some(assertFromBigDecimal(a add b))), + ("SUB_BIGNUMERIC", (a, b) => Some(assertFromBigDecimal(a subtract b))), + ("MUL_BIGNUMERIC ", (a, b) => Some(assertFromBigDecimal(a multiply b))), + ( + "DIV_BIGNUMERIC 10 ROUNDING_HALF_EVEN", + { + case (a, b) if b.signum != 0 => + Some(assertFromBigDecimal(a.divide(b, 10, java.math.RoundingMode.HALF_EVEN))) + case _ => None + }, + ), + ("LESS_EQ @BigNumeric", (a, b) => Some(SBool(BigDec(a) <= BigDec(b)))), + ("GREATER_EQ @BigNumeric", (a, b) => Some(SBool(BigDec(a) >= BigDec(b)))), + ("LESS @BigNumeric", (a, b) => Some(SBool(BigDec(a) < BigDec(b)))), + ("GREATER @BigNumeric", (a, b) => Some(SBool(BigDec(a) > BigDec(b)))), + ("EQUAL @BigNumeric", (a, b) => Some(SBool(BigDec(a) == BigDec(b)))), + ) + + forEvery(testCases) { (builtin, ref) => + forEvery(decimals) { a => + forEvery(decimals) { b => + val actualResult = eval( + e"$builtin (TO_BIGNUMERIC_NUMERIC @10 ${s(10, a)}) (TO_BIGNUMERIC_NUMERIC @10 ${s(10, b)})" + ) + + actualResult.toOption shouldBe ref(n(10, a), n(10, b)) + } + } + } + } + + "TO_TEXT_BIGNUMERIC" - { + "return proper result" in { + val testCases = Table( + "expression" -> "regex", + "BigNumeric:zero" -> "0\\.0", + "BigNumeric:one" -> "1\\.0", + "BigNumeric:minusOne" -> "-1\\.0", + "BigNumeric:minPositive" -> s"0\\.(0{${SBigNumeric.MaxScale - 1}})1", + "BigNumeric:maxPositive" -> s"(9{${SBigNumeric.MaxScale}})\\.(9{${SBigNumeric.MaxScale}})", + "BigNumeric:maxNegative" -> s"-0\\.(0{${SBigNumeric.MaxScale - 1}})1", + "BigNumeric:minNegative" -> s"-(9{${SBigNumeric.MaxScale}})\\.(9{${SBigNumeric.MaxScale}})", + ) + + forEvery(testCases) { (exp, regexp) => + val p = regexp.r.pattern + inside(eval(e"TO_TEXT_BIGNUMERIC $exp")) { + case Right(SText(x)) if p.matcher(x).find() => + } + } + } + } + + "ADD_BIGNUMERIC" - { + val builtin = "ADD_BIGNUMERIC" + + "throws an exception in case of overflow" in { + eval(e"$builtin BigNumeric:maxPositive BigNumeric:maxNegative") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:maxPositive BigNumeric:minPositive") shouldBe a[Left[_, _]] + eval(e"$builtin BigNumeric:minNegative BigNumeric:minPositive") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:minNegative BigNumeric:maxNegative") shouldBe a[Left[_, _]] + eval(e"$builtin BigNumeric:x BigNumeric:almostX") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:x BigNumeric:x") shouldBe a[Left[_, _]] + } + } + + "SUB_BIGNUMERIC" - { + val builtin = "SUB_BIGNUMERIC" + + "throws an exception in case of overflow" in { + eval(e"$builtin BigNumeric:minNegative BigNumeric:maxNegative") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:minNegative BigNumeric:minPositive") shouldBe a[Left[_, _]] + eval(e"$builtin BigNumeric:maxPositive BigNumeric:minPositive") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:maxPositive BigNumeric:maxNegative") shouldBe a[Left[_, _]] + eval(e"$builtin BigNumeric:minusX BigNumeric:almostX") shouldBe a[Right[_, _]] + eval(e"$builtin BigNumeric:minusX BigNumeric:x") shouldBe a[Left[_, _]] + } + } + + "MUL_BIGNUMERIC" - { + "throws an exception in case of overflow" in { + + val testCases = Table( + "arguments" -> "success", + s"(SHIFT_BIGNUMERIC ${MinScale} BigNumeric:one) BigNumeric:one" -> true, + s"(SHIFT_BIGNUMERIC ${MinScale} BigNumeric:one) BigNumeric:ten" -> false, + s"(SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one) (SHIFT_BIGNUMERIC ${MinScale / 2} BigNumeric:one)" -> true, + s"(SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one) (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one)" -> false, + s"(SHIFT_BIGNUMERIC ${MaxScale} BigNumeric:one) BigNumeric:one" -> true, + s"(SHIFT_BIGNUMERIC ${MaxScale} BigNumeric:one) (SHIFT_BIGNUMERIC 1 BigNumeric:one)" -> false, + s"(SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one) (SHIFT_BIGNUMERIC ${MinScale / 2} BigNumeric:one)" -> true, + s"(SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one) (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one)" -> false, + s"(SHIFT_BIGNUMERIC ${MinScale / 2} BigNumeric:underSqrtOfTen) (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:underSqrtOfTen)" -> true, + s"(SHIFT_BIGNUMERIC ${MinScale / 2} BigNumeric:overSqrtOfTen) (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:overSqrtOfTen)" -> false, + ) + + forEvery(testCases)((args, success) => + eval(e"MUL_BIGNUMERIC $args") shouldBe (if (success) a[Right[_, _]] else a[Left[_, _]]) + ) + } + } + + "DIV_BIGNUMERIC" - { + "throws an exception in case of overflow" in { + val testCases = Table( + "arguments" -> "success", + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale / 2} BigNumeric:one) (SHIFT_BIGNUMERIC ${-(MinScale / 2 - 1)} BigNumeric:one)" -> true, + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:one) (SHIFT_BIGNUMERIC ${-(MinScale / 2 - 1)} BigNumeric:one)" -> false, + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale} BigNumeric:one) BigNumeric:one" -> true, + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale} BigNumeric:one) (SHIFT_BIGNUMERIC 1 BigNumeric:one)" -> false, + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:underSqrtOfTen) (SHIFT_BIGNUMERIC ${-(MinScale / 2 - 1)} BigNumeric:overSqrtOfTen)" -> true, + s"-1000 ROUNDING_DOWN (SHIFT_BIGNUMERIC ${MinScale / 2 - 1} BigNumeric:overSqrtOfTen) (SHIFT_BIGNUMERIC ${-(MinScale / 2 - 1)} BigNumeric:overSqrtOfTen)" -> false, + s"${MinScale} ROUNDING_UP BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> false, + s"${MinScale} ROUNDING_DOWN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> true, + s"${MinScale} ROUNDING_CEILING BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> false, + s"${MinScale} ROUNDING_FLOOR BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> true, + s"${MinScale} ROUNDING_HALF_UP BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> false, + s"${MinScale} ROUNDING_HALF_DOWN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> true, + s"${MinScale} ROUNDING_HALF_EVEN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:two)" -> false, + s"${MinScale} ROUNDING_UP BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_DOWN BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_CEILING BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_FLOOR BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_HALF_UP BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_HALF_DOWN BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_HALF_EVEN BigNumeric:twentyEight (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_UP BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_DOWN BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_CEILING BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_FLOOR BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> true, + s"${MinScale} ROUNDING_HALF_UP BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_HALF_DOWN BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_HALF_EVEN BigNumeric:twentyNine (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:three)" -> false, + s"${MinScale} ROUNDING_UP BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> false, + s"${MinScale} ROUNDING_DOWN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> true, + s"${MinScale} ROUNDING_CEILING BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> true, + s"${MinScale} ROUNDING_FLOOR BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> false, + s"${MinScale} ROUNDING_HALF_UP BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> false, + s"${MinScale} ROUNDING_HALF_DOWN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> true, + s"${MinScale} ROUNDING_HALF_EVEN BigNumeric:nineteen (SHIFT_BIGNUMERIC ${-MinScale} BigNumeric:minusTwo)" -> false, + ) + forEvery(testCases)((args, success) => + eval(e"DIV_BIGNUMERIC $args") shouldBe (if (success) a[Right[_, _]] else a[Left[_, _]]) + ) + } + + "crash if cannot compute the result without rounding" in { + val testCases = Table( + "arguments" -> "success", + s"${MaxScale} ROUNDING_UNNECESSARY BigNumeric:one BigNumeric:two" -> true, + s"${MaxScale} ROUNDING_UNNECESSARY BigNumeric:one BigNumeric:three" -> false, + s"${MaxScale / 2} ROUNDING_UNNECESSARY BigNumeric:one BigNumeric:three" -> false, + "1 ROUNDING_UNNECESSARY BigNumeric:one BigNumeric:two" -> true, + "0 ROUNDING_UNNECESSARY BigNumeric:one BigNumeric:two" -> false, + ) + + forEvery(testCases)((args, success) => + eval(e"DIV_BIGNUMERIC $args") shouldBe (if (success) a[Right[_, _]] else a[Left[_, _]]) + ) + } + + "round as expected" in { + + val testCases = Table( + "arguments" -> "success", + "0 ROUNDING_UP BigNumeric:nineteen BigNumeric:two" -> "10", + "0 ROUNDING_DOWN BigNumeric:nineteen BigNumeric:two" -> "9", + "0 ROUNDING_CEILING BigNumeric:nineteen BigNumeric:two" -> "10", + "0 ROUNDING_FLOOR BigNumeric:nineteen BigNumeric:two" -> "9", + "0 ROUNDING_HALF_UP BigNumeric:nineteen BigNumeric:two" -> "10", + "0 ROUNDING_HALF_DOWN BigNumeric:nineteen BigNumeric:two" -> "9", + "0 ROUNDING_HALF_EVEN BigNumeric:nineteen BigNumeric:two" -> "10", + "1 ROUNDING_UP BigNumeric:twentyEight BigNumeric:three" -> "9.4", + "1 ROUNDING_DOWN BigNumeric:twentyEight BigNumeric:three" -> "9.3", + "1 ROUNDING_CEILING BigNumeric:twentyEight BigNumeric:three" -> "9.4", + "1 ROUNDING_FLOOR BigNumeric:twentyEight BigNumeric:three" -> "9.3", + "1 ROUNDING_HALF_UP BigNumeric:twentyEight BigNumeric:three" -> "9.3", + "1 ROUNDING_HALF_DOWN BigNumeric:twentyEight BigNumeric:three" -> "9.3", + "1 ROUNDING_HALF_EVEN BigNumeric:twentyEight BigNumeric:three" -> "9.3", + "2 ROUNDING_UP BigNumeric:twentyNine BigNumeric:three" -> "9.67", + "2 ROUNDING_DOWN BigNumeric:twentyNine BigNumeric:three" -> "9.66", + "2 ROUNDING_CEILING BigNumeric:twentyNine BigNumeric:three" -> "9.67", + "2 ROUNDING_FLOOR BigNumeric:twentyNine BigNumeric:three" -> "9.66", + "2 ROUNDING_HALF_UP BigNumeric:twentyNine BigNumeric:three" -> "9.67", + "2 ROUNDING_HALF_DOWN BigNumeric:twentyNine BigNumeric:three" -> "9.67", + "2 ROUNDING_HALF_EVEN BigNumeric:twentyNine BigNumeric:three" -> "9.67", + "0 ROUNDING_UP BigNumeric:nineteen BigNumeric:minusTwo" -> "-10", + "0 ROUNDING_DOWN BigNumeric:nineteen BigNumeric:minusTwo" -> "-9", + "0 ROUNDING_CEILING BigNumeric:nineteen BigNumeric:minusTwo" -> "-9", + "0 ROUNDING_FLOOR BigNumeric:nineteen BigNumeric:minusTwo" -> "-10", + "0 ROUNDING_HALF_UP BigNumeric:nineteen BigNumeric:minusTwo" -> "-10", + "0 ROUNDING_HALF_DOWN BigNumeric:nineteen BigNumeric:minusTwo" -> "-9", + "0 ROUNDING_HALF_EVEN BigNumeric:nineteen BigNumeric:minusTwo" -> "-10", + ) + forEvery(testCases)((args, result) => + inside(eval(e"DIV_BIGNUMERIC $args")) { case Right(SBigNumeric(x)) => + assert(x == new BigDecimal(result).stripTrailingZeros()) + } + ) + } + + } + + "SHIFT_BIGNUMERIC" - { + import java.math.BigDecimal + import SBigNumeric.assertFromBigDecimal + + "returns proper result" in { + val testCases = Table[Int, Int, String, String]( + ("input scale", "output scale", "input", "output"), + (0, 1, s(0, Numeric.maxValue(0)), s(1, Numeric.maxValue(1))), + (0, 37, tenPowerOf(1, 0), tenPowerOf(-36, 37)), + (20, 10, tenPowerOf(15, 20), tenPowerOf(5, 30)), + (20, -10, tenPowerOf(15, 20), tenPowerOf(25, 10)), + (10, 10, tenPowerOf(-5, 10), tenPowerOf(-15, 20)), + (20, -10, tenPowerOf(-5, 20), tenPowerOf(5, 10)), + (10, 10, tenPowerOf(10, 10), tenPowerOf(0, 20)), + (20, -10, tenPowerOf(10, 20), tenPowerOf(20, 10)), + ) + forEvery(testCases) { (inputScale, shifting, input, output) => + eval( + e"SHIFT_BIGNUMERIC $shifting (TO_BIGNUMERIC_NUMERIC @$inputScale $input)" + ) shouldBe Right(assertFromBigDecimal(new BigDecimal(output))) + } + } + + "throws an exception in case of underflow/overflow" in { + val testCases = Table( + "arguments" -> "success", + "0 BigNumeric:maxPositive" -> true, + "1 BigNumeric:maxPositive" -> false, + "-1 BigNumeric:maxPositive" -> false, + s"${MaxScale} BigNumeric:one" -> true, + s"${MaxScale + 1} BigNumeric:one" -> false, + s"${MinScale} BigNumeric:one" -> true, + s"${MinScale - 1} BigNumeric:one" -> false, + ) + + forEvery(testCases)((args, success) => + eval(e"SHIFT_BIGNUMERIC $args") shouldBe (if (success) a[Right[_, _]] else a[Left[_, _]]) + ) + } + } + } + +} + +object SBuiltinBigNumericTest { + + private val pkg = + p""" + module BigNumeric { + + val maxScale: Int64 = ${SValue.SBigNumeric.MaxScale}; + val minScale: Int64 = SUB_INT64 1 BigNumeric:maxScale; + + val zero: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 0. ; + val one: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 1. ; + val two: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 2. ; + val three: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 3. ; + val minusOne: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 -1. ; + val minusTwo: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 -2. ; + val ten: BigNumeric = TO_BIGNUMERIC_NUMERIC @0 10. ; + val underSqrtOfTen: BigNumeric = + TO_BIGNUMERIC_NUMERIC @37 3.1622776601683793319988935444327185337; + val overSqrtOfTen: BigNumeric = + TO_BIGNUMERIC_NUMERIC @37 3.1622776601683793319988935444327185338; + val nineteen: BigNumeric = + TO_BIGNUMERIC_NUMERIC @0 19.; + val twentyEight: BigNumeric = + TO_BIGNUMERIC_NUMERIC @0 28.; + val twentyNine: BigNumeric = + TO_BIGNUMERIC_NUMERIC @0 29.; + val minPositive: BigNumeric = + SHIFT_BIGNUMERIC BigNumeric:maxScale BigNumeric:one; + val maxPositive: BigNumeric = + let x: BigNumeric = SUB_BIGNUMERIC BigNumeric:one BigNumeric:minPositive in + ADD_BIGNUMERIC x (SHIFT_BIGNUMERIC (SUB_INT64 0 BigNumeric:maxScale) x); + val maxNegative: BigNumeric = + SHIFT_BIGNUMERIC BigNumeric:maxScale BigNumeric:minusOne; + val minNegative: BigNumeric = + let x: BigNumeric = ADD_BIGNUMERIC BigNumeric:minusOne BigNumeric:minPositive in + ADD_BIGNUMERIC x (SHIFT_BIGNUMERIC (SUB_INT64 0 BigNumeric:maxScale) x); + + val tenPower: Int64 -> BigNumeric = \(n: Int64) -> + SHIFT_BIGNUMERIC (SUB_INT64 0 n) BigNumeric:one; + val x: BigNumeric = SHIFT_BIGNUMERIC ${SValue.SBigNumeric.MinScale} (TO_BIGNUMERIC_NUMERIC @0 5.); + val almostX: BigNumeric = SUB_BIGNUMERIC BigNumeric:x BigNumeric:minPositive; + val minusX: BigNumeric = SUB_BIGNUMERIC BigNumeric:zero BigNumeric:x; + } + + """ + + val compiledPackages = { + val x = PureCompiledPackages(Map(defaultParserParameters.defaultPackageId -> pkg)) + x.toOption.get + } + + private def eval(e: Expr, onLedger: Boolean = true): Either[SError, SValue] = { + evalSExpr(compiledPackages.compiler.unsafeCompile(e), onLedger) + } + + private def evalSExpr(e: SExpr, onLedger: Boolean): Either[SError, SValue] = { + val machine = if (onLedger) { + val seed = crypto.Hash.hashPrivateKey("SBuiltinTest") + Speedy.Machine.fromScenarioSExpr( + compiledPackages, + transactionSeed = seed, + scenario = SEApp(SEMakeClo(Array(), 2, SELocA(0)), Array(e)), + ) + } else { + Speedy.Machine.fromPureSExpr(compiledPackages, e) + } + final case class Goodbye(e: SError) extends RuntimeException("", null, false, false) + try { + val value = machine.run() match { + case SResultFinalValue(v) => v + case SResultError(err) => throw Goodbye(err) + case res => throw new RuntimeException(s"Got unexpected interpretation result $res") + } + + Right(value) + } catch { case Goodbye(err) => Left(err) } + } + +} diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala index 6a30d6f91919..61d2cd559c30 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala @@ -6,7 +6,6 @@ package speedy import java.util -import com.daml.lf.crypto import com.daml.lf.data._ import com.daml.lf.language.Ast._ import com.daml.lf.speedy.SError.{DamlEArithmeticError, SError, SErrorCrash} @@ -14,12 +13,13 @@ import com.daml.lf.speedy.SExpr._ import com.daml.lf.speedy.SResult.{SResultError, SResultFinalValue} import com.daml.lf.speedy.SValue.{SValue => _, _} import com.daml.lf.testing.parser.Implicits._ -import com.daml.lf.value.{Value} +import com.daml.lf.value.Value import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.matchers.should.Matchers import org.scalatest.freespec.AnyFreeSpec import scala.language.implicitConversions + class SBuiltinTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChecks { import SBuiltinTest._ @@ -204,9 +204,7 @@ class SBuiltinTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChe } - "Decimal operations" - { - // TODO https://github.com/digital-asset/daml/issues/8719 - // Add extensive test for BigNumeric builtins + "Numeric operations" - { val maxDecimal = Decimal.MaxValue @@ -366,7 +364,7 @@ class SBuiltinTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChe } } - "Numeric binary operations compute proper results" in { + "Decimal binary operations compute proper results" in { def round(x: BigDecimal) = n(10, x.setScale(10, BigDecimal.RoundingMode.HALF_EVEN)) @@ -401,71 +399,6 @@ class SBuiltinTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChe } } - "BigNumeric binary operations compute proper results" in { - import java.math.BigDecimal - import scala.math.{BigDecimal => BigDec} - import SBigNumeric.assertFromBigDecimal - - val testCases = Table[String, (BigDecimal, BigDecimal) => Option[SValue]]( - ("builtin", "reference"), - ("ADD_BIGNUMERIC", (a, b) => Some(assertFromBigDecimal(a add b))), - ("SUB_BIGNUMERIC", (a, b) => Some(assertFromBigDecimal(a subtract b))), - ("MUL_BIGNUMERIC ", (a, b) => Some(assertFromBigDecimal(a multiply b))), - ( - "DIV_BIGNUMERIC 10 ROUNDING_HALF_EVEN", - { - case (a, b) if b.signum != 0 => - Some(assertFromBigDecimal(a.divide(b, 10, java.math.RoundingMode.HALF_EVEN))) - case _ => None - }, - ), - ("LESS_EQ @BigNumeric", (a, b) => Some(SBool(BigDec(a) <= BigDec(b)))), - ("GREATER_EQ @BigNumeric", (a, b) => Some(SBool(BigDec(a) >= BigDec(b)))), - ("LESS @BigNumeric", (a, b) => Some(SBool(BigDec(a) < BigDec(b)))), - ("GREATER @BigNumeric", (a, b) => Some(SBool(BigDec(a) > BigDec(b)))), - ("EQUAL @BigNumeric", (a, b) => Some(SBool(BigDec(a) == BigDec(b)))), - ) - - forEvery(testCases) { (builtin, ref) => - forEvery(decimals) { a => - forEvery(decimals) { b => - val actualResult = eval( - e"$builtin (TO_BIGNUMERIC_NUMERIC @10 ${s(10, a)}) (TO_BIGNUMERIC_NUMERIC @10 ${s(10, b)})" - ) - - val expectedResult = ref(n(10, a), n(10, b)) - - if (actualResult.toOption != expectedResult) - actualResult shouldBe expectedResult - } - } - } - } - - "SHIFT_BIGNUMERIC" - { - import java.math.BigDecimal - import SBigNumeric.assertFromBigDecimal - - "returns proper result" in { - val testCases = Table[Int, Int, String, String]( - ("input scale", "output scale", "input", "output"), - (0, 1, s(0, Numeric.maxValue(0)), s(1, Numeric.maxValue(1))), - (0, 37, tenPowerOf(1, 0), tenPowerOf(-36, 37)), - (20, 10, tenPowerOf(15, 20), tenPowerOf(5, 30)), - (20, -10, tenPowerOf(15, 20), tenPowerOf(25, 10)), - (10, 10, tenPowerOf(-5, 10), tenPowerOf(-15, 20)), - (20, -10, tenPowerOf(-5, 20), tenPowerOf(5, 10)), - (10, 10, tenPowerOf(10, 10), tenPowerOf(0, 20)), - (20, -10, tenPowerOf(10, 20), tenPowerOf(20, 10)), - ) - forEvery(testCases) { (inputScale, shifting, input, output) => - eval( - e"SHIFT_BIGNUMERIC $shifting (TO_BIGNUMERIC_NUMERIC @$inputScale $input)" - ) shouldBe Right(assertFromBigDecimal(new BigDecimal(output))) - } - } - } - "TO_TEXT_NUMERIC" - { "returns proper results" in { forEvery(decimals) { a =>