Skip to content

Commit

Permalink
choice name overloading in lf/interface (digital-asset#13938)
Browse files Browse the repository at this point in the history
* introduce TemplateChoices to mediate template choice resolution
* if any resolutions fail in EnvironmentInterface, discard the template's resolution
* if any resolutions fail in Interface, discard the remaining unresolved

CHANGELOG_BEGIN
CHANGELOG_END
  • Loading branch information
S11001001 authored May 31, 2022
1 parent 00fd847 commit 96a6fe6
Show file tree
Hide file tree
Showing 21 changed files with 425 additions and 119 deletions.
13 changes: 11 additions & 2 deletions daml-lf/interface/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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 = [
Expand All @@ -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",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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 {
Expand All @@ -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(_))
}
}
}

Expand All @@ -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] {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
})
Expand Down
Loading

0 comments on commit 96a6fe6

Please sign in to comment.