diff --git a/daml-lf/interface/BUILD.bazel b/daml-lf/interface/BUILD.bazel index 6ad1c8b867a0..e01a06e9dea4 100644 --- a/daml-lf/interface/BUILD.bazel +++ b/daml-lf/interface/BUILD.bazel @@ -15,6 +15,7 @@ da_scala_library( name = "interface", srcs = glob(["src/main/**/*.scala"]), scala_deps = [ + "@maven//:com_typesafe_scala_logging_scala_logging", "@maven//:org_scalaz_scalaz_core", ], scalacopts = lf_scalacopts_stricter, @@ -32,14 +33,16 @@ da_scala_library( "//daml-lf/archive:daml_lf_archive_reader", "//daml-lf/data", "//daml-lf/language", + "//libs-scala/nonempty", "@maven//:com_google_protobuf_protobuf_java", + "@maven//:org_slf4j_slf4j_api", ], ) daml_compile( name = "InterfaceTestPackage", - srcs = ["src/test/daml/InterfaceTestPackage.daml"], - target = lf_version_configuration.get("dev"), # TODO(SC) remove when interfaces in default + srcs = glob(["src/test/daml/**/*.daml"]), + target = lf_version_configuration.get("dev"), # TODO(#13296) remove when interfaces in default ) da_scala_test( @@ -48,7 +51,10 @@ da_scala_test( srcs = glob(["src/test/**/*.scala"]), data = [":InterfaceTestPackage.dar"], scala_deps = [ + "@maven//:org_scalacheck_scalacheck", + "@maven//:org_scalatestplus_scalacheck_1_15", "@maven//:org_scalaz_scalaz_core", + "@maven//:org_scalaz_scalaz_scalacheck_binding", ], scalacopts = lf_scalacopts, deps = [ @@ -59,6 +65,9 @@ da_scala_test( "//daml-lf/data", "//daml-lf/language", "//daml-lf/parser", + "//daml-lf/transaction-test-lib", + "//libs-scala/nonempty", + "//libs-scala/scalatest-utils", "@maven//:com_google_protobuf_protobuf_java", ], ) diff --git a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/DefDataType.scala b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/DefDataType.scala index bb0aa995a2cd..aa36066179c5 100644 --- a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/DefDataType.scala +++ b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/DefDataType.scala @@ -5,15 +5,20 @@ package com.daml.lf.iface import scalaz.std.map._ import scalaz.std.option._ +import scalaz.std.set._ import scalaz.std.tuple._ import scalaz.syntax.applicative.^ import scalaz.syntax.semigroup._ import scalaz.syntax.traverse._ +import scalaz.syntax.std.map._ +import scalaz.syntax.std.option._ import scalaz.{Applicative, Bifunctor, Bitraverse, Bifoldable, Foldable, Functor, Monoid, Traverse} +import scalaz.Tags.FirstVal import java.{util => j} import com.daml.lf.data.ImmArray.ImmArraySeq import com.daml.lf.data.Ref +import com.daml.nonempty.NonEmpty import scala.jdk.CollectionConverters._ @@ -174,34 +179,29 @@ final case class Enum(constructors: ImmArraySeq[Ref.Name]) extends DataType[Noth } final case class DefTemplate[+Ty]( - choices: Map[Ref.ChoiceName, TemplateChoice[Ty]], - unresolvedInheritedChoices: Map[Ref.ChoiceName, Ref.TypeConName], + tChoices: TemplateChoices[Ty], key: Option[Ty], implementedInterfaces: Seq[Ref.TypeConName], -) extends DefTemplate.GetChoices[Ty] { +) { def map[B](f: Ty => B): DefTemplate[B] = Functor[DefTemplate].map(this)(f) + @deprecated("use tChoices.directChoices or tChoices.resolvedChoices instead", since = "2.3.0") + private[daml] def choices = tChoices.directChoices + /** Remove choices from `unresolvedInheritedChoices` and add to `choices` * given the `astInterfaces` from an [[EnvironmentInterface]]. If the result * has any `unresolvedInheritedChoices` left, these choices were not found. */ def resolveChoices[O >: Ty]( astInterfaces: PartialFunction[Ref.TypeConName, DefInterface[O]] - ): DefTemplate[O] = { - val getAstInterface = astInterfaces.lift - val (missing, resolved) = unresolvedInheritedChoices.partitionMap { - case pair @ (choiceName, tcn) => - val resolution = for { - astIf <- getAstInterface(tcn) - tchoice <- astIf.choices get choiceName - } yield (choiceName, tchoice) - resolution toRight pair - } - this.copy(choices = choices ++ resolved, unresolvedInheritedChoices = missing.toMap) + ): Either[TemplateChoices.ResolveError[DefTemplate[O]], DefTemplate[O]] = { + import scalaz.std.either._ + import scalaz.syntax.bifunctor._ + tChoices resolveChoices astInterfaces bimap (_.map(r => copy(tChoices = r)), r => + copy(tChoices = r)) } - def getKey: j.Optional[_ <: Ty] = - key.fold(j.Optional.empty[Ty])(k => j.Optional.of(k)) + def getKey: j.Optional[_ <: Ty] = toOptional(key) } object DefTemplate { @@ -210,20 +210,168 @@ object DefTemplate { implicit val `TemplateDecl traverse`: Traverse[DefTemplate] = new Traverse[DefTemplate] with Foldable.FromFoldMap[DefTemplate] { override def foldMap[A, B: Monoid](fa: DefTemplate[A])(f: A => B): B = - fa.choices.foldMap(_ foldMap f) |+| (fa.key foldMap f) + (fa.tChoices foldMap f) |+| (fa.key foldMap f) override def traverseImpl[G[_]: Applicative, A, B]( fab: DefTemplate[A] )(f: A => G[B]): G[DefTemplate[B]] = - ^(fab.choices traverse (_ traverse f), fab.key traverse f) { (choices, key) => - fab.copy(choices = choices, key = key) + ^(fab.tChoices traverse f, fab.key traverse f) { (choices, key) => + fab.copy(tChoices = choices, key = key) } } - sealed trait GetChoices[+Ty] { - def choices: Map[Ref.ChoiceName, TemplateChoice[Ty]] - final def getChoices: j.Map[Ref.ChoiceName, _ <: TemplateChoice[Ty]] = - choices.asJava + private[daml] val Empty: DefTemplate[Nothing] = + DefTemplate(TemplateChoices.Resolved(Map.empty), None, Seq.empty) +} + +/** Choices in a [[DefTemplate]]. */ +sealed abstract class TemplateChoices[+Ty] extends Product with Serializable { + import TemplateChoices.{Resolved, Unresolved, ResolveError, directAsResolved, logger} + + /** Choices defined directly on the template */ + def directChoices: Map[Ref.ChoiceName, TemplateChoice[Ty]] + + /** Choices defined on the template, or on resolved implemented interfaces if + * resolved + */ + def resolvedChoices + : Map[Ref.ChoiceName, NonEmpty[Map[Option[Ref.TypeConName], TemplateChoice[Ty]]]] + + /** A shim function to delay porting a component to overloaded choices. + * Discards essential data, so not a substitute for a proper port. + * TODO (#13974) delete when there are no more callers + */ + private[daml] def assumeNoOverloadedChoices( + githubIssue: Int + ): Map[Ref.ChoiceName, TemplateChoice[Ty]] = this match { + case Unresolved(directChoices, _) => directChoices + case Resolved(resolvedChoices) => + resolvedChoices.transform { (choiceName, overloads) => + if (overloads.sizeIs == 1) overloads.head1._2 + else + overloads + .get(None) + .cata( + { directChoice => + logger.warn(s"discarded inherited choices for $choiceName, see #$githubIssue") + directChoice + }, { + val (Some(randomKey), randomChoice) = overloads.head1 + logger.warn( + s"selected $randomKey-inherited choice but discarded others for $choiceName, see #$githubIssue" + ) + randomChoice + }, + ) + } + } + + final def getDirectChoices: j.Map[Ref.ChoiceName, _ <: TemplateChoice[Ty]] = + directChoices.asJava + + final def getResolvedChoices + : j.Map[Ref.ChoiceName, _ <: j.Map[j.Optional[Ref.TypeConName], _ <: TemplateChoice[Ty]]] = + resolvedChoices.transform((_, m) => m.forgetNE.mapKeys(toOptional).asJava).asJava + + /** Coerce to [[Resolved]] based on the environment `astInterfaces`, or fail + * with the choices that could not be resolved. + */ + def resolveChoices[O >: Ty]( + astInterfaces: PartialFunction[Ref.TypeConName, DefInterface[O]] + ): Either[ResolveError[Resolved[O]], Resolved[O]] = this match { + case Unresolved(direct, unresolved) => + val getAstInterface = astInterfaces.lift + import scalaz.std.iterable._ + type ChoiceMap[C] = Map[Ref.ChoiceName, NonEmpty[Map[Option[Ref.TypeConName], C]]] + val (missing, resolved): ( + Map[Ref.TypeConName, NonEmpty[Set[Ref.ChoiceName]]], + Map[Ref.ChoiceName, NonEmpty[Map[Option[Ref.TypeConName], TemplateChoice[O]]]], + ) = (unresolved: Iterable[(Ref.TypeConName, NonEmpty[Set[Ref.ChoiceName]])]) + .foldMap { case (tcn, choiceNames) => + getAstInterface(tcn).cata( + { astIf => + val (tcnMissing, tcnResolved) = choiceNames + .partitionMap(choiceName => + astIf.choices get choiceName map { tc => + (choiceName, NonEmpty(Map, some(tcn) -> tc)) + } toRight choiceName + ) + ( + NonEmpty from tcnMissing foldMap (yesMissing => Map(tcn -> yesMissing)), + FirstVal.subst[ChoiceMap, TemplateChoice[O]](tcnResolved.toMap), + ) + }, + (Map(tcn -> choiceNames), Map.empty: ChoiceMap[Nothing]), + ) + } + .map(FirstVal.unsubst[ChoiceMap, TemplateChoice[O]]) + val rChoices = Resolved(resolved.unionWith(directAsResolved(direct))(_ ++ _)) + missing match { + case NonEmpty(missing) => Left(ResolveError(missing, rChoices)) + case _ => Right(rChoices) + } + case r @ Resolved(_) => Right(r) + } +} + +object TemplateChoices { + private val logger = com.typesafe.scalalogging.Logger(getClass) + + final case class ResolveError[+Partial]( + missingChoices: NonEmpty[Map[Ref.TypeConName, NonEmpty[Set[Ref.ChoiceName]]]], + partialResolution: Partial, + ) { + private[iface] def describeError: String = + missingChoices.view + .map { case (tc, cns) => s"$tc(${cns mkString ", "})" } + .mkString(", ") + + private[iface] def map[B](f: Partial => B): ResolveError[B] = + copy(partialResolution = f(partialResolution)) + } + + final case class Unresolved[+Ty]( + directChoices: Map[Ref.ChoiceName, TemplateChoice[Ty]], + unresolvedInheritedChoices: NonEmpty[Map[Ref.TypeConName, NonEmpty[Set[Ref.ChoiceName]]]], + ) extends TemplateChoices[Ty] { + override def resolvedChoices = + directAsResolved(directChoices) + } + + private[TemplateChoices] def directAsResolved[Ty]( + directChoices: Map[Ref.ChoiceName, TemplateChoice[Ty]] + ) = + directChoices transform ((_, c) => NonEmpty(Map, (none[Ref.TypeConName], c))) + + final case class Resolved[+Ty]( + resolvedChoices: Map[Ref.ChoiceName, NonEmpty[ + Map[Option[Ref.TypeConName], TemplateChoice[Ty]] + ]] + ) extends TemplateChoices[Ty] { + override def directChoices = resolvedChoices collect (Function unlift { case (cn, m) => + m get None map ((cn, _)) + }) + } + + object Resolved { + private[daml] def fromDirect[Ty](directChoices: Map[Ref.ChoiceName, TemplateChoice[Ty]]) = + Resolved(directAsResolved(directChoices)) + } + + implicit val `TemplateChoices traverse`: Traverse[TemplateChoices] = new Traverse[TemplateChoices] + with Foldable.FromFoldMap[TemplateChoices] { + override def foldMap[A, B: Monoid](fa: TemplateChoices[A])(f: A => B): B = fa match { + case Unresolved(direct, _) => direct foldMap (_ foldMap f) + case Resolved(resolved) => resolved foldMap (_.toNEF foldMap (_ foldMap f)) + } + + override def traverseImpl[G[_]: Applicative, A, B]( + fa: TemplateChoices[A] + )(f: A => G[B]): G[TemplateChoices[B]] = fa match { + case u @ Unresolved(_, _) => + u.directChoices traverse (_ traverse f) map (dc => u.copy(directChoices = dc)) + case Resolved(r) => r traverse (_.toNEF traverse (_ traverse f)) map (Resolved(_)) + } } } @@ -245,8 +393,10 @@ object TemplateChoice { } } -final case class DefInterface[+Ty](choices: Map[Ref.ChoiceName, TemplateChoice[Ty]]) - extends DefTemplate.GetChoices[Ty] +final case class DefInterface[+Ty](choices: Map[Ref.ChoiceName, TemplateChoice[Ty]]) { + def getChoices: j.Map[Ref.ChoiceName, _ <: TemplateChoice[Ty]] = + choices.asJava +} object DefInterface extends FWTLike[DefInterface] { diff --git a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/EnvironmentInterface.scala b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/EnvironmentInterface.scala index 7112929f5e2c..587dff0b4a55 100644 --- a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/EnvironmentInterface.scala +++ b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/EnvironmentInterface.scala @@ -18,9 +18,9 @@ final case class EnvironmentInterface( astInterfaces: Map[Ref.TypeConName, DefInterface.FWT], ) { - // TODO(SC #13154) should fail instead in case of unresolved inherited choices? /** Replace all resolvable `inheritedChoices` in `typeDecls` with concrete - * choices copied from `astInterfaces`. Idempotent. + * choices copied from `astInterfaces`. If a template has any missing choices, + * none of its inherited choices are resolved. Idempotent. * * This is not distributive because we delay resolution, because successful * lookup can require the presence of another DAR. In other words, @@ -38,8 +38,8 @@ final case class EnvironmentInterface( copy(typeDecls = typeDecls.transform { (_, it) => it match { case itpl: InterfaceType.Template => - val tpl2 = itpl.template resolveChoices astInterfaces - itpl.copy(template = tpl2) + val errOrTpl2 = itpl.template resolveChoices astInterfaces + errOrTpl2.fold(_ => itpl, tpl2 => itpl.copy(template = tpl2)) case z: InterfaceType.Normal => z } }) diff --git a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/Interface.scala b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/Interface.scala index 54e7e9f06c39..dd0e0d4da625 100644 --- a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/Interface.scala +++ b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/Interface.scala @@ -11,6 +11,9 @@ import com.daml.lf.iface.reader.Errors import com.daml.daml_lf_dev.DamlLf import com.daml.lf.archive.ArchivePayload +import scalaz.std.either._ +import scalaz.syntax.bifunctor._ + import scala.collection.immutable.Map import scala.jdk.CollectionConverters._ @@ -62,21 +65,6 @@ final case class Interface( def getTypeDecls: j.Map[QualifiedName, InterfaceType] = typeDecls.asJava def getAstInterfaces: j.Map[QualifiedName, DefInterface.FWT] = astInterfaces.asJava - /** Like [[EnvironmentInterface]]'s version, but permits incremental - * resolution of newly-loaded interfaces, such as json-api does. - * - * {{{ - * // suppose - * i: Interface; ei: EnvironmentInterface - * val eidelta = EnvironmentInterface.fromReaderInterfaces(i) - * // such that - * ei |+| eidelta - * // contains the whole environment of i. Then - * ei |+| eidelta.resolveChoices(ei.astInterfaces) - * === (ei |+| eidelta).resolveChoices - * // but faster. - * }}} - */ private def resolveChoices( findInterface: PartialFunction[Ref.TypeConName, DefInterface.FWT], failIfUnresolvedChoicesLeft: Boolean, @@ -86,18 +74,21 @@ final case class Interface( if (id.packageId == packageId) astInterfaces get id.qualifiedName else outside(id) val tplFindIface = Function unlift findIface - val transformTemplate = { - def transform(ift: InterfaceType.Template) = - ift.copy(template = ift.template resolveChoices tplFindIface) - if (failIfUnresolvedChoicesLeft) - transform _ andThen (res => - if (res.template.unresolvedInheritedChoices.isEmpty) res - else + def transformTemplate(ift: InterfaceType.Template) = { + val errOrItt = (ift.template resolveChoices tplFindIface) + .bimap( + _.map(partial => ift.copy(template = partial)), + resolved => ift.copy(template = resolved), + ) + errOrItt.fold( + e => + if (failIfUnresolvedChoicesLeft) throw new IllegalStateException( - s"Couldn't resolve all inherited choices for template $res" + s"Couldn't resolve inherited choices ${e.describeError}" ) - ) - else transform _ + else e.partialResolution, + identity, + ) } copy(typeDecls = typeDecls transform { (_, ift) => ift match { @@ -107,10 +98,29 @@ final case class Interface( }) } + /** Like [[EnvironmentInterface#resolveChoices]], but permits incremental + * resolution of newly-loaded interfaces, such as json-api does. + * + * {{{ + * // suppose + * i: Interface; ei: EnvironmentInterface + * val eidelta = EnvironmentInterface.fromReaderInterfaces(i) + * // such that + * ei |+| eidelta + * // contains the whole environment of i. Then + * ei |+| eidelta.resolveChoicesAndFailOnUnresolvableChoices(ei.astInterfaces) + * === (ei |+| eidelta).resolveChoices + * // but faster. + * }}} + */ def resolveChoicesAndFailOnUnresolvableChoices( findInterface: PartialFunction[Ref.TypeConName, DefInterface.FWT] ): Interface = resolveChoices(findInterface, failIfUnresolvedChoicesLeft = true) + /** Like resolveChoicesAndFailOnUnresolvableChoices, but simply discard + * unresolved choices from the structure. Not wise to use on a receiver + * without a complete environment provided as the argument. + */ def resolveChoicesAndIgnoreUnresolvedChoices( findInterface: PartialFunction[Ref.TypeConName, DefInterface.FWT] ): Interface = resolveChoices(findInterface, failIfUnresolvedChoicesLeft = false) diff --git a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReader.scala b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReader.scala index c2a294137e61..4eb0a6f7d2d2 100644 --- a/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReader.scala +++ b/daml-lf/interface/src/main/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReader.scala @@ -17,6 +17,7 @@ import com.daml.lf.data.ImmArray.ImmArraySeq import com.daml.lf.data.Ref.{PackageId, QualifiedName} import com.daml.lf.language.Ast import com.daml.lf.language.{Util => AstUtil} +import com.daml.nonempty.NonEmpty import scala.collection.immutable.Map @@ -181,9 +182,25 @@ object InterfaceReader { key <- dfn.key traverse (k => toIfaceType(name, k.typ)) } yield name -> (iface.InterfaceType.Template( Record(fields), - DefTemplate(choices, dfn.inheritedChoices, key, dfn.implements.keys), + DefTemplate(visitChoices(choices, dfn.implements), key, dfn.implements.keys), ): T) + private[this] def visitChoices[Ty]( + choices: Map[Ref.ChoiceName, TemplateChoice[Ty]], + astInterfaces: Map[Ref.TypeConName, Ast.GenTemplateImplements[_]], + ): TemplateChoices[Ty] = { + val inheritedChoices: Map[Ref.TypeConName, NonEmpty[Set[Ref.ChoiceName]]] = + astInterfaces.collect { + case (ifId, Ast.GenTemplateImplements(_, _, NonEmpty(inheritedChoices))) => + (ifId, inheritedChoices) + } + inheritedChoices match { + case NonEmpty(unresolvedInherited) => + TemplateChoices.Unresolved(choices, unresolvedInherited) + case _ => TemplateChoices.Resolved fromDirect choices + } + } + private def visitChoice( ctx: QualifiedName, choice: Ast.TemplateChoice, diff --git a/daml-lf/interface/src/test/daml/InterfaceTestLib.daml b/daml-lf/interface/src/test/daml/InterfaceTestLib.daml new file mode 100644 index 000000000000..5722564c1f1c --- /dev/null +++ b/daml-lf/interface/src/test/daml/InterfaceTestLib.daml @@ -0,0 +1,13 @@ +-- Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module InterfaceTestLib where + +interface TIf where + getOwner: Party + dup: Update (ContractId TIf) + choice Useless: ContractId TIf with + interfacely: ContractId TIf + controller getOwner this + do + dup this diff --git a/daml-lf/interface/src/test/daml/InterfaceTestPackage.daml b/daml-lf/interface/src/test/daml/InterfaceTestPackage.daml index 29a61faccb2f..42f9e454b1b9 100644 --- a/daml-lf/interface/src/test/daml/InterfaceTestPackage.daml +++ b/daml-lf/interface/src/test/daml/InterfaceTestPackage.daml @@ -3,6 +3,8 @@ module InterfaceTestPackage where +import qualified InterfaceTestLib as L + template Foo with party: Party where @@ -16,6 +18,10 @@ template Foo with getOwner = party dup = toInterfaceContractId <$> create this + implements L.TIf where + getOwner = party + dup = toInterfaceContractId <$> create this + interface TIf where getOwner: Party dup: Update (ContractId TIf) diff --git a/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/DefDataTypeSpec.scala b/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/DefDataTypeSpec.scala new file mode 100644 index 000000000000..7aa475473a9c --- /dev/null +++ b/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/DefDataTypeSpec.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf.iface + +import com.daml.nonempty.NonEmpty +import com.daml.scalatest.WordSpecCheckLaws +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import scalaz.Equal +import scalaz.scalacheck.{ScalazProperties => SZP} + +class DefDataTypeSpec extends AnyWordSpec with Matchers with WordSpecCheckLaws { + import DefDataTypeSpec._ + "TemplateChoices Traverse" should { + checkLaws(SZP.traverse.laws[TemplateChoices]) + } +} + +object DefDataTypeSpec { + import org.scalacheck.{Arbitrary, Gen}, Arbitrary.arbitrary + import com.daml.lf.data.Ref + import com.daml.lf.value.test.ValueGenerators + + implicit def `TemplateChoices arb`[Ty: Arbitrary]: Arbitrary[TemplateChoices[Ty]] = + Arbitrary( + Gen.oneOf( + mappedGen(TemplateChoices.Resolved[Ty] _), + mappedGen((TemplateChoices.Unresolved[Ty] _).tupled), + ) + ) + + private[this] implicit def `TemplateChoice arb`[Ty: Arbitrary]: Arbitrary[TemplateChoice[Ty]] = + Arbitrary(mappedGen((TemplateChoice[Ty] _).tupled)) + + // equal is inductively natural; not bothering to write the non-natural case -SC + implicit val `TemplateChoices eq`: Equal[TemplateChoices[Int]] = Equal.equalA + + private[this] implicit def `nonempty map arb`[K: Arbitrary, V: Arbitrary] + : Arbitrary[NonEmpty[Map[K, V]]] = + Arbitrary( + arbitrary[((K, V), Map[K, V])] map { case (kv, m) => NonEmpty(Map, kv) ++ m } + ) + + private[this] implicit def `nonempty set arb`[A: Arbitrary]: Arbitrary[NonEmpty[Set[A]]] = + Arbitrary(arbitrary[(A, Set[A])] map { case (hd, tl) => NonEmpty(Set, hd) ++ tl }) + + private[this] implicit def `ChoiceName arb`: Arbitrary[Ref.ChoiceName] = Arbitrary( + ValueGenerators.nameGen + ) + private[this] implicit def `TypeConName arb`: Arbitrary[Ref.TypeConName] = Arbitrary( + ValueGenerators.idGen + ) + + // helper to avoid restating the A type + private def mappedGen[A: Arbitrary, B](f: A => B): Gen[B] = + arbitrary[A] map f +} diff --git a/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReaderSpec.scala b/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReaderSpec.scala index 96629babf9ff..2c3112fe0916 100644 --- a/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReaderSpec.scala +++ b/daml-lf/interface/src/test/scala/com/digitalasset/daml/lf/iface/reader/InterfaceReaderSpec.scala @@ -17,6 +17,7 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import scalaz.\/- import scalaz.syntax.functor._ +import scalaz.syntax.std.map._ import scala.language.implicitConversions @@ -207,46 +208,74 @@ class InterfaceReaderSpec extends AnyWordSpec with Matchers with Inside { } import QualifiedName.{assertFromString => qn} + import Ref.ChoiceName.{assertFromString => cn} val Foo = qn("InterfaceTestPackage:Foo") + val Bar = cn("Bar") + val Archive = cn("Archive") val TIf = qn("InterfaceTestPackage:TIf") - val Useless = Ref.ChoiceName.assertFromString("Useless") + val LibTIf = qn("InterfaceTestLib:TIf") + val Useless = cn("Useless") val UselessTy = qn("InterfaceTestPackage:Useless") import itp.main.{packageId => itpPid} "exclude interface choices with template choices" in { inside(itp.main.typeDecls get Foo) { case Some(InterfaceType.Template(_, tpl)) => - tpl.choices.keySet should ===(Set("Bar", "Archive")) + tpl.tChoices.directChoices.keySet should ===(Set("Bar", "Archive")) } } "include interface choices in separate inheritedChoices" in { - inside(itp.main.typeDecls get Foo) { case Some(InterfaceType.Template(_, tpl)) => - tpl.unresolvedInheritedChoices.transform((_, tcn) => tcn.qualifiedName) should ===( - Map("Useless" -> TIf) - ) + inside(itp.main.typeDecls get Foo) { + case Some( + InterfaceType.Template(_, DefTemplate(TemplateChoices.Unresolved(_, inherited), _, _)) + ) => + inherited.forgetNE.mapKeys(_.qualifiedName) should ===( + Map(TIf -> Set(Useless), LibTIf -> Set(Useless)) + ) } } - lazy val theUselessChoice = TemplateChoice( - TypeCon(TypeConName(Ref.Identifier(itpPid, UselessTy)), ImmArraySeq.empty), - true, - TypePrim( - PrimType.ContractId, - ImmArraySeq(TypeCon(TypeConName(Ref.Identifier(itpPid, TIf)), ImmArraySeq.empty)), - ), - ) + object TheUselessChoice { + def unapply(ty: TemplateChoice.FWT): Option[(QualifiedName, QualifiedName)] = { + val ItpPid = itpPid + ty match { + case TemplateChoice( + TypeCon(TypeConName(Ref.Identifier(ItpPid, uselessTy)), Seq()), + true, + TypePrim( + PrimType.ContractId, + Seq(TypeCon(TypeConName(Ref.Identifier(ItpPid, tIf)), Seq())), + ), + ) => + Some((uselessTy, tIf)) + case _ => None + } + } + } - "have an interface with a choice" in { - itp.main.astInterfaces.keySet should ===(Set(TIf)) - itp.main.astInterfaces(TIf).choices get Useless should ===(Some(theUselessChoice)) + "have interfaces with choices" in { + itp.main.astInterfaces.keySet should ===(Set(LibTIf, TIf)) + inside(itp.main.astInterfaces(TIf).choices get Useless) { + case Some(TheUselessChoice(UselessTy, TIf)) => + } } - def foundUselessChoice(foo: Option[InterfaceType]) = inside(foo) { - case Some(InterfaceType.Template(_, tpl)) => - tpl.unresolvedInheritedChoices shouldBe empty - tpl.choices get Useless should ===(Some(theUselessChoice)) + def foundResolvedChoices(foo: Option[InterfaceType]) = inside(foo) { + case Some(InterfaceType.Template(_, DefTemplate(TemplateChoices.Resolved(resolved), _, _))) => + resolved } + def foundUselessChoice(foo: Option[InterfaceType]) = + inside(foundResolvedChoices(foo).get(Useless).map(_.forgetNE.toSeq)) { + case Some(Seq((Some(origin1), choice1), (Some(origin2), choice2))) => + Seq(origin1, origin2) should contain theSameElementsAs Seq( + Ref.Identifier(itpPid, TIf), + Ref.Identifier(itpPid, LibTIf), + ) + inside(choice1) { case TheUselessChoice(_, tIf) => tIf should ===(origin1.qualifiedName) } + inside(choice2) { case TheUselessChoice(_, tIf) => tIf should ===(origin2.qualifiedName) } + } + "resolve inherited choices" in { foundUselessChoice(itpEI.typeDecls get Ref.Identifier(itpPid, Foo)) } @@ -256,6 +285,15 @@ class InterfaceReaderSpec extends AnyWordSpec with Matchers with Inside { itp.main.resolveChoicesAndIgnoreUnresolvedChoices(PartialFunction.empty).typeDecls get Foo ) } + + "collect direct and resolved choices in one map" in { + foundResolvedChoices(itpEI.typeDecls get Ref.Identifier(itpPid, Foo)) + .transform((_, cs) => cs.keySet) should contain theSameElementsAs Map( + Useless -> Set(Some(Ref.Identifier(itpPid, TIf)), Some(Ref.Identifier(itpPid, LibTIf))), + Bar -> Set(None), + Archive -> Set(None), + ) + } } private def wrappInModule(dataName: DottedName, dfn: Ast.DDataType) = diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/RunnerMain.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/RunnerMain.scala index ac6c6ccc4b12..fd7cd6504f12 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/RunnerMain.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/RunnerMain.scala @@ -91,6 +91,7 @@ object RunnerMain { if (config.jsonApi) { val ifaceDar = dar.map(pkg => InterfaceReader.readInterface(() => \/-(pkg))._2) val envIface = EnvironmentInterface.fromReaderInterfaces(ifaceDar) + // TODO (#13973) resolve envIface, or not, depending on whether inherited choices are needed Runner.jsonClients(participantParams, envIface) } else { Runner.connect(participantParams, config.tlsConfig, config.maxInboundMessageSize) diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala index 42d00490518a..e86206e7e853 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala @@ -368,11 +368,7 @@ class JsonLedgerClient( argument: Value, partySets: Option[SubmitParties], ): Future[Either[StatusRuntimeException, List[ScriptLedgerClient.ExerciseResult]]] = { - val choiceDef = envIface - .typeDecls(tplId) - .asInstanceOf[InterfaceType.Template] - .template - .choices(choice) + val choiceDef = lookupChoice(tplId, choice) val jsonArgument = LfValueCodec.apiValueToJsValue(argument) commandRequest[ExerciseArgs, ExerciseResponse]( "exercise", @@ -400,11 +396,7 @@ class JsonLedgerClient( argument: Value, partySets: Option[SubmitParties], ): Future[Either[StatusRuntimeException, List[ScriptLedgerClient.ExerciseResult]]] = { - val choiceDef = envIface - .typeDecls(tplId) - .asInstanceOf[InterfaceType.Template] - .template - .choices(choice) + val choiceDef = lookupChoice(tplId, choice) val jsonKey = LfValueCodec.apiValueToJsValue(key) val jsonArgument = LfValueCodec.apiValueToJsValue(argument) commandRequest[ExerciseByKeyArgs, ExerciseResponse]( @@ -432,11 +424,7 @@ class JsonLedgerClient( argument: Value, partySets: Option[SubmitParties], ): Future[Either[StatusRuntimeException, List[ScriptLedgerClient.CommandResult]]] = { - val choiceDef = envIface - .typeDecls(tplId) - .asInstanceOf[InterfaceType.Template] - .template - .choices(choice) + val choiceDef = lookupChoice(tplId, choice) val jsonTemplate = LfValueCodec.apiValueToJsValue(template) val jsonArgument = LfValueCodec.apiValueToJsValue(argument) commandRequest[CreateAndExerciseArgs, CreateAndExerciseResponse]( @@ -460,6 +448,15 @@ class JsonLedgerClient( }) } + // TODO (#13973) this is not enough data to pick an interface choice + private[this] def lookupChoice(tplId: Identifier, choice: ChoiceName) = + envIface + .typeDecls(tplId) + .asInstanceOf[InterfaceType.Template] + .template + .tChoices + .assumeNoOverloadedChoices(githubIssue = 13973)(choice) + private[this] val SubmissionFailures: Set[StatusCode] = { import StatusCodes._ Set(InternalServerError, BadRequest, Conflict, NotFound) diff --git a/language-support/codegen-common/BUILD.bazel b/language-support/codegen-common/BUILD.bazel index c9f12d3c535a..83f445682455 100644 --- a/language-support/codegen-common/BUILD.bazel +++ b/language-support/codegen-common/BUILD.bazel @@ -42,6 +42,7 @@ da_scala_test( "//daml-lf/data", "//daml-lf/interface", "//daml-lf/transaction-test-lib", + "//libs-scala/nonempty", "@maven//:ch_qos_logback_logback_classic", ], ) diff --git a/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/UtilSpec.scala b/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/UtilSpec.scala index 7397b15f6556..b074274367db 100644 --- a/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/UtilSpec.scala +++ b/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/UtilSpec.scala @@ -42,7 +42,7 @@ object UtilSpec { val trivialDeclarations: Gen[Map[Identifier, InterfaceType]] = { val fooRec = Record(ImmArraySeq.empty) - val fooTmpl = InterfaceType.Template(fooRec, DefTemplate(Map.empty, Map.empty, None, Seq.empty)) + val fooTmpl = InterfaceType.Template(fooRec, DefTemplate.Empty) val fooNorm = InterfaceType.Normal(DefDataType(ImmArraySeq.empty, fooRec)) implicit val idArb: Arbitrary[Identifier] = Arbitrary(idGen) arbitrary[Map[Identifier, Boolean]] map { diff --git a/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/dependencygraph/DependencyGraphSpec.scala b/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/dependencygraph/DependencyGraphSpec.scala index b490b5bfb85e..ef3a8ccdc6bc 100644 --- a/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/dependencygraph/DependencyGraphSpec.scala +++ b/language-support/codegen-common/src/test/scala/com/digitalasset/daml/lf/codegen/dependencygraph/DependencyGraphSpec.scala @@ -71,14 +71,13 @@ object DependencyGraphSpec { "a:b:HasKey" -> InterfaceType.Template( fooRec, DefTemplate( - Map.empty, - Map.empty, + TemplateChoices.Resolved(Map.empty), Some(TypeCon(TypeConName(Ref.Identifier assertFromString "a:b:It"), ImmArraySeq.empty)), Seq.empty, ), ), "a:b:NoKey" -> InterfaceType - .Template(fooRec, DefTemplate(Map.empty, Map.empty, None, Seq.empty)), + .Template(fooRec, DefTemplate.Empty), "a:b:It" -> InterfaceType.Normal(DefDataType(ImmArraySeq.empty, fooRec)), ) mapKeys Ref.Identifier.assertFromString diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala index 3581b62f40e6..9a7f0f093dbc 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala @@ -30,6 +30,8 @@ private[inner] object TemplateClass extends StrictLogging { val fields = getFieldsWithTypes(record.fields, packagePrefixes) val staticCreateMethod = generateStaticCreateMethod(fields, className) + // TODO(SC #13921) replace with a call to TemplateChoices#directChoices + val templateChoices = template.tChoices.assumeNoOverloadedChoices(githubIssue = 13921) val templateType = TypeSpec .classBuilder(className) .addModifiers(Modifier.FINAL, Modifier.PUBLIC) @@ -39,7 +41,7 @@ private[inner] object TemplateClass extends StrictLogging { .addMethods( generateStaticExerciseByKeyMethods( className, - template.choices, + templateChoices, template.key, typeWithContext.interface.typeDecls, typeWithContext.packageId, @@ -49,7 +51,7 @@ private[inner] object TemplateClass extends StrictLogging { .addMethods( generateCreateAndExerciseMethods( className, - template.choices, + templateChoices, typeWithContext.interface.typeDecls, typeWithContext.packageId, packagePrefixes, @@ -60,7 +62,7 @@ private[inner] object TemplateClass extends StrictLogging { ContractIdClass .builder( className, - template.choices, + templateChoices, packagePrefixes, ) .addFlattenedExerciseMethods( diff --git a/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/DamlContractTemplateGen.scala b/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/DamlContractTemplateGen.scala index f5f5d9a21a10..31f2326a5abe 100644 --- a/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/DamlContractTemplateGen.scala +++ b/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/DamlContractTemplateGen.scala @@ -37,14 +37,17 @@ object DamlContractTemplateGen { logger.debug(s"generate templateDecl: ${templateName.toString}, ${templateInterface.toString}") - val templateChoiceMethods = templateInterface.template.choices.flatMap { case (id, interface) => - util.genTemplateChoiceMethods( - templateType = tq"${TypeName(templateName.name)}", - idType = syntaxIdType, - id, - interface, - ) - } + // TODO (#13921) replace assumeNoOverloadedChoices with directChoices + val templateChoiceMethods = + templateInterface.template.tChoices.assumeNoOverloadedChoices(githubIssue = 13921).flatMap { + case (id, interface) => + util.genTemplateChoiceMethods( + templateType = tq"${TypeName(templateName.name)}", + idType = syntaxIdType, + id, + interface, + ) + } def toNamedArgumentsMethod = q""" diff --git a/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/LFUtil.scala b/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/LFUtil.scala index 78b70771ab34..1be46a06ec21 100644 --- a/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/LFUtil.scala +++ b/language-support/scala/codegen/src/main/scala/com/digitalasset/lf/codegen/lf/LFUtil.scala @@ -336,7 +336,7 @@ object LFUtil { List.fill(number)(prefix).zipWithIndex.map(t => toIdent(s"${t._1}${t._2}")) private[lf] def genConsumingChoicesMethod(templateInterface: DefTemplate[_]) = { - val consumingChoicesIds = templateInterface.choices + val consumingChoicesIds = templateInterface.tChoices.directChoices .collect { case (id, ci) if ci.consuming => id } diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/PackageService.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/PackageService.scala index 89d2e58533f1..b479cc108704 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/PackageService.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/PackageService.scala @@ -336,13 +336,14 @@ object PackageService { def getChoiceTypeMap(packageStore: PackageStore): ChoiceTypeMap = packageStore.flatMap { case (_, interface) => getChoices(interface) } + // TODO (#13923) probably needs to change signature private def getChoices( interface: iface.Interface ): Map[(TemplateId.RequiredPkg, Choice), iface.Type] = { val allChoices: Iterator[(Ref.QualifiedName, Map[Ref.ChoiceName, iface.TemplateChoice.FWT])] = interface.typeDecls.iterator.collect { - case (qn, iface.InterfaceType.Template(_, iface.DefTemplate(choices, _, _, _))) => - (qn, choices) + case (qn, iface.InterfaceType.Template(_, iface.DefTemplate(choices, _, _))) => + (qn, choices.assumeNoOverloadedChoices(githubIssue = 13923)) } ++ interface.astInterfaces.iterator.map { case (qn, defIf) => (qn, defIf.choices) } @@ -370,7 +371,7 @@ object PackageService { case ( qn, iface.InterfaceType - .Template(_, iface.DefTemplate(_, _, Some(keyType), _)), + .Template(_, iface.DefTemplate(_, Some(keyType), _)), ) => val templateId = TemplateId(interface.packageId, qn.module.dottedName, qn.name.dottedName) (templateId, keyType) diff --git a/libs-scala/nonempty/src/main/scala/nonempty/NonEmptyReturningOps.scala b/libs-scala/nonempty/src/main/scala/nonempty/NonEmptyReturningOps.scala index d6d2e10c87e2..5b5b484273d7 100644 --- a/libs-scala/nonempty/src/main/scala/nonempty/NonEmptyReturningOps.scala +++ b/libs-scala/nonempty/src/main/scala/nonempty/NonEmptyReturningOps.scala @@ -18,6 +18,9 @@ object NonEmptyReturningOps { ) extends AnyVal { def groupBy1[K](f: A => K): Map[K, NonEmpty[C]] = NonEmptyColl.Instance.subst[Lambda[f[_] => Map[K, f[C]]]](self groupBy f) + + def groupMap1[K, B](key: A => K)(f: A => B): Map[K, NonEmpty[CC[B]]] = + NonEmptyColl.Instance.subst[Lambda[f[_] => Map[K, f[CC[B]]]]](self.groupMap(key)(f)) } implicit final class `NE Seq Ops`[A, CC[X] <: imm.Seq[X], C]( diff --git a/navigator/backend/src/main/scala/com/digitalasset/navigator/model/PackageRegistry.scala b/navigator/backend/src/main/scala/com/digitalasset/navigator/model/PackageRegistry.scala index 561ea991d862..a80e4315cf69 100644 --- a/navigator/backend/src/main/scala/com/digitalasset/navigator/model/PackageRegistry.scala +++ b/navigator/backend/src/main/scala/com/digitalasset/navigator/model/PackageRegistry.scala @@ -14,13 +14,14 @@ case class PackageRegistry( private val templates: Map[DamlLfIdentifier, Template] = Map.empty, private val typeDefs: Map[DamlLfIdentifier, DamlLfDefDataType] = Map.empty, ) { + // TODO (#13969) ignores inherited choices; interfaces aren't handled at all private[this] def template( packageId: DamlLfRef.PackageId, qname: DamlLfQualifiedName, t: DamlLfIface.DefTemplate[DamlLfIface.Type], ): Template = Template( DamlLfIdentifier(packageId, qname), - t.choices.toList.map(c => choice(c._1, c._2)), + t.tChoices.directChoices.toList.map(c => choice(c._1, c._2)), t.key, ) 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 48d4ce1d6813..af4785941fe2 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 @@ -284,13 +284,12 @@ case object DamlConstants { val simpleRecordTemplate = DamlLfIface.InterfaceType.Template( simpleRecordT, DamlLfIface.DefTemplate( - Map( + DamlLfIface.TemplateChoices.Resolved fromDirect Map( ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT), choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT), choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT), ChoiceReplace -> DamlLfIface.TemplateChoice(simpleRecordTC, false, simpleUnitT), ), - Map.empty, None, Seq.empty, ), @@ -298,13 +297,12 @@ case object DamlConstants { val complexRecordTemplate = DamlLfIface.InterfaceType.Template( complexRecordT, DamlLfIface.DefTemplate( - Map( + DamlLfIface.TemplateChoices.Resolved fromDirect Map( ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT), choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT), choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT), ChoiceReplace -> DamlLfIface.TemplateChoice(complexRecordTC, false, simpleUnitT), ), - Map.empty, None, Seq.empty, ), @@ -312,13 +310,12 @@ case object DamlConstants { val treeNodeTemplate = DamlLfIface.InterfaceType.Template( treeNodeT, DamlLfIface.DefTemplate( - Map( + DamlLfIface.TemplateChoices.Resolved fromDirect Map( ChoiceUnit -> DamlLfIface.TemplateChoice(simpleUnitT, false, simpleUnitT), choiceText -> DamlLfIface.TemplateChoice(simpleTextT, false, simpleUnitT), choiceNonconsuming -> DamlLfIface.TemplateChoice(simpleUnitT, true, simpleUnitT), ChoiceReplace -> DamlLfIface.TemplateChoice(treeNodeTC, false, simpleUnitT), ), - Map.empty, None, Seq.empty, ),