From c7df212d4202437015c8700b5e3f1e042afea71d Mon Sep 17 00:00:00 2001 From: Remy <45566104+remyhaemmerle-da@users.noreply.github.com> Date: Tue, 14 May 2019 20:10:45 +0200 Subject: [PATCH] Daml lf type safty (Decimal) (#1098) * daml-lf: make Decimal type safe * daml-lf: create Utf8String type * daml-lf: cleanup in data package * Address Stephen Comments * daml-lf: remove UTF8String --- .../digitalasset/daml/lf/archive/Reader.scala | 2 +- .../daml/lf/data/ArrayFactory.scala | 14 ++ .../digitalasset/daml/lf/data/Decimal.scala | 120 ----------------- .../daml/lf/data/DecimalModule.scala | 124 ++++++++++++++++++ .../daml/lf/data/MatchingStringModule.scala | 17 +-- .../com/digitalasset/daml/lf/data/Ref.scala | 11 +- .../daml/lf/data/SortedLookupList.scala | 10 +- .../com/digitalasset/daml/lf/data/Time.scala | 22 ++-- .../com/digitalasset/daml/lf/data/UTF8.scala | 70 ---------- .../com/digitalasset/daml/lf/data/Utf8.scala | 65 +++++++++ .../digitalasset/daml/lf/data/package.scala | 18 +++ .../daml/lf/data/SortedLookupListSpec.scala | 8 +- .../data/{UTF8Spec.scala => Utf8Spec.scala} | 26 ++-- .../daml/lf/engine/EngineTest.scala | 2 +- .../daml/lf/speedy/SBuiltin.scala | 28 ++-- .../digitalasset/daml/lf/speedy/SValue.scala | 3 +- .../daml/lf/interp/testing/ToTextTest.scala | 6 +- .../daml/lf/speedy/SBuiltinTest.scala | 38 +++--- .../digitalasset/daml/lf/lfpackage/Ast.scala | 4 +- .../daml/lf/lfpackage/AstSpec.scala | 9 +- .../daml/lf/testing/parser/Implicits.scala | 9 +- .../daml/lf/testing/parser/Token.scala | 2 +- .../daml/lf/testing/parser/ParsersSpec.scala | 9 +- .../com/digitalasset/daml/lf/repl/Main.scala | 7 +- daml-lf/scenario-interpreter/BUILD.bazel | 1 + .../daml/lf/speedy/ScenarioRunnerTest.scala | 4 +- .../lf/engine/testing/SemanticTester.scala | 2 +- .../daml/lf/value/ValueGenerators.scala | 11 +- .../lf/transaction/TransactionCoder.scala | 2 +- .../digitalasset/daml/lf/value/Value.scala | 3 +- .../daml/lf/value/ValueCoder.scala | 2 +- .../lf/transaction/TransactionCoderSpec.scala | 4 +- .../daml/lf/transaction/TransactionSpec.scala | 3 +- .../daml/lf/value/ValueCoderSpec.scala | 6 +- .../extractor/json/JsonConverters.scala | 4 +- .../extractor/ledger/types/LedgerValue.scala | 2 +- .../participant/util/LfEngineToApi.scala | 3 +- .../api/ValueConversionRoundTripTest.scala | 6 +- ...ommandSubmissionRequestValidatorTest.scala | 20 ++- .../api/testtool/LedgerApiTestTool.scala | 4 +- .../ledger/sql/serialisation/KeyHasher.scala | 5 +- .../transaction/EventConverterSpec.scala | 3 +- .../stores/ledger/sql/KeyHasherSpec.scala | 21 ++- .../stores/ledger/sql/PostgresDaoSpec.scala | 16 ++- .../navigator/json/ApiCodecCompressed.scala | 10 +- .../navigator/json/ApiCodecVerbose.scala | 5 +- .../model/converter/LedgerApiV1.scala | 2 +- .../navigator/backend/DamlConstants.scala | 3 +- 48 files changed, 404 insertions(+), 362 deletions(-) create mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/ArrayFactory.scala delete mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Decimal.scala create mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/DecimalModule.scala delete mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/UTF8.scala create mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Utf8.scala create mode 100644 daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/package.scala rename daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/{UTF8Spec.scala => Utf8Spec.scala} (82%) diff --git a/daml-lf/archive/src/main/scala/com/digitalasset/daml/lf/archive/Reader.scala b/daml-lf/archive/src/main/scala/com/digitalasset/daml/lf/archive/Reader.scala index 1bb90c7556..9b350ad631 100644 --- a/daml-lf/archive/src/main/scala/com/digitalasset/daml/lf/archive/Reader.scala +++ b/daml-lf/archive/src/main/scala/com/digitalasset/daml/lf/archive/Reader.scala @@ -43,7 +43,7 @@ abstract class Reader[+Pkg] { final def readArchiveAndVersion(lf: DamlLf.Archive): (Pkg, LanguageMajorVersion) = { lf.getHashFunction match { case DamlLf.HashFunction.SHA256 => - val payload = lf.getPayload.toByteArray() + val payload = lf.getPayload.toByteArray val theirHash = PackageId.fromString(lf.getHash) match { case Right(hash) => hash case Left(err) => throw ParseError(s"Invalid hash: $err") diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/ArrayFactory.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/ArrayFactory.scala new file mode 100644 index 0000000000..60ff63d71c --- /dev/null +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/ArrayFactory.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.digitalasset.daml.lf.data + +import scala.reflect.ClassTag + +final class ArrayFactory[T](implicit classTag: ClassTag[T]) { + + def apply(xs: T*): Array[T] = xs.toArray + + def ofDim(n: Int): Array[T] = Array.ofDim(n) + + val empty: Array[T] = ofDim(0) +} diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Decimal.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Decimal.scala deleted file mode 100644 index 98f8ce2ce5..0000000000 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Decimal.scala +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.daml.lf.data -import java.math.MathContext - -import scala.math.BigDecimal - -/** The model of our floating point decimal numbers. - * - * These are numbers of precision 38 (38 decimal digits), and scale 10 (10 digits after the comma) - */ -object Decimal { - type Decimal = BigDecimal - - val scale: Int = 10 - val context: MathContext = new MathContext(38, java.math.RoundingMode.HALF_EVEN) - - private[this] def unlimitedBigDecimal(s: String) = - BigDecimal.decimal(new java.math.BigDecimal(s), MathContext.UNLIMITED) - private[this] def unlimitedBigDecimal(x: Long) = - BigDecimal(new java.math.BigDecimal(x, MathContext.UNLIMITED)) - - // we use these to compare only, therefore set the precision to unlimited to make sure - // we can compare every number we're given - val max: Decimal = unlimitedBigDecimal("9999999999999999999999999999.9999999999") - - val min: Decimal = unlimitedBigDecimal("-9999999999999999999999999999.9999999999") - - /** Checks that a `Decimal` falls between `min` and `max`, and - * round the number according to `scale`. Note that it does _not_ - * fail if the number contains data beyond `scale`. - */ - def checkWithinBoundsAndRound(x0: Decimal): Either[String, Decimal] = { - if (x0 > max || x0 < min) { - Left(s"out-of-bounds Decimal $x0") - } else { - val x1 = new BigDecimal(x0.bigDecimal, context) - val x2 = x1.setScale(scale, BigDecimal.RoundingMode.HALF_EVEN) - Right(x2) - } - } - - /** Like `checkWithinBoundsAndRound`, but _fails_ if the given number contains - * any data beyond `scale`. - */ - def checkWithinBoundsAndWithinScale(x0: Decimal): Either[String, Decimal] = { - for { - x1 <- checkWithinBoundsAndRound(x0) - // if we've lost any data at all, it means that we weren't within the - // scale. - x2 <- Either.cond(x0 == x1, x1, s"out-of-bounds Decimal $x0") - } yield x2 - } - - def add(x: Decimal, y: Decimal): Either[String, Decimal] = checkWithinBoundsAndRound(x + y) - def div(x: Decimal, y: Decimal): Either[String, Decimal] = checkWithinBoundsAndRound(x / y) - def mult(x: Decimal, y: Decimal): Either[String, Decimal] = checkWithinBoundsAndRound(x * y) - def sub(x: Decimal, y: Decimal): Either[String, Decimal] = checkWithinBoundsAndRound(x - y) - - def round(newScale: Long, x0: Decimal): Either[String, Decimal] = - // check to make sure the rounding mode is OK - checkWithinBoundsAndRound(x0).flatMap(x => - if (newScale > scale || newScale < -27) { - Left(s"Bad scale $newScale, must be between -27 and $scale") - } else { - // we know toIntExact won't crash because we checked the scale above - // we set the scale again to make sure that every Decimal has scale 10, which - // affects equality - Right( - x.setScale(Math.toIntExact(newScale), BigDecimal.RoundingMode.HALF_EVEN) - .setScale(scale)) - }) - - private val hasExpectedFormat = - """[+-]?\d{1,28}(\.\d{1,10})?""".r.pattern - - def fromString(s: String): Either[String, Decimal] = - if (hasExpectedFormat.matcher(s).matches()) - checkWithinBoundsAndWithinScale(unlimitedBigDecimal(s)) - else - Left(s"""Could not read Decimal string "$s"""") - - def assertFromString(s: String): Decimal = - assert(fromString(s)) - - def toString(d: Decimal): 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. - // - // Moreover, add a single trailing zero if we have no decimal part. - // this mimicks the behavior of `formatScientific Fixed Nothing` which - // we've been using in Haskell to render Decimals - // http://hackage.haskell.org/package/scientific-0.3.6.2/docs/Data-Scientific.html#v:formatScientific - val s = d.bigDecimal.stripTrailingZeros.toPlainString - if (s.contains(".")) { - s - } else { - s + ".0" - } - } - - def fromLong(x: Long): Decimal = - BigDecimal(new java.math.BigDecimal(x, context)).setScale(scale) - - private val toLongLowerBound = unlimitedBigDecimal(Long.MinValue) - 1 - private val toLongUpperBound = unlimitedBigDecimal(Long.MaxValue) + 1 - - def toLong(x: Decimal): Either[String, Long] = { - if (toLongLowerBound < x && x < toLongUpperBound) - Right(x.longValue) - else - Left(s"Decimal $x does not fit into an Int64") - } - - private def assert[X](either: Either[String, X]): X = - either.fold(e => throw new IllegalArgumentException(e), identity) - -} diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/DecimalModule.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/DecimalModule.scala new file mode 100644 index 0000000000..0d15b2e3fc --- /dev/null +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/DecimalModule.scala @@ -0,0 +1,124 @@ +// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.daml.lf.data + +import java.math.MathContext + +import scala.math.BigDecimal + +/** The model of our floating point decimal numbers. + * + * These are numbers of precision 38 (38 decimal digits), and scale 10 (10 digits after the comma) + */ +abstract class DecimalModule { + + type T <: BigDecimal + + protected def cast(x: BigDecimal): T + + val scale: Int = 10 + val context: MathContext = new MathContext(38, java.math.RoundingMode.HALF_EVEN) + + private def unlimitedBigDecimal(s: String): T = + cast(BigDecimal.decimal(new java.math.BigDecimal(s), MathContext.UNLIMITED)) + + private def unlimitedBigDecimal(x: Long): T = + cast(BigDecimal(new java.math.BigDecimal(x, MathContext.UNLIMITED))) + + // we use these to compare only, therefore set the precision to unlimited to make sure + // we can compare every number we're given + val max: T = unlimitedBigDecimal("9999999999999999999999999999.9999999999") + + val min: T = unlimitedBigDecimal("-9999999999999999999999999999.9999999999") + + /** Checks that a `T` falls between `min` and `max`, and + * round the number according to `scale`. Note that it does _not_ + * fail if the number contains data beyond `scale`. + */ + private def checkWithinBoundsAndRound(x0: BigDecimal): Either[String, T] = { + if (x0 > max || x0 < min) { + Left(s"out-of-bounds Decimal $x0") + } else { + val x1 = new BigDecimal(x0.bigDecimal, context) + val x2 = x1.setScale(scale, BigDecimal.RoundingMode.HALF_EVEN) + Right(cast(x2)) + } + } + + /** Like `checkWithinBoundsAndRound`, but _fails_ if the given number contains + * any data beyond `scale`. + */ + final def fromBigDecimal(x0: BigDecimal): Either[String, T] = + for { + x1 <- checkWithinBoundsAndRound(x0) + // if we've lost any data at all, it means that we weren't within the + // scale. + x2 <- Either.cond(x0 == x1, x1, s"out-of-bounds Decimal $x0") + } yield x2 + + final def assertFromBigDecimal(x: BigDecimal): T = + assert(fromBigDecimal(x)) + + final def add(x: T, y: T): Either[String, T] = checkWithinBoundsAndRound(x + y) + + final def div(x: T, y: T): Either[String, T] = checkWithinBoundsAndRound(x / y) + + final def mult(x: T, y: T): Either[String, T] = checkWithinBoundsAndRound(x * y) + + final def sub(x: T, y: T): Either[String, T] = checkWithinBoundsAndRound(x - y) + + final def round(newScale: Long, x0: T): Either[String, T] = + // check to make sure the rounding mode is OK + checkWithinBoundsAndRound(x0).flatMap( + x => + if (newScale > scale || newScale < -27) + Left(s"Bad scale $newScale, must be between -27 and $scale") + else + // we know toIntExact won't crash because we checked the scale above + // we set the scale again to make sure that every Decimal has scale 10, which + // affects equality + Right( + cast( + x.setScale(Math.toIntExact(newScale), BigDecimal.RoundingMode.HALF_EVEN) + .setScale(scale)))) + + private val hasExpectedFormat = + """[+-]?\d{1,28}(\.\d{1,10})?""".r.pattern + + final def fromString(s: String): Either[String, T] = + if (hasExpectedFormat.matcher(s).matches()) + fromBigDecimal(unlimitedBigDecimal(s)) + else + Left(s"""Could not read Decimal string "$s"""") + + @throws[IllegalArgumentException] + final def assertFromString(s: String): T = + assert(fromString(s)) + + final def toString(d: T): 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. + // + // Moreover, add a single trailing zero if we have no decimal part. + // this mimicks the behavior of `formatScientific Fixed Nothing` which + // we've been using in Haskell to render Decimals + // http://hackage.haskell.org/package/scientific-0.3.6.2/docs/Data-Scientific.html#v:formatScientific + val s = d.bigDecimal.stripTrailingZeros.toPlainString + if (s.contains(".")) s else s + ".0" + } + + final def fromLong(x: Long): T = + cast(BigDecimal(new java.math.BigDecimal(x, context)).setScale(scale)) + + private val toLongLowerBound = unlimitedBigDecimal(Long.MinValue) - 1 + private val toLongUpperBound = unlimitedBigDecimal(Long.MaxValue) + 1 + + final def toLong(x: T): Either[String, Long] = + Either.cond( + toLongLowerBound < x && x < toLongUpperBound, + x.longValue, + s"Decimal $x does not fit into an Int64") + +} diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/MatchingStringModule.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/MatchingStringModule.scala index b23c5673de..48650b78a2 100644 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/MatchingStringModule.scala +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/MatchingStringModule.scala @@ -1,21 +1,20 @@ // Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 - package com.digitalasset.daml.lf.data import scalaz.Equal -import scala.reflect.ClassTag import scala.util.matching.Regex sealed abstract class MatchingStringModule { + type T <: String def fromString(s: String): Either[String, T] @throws[IllegalArgumentException] final def assertFromString(s: String): T = - fromString(s).fold(e => throw new IllegalArgumentException(e), identity) + assert(fromString(s)) def equalInstance: Equal[T] @@ -25,16 +24,6 @@ sealed abstract class MatchingStringModule { // * https://github.com/digital-asset/daml/pull/983#discussion_r282513324 // * https://github.com/scala/bug/issues/9565 val Array: ArrayFactory[T] - -} - -sealed abstract class ArrayFactory[T](implicit classTag: ClassTag[T]) { - - def apply(xs: T*): Array[T] = xs.toArray - - def ofDim(n: Int): Array[T] = Array.ofDim(n) - - val empty: Array[T] = ofDim(0) } object MatchingStringModule extends (Regex => MatchingStringModule) { @@ -49,7 +38,7 @@ object MatchingStringModule extends (Regex => MatchingStringModule) { def equalInstance: Equal[T] = scalaz.std.string.stringInstance - val Array: ArrayFactory[T] = new ArrayFactory[T] {} + val Array: ArrayFactory[T] = new ArrayFactory[T] } } diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Ref.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Ref.scala index b99fda23e9..ae6ce777df 100644 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Ref.scala +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Ref.scala @@ -55,6 +55,8 @@ object Ref { } object DottedName { + type T = DottedName + def fromString(s: String): Either[String, DottedName] = if (s.isEmpty) Left(s"Expected a non-empty string") @@ -62,7 +64,7 @@ object Ref { fromSegments(split(s, '.').toSeq) @throws[IllegalArgumentException] - def assertFromString(s: String): DottedName = + final def assertFromString(s: String): T = assert(fromString(s)) def fromSegments(strings: Iterable[String]): Either[String, DottedName] = { @@ -102,6 +104,8 @@ object Ref { def qualifiedName: String = toString } object QualifiedName { + type T = QualifiedName + def fromString(s: String): Either[String, QualifiedName] = { val segments = split(s, ':') if (segments.length != 2) @@ -115,7 +119,7 @@ object Ref { } @throws[IllegalArgumentException] - def assertFromString(s: String): QualifiedName = + final def assertFromString(s: String): T = assert(fromString(s)) } @@ -153,7 +157,4 @@ object Ref { type TypeConName = Identifier val TypeConName = Identifier - private def assert[X](either: Either[String, X]): X = - either.fold(e => throw new IllegalArgumentException(e), identity) - } diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/SortedLookupList.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/SortedLookupList.scala index 33f6b2cf6e..97762f5b9b 100644 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/SortedLookupList.scala +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/SortedLookupList.scala @@ -4,13 +4,14 @@ package com.digitalasset.daml.lf.data import scalaz.Equal -import scalaz.std.string._ import scalaz.std.tuple._ +import scalaz.std.string._ import scalaz.syntax.equal._ import scala.collection.immutable.HashMap /** We use this container to pass around DAML-LF maps as flat lists in various parts of the codebase. */ +// Note that keys are ordered using Utf8 ordering final class SortedLookupList[+X] private (entries: ImmArray[(String, X)]) extends Equals { def mapValue[Y](f: X => Y) = new SortedLookupList(entries.map { case (k, v) => k -> f(v) }) @@ -40,16 +41,13 @@ final class SortedLookupList[+X] private (entries: ImmArray[(String, X)]) extend object SortedLookupList { - // Note: it's important that this ordering is the same as the DAML-LF ordering. - private implicit val keyOrdering: Ordering[String] = UTF8.ordering - def fromImmArray[X](entries: ImmArray[(String, X)]): Either[String, SortedLookupList[X]] = { entries.toSeq .groupBy(_._1) .collectFirst { case (k, l) if l.size > 1 => s"key $k duplicated when trying to build map" } - .toLeft(new SortedLookupList(entries.toSeq.sortBy(_._1).toImmArray)) + .toLeft(new SortedLookupList(entries.toSeq.sortBy(_._1)(Utf8.Ordering).toImmArray)) } def fromSortedImmArray[X](entries: ImmArray[(String, X)]): Either[String, SortedLookupList[X]] = { @@ -58,7 +56,7 @@ object SortedLookupList { .toSeq .sliding(2) .collectFirst { - case Seq(k1, k2) if keyOrdering.gteq(k1, k2) => s"the list $entries is not sorted by key" + case Seq(k1, k2) if Utf8.Ordering.gteq(k1, k2) => s"the list $entries is not sorted by key" } .toLeft(new SortedLookupList(entries)) } diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Time.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Time.scala index e78c6d1de6..36bb51c947 100644 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Time.scala +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Time.scala @@ -23,6 +23,8 @@ object Time { object Date { + type T = Date + private def apply(days: Int): Date = new Date(days) @@ -59,13 +61,13 @@ object Time { .map(_ => s"cannot interpret $str as Date") .flatMap(fromDaysSinceEpoch) + @throws[IllegalArgumentException] + final def assertFromString(s: String): T = + assert(fromString(s)) + def assertFromDaysSinceEpoch(days: Int): Date = assert(fromDaysSinceEpoch(days)) - @throws[IllegalArgumentException] - def assertFromString(str: String): Date = - assert(fromString(str)) - } case class Timestamp private (micros: Long) extends Ordered[Timestamp] { @@ -89,6 +91,8 @@ object Time { object Timestamp { + type T = Timestamp + private def apply(micros: Long): Timestamp = new Timestamp(micros) @@ -129,8 +133,8 @@ object Time { .flatMap(fromLong) @throws[IllegalArgumentException] - def assertFromString(str: String): Timestamp = - assertFromLong(assertMicrosFromString(str)) + final def assertFromString(s: String): T = + assert(fromString(s)) def fromInstant(i: Instant): Either[String, Timestamp] = Try(assertMicrosFromInstant(i)).toEither.left @@ -142,10 +146,4 @@ object Time { } - private def assert[X](e: Either[String, X]): X = - e match { - case Left(err) => throw new IllegalArgumentException(err) - case Right(date) => date - } - } diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/UTF8.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/UTF8.scala deleted file mode 100644 index 3438c6a320..0000000000 --- a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/UTF8.scala +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package com.digitalasset.daml.lf.data - -import java.nio.charset.StandardCharsets - -import scala.annotation.tailrec -import scala.collection.mutable - -// This object defines functions to emulates UTF8 string -// behavior while still using underlying UTF-16 encoding. -object UTF8 { - - // The DAML-LF strings are supposed to be UTF-8. - // However standard "exploding" java/scala methods like - // _.toList split in Character which are not Unicode codepoint. - def explode(s: String): ImmArray[String] = { - val len = s.length - val arr = new mutable.ArraySeq[String](s.codePointCount(0, len)) - var i = 0 - var j = 0 - while (i < len) { - // if s(i) is a high surrogate the current codepoint uses 2 chars - val next = if (s(i).isHighSurrogate) i + 2 else i + 1 - arr(j) = s.substring(i, next) - j += 1 - i = next - } - ImmArray.unsafeFromArraySeq(arr) - } - - // The DAML-LF should sort string according UTF-8 encoding. - // Java standard string ordering uses UTF-16, which does not match - // expected one. Note that unlike UTF-16, UTF-8 ordering matches - // Unicode ordering, so we can order by codepoints. - // - // For instance consider the two following unicode code points: - // - // 。 (Unicode 0x00ff61, UTF-8 [0xef, 0xbd, 0xa1], , UTF-16 [0xff61]) - // 😂 (Unicode 0x01f602, UTF-8 [0xf0, 0x9f, 0x98, 0x82], UTF-16 [0xd83d, 0xde02]) - // - // The comparison "。" < "😂" returns false in java/scala, but it - // should return true. Note it returns True in Haskell. - // - // See https://ssl.icu-project.org/docs/papers/utf16_code_point_order.html - // for more explanations. - - val ordering: Ordering[String] = new Ordering[String] { - override def compare(xs: String, ys: String): Int = { - val lim = xs.length min ys.length - @tailrec def lp(i: Int): Int = - if (i < lim) { - val x = xs(i) - val y = ys(i) - if (x != y) { - // If x is a low surrogate, then the current codepoint starts at the - // previous char, otherwise the codepoint starts at the current char. - val j = if (x.isLowSurrogate) i - 1 else i - xs.codePointAt(j) - ys.codePointAt(j) - } else lp(i + 1) - } else xs.length - ys.length - lp(0) - } - } - - def getBytes(s: String): Array[Byte] = - s.getBytes(StandardCharsets.UTF_8) - -} diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Utf8.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Utf8.scala new file mode 100644 index 0000000000..fde9aa3df2 --- /dev/null +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/Utf8.scala @@ -0,0 +1,65 @@ +// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.daml.lf.data + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +import scala.annotation.tailrec + +// The DAML-LF strings are supposed to be UTF-8 while standard java strings are UTF16 +// Note number of UTF16 operations are not Utf8 equivalent (for instance length, charAt, ordering ...) +// This module provide UTF8 emulation functions. +object Utf8 { + + // The DAML-LF strings are supposed to be UTF-8. + // However standard "exploding" java/scala methods like + // _.toList split in Character which are not Unicode codepoint. + def explode(s: String): ImmArray[String] = { + val len = s.length + val arr = ImmArray.newBuilder[String] + var i = 0 + var j = 0 + while (i < len) { + // if s(i) is a high surrogate the current codepoint uses 2 chars + val next = if (s(i).isHighSurrogate) i + 2 else i + 1 + arr += s.substring(i, next) + j += 1 + i = next + } + arr.result() + } + + def getBytes(s: String): Array[Byte] = + s.getBytes(StandardCharsets.UTF_8) + + def sha256(s: String): String = { + val digest = MessageDigest.getInstance("SHA-256") + val array = digest.digest(getBytes(s)) + array.map("%02x" format _).mkString + } + + def implode(ts: ImmArray[String]): String = + ts.toSeq.mkString + + val Ordering: Ordering[String] = (xs: String, ys: String) => { + val lim = xs.length min ys.length + + @tailrec + def lp(i: Int): Int = + if (i < lim) { + val x = xs(i) + val y = ys(i) + if (x != y) { + // If x is a low surrogate, then the current codepoint starts at the + // previous char, otherwise the codepoint starts at the current char. + val j = if (x.isLowSurrogate) i - 1 else i + xs.codePointAt(j) - ys.codePointAt(j) + } else lp(i + 1) + } else xs.length - ys.length + + lp(0) + } + +} diff --git a/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/package.scala b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/package.scala new file mode 100644 index 0000000000..6f845b6ba8 --- /dev/null +++ b/daml-lf/data/src/main/scala/com/digitalasset/daml/lf/data/package.scala @@ -0,0 +1,18 @@ +// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.digitalasset.daml.lf + +import scala.math.BigDecimal + +package object data { + + val Decimal: DecimalModule = new DecimalModule { + type T = BigDecimal + protected def cast(x: BigDecimal): T = x + } + type Decimal = Decimal.T + + private[data] def assert[X](either: Either[String, X]): X = + either.fold(e => throw new IllegalArgumentException(e), identity) +} diff --git a/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/SortedLookupListSpec.scala b/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/SortedLookupListSpec.scala index 0454ca90e1..9fef8e1758 100644 --- a/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/SortedLookupListSpec.scala +++ b/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/SortedLookupListSpec.scala @@ -3,7 +3,7 @@ package com.digitalasset.daml.lf.data -import org.scalatest.prop.PropertyChecks +import org.scalatest.prop.{PropertyChecks} import org.scalatest.{Matchers, WordSpec} class SortedLookupListSpec extends WordSpec with Matchers with PropertyChecks { @@ -15,7 +15,8 @@ class SortedLookupListSpec extends WordSpec with Matchers with PropertyChecks { ImmArray.empty[(String, Int)], ImmArray("1" -> 1), ImmArray("1" -> 1, "2" -> 2, "3" -> 3), - ImmArray("2" -> 2, "3" -> 3, "1" -> 1)) + ImmArray("2" -> 2, "3" -> 3, "1" -> 1) + ) val positiveTestCases = Table("list", ImmArray("1" -> 1, "1" -> 2), ImmArray("1" -> 1, "2" -> 2, "3" -> 3, "1" -> 2)) @@ -39,7 +40,8 @@ class SortedLookupListSpec extends WordSpec with Matchers with PropertyChecks { "list", ImmArray("1" -> 1, "1" -> 2), ImmArray("1" -> 1, "2" -> 2, "3" -> 3, "1" -> 2), - ImmArray("2" -> 2, "3" -> 3, "1" -> 1)) + ImmArray("2" -> 2, "3" -> 3, "1" -> 1) + ) forAll(negativeTestCases)(l => SortedLookupList.fromSortedImmArray(l) shouldBe 'right) diff --git a/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/UTF8Spec.scala b/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/Utf8Spec.scala similarity index 82% rename from daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/UTF8Spec.scala rename to daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/Utf8Spec.scala index 60e1949643..a43c34943c 100644 --- a/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/UTF8Spec.scala +++ b/daml-lf/data/src/test/scala/com/digitalasset/daml/lf/data/Utf8Spec.scala @@ -12,7 +12,7 @@ import scala.collection.JavaConverters._ import scala.util.Random @SuppressWarnings(Array("org.wartremover.warts.Any")) -class UTF8Spec extends WordSpec with Matchers { +class Utf8Spec extends WordSpec with Matchers { private def codepointToString(cp: Int): String = Character.toChars(cp).mkString @@ -47,7 +47,7 @@ class UTF8Spec extends WordSpec with Matchers { "explode properly counter example" in { "a¶‱😂".toList shouldNot be(List("a", "¶", "‱", "😂")) - UTF8.explode("a¶‱😂") shouldBe ImmArray("a", "¶", "‱", "😂") + Utf8.explode("a¶‱😂") shouldBe ImmArray("a", "¶", "‱", "😂") } "explode in a same way a naive implementation" in { @@ -55,7 +55,7 @@ class UTF8Spec extends WordSpec with Matchers { ImmArray(s.codePoints().iterator().asScala.map(codepointToString(_)).toIterable) forAll(strings) { s => - naiveExplode(s) == UTF8.explode(s) + naiveExplode(s) == Utf8.explode(s) } } @@ -63,33 +63,33 @@ class UTF8Spec extends WordSpec with Matchers { "Unicode.Ordering" should { - "do not have basic UTF16 ordering issue" in { + "do not have basic Utf16 ordering issue" in { val s1 = "。" val s2 = "😂" val List(cp1) = s1.codePoints().iterator().asScala.toList val List(cp2) = s2.codePoints().iterator().asScala.toList Ordering.String.lt(s1, s2) shouldNot be(Ordering.Int.lt(cp1, cp2)) - UTF8.ordering.lt(s1, s2) shouldBe Ordering.Int.lt(cp1, cp2) + Utf8.Ordering.lt(s1, s2) shouldBe Ordering.Int.lt(cp1, cp2) } "be reflexive" in { forAll(strings) { x => - UTF8.ordering.compare(x, x) == 0 + Utf8.Ordering.compare(x, x) == 0 } } "consistent when flipping its arguments" in { forAll(strings, strings) { (x, y) => - UTF8.ordering.compare(x, y).signum == -UTF8.ordering.compare(y, x).signum + Utf8.Ordering.compare(x, y).signum == -Utf8.Ordering.compare(y, x).signum } } "be transitive" in { - import UTF8.ordering.lteq + import Utf8.Ordering.lteq forAll(strings, strings, strings) { (x_, y_, z_) => - val List(x, y, z) = List(x_, y_, z_).sorted(UTF8.ordering) + val List(x, y, z) = List(x_, y_, z_).sorted(Utf8.Ordering) lteq(x, y) && lteq(y, z) && lteq(x, z) } } @@ -98,11 +98,11 @@ class UTF8Spec extends WordSpec with Matchers { val shuffledCodepoints = new Random(0).shuffle(validCodepoints) - // Sort according UTF16 + // Sort according Utf16 val negativeCase = shuffledCodepoints.sorted // Sort according our ad hoc ordering - val positiveCase = shuffledCodepoints.sorted(UTF8.ordering) + val positiveCase = shuffledCodepoints.sorted(Utf8.Ordering) negativeCase shouldNot be(validCodepoints) positiveCase shouldBe validCodepoints @@ -116,14 +116,14 @@ class UTF8Spec extends WordSpec with Matchers { Ordering.by((s: String) => s.codePoints().toArray.toIterable) forAll(Gen.listOfN(20, strings)) { list => - list.sorted(naiveOrdering) == list.sorted(UTF8.ordering) + list.sorted(naiveOrdering) == list.sorted(Utf8.Ordering) } } "be strict on individual codepoints" in { (validCodepoints zip validCodepoints.tail).foreach { - case (x, y) => UTF8.ordering.compare(x, y) should be < 0 + case (x, y) => Utf8.Ordering.compare(x, y) should be < 0 } } diff --git a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala index 69d7854990..b3d5324b99 100644 --- a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala +++ b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala @@ -8,7 +8,7 @@ import java.io.File import com.digitalasset.daml.bazeltools.BazelRunfiles import com.digitalasset.daml.lf.data.Ref._ -import com.digitalasset.daml.lf.data.{FrontStack, ImmArray, Ref, Time} +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.lfpackage.Decode import com.digitalasset.daml.lf.lfpackage.Util._ 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 c75df4abfc..a3100116e1 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 @@ -3,7 +3,6 @@ package com.digitalasset.daml.lf.speedy -import java.security.MessageDigest import java.util import com.digitalasset.daml.lf.data.Ref._ @@ -156,7 +155,7 @@ object SBuiltin { machine.ctrl = CtrlValue( args.get(0) match { case SText(t) => - SList(FrontStack(UTF8.explode(t).map(SText))) + SList(FrontStack(Utf8.explode(t).map(SText))) case _ => throw SErrorCrash(s"type mismatch explodeText: $args") } @@ -174,7 +173,7 @@ object SBuiltin { case v => throw SErrorCrash(s"type mismatch implodeText: expected SText, got $v") } - SText(ts.iterator.mkString) + SText(Utf8.implode(ts.toImmArray)) case _ => throw SErrorCrash(s"type mismatch implodeText: $args") } @@ -187,7 +186,7 @@ object SBuiltin { machine.ctrl = CtrlValue( (args.get(0), args.get(1)) match { case (SText(head), SText(tail)) => - SText(head ++ tail) + SText(head + tail) case _ => throw SErrorCrash(s"type mismatch appendText: $args") } @@ -237,17 +236,11 @@ object SBuiltin { final case object SBSHA256Text extends SBuiltin(1) { def execute(args: util.ArrayList[SValue], machine: Machine): Unit = { machine.ctrl = CtrlValue(args.get(0) match { - case SText(t) => SText(hash(t)) + case SText(t) => SText(Utf8.sha256(t)) case _ => throw SErrorCrash(s"type mismatch textSHA256: $args") }) } - - private def hash(t: String): String = { - val digest = MessageDigest.getInstance("SHA-256") - val array = digest.digest(UTF8.getBytes(t)) - array.map("%02x" format _).mkString - } } final case object SBMapEmpty extends SBuiltin(0) { @@ -308,6 +301,8 @@ object SBuiltin { final case object SBMapToList extends SBuiltin(1) { + // implicit val classTag: ClassTag[Ref.Name.T] = Ref.Name.classTag + private val entryFields = Name.Array(Ast.keyFieldName, Ast.valueFieldName) @@ -437,7 +432,7 @@ object SBuiltin { case (SInt64(a), SInt64(b)) => a < b case (SDecimal(a), SDecimal(b)) => a < b case (STimestamp(a), STimestamp(b)) => a < b - case (SText(a), SText(b)) => UTF8.ordering.lt(a, b) + case (SText(a), SText(b)) => Utf8.Ordering.lt(a, b) case (SDate(a), SDate(b)) => a < b case (SParty(a), SParty(b)) => a < b case _ => @@ -452,7 +447,7 @@ object SBuiltin { case (SInt64(a), SInt64(b)) => a <= b case (SDecimal(a), SDecimal(b)) => a <= b case (STimestamp(a), STimestamp(b)) => a <= b - case (SText(a), SText(b)) => UTF8.ordering.lteq(a, b) + case (SText(a), SText(b)) => Utf8.Ordering.lteq(a, b) case (SDate(a), SDate(b)) => a <= b case (SParty(a), SParty(b)) => a <= b case _ => @@ -467,7 +462,7 @@ object SBuiltin { case (SInt64(a), SInt64(b)) => a > b case (SDecimal(a), SDecimal(b)) => a > b case (STimestamp(a), STimestamp(b)) => a > b - case (SText(a), SText(b)) => UTF8.ordering.gt(a, b) + case (SText(a), SText(b)) => Utf8.Ordering.gt(a, b) case (SDate(a), SDate(b)) => a > b case (SParty(a), SParty(b)) => a > b case _ => @@ -482,7 +477,7 @@ object SBuiltin { case (SInt64(a), SInt64(b)) => a >= b case (SDecimal(a), SDecimal(b)) => a >= b case (STimestamp(a), STimestamp(b)) => a >= b - case (SText(a), SText(b)) => UTF8.ordering.gteq(a, b) + case (SText(a), SText(b)) => Utf8.Ordering.gteq(a, b) case (SDate(a), SDate(b)) => a >= b case (SParty(a), SParty(b)) => a >= b case _ => @@ -1111,9 +1106,8 @@ object SBuiltin { /** $error :: Text -> a */ final case object SBError extends SBuiltin(1) { - def execute(args: util.ArrayList[SValue], machine: Machine): Unit = { + def execute(args: util.ArrayList[SValue], machine: Machine): Unit = throw DamlEUserError(args.get(0).asInstanceOf[SText].value) - } } // Helpers 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 3b6da5cbaf..a9373afb31 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 @@ -5,9 +5,8 @@ package com.digitalasset.daml.lf.speedy import java.util -import com.digitalasset.daml.lf.data.Decimal.Decimal +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.data.Ref._ -import com.digitalasset.daml.lf.data.{FrontStack, ImmArray, SortedLookupList, Time} import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.speedy.SError.SErrorCrash import com.digitalasset.daml.lf.value.{Value => V} diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/interp/testing/ToTextTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/interp/testing/ToTextTest.scala index c67de6f1f8..58111ce06d 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/interp/testing/ToTextTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/interp/testing/ToTextTest.scala @@ -3,7 +3,7 @@ package com.digitalasset.daml.lf.interp.testing -import com.digitalasset.daml.lf.data.{Decimal} +import com.digitalasset.daml.lf.data.Decimal import com.digitalasset.daml.lf.speedy.{SBuiltin, SValue} import com.digitalasset.daml.lf.speedy.SValue._ import org.scalatest.{Matchers, WordSpec} @@ -18,8 +18,8 @@ class ToTextTest extends WordSpec with Matchers { "toString" should { "Decimal" in { - litToText(SDecimal(Decimal.fromString("123.4560000").toOption.get)) shouldBe "123.456" - litToText(SDecimal(Decimal.fromString("123.0000000").toOption.get)) shouldBe "123.0" + litToText(SDecimal(Decimal.fromString("123.4560000").toOption.get)) shouldBe ("123.456") + litToText(SDecimal(Decimal.fromString("123.0000000").toOption.get)) shouldBe ("123.0") } "Text" in { 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 f3ce98fe5f..1ee80a9937 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,7 @@ package com.digitalasset.daml.lf.speedy import java.util import com.digitalasset.daml.lf.PureCompiledPackages -import com.digitalasset.daml.lf.data.{Decimal, FrontStack, Ref, Time} +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.speedy.SError.SError import com.digitalasset.daml.lf.speedy.SResult.{SResultContinue, SResultError} @@ -16,6 +16,7 @@ import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.{FreeSpec, Matchers} import scala.collection.immutable.HashMap +import scala.language.implicitConversions @SuppressWarnings(Array("org.wartremover.warts.Any")) class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks { @@ -209,9 +210,9 @@ class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks "MUL_DECIMAL" - { "throws exception in case of overflow" in { - eval(e"MUL_DECIMAL 1.1 2.2") shouldBe Right(SDecimal(2.42)) + eval(e"MUL_DECIMAL 1.1 2.2") shouldBe Right(SDecimal(decimal(2.42))) eval(e"MUL_DECIMAL $bigBigDecimal $bigBigDecimal") shouldBe 'left - eval(e"MUL_DECIMAL ${1E13} ${1E14}") shouldBe Right(SDecimal(1E27)) + eval(e"MUL_DECIMAL ${1E13} ${1E14}") shouldBe Right(SDecimal(decimal(1E27))) eval(e"MUL_DECIMAL ${1E14} ${1E14}") shouldBe 'left } } @@ -237,23 +238,22 @@ class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks (10, d, d) ) - forEvery(testCases) { (rounding, decimal, result) => - eval(e"ROUND_DECIMAL $rounding $decimal") shouldBe Right(SDecimal(BigDecimal(result))) + forEvery(testCases) { (rounding, input, result) => + eval(e"ROUND_DECIMAL $rounding $input") shouldBe Right(SDecimal(BigDecimal(result))) } } } - "Decimal binary operations computes proper results" in { + "Decimal binary operations compute proper results" in { - val testCases = Table[String, (BigDecimal, BigDecimal) => Either[Any, SValue]]( + def round(x: BigDecimal) = x.setScale(10, BigDecimal.RoundingMode.HALF_EVEN) + + val testCases = Table[String, (Decimal, Decimal) => Either[Any, SValue]]( ("builtin", "reference"), - ("ADD_DECIMAL", (a, b) => Decimal.checkWithinBoundsAndRound(a + b).map(SDecimal)), - ("SUB_DECIMAL", (a, b) => Decimal.checkWithinBoundsAndRound(a - b).map(SDecimal)), - ("MUL_DECIMAL", (a, b) => Decimal.checkWithinBoundsAndRound(a * b).map(SDecimal)), - ( - "DIV_DECIMAL", - (a, b) => - if (b == 0) Left(()) else Decimal.checkWithinBoundsAndRound(a / b).map(SDecimal)), + ("ADD_DECIMAL", (a, b) => Right(SDecimal(a + b))), + ("SUB_DECIMAL", (a, b) => Right(SDecimal(a - b))), + ("MUL_DECIMAL", (a, b) => Right(SDecimal(round(a * b)))), + ("DIV_DECIMAL", (a, b) => Either.cond(b != 0, SDecimal(round(a / b)), ())), ("LESS_EQ_DECIMAL", (a, b) => Right(SBool(a <= b))), ("GREATER_EQ_DECIMAL", (a, b) => Right(SBool(a >= b))), ("LESS_DECIMAL", (a, b) => Right(SBool(a < b))), @@ -264,7 +264,8 @@ class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks forEvery(testCases) { (builtin, ref) => forEvery(decimals) { a => forEvery(decimals) { b => - eval(e"$builtin $a $b").left.map(_ => ()) shouldBe ref(BigDecimal(a), BigDecimal(b)) + eval(e"$builtin $a $b").left + .map(_ => ()) shouldBe ref(decimal(BigDecimal(a)), decimal(BigDecimal(b))) } } } @@ -650,7 +651,7 @@ class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks val testCases = Table[Long]("Int64", 167, 11, 2, 1, 0, -1, -2, -13, -113) forEvery(testCases) { int64 => - eval(e"INT64_TO_DECIMAL $int64") shouldBe Right(SDecimal(int64)) + eval(e"INT64_TO_DECIMAL $int64") shouldBe Right(SDecimal(decimal(int64))) } } } @@ -761,7 +762,7 @@ class SBuiltinTest extends FreeSpec with Matchers with TableDrivenPropertyChecks val testCases = Table[String, SValue]( "expression" -> "result", "1" -> SInt64(1), - "1.0" -> SDecimal(1), + "1.0" -> SDecimal(decimal(1)), "True" -> SBool(true), "()" -> SUnit(()), """ "text" """ -> SText("text"), @@ -827,4 +828,7 @@ object SBuiltinTest { args.add(v) STuple(entryFields, args) } + + private implicit def decimal(x: BigDecimal): Decimal = Decimal.assertFromBigDecimal(x) + } diff --git a/daml-lf/lfpackage/src/main/scala/com/digitalasset/daml/lf/lfpackage/Ast.scala b/daml-lf/lfpackage/src/main/scala/com/digitalasset/daml/lf/lfpackage/Ast.scala index 881ee10f79..ba6b6923d8 100644 --- a/daml-lf/lfpackage/src/main/scala/com/digitalasset/daml/lf/lfpackage/Ast.scala +++ b/daml-lf/lfpackage/src/main/scala/com/digitalasset/daml/lf/lfpackage/Ast.scala @@ -5,9 +5,8 @@ package com.digitalasset.daml.lf.lfpackage import com.digitalasset.daml.lf.archive.LanguageVersion import com.digitalasset.daml.lf.archive.Reader.ParseError -import com.digitalasset.daml.lf.data.Decimal.Decimal import com.digitalasset.daml.lf.data.Ref._ -import com.digitalasset.daml.lf.data.{ImmArray, Time} +import com.digitalasset.daml.lf.data.{Decimal, ImmArray, Time} object Ast { // @@ -269,6 +268,7 @@ object Ast { final case class PLInt64(override val value: Long) extends PrimLit final case class PLDecimal(override val value: Decimal) extends PrimLit + // Text should be treated as Utf8, data.Utf8 provide emulation functions for that final case class PLText(override val value: String) extends PrimLit final case class PLTimestamp(override val value: Time.Timestamp) extends PrimLit final case class PLParty(override val value: Party) extends PrimLit diff --git a/daml-lf/lfpackage/src/test/scala/com/digitalasset/daml/lf/lfpackage/AstSpec.scala b/daml-lf/lfpackage/src/test/scala/com/digitalasset/daml/lf/lfpackage/AstSpec.scala index b82b3548a4..2e373fe602 100644 --- a/daml-lf/lfpackage/src/test/scala/com/digitalasset/daml/lf/lfpackage/AstSpec.scala +++ b/daml-lf/lfpackage/src/test/scala/com/digitalasset/daml/lf/lfpackage/AstSpec.scala @@ -6,7 +6,6 @@ package com.digitalasset.daml.lf.lfpackage import com.digitalasset.daml.lf.archive.LanguageVersion import com.digitalasset.daml.lf.data.ImmArray import com.digitalasset.daml.lf.data.Ref.{ChoiceName, DottedName, Name} -import com.digitalasset.daml.lf.data.Ref.Name.{assertFromString => id} import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.lfpackage.Decode.ParseError import org.scalatest.prop.TableDrivenPropertyChecks @@ -37,7 +36,7 @@ class AstSpec extends WordSpec with TableDrivenPropertyChecks with Matchers { "Module.apply" should { val template = Template( - param = id("x"), + param = Name.assertFromString("x"), precond = eTrue, signatories = eParties, agreementText = eText, @@ -154,7 +153,7 @@ class AstSpec extends WordSpec with TableDrivenPropertyChecks with Matchers { name = name, consuming = true, controllers = eParties, - selfBinder = id("self"), + selfBinder = Name.assertFromString("self"), argBinder = (None, tUnit), returnType = tUnit, update = EUpdate(UpdatePure(typ, expr)), @@ -166,7 +165,7 @@ class AstSpec extends WordSpec with TableDrivenPropertyChecks with Matchers { "catch choice name collisions" in { Template( - param = id("x"), + param = Name.assertFromString("x"), precond = eTrue, signatories = eParties, agreementText = eText, @@ -181,7 +180,7 @@ class AstSpec extends WordSpec with TableDrivenPropertyChecks with Matchers { an[ParseError] shouldBe thrownBy( Template( - param = id("x"), + param = Name.assertFromString("x"), precond = eTrue, signatories = eParties, agreementText = eText, diff --git a/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Implicits.scala b/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Implicits.scala index 248bc621c2..ed012928af 100644 --- a/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Implicits.scala +++ b/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Implicits.scala @@ -26,11 +26,14 @@ object Implicits { Parsers.parseAll(Parsers.phrase(p), sc.standardInterpolator(identity, args.map(prettyPrint))) } + private def toString(x: BigDecimal) = + Decimal.toString(Decimal.assertFromBigDecimal(x)) + private def prettyPrint(x: Any): String = x match { - case d: BigDecimal => Decimal.toString(d.bigDecimal) - case d: Float => Decimal.toString(d.toDouble) - case d: Double => Decimal.toString(d) + case d: BigDecimal => toString(d) + case d: Float => toString(d.toDouble) + case d: Double => toString(d) case other: Any => other.toString } } diff --git a/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Token.scala b/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Token.scala index f269366dc6..102f2218ea 100644 --- a/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Token.scala +++ b/daml-lf/parser/src/main/scala/com/digitalasset/daml/lf/testing/parser/Token.scala @@ -55,7 +55,7 @@ private[parser] object Token { final case class ContractId(s: String) extends Token final case class Timestamp(value: data.Time.Timestamp) extends Token final case class Date(value: data.Time.Date) extends Token - final case class Decimal(value: data.Decimal.Decimal) extends Token + final case class Decimal(value: data.Decimal) extends Token final case class Number(value: Long) extends Token final case class SimpleString(s: String) extends Token final case class Text(s: String) extends Token diff --git a/daml-lf/parser/src/test/scala/com/digitalasset/daml/lf/testing/parser/ParsersSpec.scala b/daml-lf/parser/src/test/scala/com/digitalasset/daml/lf/testing/parser/ParsersSpec.scala index 542f694d20..42f56fb6f9 100644 --- a/daml-lf/parser/src/test/scala/com/digitalasset/daml/lf/testing/parser/ParsersSpec.scala +++ b/daml-lf/parser/src/test/scala/com/digitalasset/daml/lf/testing/parser/ParsersSpec.scala @@ -4,8 +4,7 @@ package com.digitalasset.daml.lf.testing.parser import com.digitalasset.daml.lf.data.Ref._ -import com.digitalasset.daml.lf.data.Time -import com.digitalasset.daml.lf.data.ImmArray +import com.digitalasset.daml.lf.data.{Decimal, ImmArray, Time} import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.testing.parser.Implicits._ import org.scalatest.prop.TableDrivenPropertyChecks @@ -116,9 +115,9 @@ class ParsersSpec extends WordSpec with TableDrivenPropertyChecks with Matchers "string to parse" -> "expected literal", "1" -> PLInt64(1), "-2" -> PLInt64(-2), - "1.0" -> PLDecimal(1), - "1.0" -> PLDecimal(1), - "-1.0" -> PLDecimal(-1), + "1.0" -> PLDecimal(Decimal.assertFromBigDecimal(1)), + "1.0" -> PLDecimal(Decimal.assertFromBigDecimal(1)), + "-1.0" -> PLDecimal(Decimal.assertFromBigDecimal(-1)), """"some text"""" -> PLText("some text"), """ " \n\r\"\\ " """ -> PLText(" \n\r\"\\ "), """ "français" """ -> PLText("français"), diff --git a/daml-lf/repl/src/main/scala/com/digitalasset/daml/lf/repl/Main.scala b/daml-lf/repl/src/main/scala/com/digitalasset/daml/lf/repl/Main.scala index 2092f40141..4d272d128a 100644 --- a/daml-lf/repl/src/main/scala/com/digitalasset/daml/lf/repl/Main.scala +++ b/daml-lf/repl/src/main/scala/com/digitalasset/daml/lf/repl/Main.scala @@ -3,7 +3,7 @@ package com.digitalasset.daml.lf.speedy -import com.digitalasset.daml.lf.data.{FrontStack, ImmArray, Ref} +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.data.Ref._ import com.digitalasset.daml.lf.lfpackage.Ast._ import com.digitalasset.daml.lf.lfpackage.Decode @@ -51,13 +51,12 @@ object Main extends App { } - def defaultCommand(possibleFile: String): Unit = { + def defaultCommand(possibleFile: String): Unit = if (!Paths.get(possibleFile).toFile.isFile) { usage() System.exit(1) } else Repl.repl(possibleFile) - } if (args.isEmpty) { usage() @@ -556,7 +555,7 @@ object Repl { QualifiedName.assertFromString("Dummy:Dummy")) def pDecimal: Parser[Value[Nothing]] = """\d+\.\d+""".r ^^ { s => - ValueDecimal(BigDecimal(s)) + ValueDecimal(Decimal.assertFromString(s)) } def pInt64: Parser[Value[Nothing]] = """\d+""".r ^^ { s => ValueInt64(s.toLong) diff --git a/daml-lf/scenario-interpreter/BUILD.bazel b/daml-lf/scenario-interpreter/BUILD.bazel index 8a52cb16c1..d5eb550998 100644 --- a/daml-lf/scenario-interpreter/BUILD.bazel +++ b/daml-lf/scenario-interpreter/BUILD.bazel @@ -33,6 +33,7 @@ da_scala_test_suite( scalacopts = lf_scalacopts, deps = [ ":scenario-interpreter", + "//3rdparty/jvm/org/scalaz:scalaz_core", "//daml-lf/data", "//daml-lf/interpreter", "//daml-lf/lfpackage", diff --git a/daml-lf/scenario-interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ScenarioRunnerTest.scala b/daml-lf/scenario-interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ScenarioRunnerTest.scala index e6952d41c8..dd27476095 100644 --- a/daml-lf/scenario-interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ScenarioRunnerTest.scala +++ b/daml-lf/scenario-interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ScenarioRunnerTest.scala @@ -14,9 +14,9 @@ class ScenarioRunnerTest extends AsyncWordSpec with Matchers with ScalaFutures { "ScenarioRunner" can { "mangle party names correctly" in { - val e = Ast.EScenario(ScenarioGetParty(Ast.EPrimLit(Ast.PLText("foo-bar")))) + val e = Ast.EScenario(ScenarioGetParty(Ast.EPrimLit(Ast.PLText(("foo-bar"))))) val m = Speedy.Machine.fromExpr(e, PureCompiledPackages(Map.empty).right.get, true) - val sr = ScenarioRunner(m, (s) => s + "-XXX") + val sr = ScenarioRunner(m, _ + "-XXX") sr.run() m.ctrl shouldBe Speedy.CtrlValue(SValue.SParty(Ref.Party.assertFromString("foo-bar-XXX"))) } diff --git a/daml-lf/testing-tools/src/main/scala/com/digitalasset/daml/lf/engine/testing/SemanticTester.scala b/daml-lf/testing-tools/src/main/scala/com/digitalasset/daml/lf/engine/testing/SemanticTester.scala index 98e2175fac..bf3df057d9 100644 --- a/daml-lf/testing-tools/src/main/scala/com/digitalasset/daml/lf/engine/testing/SemanticTester.scala +++ b/daml-lf/testing-tools/src/main/scala/com/digitalasset/daml/lf/engine/testing/SemanticTester.scala @@ -7,7 +7,7 @@ import com.digitalasset.daml.lf.PureCompiledPackages import com.digitalasset.daml.lf.command._ import com.digitalasset.daml.lf.data.Ref.{PackageId, Party, QualifiedName} import com.digitalasset.daml.lf.data.Relation.Relation -import com.digitalasset.daml.lf.data.{FrontStack, FrontStackCons, ImmArray, Time} +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.engine.Event.Events import com.digitalasset.daml.lf.engine._ import com.digitalasset.daml.lf.lfpackage.Ast._ diff --git a/daml-lf/transaction-scalacheck/src/main/scala/com/digitalasset/daml/lf/value/ValueGenerators.scala b/daml-lf/transaction-scalacheck/src/main/scala/com/digitalasset/daml/lf/value/ValueGenerators.scala index 93d3f85a3b..f81169f724 100644 --- a/daml-lf/transaction-scalacheck/src/main/scala/com/digitalasset/daml/lf/value/ValueGenerators.scala +++ b/daml-lf/transaction-scalacheck/src/main/scala/com/digitalasset/daml/lf/value/ValueGenerators.scala @@ -95,7 +95,7 @@ object ValueGenerators { (1, Gen.const(Decimal.min)), (5, bd) ) - .map(d => ValueDecimal(d)) + .map(d => ValueDecimal(Decimal.assertFromBigDecimal(d))) } val moduleSegmentGen: Gen[String] = for { @@ -191,8 +191,9 @@ object ValueGenerators { private def valueMapGen(nesting: Int) = for { - list <- Gen.listOf( - for { k <- Gen.asciiPrintableStr; v <- Gen.lzy(valueGen(nesting)) } yield k -> v) + list <- Gen.listOf(for { + k <- Gen.asciiPrintableStr; v <- Gen.lzy(valueGen(nesting)) + } yield k -> v) } yield ValueMap[ContractId](SortedLookupList(Map(list: _*))) def valueMapGen: Gen[ValueMap[ContractId]] = valueMapGen(0) @@ -220,10 +221,10 @@ object ValueGenerators { ) val flat = List( (sz + 1, dateGen.map(ValueDate)), - (sz + 1, Gen.alphaStr.map(ValueText)), + (sz + 1, Gen.alphaStr.map(x => ValueText(x))), (sz + 1, decimalGen), (sz + 1, Arbitrary.arbLong.arbitrary.map(ValueInt64)), - (sz + 1, Gen.alphaStr.map(ValueText)), + (sz + 1, Gen.alphaStr.map(x => ValueText(x))), (sz + 1, timestampGen.map(ValueTimestamp)), (sz + 1, coidValueGen), (sz + 1, party.map(ValueParty)), diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/TransactionCoder.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/TransactionCoder.scala index 42e2ebf82c..12b2637604 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/TransactionCoder.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/transaction/TransactionCoder.scala @@ -63,7 +63,7 @@ object TransactionCoder { : Either[DecodeError, ContractInst[Val]] = { ValueCoder.decodeIdentifier(protoCoinst.getTemplateId).flatMap { id => decodeVal(protoCoinst.getValue) - .map(a => ContractInst(id, a, protoCoinst.getAgreement)) + .map(a => ContractInst(id, a, (protoCoinst.getAgreement))) } } diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala index 2573054ccc..2f62b8c27a 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala @@ -181,7 +181,8 @@ object Value { */ final case class ValueList[+Cid](values: FrontStack[Value[Cid]]) extends Value[Cid] final case class ValueInt64(value: Long) extends Value[Nothing] - final case class ValueDecimal(value: BigDecimal) extends Value[Nothing] + final case class ValueDecimal(value: Decimal) extends Value[Nothing] + // Note that Text are assume to be UTF8 final case class ValueText(value: String) extends Value[Nothing] final case class ValueTimestamp(value: Time.Timestamp) extends Value[Nothing] final case class ValueDate(value: Time.Date) extends Value[Nothing] diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/ValueCoder.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/ValueCoder.scala index bcc210b8e3..55ba68a7c9 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/ValueCoder.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/ValueCoder.scala @@ -358,7 +358,7 @@ object ValueCoder { case proto.Value.SumCase.MAP => val entries = ImmArray(protoValue.getMap.getEntriesList.asScala.map(entry => - entry.getKey -> go(newNesting, entry.getValue))) + (entry.getKey) -> go(newNesting, entry.getValue))) val map = SortedLookupList .fromImmArray(entries) diff --git a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionCoderSpec.scala b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionCoderSpec.scala index 3e80d37f23..99e10e883b 100644 --- a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionCoderSpec.scala +++ b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionCoderSpec.scala @@ -4,7 +4,7 @@ package com.digitalasset.daml.lf.transaction import com.digitalasset.daml.lf.EitherAssertions -import com.digitalasset.daml.lf.data.ImmArray +import com.digitalasset.daml.lf.data.{ImmArray} import com.digitalasset.daml.lf.data.Ref.{Identifier, PackageId, Party, QualifiedName} import com.digitalasset.daml.lf.transaction.Node.{GenNode, NodeCreate, NodeExercises, NodeFetch} import com.digitalasset.daml.lf.transaction.{Transaction => Tx, TransactionOuterClass => proto} @@ -244,7 +244,7 @@ class TransactionCoderSpec VersionedValue( ValueVersions.acceptedVersions.last, ValueParty(Party.assertFromString("francesco"))), - "agreement" + ("agreement") ), None, Set(Party.assertFromString("alice")), diff --git a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala index 0bf8d78b6e..ae802eb4ea 100644 --- a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala +++ b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/transaction/TransactionSpec.scala @@ -145,7 +145,8 @@ object TransactionSpec { PackageId.assertFromString("-dummyPkg-"), QualifiedName.assertFromString("DummyModule:dummyName")), V.ValueUnit, - "dummyAgreement"), + ("dummyAgreement") + ), None, Set.empty, Set.empty, diff --git a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/value/ValueCoderSpec.scala b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/value/ValueCoderSpec.scala index 97f1c383b0..1d7de6cece 100644 --- a/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/value/ValueCoderSpec.scala +++ b/daml-lf/transaction/src/test/scala/com/digitalasset/daml/lf/value/ValueCoderSpec.scala @@ -5,7 +5,7 @@ package com.digitalasset.daml.lf.value import com.digitalasset.daml.lf.EitherAssertions import com.digitalasset.daml.lf.data.Ref.Party -import com.digitalasset.daml.lf.data.{Decimal, ImmArray, Ref, Time} +import com.digitalasset.daml.lf.data._ import com.digitalasset.daml.lf.value.Value._ import com.digitalasset.daml.lf.value.ValueCoder.DecodeError import com.digitalasset.daml.lf.value.{ValueOuterClass => proto} @@ -45,8 +45,8 @@ class ValueCoderSpec extends WordSpec with Matchers with EitherAssertions with P "do Decimal" in { forAll("Decimal (BigDecimal) invariant") { d: BigDecimal => // we are filtering on decimals invariant under string conversion - whenever(Decimal.fromString(Decimal.toString(d)).isRight) { - val Right(dec) = Decimal.fromString(Decimal.toString(d)) + whenever(Decimal.fromBigDecimal(d).isRight) { + val Right(dec) = Decimal.fromBigDecimal(d) val value = ValueDecimal(dec) val recoveredDecimal = ValueCoder.decodeValue[ContractId]( defaultCidDecode, diff --git a/extractor/src/main/scala/com/digitalasset/extractor/json/JsonConverters.scala b/extractor/src/main/scala/com/digitalasset/extractor/json/JsonConverters.scala index 0933bc20ac..d278347cea 100644 --- a/extractor/src/main/scala/com/digitalasset/extractor/json/JsonConverters.scala +++ b/extractor/src/main/scala/com/digitalasset/extractor/json/JsonConverters.scala @@ -67,7 +67,9 @@ object JsonConverters { implicit val mapEncoder: Encoder[SortedLookupList[LedgerValue]] = m => JsonObject( "Map" -> - JsonObject.fromIterable(m.mapValue(_.asJson).toImmArray.toSeq).asJson).asJson + JsonObject + .fromIterable(m.toImmArray.map { case (k, v) => k -> v.asJson }.toSeq) + .asJson).asJson implicit val idKeyEncoder: KeyEncoder[Identifier] = id => s"${id.packageId}@${id.name}" implicit val idKeyDecoder: KeyDecoder[Identifier] = { diff --git a/extractor/src/main/scala/com/digitalasset/extractor/ledger/types/LedgerValue.scala b/extractor/src/main/scala/com/digitalasset/extractor/ledger/types/LedgerValue.scala index 6e01c1690c..e717982971 100644 --- a/extractor/src/main/scala/com/digitalasset/extractor/ledger/types/LedgerValue.scala +++ b/extractor/src/main/scala/com/digitalasset/extractor/ledger/types/LedgerValue.scala @@ -8,7 +8,7 @@ import api.value.Value.Sum import RecordField._ import scalaz.{Optional => _, _} import Scalaz._ -import com.digitalasset.daml.lf.data.{SortedLookupList, ImmArray} +import com.digitalasset.daml.lf.data.{ImmArray, SortedLookupList} sealed trait LedgerValue diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala index d2b3d15a34..f616455984 100644 --- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala +++ b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala @@ -90,7 +90,8 @@ object LfEngineToApi { value0: LfValue[Lf.AbsoluteContractId]): Either[String, ApiValue] = value0 match { case Lf.ValueUnit => Right(ApiValue(ApiValue.Sum.Unit(Empty()))) - case Lf.ValueDecimal(d) => Right(ApiValue(ApiValue.Sum.Decimal(Decimal.toString(d)))) + case Lf.ValueDecimal(d) => + Right(ApiValue(ApiValue.Sum.Decimal(Decimal.toString(d)))) case Lf.ValueContractId(c) => Right(ApiValue(ApiValue.Sum.ContractId(c.coid))) case Lf.ValueBool(b) => Right(ApiValue(ApiValue.Sum.Bool(b))) case Lf.ValueDate(d) => Right(ApiValue(ApiValue.Sum.Date(d.days))) diff --git a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/ValueConversionRoundTripTest.scala b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/ValueConversionRoundTripTest.scala index a7ef71856f..066fe15bcb 100644 --- a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/ValueConversionRoundTripTest.scala +++ b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/ValueConversionRoundTripTest.scala @@ -3,7 +3,8 @@ package com.digitalasset.ledger.api -import com.digitalasset.daml.lf.data.{Time, UTF8} +import com.digitalasset.daml.lf.data.Time +import com.digitalasset.daml.lf.testing.parser.Implicits._ import com.digitalasset.ledger.api.v1.value.Value.Sum import com.digitalasset.ledger.api.v1.value.{ List => ApiList, @@ -20,7 +21,6 @@ import com.digitalasset.platform.server.api.validation.IdentifierResolver import com.google.protobuf.empty.Empty import org.scalatest.WordSpec import org.scalatest.prop.{TableDrivenPropertyChecks, TableFor1} -import com.digitalasset.daml.lf.testing.parser.Implicits._ import scala.concurrent.Future @@ -115,7 +115,7 @@ class ValueConversionRoundTripTest val entries = List("‱", "1", "😂", "😃", "a").zipWithIndex.map { case (k, v) => ApiMap.Entry(k, Some(Value(Sum.Int64(v.toLong)))) } - val sortedEntries = entries.sortBy(_.key)(UTF8.ordering) + val sortedEntries = entries.sortBy(_.key) // just to be sure we did not write the entries sorted assert(entries != sortedEntries) diff --git a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/validation/CommandSubmissionRequestValidatorTest.scala b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/validation/CommandSubmissionRequestValidatorTest.scala index f32b639112..068697ad8a 100644 --- a/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/validation/CommandSubmissionRequestValidatorTest.scala +++ b/ledger/ledger-api-common/src/test/suite/scala/com/digitalasset/ledger/api/validation/CommandSubmissionRequestValidatorTest.scala @@ -3,7 +3,6 @@ package com.digitalasset.ledger.api.validation -import java.security.MessageDigest import java.time.Instant import com.digitalasset.api.util.TimestampConversion @@ -485,9 +484,11 @@ class CommandSubmissionRequestValidatorTest "convert valid maps" in { val entries = ImmArray(1 until 5).map { x => - hash(x.toString) -> x.toLong + Utf8.sha256(x.toString) -> x.toLong + } + val apiEntries = entries.map { + case (k, v) => ApiMap.Entry(k, Some(Value(Sum.Int64(v)))) } - val apiEntries = entries.map { case (k, v) => ApiMap.Entry(k, Some(Value(Sum.Int64(v)))) } val input = Value(Sum.Map(ApiMap(apiEntries.toSeq))) val lfEntries = entries.map { case (k, v) => k -> Lf.ValueInt64(v) } val expected = @@ -498,14 +499,16 @@ class CommandSubmissionRequestValidatorTest "reject maps with repeated keys" in { val entries = ImmArray(1 +: (1 until 5)).map { x => - hash(x.toString) -> x.toLong + Utf8.sha256(x.toString) -> x.toLong + } + val apiEntries = entries.map { + case (k, v) => ApiMap.Entry(k, Some(Value(Sum.Int64(v)))) } - val apiEntries = entries.map { case (k, v) => ApiMap.Entry(k, Some(Value(Sum.Int64(v)))) } val input = Value(Sum.Map(ApiMap(apiEntries.toSeq))) requestMustFailWith( sut.validateValue(input), INVALID_ARGUMENT, - s"Invalid argument: key ${hash(1.toString)} duplicated when trying to build map") + s"Invalid argument: key ${Utf8.sha256("1")} duplicated when trying to build map") } "reject maps containing invalid value" in { @@ -523,9 +526,4 @@ class CommandSubmissionRequestValidatorTest } - private def hash(t: String): String = { - val digest = MessageDigest.getInstance("SHA-256") - val array = digest.digest(UTF8.getBytes(t)) - array.map("%02x" format _).mkString - } } diff --git a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/LedgerApiTestTool.scala b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/LedgerApiTestTool.scala index dd4ca913a2..bac983e548 100644 --- a/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/LedgerApiTestTool.scala +++ b/ledger/ledger-api-test-tool/src/main/scala/com/daml/ledger/api/testtool/LedgerApiTestTool.scala @@ -63,8 +63,8 @@ object LedgerApiTestTool { } var failed = false - val runSuffix = Random.alphanumeric.take(10).mkString - val partyNameMangler = (partyText: String) => s"$partyText-$runSuffix" + val runSuffix = "-" + Random.alphanumeric.take(10).mkString + val partyNameMangler = (partyText: String) => partyText + runSuffix val commandIdMangler: ((QualifiedName, Int, L.NodeId) => String) = (scenario, stepId, nodeId) => s"ledger-api-test-tool-$scenario-$stepId-$nodeId-$runSuffix" diff --git a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/serialisation/KeyHasher.scala b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/serialisation/KeyHasher.scala index 62eadc2aa2..6ea10cabaa 100644 --- a/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/serialisation/KeyHasher.scala +++ b/ledger/sandbox/src/main/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/serialisation/KeyHasher.scala @@ -6,6 +6,7 @@ package com.digitalasset.platform.sandbox.stores.ledger.sql.serialisation import java.nio.ByteBuffer import java.security.MessageDigest +import com.digitalasset.daml.lf.data.Utf8 import com.digitalasset.daml.lf.transaction.Node.GlobalKey import com.digitalasset.daml.lf.value.Value import com.digitalasset.daml.lf.value.Value.AbsoluteContractId @@ -109,9 +110,10 @@ object KeyHasher extends KeyHasher { digest.update(ByteBuffer.allocate(8).putLong(value).array()) private[this] def putStringContent(digest: MessageDigest, value: String): Unit = - digest.update(value.getBytes("UTF8")) + digest.update(Utf8.getBytes(value)) private[this] def putString(digest: MessageDigest, value: String): Unit = { + // FixMe we probably should not use UTF16 length. putInt(digest, value.length) putStringContent(digest, value) } @@ -140,6 +142,7 @@ object KeyHasher extends KeyHasher { case HashTokenInt(v) => putInt(d, v) case HashTokenLong(v) => putLong(d, v) case HashTokenText(v) => putString(d, v) + // FixMe we probably should use Decimal.toString // Java docs: "The toString() method provides a canonical representation of a BigDecimal." case HashTokenBigDecimal(v) => putString(d, v.toString) case HashTokenCollectionBegin(length) => putInt(d, length) diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/services/transaction/EventConverterSpec.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/services/transaction/EventConverterSpec.scala index 6bee15c470..7d722ae298 100644 --- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/services/transaction/EventConverterSpec.scala +++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/services/transaction/EventConverterSpec.scala @@ -48,11 +48,12 @@ class EventConverterSpec with TestHelpers with AkkaBeforeAndAfterAll with Inside { + private implicit def qualifiedNameStr(s: String): QualifiedName = QualifiedName.assertFromString(s) private implicit def party(s: String): Ref.Party = Ref.Party.assertFromString(s) private implicit def pkgId(s: String): Ref.PackageId = Ref.PackageId.assertFromString(s) - private implicit def id(s: String): Ref.Name = Ref.Name.assertFromString(s) + private implicit def name(s: String): Ref.Name = Ref.Name.assertFromString(s) type LfTx = com.digitalasset.daml.lf.transaction.Transaction.Transaction diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/KeyHasherSpec.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/KeyHasherSpec.scala index 9cdf2c0dbe..92a08fc32d 100644 --- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/KeyHasherSpec.scala +++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/KeyHasherSpec.scala @@ -11,6 +11,8 @@ import com.digitalasset.daml.lf.value.{Value, ValueVersion} import org.scalatest.{Matchers, WordSpec} import com.digitalasset.platform.sandbox.stores.ledger.sql.serialisation.KeyHasher +import scala.language.implicitConversions + class KeyHasherSpec extends WordSpec with Matchers { private[this] def templateId(module: String, name: String) = Identifier( PackageId.assertFromString("package"), @@ -25,8 +27,8 @@ class KeyHasherSpec extends WordSpec with Matchers { builder += None -> ValueInt64(0) builder += None -> ValueInt64(123456) builder += None -> ValueInt64(-1) - builder += None -> ValueDecimal(Decimal.assertFromString("0")) - builder += None -> ValueDecimal(Decimal.assertFromString("0.3333333333")) + builder += None -> ValueDecimal(decimal(0)) + builder += None -> ValueDecimal(decimal(BigDecimal("0.3333333333"))) builder += None -> ValueBool(true) builder += None -> ValueBool(false) builder += None -> ValueDate(Time.Date.assertFromDaysSinceEpoch(0)) @@ -125,9 +127,13 @@ class KeyHasherSpec extends WordSpec with Matchers { "not produce collision in list of decimals" in { // Testing whether decimals are delimited: [10, 10] vs [101, 0] val value1 = - VersionedValue(ValueVersion("4"), ValueList(FrontStack(ValueDecimal(10), ValueDecimal(10)))) + VersionedValue( + ValueVersion("4"), + ValueList(FrontStack(ValueDecimal(decimal(10)), ValueDecimal(decimal(10))))) val value2 = - VersionedValue(ValueVersion("4"), ValueList(FrontStack(ValueDecimal(101), ValueDecimal(0)))) + VersionedValue( + ValueVersion("4"), + ValueList(FrontStack(ValueDecimal(decimal(101)), ValueDecimal(decimal(0))))) val tid = templateId("module", "name") @@ -269,8 +275,8 @@ class KeyHasherSpec extends WordSpec with Matchers { } "not produce collision in Decimal" in { - val value1 = VersionedValue(ValueVersion("4"), ValueDecimal(0)) - val value2 = VersionedValue(ValueVersion("4"), ValueDecimal(1)) + val value1 = VersionedValue(ValueVersion("4"), ValueDecimal(decimal(0))) + val value2 = VersionedValue(ValueVersion("4"), ValueDecimal(decimal(1))) val tid = templateId("module", "name") @@ -346,4 +352,7 @@ class KeyHasherSpec extends WordSpec with Matchers { hash1.equals(hash2) shouldBe false } } + + private implicit def decimal(x: BigDecimal): Decimal = Decimal.assertFromBigDecimal(x) + } diff --git a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/PostgresDaoSpec.scala b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/PostgresDaoSpec.scala index cbe7229119..83ebfcb4dd 100644 --- a/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/PostgresDaoSpec.scala +++ b/ledger/sandbox/src/test/suite/scala/com/digitalasset/platform/sandbox/stores/ledger/sql/PostgresDaoSpec.scala @@ -69,6 +69,8 @@ class PostgresDaoSpec private val alice = Party.assertFromString("Alice") private val bob = Party.assertFromString("Bob") + private val someValueText = ValueText("some text") + private val agreement = "agreement" "Postgres Ledger DAO" should { @@ -82,8 +84,8 @@ class PostgresDaoSpec Ref.QualifiedName( Ref.ModuleName.assertFromString("moduleName"), Ref.DottedName.assertFromString("name"))), - VersionedValue(ValueVersions.acceptedVersions.head, ValueText("some text")), - "agreement" + VersionedValue(ValueVersions.acceptedVersions.head, someValueText), + agreement ) val keyWithMaintainers = KeyWithMaintainers( VersionedValue(ValueVersions.acceptedVersions.head, ValueText("key")), @@ -144,7 +146,7 @@ class PostgresDaoSpec entry <- ledgerDao.lookupLedgerEntry(offset) endingOffset <- ledgerDao.lookupLedgerEnd() } yield { - entry shouldEqual (Some(checkpoint)) + entry shouldEqual Some(checkpoint) endingOffset shouldEqual (startingOffset + 1) } } @@ -198,8 +200,8 @@ class PostgresDaoSpec Ref.QualifiedName( Ref.ModuleName.assertFromString("moduleName"), Ref.DottedName.assertFromString("name"))), - VersionedValue(ValueVersions.acceptedVersions.head, ValueText("some text")), - "agreement" + VersionedValue(ValueVersions.acceptedVersions.head, someValueText), + agreement ) val keyWithMaintainers = KeyWithMaintainers( @@ -265,8 +267,8 @@ class PostgresDaoSpec val let = Instant.now val contractInstance = ContractInst( templateId, - VersionedValue(ValueVersions.acceptedVersions.head, ValueText("some text")), - "agreement" + VersionedValue(ValueVersions.acceptedVersions.head, someValueText), + agreement ) val contract = Contract( absCid, diff --git a/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecCompressed.scala b/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecCompressed.scala index af4bab918e..b36991cfbd 100644 --- a/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecCompressed.scala +++ b/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecCompressed.scala @@ -57,7 +57,11 @@ object ApiCodecCompressed { JsObject(value.fields.map(f => f.label -> apiValueToJsValue(f.value)).toMap) def apiMapToJsValue(value: Model.ApiMap): JsValue = - JsObject(value.value.mapValue(apiValueToJsValue).toHashMap) + JsObject( + value.value.toImmArray + .map { case (k, v) => k -> apiValueToJsValue(v) } + .toSeq + .toMap) // ------------------------------------------------------------------------------------------------------------------ // Decoding - this needs access to DAML-LF types @@ -88,7 +92,9 @@ object ApiCodecCompressed { case None => deserializationError(s"Can't read ${value.prettyPrint} as Optional") } case (JsObject(a), Model.DamlLfPrimType.Map) => - Model.ApiMap(SortedLookupList(a).mapValue(jsValueToApiType(_, prim.typArgs.head, defs))) + Model.ApiMap(SortedLookupList(a.map { + case (k, v) => k -> jsValueToApiType(v, prim.typArgs.head, defs) + })) case _ => deserializationError(s"Can't read ${value.prettyPrint} as $prim") } } diff --git a/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecVerbose.scala b/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecVerbose.scala index 59acf3df6f..da1a3439fe 100644 --- a/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecVerbose.scala +++ b/navigator/backend/src/main/scala/com/digitalasset/navigator/json/ApiCodecVerbose.scala @@ -3,7 +3,7 @@ package com.digitalasset.navigator.json -import com.digitalasset.daml.lf.data.{SortedLookupList, ImmArray} +import com.digitalasset.daml.lf.data.{ImmArray, SortedLookupList} import com.digitalasset.navigator.{model => Model} import com.digitalasset.navigator.json.DamlLfCodec.JsonImplicits._ import com.digitalasset.navigator.json.Util._ @@ -81,7 +81,8 @@ object ApiCodecVerbose { JsObject( propType -> JsString(tagMap), propValue -> JsArray(value.value.toImmArray.toSeq.toVector.map { - case (k, v) => JsObject(fieldKey -> JsString(k), fieldValue -> apiValueToJsValue(v)) + case (k, v) => + JsObject(fieldKey -> JsString(k), fieldValue -> apiValueToJsValue(v)) }) ) diff --git a/navigator/backend/src/main/scala/com/digitalasset/navigator/model/converter/LedgerApiV1.scala b/navigator/backend/src/main/scala/com/digitalasset/navigator/model/converter/LedgerApiV1.scala index 6f441658d0..30139375d0 100644 --- a/navigator/backend/src/main/scala/com/digitalasset/navigator/model/converter/LedgerApiV1.scala +++ b/navigator/backend/src/main/scala/com/digitalasset/navigator/model/converter/LedgerApiV1.scala @@ -7,7 +7,7 @@ import java.time.{Instant, LocalDate} import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit -import com.digitalasset.daml.lf.data.{SortedLookupList, ImmArray} +import com.digitalasset.daml.lf.data.{ImmArray, SortedLookupList} import com.digitalasset.ledger.api.{v1 => V1} import com.digitalasset.ledger.api.refinements.ApiTypes import com.digitalasset.navigator.{model => Model} diff --git a/navigator/backend/src/test/scala/com/digitalasset/navigator/backend/DamlConstants.scala b/navigator/backend/src/test/scala/com/digitalasset/navigator/backend/DamlConstants.scala index 56679a6e7b..72d6f2a12c 100644 --- a/navigator/backend/src/test/scala/com/digitalasset/navigator/backend/DamlConstants.scala +++ b/navigator/backend/src/test/scala/com/digitalasset/navigator/backend/DamlConstants.scala @@ -3,10 +3,9 @@ package com.digitalasset.navigator -import com.digitalasset.daml.lf.data.SortedLookupList +import com.digitalasset.daml.lf.data.{SortedLookupList, Ref => DamlLfRef} import com.digitalasset.navigator.model._ import com.digitalasset.daml.lf.{iface => DamlLfIface} -import com.digitalasset.daml.lf.data.{Ref => DamlLfRef} import scala.language.implicitConversions