Repurpose templateId in ExerciseCommand and add interfaceId in ExerciseEvent (#13660)

part of #13653

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Remy 2022-05-16 17:30:07 +02:00 committed by GitHub
parent aad0e05533
commit 6f6a3052a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 459 additions and 248 deletions

View File

@ -81,20 +81,22 @@ final class ValueEnricher(
}
def enrichChoiceArgument(
tyCon: Identifier,
templateId: Identifier,
interfaceId: Option[Identifier],
choiceName: Name,
value: Value,
): Result[Value] =
handleLookup(interface.lookupChoice(tyCon, choiceName))
.flatMap(choiceInfo => enrichValue(choiceInfo.choice.argBinder._2, value))
handleLookup(interface.lookupChoice(templateId, interfaceId, choiceName))
.flatMap(choice => enrichValue(choice.argBinder._2, value))
def enrichChoiceResult(
tyCon: Identifier,
templateId: Identifier,
interfaceId: Option[Identifier],
choiceName: Name,
value: Value,
): Result[Value] =
handleLookup(interface.lookupChoice(tyCon, choiceName))
.flatMap(choiceInfo => enrichValue(choiceInfo.choice.returnType, value))
handleLookup(interface.lookupChoice(templateId, interfaceId, choiceName))
.flatMap(choice => enrichValue(choice.returnType, value))
def enrichContractKey(tyCon: Identifier, value: Value): Result[Value] =
handleLookup(interface.lookupTemplateKey(tyCon))
@ -155,10 +157,17 @@ final class ValueEnricher(
} yield lookup.copy(key = key)
case exe: Node.Exercise =>
for {
choiceArg <- enrichChoiceArgument(exe.templateId, exe.choiceId, exe.chosenValue)
choiceArg <- enrichChoiceArgument(
exe.templateId,
exe.interfaceId,
exe.choiceId,
exe.chosenValue,
)
result <- exe.exerciseResult match {
case Some(exeResult) =>
enrichChoiceResult(exe.templateId, exe.choiceId, exeResult).map(Some(_))
enrichChoiceResult(exe.templateId, exe.interfaceId, exe.choiceId, exeResult).map(
Some(_)
)
case None =>
ResultNone
}

View File

@ -6,11 +6,11 @@ package engine
package preprocessing
import com.daml.lf.data._
import com.daml.lf.language.Ast
import com.daml.lf.language.{Ast, PackageInterface}
import com.daml.lf.value.Value
import com.daml.scalautil.Statement.discard
import scala.annotation.tailrec
import scala.annotation.{nowarn, tailrec}
private[lf] final class CommandPreprocessor(
interface: language.PackageInterface,
@ -35,53 +35,74 @@ private[lf] final class CommandPreprocessor(
speedy.Command.Create(templateId, arg)
}
// TODO: https://github.com/digital-asset/daml/issues/12051
// Drop this once Canton support ambiguous choices properly
@throws[Error.Preprocessing.Error]
@deprecated
private def unsafePreprocessLenientExercise(
templateId: Ref.Identifier,
contractId: Value.ContractId,
choiceId: Ref.ChoiceName,
argument: Value,
): speedy.Command =
handleLookup(interface.lookupLenientChoice(templateId, choiceId)) match {
case PackageInterface.ChoiceInfo.Template(choice) =>
speedy.Command.ExerciseTemplate(
templateId = templateId,
contractId = valueTranslator.unsafeTranslateCid(contractId),
choiceId = choiceId,
argument = valueTranslator.unsafeTranslateValue(choice.argBinder._2, argument),
)
case PackageInterface.ChoiceInfo.Inherited(ifaceId, choice) =>
speedy.Command.ExerciseInterface(
interfaceId = ifaceId,
contractId = valueTranslator.unsafeTranslateCid(contractId),
choiceId = choiceId,
argument = valueTranslator.unsafeTranslateValue(choice.argBinder._2, argument),
)
}
def unsafePreprocessExercise(
typeId: Ref.Identifier,
contractId: Value.ContractId,
choiceId: Ref.ChoiceName,
argument: Value,
): speedy.Command = {
import language.PackageInterface.ChoiceInfo
val cid = valueTranslator.unsafeTranslateCid(contractId)
def command(
choice: Ast.TemplateChoiceSignature,
toSpeedyCommand: speedy.SValue => speedy.Command,
) = {
val arg = valueTranslator.unsafeTranslateValue(choice.argBinder._2, argument)
toSpeedyCommand(arg)
): speedy.Command =
handleLookup(interface.lookupTemplateOrInterface(typeId)) match {
case Left(_) =>
unsafePreprocessExerciseTemplate(typeId, contractId, choiceId, argument)
case Right(_) =>
unsafePreprocessExerciseInterface(typeId, contractId, choiceId, argument)
}
handleLookup(interface.lookupChoice(typeId, choiceId)) match {
case ChoiceInfo.Template(choice) =>
command(choice, speedy.Command.ExerciseTemplate(typeId, cid, choiceId, _))
case ChoiceInfo.Interface(choice) =>
command(choice, speedy.Command.ExerciseInterface(typeId, cid, choiceId, _))
case ChoiceInfo.Inherited(ifaceId, choice) =>
command(choice, speedy.Command.ExerciseByInterface(ifaceId, typeId, cid, choiceId, _))
case ChoiceInfo.InterfaceInherited(ifaceId, choice) =>
command(
choice,
speedy.Command
.ExerciseByInheritedInterface(ifaceId, typeId, cid, choiceId, _),
)
}
}
/* Like unsafePreprocessExercise, but expects the choice to come from the template specifically, not inherited from an interface. */
@throws[Error.Preprocessing.Error]
def unsafePreprocessExerciseTemplate(
templateId: Ref.Identifier,
contractId: Value.ContractId,
choiceId: Ref.ChoiceName,
argument: Value,
): speedy.Command.ExerciseTemplate = {
val cid = valueTranslator.unsafeTranslateCid(contractId)
val choiceArgType = handleLookup(
interface.lookupTemplateChoice(templateId, choiceId)
).argBinder._2
val arg = valueTranslator.unsafeTranslateValue(choiceArgType, argument)
speedy.Command.ExerciseTemplate(templateId, cid, choiceId, arg)
): speedy.Command = {
val choice = handleLookup(interface.lookupTemplateChoice(templateId, choiceId))
speedy.Command.ExerciseTemplate(
templateId = templateId,
contractId = valueTranslator.unsafeTranslateCid(contractId),
choiceId = choiceId,
argument = valueTranslator.unsafeTranslateValue(choice.argBinder._2, argument),
)
}
def unsafePreprocessExerciseInterface(
ifaceId: Ref.Identifier,
contractId: Value.ContractId,
choiceId: Ref.ChoiceName,
argument: Value,
): speedy.Command = {
val choice = handleLookup(interface.lookupInterfaceChoice(ifaceId, choiceId))
speedy.Command.ExerciseInterface(
interfaceId = ifaceId,
contractId = valueTranslator.unsafeTranslateCid(contractId),
choiceId = choiceId,
argument = valueTranslator.unsafeTranslateValue(choice.argBinder._2, argument),
)
}
@throws[Error.Preprocessing.Error]
@ -161,14 +182,22 @@ private[lf] final class CommandPreprocessor(
// returns the speedy translation of an Replay command.
@throws[Error.Preprocessing.Error]
@nowarn("msg=deprecated")
private[preprocessing] def unsafePreprocessReplayCommand(
cmd: command.ReplayCommand
): speedy.Command =
cmd match {
case command.ReplayCommand.Create(templateId, argument) =>
unsafePreprocessCreate(templateId, argument)
case command.ReplayCommand.Exercise(templateId, coid, choiceId, argument) =>
unsafePreprocessExercise(templateId, coid, choiceId, argument)
case command.ReplayCommand.LenientExercise(typeId, coid, choiceId, argument) =>
unsafePreprocessLenientExercise(typeId, coid, choiceId, argument)
case command.ReplayCommand.Exercise(templateId, mbIfaceId, coid, choiceId, argument) =>
mbIfaceId match {
case Some(ifaceId) =>
unsafePreprocessExerciseInterface(ifaceId, coid, choiceId, argument)
case None =>
unsafePreprocessExerciseTemplate(templateId, coid, choiceId, argument)
}
case command.ReplayCommand.ExerciseByKey(
templateId,
contractKey,

View File

@ -134,7 +134,7 @@ private[engine] final class Preprocessor(
private[engine] def preprocessApiCommand(
cmd: command.ApiCommand
): Result[speedy.Command] =
safelyRun(pullTemplatePackage(List(cmd.templateId))) {
safelyRun(pullTemplatePackage(List(cmd.typeId))) {
commandPreprocessor.unsafePreprocessApiCommand(cmd)
}
@ -143,7 +143,7 @@ private[engine] final class Preprocessor(
def preprocessApiCommands(
cmds: data.ImmArray[command.ApiCommand]
): Result[ImmArray[speedy.Command]] =
safelyRun(pullTemplatePackage(cmds.toSeq.view.map(_.templateId))) {
safelyRun(pullTemplatePackage(cmds.toSeq.view.map(_.typeId))) {
commandPreprocessor.unsafePreprocessApiCommands(cmds)
}

View File

@ -116,7 +116,7 @@ class ApiCommandPreprocessorSpec
ValueRecord("", ImmArray("owners" -> valueParties, "data" -> ValueInt64(42))),
)
// TEST_EVIDENCE: Input Validation: well formed exercise API command is accepted
val validExe = ApiCommand.Exercise(
val validExeTemplate = ApiCommand.Exercise(
"Mod:Record",
newCid,
"Transfer",
@ -130,20 +130,12 @@ class ApiCommandPreprocessorSpec
ValueRecord("", ImmArray("content" -> ValueList(FrontStack(ValueParty("Clara"))))),
)
// TEST_EVIDENCE: Input Validation: well formed exercise-by-interface command is accepted
val validExeByInterface = ApiCommand.Exercise(
val validExeInterface = ApiCommand.Exercise(
"Mod:Iface",
newCid,
"IfaceChoice",
ValueUnit,
)
// TEST_EVIDENCE: Input Validation: well formed exercise-by-interface via required interface command is accepted
val validExeByRequiredInterface = ApiCommand.Exercise(
"Mod:Iface",
newCid,
"IfaceChoice3",
ValueUnit,
)
// TEST_EVIDENCE: Input Validation: well formed create-and-exercise API command is accepted
val validCreateAndExe = ApiCommand.CreateAndExercise(
"Mod:Record",
@ -154,10 +146,9 @@ class ApiCommandPreprocessorSpec
val noErrorTestCases = Table[ApiCommand](
"command",
validCreate,
validExe,
validExeTemplate,
validExeInterface,
validExeByKey,
validExeByInterface,
validExeByRequiredInterface,
validCreateAndExe,
)
@ -169,15 +160,15 @@ class ApiCommandPreprocessorSpec
validCreate.copy(argument = ValueRecord("", ImmArray("content" -> ValueInt64(42)))) ->
a[Error.Preprocessing.TypeMismatch],
// TEST_EVIDENCE: Input Validation: ill-formed exercise API command is rejected
validExe.copy(templateId = "Mod:Undefined") ->
validExeTemplate.copy(typeId = "Mod:Undefined") ->
a[Error.Preprocessing.Lookup],
validExe.copy(choiceId = "Undefined") ->
validExeTemplate.copy(choiceId = "Undefined") ->
a[Error.Preprocessing.Lookup],
validExe.copy(argument = ValueRecord("", ImmArray("content" -> ValueInt64(42)))) ->
validExeTemplate.copy(argument = ValueRecord("", ImmArray("content" -> ValueInt64(42)))) ->
a[Error.Preprocessing.TypeMismatch],
// TEST_EVIDENCE: Input Validation: exercise-by-interface command is rejected for a
// choice of another interface.
validExeByInterface.copy(choiceId = "IfaceChoice2") ->
validExeInterface.copy(choiceId = "IfaceChoice2") ->
a[Error.Preprocessing.Lookup],
// TEST_EVIDENCE: Input Validation: ill-formed exercise-by-key API command is rejected
validExeByKey.copy(templateId = "Mod:Undefined") ->

View File

@ -2320,6 +2320,7 @@ object EngineTest {
case exe: Node.Exercise =>
ReplayCommand.Exercise(
exe.templateId,
exe.interfaceId,
exe.targetCoid,
exe.choiceId,
exe.chosenValue,

View File

@ -58,8 +58,6 @@ class InterfacesTest
val lookupPackage = allInterfacesPkgs.get(_)
val idI1 = Identifier(interfacesPkgId, "Interfaces:I1")
val idI2 = Identifier(interfacesPkgId, "Interfaces:I2")
val idI3 = Identifier(interfacesPkgId, "Interfaces:I3")
val idI4 = Identifier(interfacesPkgId, "Interfaces:I4")
val idT1 = Identifier(interfacesPkgId, "Interfaces:T1")
val idT2 = Identifier(interfacesPkgId, "Interfaces:T2")
val let = Time.Timestamp.now()
@ -130,56 +128,22 @@ class InterfacesTest
)
}
}
"be unable to exercise an interface I4 choice via I3 on a T1 contract" in {
val command = ApiCommand.Exercise(idI3, cid2, "C4", ValueRecord(None, ImmArray.empty))
inside(runApi(command)) { case Left(Error.Interpretation(err, _)) =>
err shouldBe Error.Interpretation.DamlException(
IE.ContractDoesNotImplementRequiringInterface(idI3, idI4, cid2, idT2)
)
}
}
"be able to exercise T1 by interface I1" in {
val command = ApiCommand.Exercise(idT1, cid1, "C1", ValueRecord(None, ImmArray.empty))
val command = ApiCommand.Exercise(idI1, cid1, "C1", ValueRecord(None, ImmArray.empty))
runApi(command) shouldBe a[Right[_, _]]
}
"be able to exercise T2 by interface I1" in {
val command = ApiCommand.Exercise(idT2, cid2, "C1", ValueRecord(None, ImmArray.empty))
val command = ApiCommand.Exercise(idI1, cid2, "C1", ValueRecord(None, ImmArray.empty))
runApi(command) shouldBe a[Right[_, _]]
}
"be able to exercise T2 by interface I2" in {
val command = ApiCommand.Exercise(idT2, cid2, "C2", ValueRecord(None, ImmArray.empty))
val command = ApiCommand.Exercise(idI2, cid2, "C2", ValueRecord(None, ImmArray.empty))
runApi(command) shouldBe a[Right[_, _]]
}
"be unable to exercise T1 by interface I2 (stopped in preprocessor)" in {
val command = ApiCommand.Exercise(idT1, cid1, "C2", ValueRecord(None, ImmArray.empty))
preprocessApi(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T1 (disguised as T2) by interface I1" in {
val command = ApiCommand.Exercise(idT2, cid1, "C1", ValueRecord(None, ImmArray.empty))
inside(runApi(command)) { case Left(Error.Interpretation(err, _)) =>
err shouldBe Error.Interpretation.DamlException(IE.WronglyTypedContract(cid1, idT2, idT1))
}
}
"be unable to exercise T2 (disguised as T1) by interface I1" in {
val command = ApiCommand.Exercise(idT1, cid2, "C1", ValueRecord(None, ImmArray.empty))
inside(runApi(command)) { case Left(Error.Interpretation(err, _)) =>
err shouldBe Error.Interpretation.DamlException(IE.WronglyTypedContract(cid2, idT1, idT2))
}
}
"be unable to exercise T2 (disguised as T1) by interface I2 (stopped in preprocessor)" in {
val command = ApiCommand.Exercise(idT1, cid2, "C2", ValueRecord(None, ImmArray.empty))
preprocessApi(command) shouldBe a[Left[_, _]]
}
"be unable to exercise T1 (disguised as T2) by interface I2 " in {
val command = ApiCommand.Exercise(idT2, cid1, "C2", ValueRecord(None, ImmArray.empty))
inside(runApi(command)) { case Left(Error.Interpretation(err, _)) =>
err shouldBe Error.Interpretation.DamlException(
IE.ContractDoesNotImplementInterface(idI2, cid1, idT1)
)
}
}
}
}

View File

@ -117,6 +117,7 @@ class ReinterpretTest
val cid = toContractId("ReinterpretTests:MySimple:1")
ReplayCommand.Exercise(
templateId,
None,
cid,
choiceName,
ValueRecord(Some(r), ImmArray.Empty),
@ -134,6 +135,7 @@ class ReinterpretTest
val cid = toContractId("ReinterpretTests:MySimple:1")
ReplayCommand.Exercise(
templateId,
None,
cid,
choiceName,
ValueRecord(Some(r), ImmArray.Empty),
@ -151,6 +153,7 @@ class ReinterpretTest
val cid = toContractId("ReinterpretTests:MySimple:1")
ReplayCommand.Exercise(
templateId,
None,
cid,
choiceName,
ValueRecord(Some(r), ImmArray.Empty),
@ -168,6 +171,7 @@ class ReinterpretTest
val cid = toContractId("ReinterpretTests:MySimple:1")
ReplayCommand.Exercise(
templateId,
None,
cid,
choiceName,
ValueRecord(Some(r), ImmArray.Empty),
@ -185,6 +189,7 @@ class ReinterpretTest
val cid = toContractId("ReinterpretTests:MySimple:1")
ReplayCommand.Exercise(
templateId,
None,
cid,
choiceName,
ValueRecord(Some(r), ImmArray.Empty),

View File

@ -85,6 +85,7 @@ class ReplayCommandPreprocessorSpec
// TEST_EVIDENCE: Input Validation: well formed exercise replay command is accepted
val validExe = ReplayCommand.Exercise(
"Mod:Record",
"",
newCid,
"Transfer",
ValueRecord("", ImmArray("content" -> ValueList(FrontStack(ValueParty("Clara"))))),
@ -199,12 +200,14 @@ class ReplayCommandPreprocessorSpec
),
ReplayCommand.Exercise(
"Mod:RecordRef",
"",
innocentCid,
"Change",
ValueContractId(culpritCid),
),
ReplayCommand.Exercise(
"Mod:RecordRef",
"",
culpritCid,
"Change",
ValueContractId(innocentCid),

View File

@ -284,9 +284,8 @@ private[lf] object Pretty {
Doc.empty
intercalate(text(", "), ex.actingParties.map(p => text(p))) &
text("exercises") &
text(ex.choiceId) + char(':') + prettyIdentifier(
ex.interfaceId.getOrElse(ex.templateId)
) &
text(ex.choiceId) + char(':') +
prettyIdentifier(ex.interfaceId.getOrElse(ex.templateId)) &
text("on") & prettyContractId(ex.targetCoid) /
(text(" ") + text("with") & prettyValue(false)(ex.chosenValue) / children)
.nested(4)

View File

@ -293,47 +293,40 @@ private[lf] class PackageInterface(signatures: PartialFunction[PackageId, Packag
}
)
import PackageInterface.ChoiceInfo
private[lf] def lookupChoice(
identifier: TypeConName,
// TODO: https://github.com/digital-asset/daml/issues/12051
// Drop this, once Canton support ambiguous choices properly
@deprecated
private[lf] def lookupLenientChoice(
templateId: TypeConName,
chName: ChoiceName,
): Either[LookupError, ChoiceInfo] = {
lazy val context = Reference.Choice(identifier, chName)
lookupTemplateOrInterface(identifier, context).flatMap {
case Left(template) =>
template.choices.get(chName) match {
case Some(choice) => Right(ChoiceInfo.Template(choice))
case None =>
template.inheritedChoices.get(chName) match {
case Some(ifaceId) =>
lookupInterfaceChoice(ifaceId, chName, context).map(
ChoiceInfo.Inherited(ifaceId, _)
)
case None =>
Left(LookupError(context, context))
}
}
case Right(interface) =>
interface.fixedChoices.get(chName) match {
case Some(choice) => Right(ChoiceInfo.Interface(choice))
case None => {
// TODO(drsk) improve the performance of this lookup. Tracked in issue
// https://github.com/digital-asset/daml/issues/13630.
interface.requires.view
.map((iface) =>
lookupInterfaceChoice(iface, chName, context).map((choice) => (choice, iface))
)
.collectFirst({ case Right((choice, iface)) => (choice, iface) }) match {
case Some((choice, iface)) =>
Right(ChoiceInfo.InterfaceInherited(iface, choice))
case None => Left(LookupError(context, context))
}
): Either[LookupError, PackageInterface.ChoiceInfo] = {
lazy val context = Reference.Choice(templateId, chName)
lookupTemplate(templateId, context).flatMap { template =>
template.choices.get(chName) match {
case Some(choice) =>
Right(PackageInterface.ChoiceInfo.Template(choice))
case None =>
template.inheritedChoices.get(chName) match {
case Some(ifaceId) =>
lookupInterfaceChoice(ifaceId, chName, context)
.map(PackageInterface.ChoiceInfo.Inherited(ifaceId, _))
case None =>
Left(LookupError(context, context))
}
}
}
}
}
private[lf] def lookupChoice(
templateId: TypeConName,
mbInterfaceId: Option[TypeConName],
chName: ChoiceName,
): Either[LookupError, TemplateChoiceSignature] =
mbInterfaceId match {
case None => lookupTemplateChoice(templateId, chName)
case Some(ifaceId) => lookupInterfaceChoice(ifaceId, chName)
}
def lookupTemplateOrInterface(
name: TypeConName
): Either[LookupError, Either[TemplateSignature, DefInterfaceSignature]] =
@ -454,18 +447,10 @@ object PackageInterface {
object ChoiceInfo {
final case class Interface(choice: TemplateChoiceSignature) extends ChoiceInfo
final case class Template(choice: TemplateChoiceSignature) extends ChoiceInfo
final case class Inherited(ifaceId: Identifier, choice: TemplateChoiceSignature)
final case class Inherited(ifaceId: TypeConName, choice: TemplateChoiceSignature)
extends ChoiceInfo
final case class InterfaceInherited(
ifaceId: Identifier,
choice: TemplateChoiceSignature,
) extends ChoiceInfo
}
}

View File

@ -10,27 +10,29 @@ import com.daml.lf.data.{ImmArray, Time}
/** Accepted commands coming from API */
sealed abstract class ApiCommand extends Product with Serializable {
val templateId: Identifier
def typeId: TypeConName
}
object ApiCommand {
/** Command for creating a contract
*
* @param templateId identifier of the template that the contract is instantiating
* @param templateId TypeConName of the template that the contract is instantiating
* @param argument value passed to the template
*/
final case class Create(templateId: Identifier, argument: Value) extends ApiCommand
final case class Create(templateId: TypeConName, argument: Value) extends ApiCommand {
def typeId: TypeConName = templateId
}
/** Command for exercising a choice on an existing contract
*
* @param templateId identifier of the original contract
* @param typeId templateId or interfaceId where the choice is defined
* @param contractId contract on which the choice is exercised
* @param choiceId identifier choice
* @param choiceId TypeConName choice
* @param argument value passed for the choice
*/
final case class Exercise(
templateId: Identifier,
typeId: TypeConName,
contractId: Value.ContractId,
choiceId: ChoiceName,
argument: Value,
@ -38,32 +40,36 @@ object ApiCommand {
/** Command for exercising a choice on an existing contract specified by its key
*
* @param templateId identifier of the original contract
* @param templateId TypeConName of the original contract
* @param contractKey key of the contract on which the choice is exercised
* @param choiceId identifier choice
* @param choiceId TypeConName choice
* @param argument value passed for the choice
*/
final case class ExerciseByKey(
templateId: Identifier,
templateId: TypeConName,
contractKey: Value,
choiceId: ChoiceName,
argument: Value,
) extends ApiCommand
) extends ApiCommand {
def typeId: TypeConName = templateId
}
/** Command for creating a contract and exercising a choice
* on that existing contract within the same transaction
*
* @param templateId identifier of the original contract
* @param templateId TypeConName of the original contract
* @param createArgument value passed to the template
* @param choiceId identifier choice
* @param choiceId TypeConName choice
* @param choiceArgument value passed for the choice
*/
final case class CreateAndExercise(
templateId: Identifier,
templateId: TypeConName,
createArgument: Value,
choiceId: ChoiceName,
choiceArgument: Value,
) extends ApiCommand
) extends ApiCommand {
def typeId: TypeConName = templateId
}
}
/** Commands input adapted from ledger-api

View File

@ -9,7 +9,7 @@ import com.daml.lf.value.Value
/** Accepted commands for replay */
sealed abstract class ReplayCommand extends Product with Serializable {
val templateId: Identifier
val templateId: TypeConName
}
object ReplayCommand {
@ -20,9 +20,20 @@ object ReplayCommand {
argument: Value,
) extends ReplayCommand
// TODO: https://github.com/digital-asset/daml/issues/12051
// Drop this, once Canton support ambiguous choices properly
@deprecated("use Exercise")
final case class LenientExercise(
templateId: TypeConName,
contractId: Value.ContractId,
choiceId: ChoiceName,
argument: Value,
) extends ReplayCommand
/** Exercise a template choice, by template Id or interface Id. */
final case class Exercise(
templateId: Identifier,
templateId: TypeConName,
interfaceId: Option[TypeConName],
contractId: Value.ContractId,
choiceId: ChoiceName,
argument: Value,

View File

@ -95,6 +95,7 @@ object TestData {
ExercisedEvent(
eventId = eventId,
templateId = Some(defaultTemplateId),
interfaceId = None,
contractId = ContractId.unwrap(contractId),
actingParties = Party.unsubst(actingParties),
choice = "Choice",

View File

@ -65,7 +65,7 @@ object ScriptIds {
}
final case class AnyTemplate(ty: Identifier, arg: SValue)
final case class AnyChoice(name: ChoiceName, arg: SValue)
final case class AnyChoice(typeId: Identifier, name: ChoiceName, arg: SValue)
final case class AnyContractKey(key: SValue)
// frames ordered from most-recent to least-recent
final case class StackTrace(frames: Vector[Location]) {
@ -160,15 +160,20 @@ object Converter {
}
private def fromAnyChoice(
lookupChoice: (Identifier, Name) => Either[String, TemplateChoiceSignature],
lookupChoice: (
Identifier,
Option[Identifier],
ChoiceName,
) => Either[String, TemplateChoiceSignature],
translator: preprocessing.ValueTranslator,
templateId: Identifier,
interfaceId: Option[Identifier],
choiceName: ChoiceName,
argument: Value,
): Either[String, SValue] = {
val contractIdTy = daInternalAny("AnyChoice")
for {
choice <- lookupChoice(templateId, choiceName)
choice <- lookupChoice(templateId, interfaceId, choiceName)
translated <- translator
.translateValue(choice.argBinder._2, argument)
.left
@ -180,12 +185,16 @@ object Converter {
)
}
def toAnyChoice(v: SValue): Either[String, AnyChoice] = {
def toAnyChoice(
v: SValue,
lookupChoiceByArgType: Identifier => Either[String, Identifier],
): Either[String, AnyChoice] = {
v match {
case SRecord(_, _, ArrayList(SAny(TTyCon(tyCon), choiceVal), _)) =>
// This exploits the fact that in Daml, choice argument type names
// and choice names match up.
ChoiceName.fromString(tyCon.qualifiedName.name.toString).map(AnyChoice(_, choiceVal))
for {
choiceTypeId <- lookupChoiceByArgType(tyCon)
chName <- ChoiceName.fromString(tyCon.qualifiedName.name.toString)
} yield AnyChoice(choiceTypeId, chName, choiceVal)
case _ => Left(s"Expected AnyChoice but got $v")
}
}
@ -228,16 +237,19 @@ object Converter {
case _ => Left(s"Expected Create but got $v")
}
def toExerciseCommand(v: SValue): Either[String, command.ApiCommand] =
def toExerciseCommand(
v: SValue,
lookupChoiceByArgType: (Identifier, Identifier) => Either[String, Identifier],
): Either[String, command.ApiCommand] =
v match {
// typerep, contract id, choice argument and continuation
case SRecord(_, _, vals) if vals.size == 4 => {
for {
tplId <- typeRepToIdentifier(vals.get(0))
cid <- toContractId(vals.get(1))
anyChoice <- toAnyChoice(vals.get(2))
anyChoice <- toAnyChoice(vals.get(2), lookupChoiceByArgType(tplId, _))
} yield command.ApiCommand.Exercise(
templateId = tplId,
typeId = anyChoice.typeId,
contractId = cid,
choiceId = anyChoice.name,
argument = anyChoice.arg.toUnnormalizedValue,
@ -253,7 +265,7 @@ object Converter {
for {
tplId <- typeRepToIdentifier(vals.get(0))
anyKey <- toAnyContractKey(vals.get(1))
anyChoice <- toAnyChoice(vals.get(2))
anyChoice <- toAnyChoice(vals.get(2), _ => Right(tplId))
} yield command.ApiCommand.ExerciseByKey(
templateId = tplId,
contractKey = anyKey.key.toUnnormalizedValue,
@ -264,12 +276,15 @@ object Converter {
case _ => Left(s"Expected ExerciseByKey but got $v")
}
def toCreateAndExerciseCommand(v: SValue): Either[String, command.ApiCommand.CreateAndExercise] =
def toCreateAndExerciseCommand(
v: SValue,
lookupChoiceByArgType: (Identifier, Identifier) => Either[String, Identifier],
): Either[String, command.ApiCommand.CreateAndExercise] =
v match {
case SRecord(_, _, vals) if vals.size == 3 => {
for {
anyTemplate <- toAnyTemplate(vals.get(0))
anyChoice <- toAnyChoice(vals.get(1))
anyChoice <- toAnyChoice(vals.get(1), lookupChoiceByArgType(anyTemplate.ty, _))
} yield command.ApiCommand.CreateAndExercise(
templateId = anyTemplate.ty,
createArgument = anyTemplate.arg.toUnnormalizedValue,
@ -318,6 +333,7 @@ object Converter {
def toCommands(
compiledPackages: CompiledPackages,
freeAp: SValue,
lookupChoiceByArgType: (Identifier, Identifier) => Either[String, Identifier],
): Either[String, List[command.ApiCommand]] = {
@tailrec
def iter(
@ -336,7 +352,7 @@ object Converter {
}
case Right((SVariant(_, "Exercise", _, exercise), v)) =>
// This cant be a for-comprehension since it trips up tailrec optimization.
toExerciseCommand(exercise) match {
toExerciseCommand(exercise, lookupChoiceByArgType) match {
case Left(err) => Left(err)
case Right(r) => iter(v, r :: commands)
}
@ -346,7 +362,7 @@ object Converter {
case Right(r) => iter(v, r :: commands)
}
case Right((SVariant(_, "CreateAndExercise", _, createAndExercise), v)) =>
toCreateAndExerciseCommand(createAndExercise) match {
toCreateAndExerciseCommand(createAndExercise, lookupChoiceByArgType) match {
case Left(err) => Left(err)
case Right(r) => iter(v, r :: commands)
}
@ -361,13 +377,17 @@ object Converter {
}
def translateExerciseResult(
lookupChoice: (Identifier, Name) => Either[String, TemplateChoiceSignature],
lookupChoice: (
Identifier,
Option[Identifier],
ChoiceName,
) => Either[String, TemplateChoiceSignature],
translator: preprocessing.ValueTranslator,
result: ScriptLedgerClient.ExerciseResult,
) = {
for {
choice <- Name.fromString(result.choice)
c <- lookupChoice(result.templateId, choice)
c <- lookupChoice(result.templateId, result.interfaceId, choice)
translated <- translator
.translateValue(c.returnType, result.result)
.left
@ -376,7 +396,11 @@ object Converter {
}
def translateTransactionTree(
lookupChoice: (Identifier, Name) => Either[String, TemplateChoiceSignature],
lookupChoice: (
Identifier,
Option[Identifier],
ChoiceName,
) => Either[String, TemplateChoiceSignature],
translator: preprocessing.ValueTranslator,
scriptIds: ScriptIds,
tree: ScriptLedgerClient.TransactionTree,
@ -395,10 +419,17 @@ object Converter {
("argument", anyTemplate),
),
)
case ScriptLedgerClient.Exercised(tplId, contractId, choiceName, arg, childEvents) =>
case ScriptLedgerClient.Exercised(
tplId,
ifaceId,
contractId,
choiceName,
arg,
childEvents,
) =>
for {
evs <- childEvents.traverse(translateTreeEvent(_))
anyChoice <- fromAnyChoice(lookupChoice, translator, tplId, choiceName, arg)
anyChoice <- fromAnyChoice(lookupChoice, translator, tplId, ifaceId, choiceName, arg)
} yield SVariant(
scriptIds.damlScript("TreeEvent"),
Name.assertFromString("ExercisedEvent"),
@ -424,7 +455,11 @@ object Converter {
// fill in the values for the continuation.
def fillCommandResults(
compiledPackages: CompiledPackages,
lookupChoice: (Identifier, Name) => Either[String, TemplateChoiceSignature],
lookupChoice: (
Identifier,
Option[Identifier],
ChoiceName,
) => Either[String, TemplateChoiceSignature],
translator: preprocessing.ValueTranslator,
initialFreeAp: SValue,
allEventResults: Seq[ScriptLedgerClient.CommandResult],
@ -445,7 +480,7 @@ object Converter {
for {
contractId <- eventResults.head match {
case ScriptLedgerClient.CreateResult(cid) => Right(SContractId(cid))
case ScriptLedgerClient.ExerciseResult(_, _, _) =>
case ScriptLedgerClient.ExerciseResult(_, _, _, _) =>
Left("Expected CreateResult but got ExerciseResult")
}
} yield (SEApp(SEValue(continue), Array(SEValue(contractId))), eventResults.tail)
@ -788,6 +823,7 @@ object Converter {
case TreeEvent.Kind.Exercised(exercised) =>
for {
tplId <- Converter.fromApiIdentifier(exercised.getTemplateId)
ifaceId <- exercised.interfaceId.traverse(Converter.fromApiIdentifier)
cid <- ContractId.fromString(exercised.contractId)
choice <- ChoiceName.fromString(exercised.choice)
choiceArg <- NoLoggingValueValidator
@ -797,6 +833,7 @@ object Converter {
childEvents <- exercised.childEventIds.toList.traverse(convEvent(_))
} yield ScriptLedgerClient.Exercised(
tplId,
ifaceId,
cid,
choice,
choiceArg,

View File

@ -31,7 +31,7 @@ import com.daml.lf.engine.script.ledgerinteraction.{
import com.daml.lf.iface.EnvironmentInterface
import com.daml.lf.iface.reader.InterfaceReader
import com.daml.lf.language.Ast._
import com.daml.lf.language.{PackageInterface, LanguageVersion}
import com.daml.lf.language.{LanguageVersion, PackageInterface}
import com.daml.lf.interpretation.{Error => IE}
import com.daml.lf.speedy.SBuiltin.SBToAny
import com.daml.lf.speedy.SExpr._
@ -64,6 +64,7 @@ import scalaz.syntax.traverse._
import scalaz.{Applicative, NonEmptyList, OneAnd, Traverse, \/-}
import spray.json._
import scala.annotation.nowarn
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
@ -420,7 +421,14 @@ private[lf] class Runner(
.flatMap {
case Right((vv, v)) =>
Converter
.toFuture(ScriptF.parse(ScriptF.Ctx(knownPackages, extendedCompiledPackages), vv, v))
.toFuture(
ScriptF.parse(
ScriptF.Ctx(knownPackages, extendedCompiledPackages),
vv,
v,
env.lookupChoiceByArgType: @nowarn("msg=deprecated"),
)
)
.flatMap { scriptF =>
scriptF match {
case ScriptF.Catch(act, handle) =>

View File

@ -5,14 +5,13 @@ package com.daml.lf
package engine.script
import java.time.Clock
import akka.stream.Materializer
import com.daml.grpc.adapter.ExecutionSequencerFactory
import com.daml.ledger.api.domain.{User, UserRight}
import com.daml.lf.data.FrontStack
import com.daml.lf.{CompiledPackages, command}
import com.daml.lf.engine.preprocessing.ValueTranslator
import com.daml.lf.data.Ref.{Identifier, Name, PackageId, Party, UserId}
import com.daml.lf.data.Ref.{Identifier, Name, PackageId, Party, QualifiedName, UserId}
import com.daml.lf.data.Time.Timestamp
import com.daml.lf.engine.script.ledgerinteraction.{ScriptLedgerClient, ScriptTimeMode}
import com.daml.lf.language.Ast
@ -74,8 +73,47 @@ object ScriptF {
_clients =
_clients.copy(party_participants = _clients.party_participants + (party -> participant))
}
def lookupChoice(id: Identifier, choice: Name): Either[String, Ast.TemplateChoiceSignature] =
compiledPackages.interface.lookupChoice(id, choice).map(_.choice).left.map(_.pretty)
def lookupChoice(
tmplId: Identifier,
ifaceId: Option[Identifier],
choice: Name,
): Either[String, Ast.TemplateChoiceSignature] =
compiledPackages.interface.lookupChoice(tmplId, ifaceId, choice).left.map(_.pretty)
private[this] val archiveId: Identifier = Identifier.assertFromString(
"d14e08374fc7197d6a0de468c968ae8ba3aadbf9315476fd39071831f5923662:DA.Internal.Template:Archive"
)
// TODO: https://github.com/digital-asset/daml/issues/13653
// infer interface ID of choice in DAML
// This is a workaround to infer the template/interface id where the choice is defined
// using the choice argument type. The inference should be done in daml.
@deprecated
def lookupChoiceByArgType(
tmplId: Identifier,
argTypeId: Identifier,
): Either[String, Identifier] =
if (argTypeId == archiveId)
Right(tmplId)
else {
val Identifier(argPkg, QualifiedName(argMod, _)) = argTypeId
val choiceName = argTypeId.qualifiedName.name.segments.head
val argTyp = Ast.TTyCon(argTypeId)
compiledPackages.interface
.lookupModule(argPkg, argMod)
.left
.map(_.pretty)
.flatMap(mod =>
(mod.templates.view.mapValues(_.choices) ++
mod.interfaces.view.mapValues(_.fixedChoices))
.collectFirst {
case (typeName, choices)
if choices.get(choiceName).exists(_.argBinder._2 == argTyp) =>
Identifier(argPkg, QualifiedName(argMod, typeName))
}
.toRight(s"cannot find choice with argument type $argTypeId")
)
}
def lookupKeyTy(id: Identifier): Either[String, Ast.Type] =
compiledPackages.interface.lookupTemplateKey(id) match {
@ -612,7 +650,11 @@ object ScriptF {
case Some(stackTrace) => Converter.toStackTrace(ctx.knownPackages, stackTrace)
}
private def parseSubmit(ctx: Ctx, v: SValue): Either[String, SubmitData] = {
private def parseSubmit(
ctx: Ctx,
v: SValue,
lookupChoiceByArgType: (Identifier, Identifier) => Either[String, Identifier],
): Either[String, SubmitData] = {
def convert(
actAs: OneAnd[List, SValue],
readAs: List[SValue],
@ -623,7 +665,7 @@ object ScriptF {
for {
actAs <- actAs.traverse(Converter.toParty(_)).map(toOneAndSet(_))
readAs <- readAs.traverse(Converter.toParty(_))
cmds <- Converter.toCommands(ctx.compiledPackages, freeAp)
cmds <- Converter.toCommands(ctx.compiledPackages, freeAp, lookupChoiceByArgType)
stackTrace <- toStackTrace(ctx, stackTrace)
} yield SubmitData(actAs, readAs.toSet, cmds, freeAp, stackTrace, continue)
v match {
@ -921,11 +963,16 @@ object ScriptF {
case _ => Left(s"Expected ListUserRights payload but got $v")
}
def parse(ctx: Ctx, constr: Ast.VariantConName, v: SValue): Either[String, ScriptF] =
def parse(
ctx: Ctx,
constr: Ast.VariantConName,
v: SValue,
lookupChoiceByArgType: (Identifier, Identifier) => Either[String, Identifier],
): Either[String, ScriptF] =
constr match {
case "Submit" => parseSubmit(ctx, v).map(Submit(_))
case "SubmitMustFail" => parseSubmit(ctx, v).map(SubmitMustFail(_))
case "SubmitTree" => parseSubmit(ctx, v).map(SubmitTree(_))
case "Submit" => parseSubmit(ctx, v, lookupChoiceByArgType).map(Submit(_))
case "SubmitMustFail" => parseSubmit(ctx, v, lookupChoiceByArgType).map(SubmitMustFail(_))
case "SubmitTree" => parseSubmit(ctx, v, lookupChoiceByArgType).map(SubmitTree(_))
case "Query" => parseQuery(ctx, v)
case "QueryContractId" => parseQueryContractId(ctx, v)
case "QueryContractKey" => parseQueryContractKey(ctx, v)

View File

@ -323,7 +323,9 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat
)
}
private def fromTreeEvent(ev: TreeEvent): Either[String, ScriptLedgerClient.CommandResult] =
private def fromTreeEvent(ev: TreeEvent): Either[String, ScriptLedgerClient.CommandResult] = {
import scalaz.std.option._
import scalaz.syntax.traverse._
ev match {
case TreeEvent(TreeEvent.Kind.Created(created)) =>
for {
@ -336,11 +338,13 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat
.left
.map(_.toString)
templateId <- Converter.fromApiIdentifier(exercised.getTemplateId)
ifaceId <- exercised.interfaceId.traverse(Converter.fromApiIdentifier)
choice <- ChoiceName.fromString(exercised.choice)
} yield ScriptLedgerClient.ExerciseResult(templateId, choice, result)
} yield ScriptLedgerClient.ExerciseResult(templateId, ifaceId, choice, result)
case TreeEvent(TreeEvent.Kind.Empty) =>
throw new ConverterException("Invalid tree event Empty")
}
}
override def createUser(
user: User,

View File

@ -210,6 +210,7 @@ class IdeLedgerClient(
case exercise: Node.Exercise =>
ScriptLedgerClient.ExerciseResult(
exercise.templateId,
exercise.interfaceId,
exercise.choiceId,
exercise.exerciseResult.get,
)
@ -263,6 +264,7 @@ class IdeLedgerClient(
Some(
ScriptLedgerClient.Exercised(
exercise.templateId,
exercise.interfaceId,
exercise.targetCoid,
exercise.choiceId,
exercise.chosenValue,

View File

@ -383,6 +383,7 @@ class JsonLedgerClient(
List(
ScriptLedgerClient.ExerciseResult(
tplId,
None,
choice,
result.convertTo[Value](
LfValueCodec.apiValueJsonReader(choiceDef.returnType, damlLfTypeLookup(_))
@ -414,6 +415,7 @@ class JsonLedgerClient(
List(
ScriptLedgerClient.ExerciseResult(
tplId,
None,
choice,
result.convertTo[Value](
LfValueCodec.apiValueJsonReader(choiceDef.returnType, damlLfTypeLookup(_))
@ -448,6 +450,7 @@ class JsonLedgerClient(
.CreateResult(ContractId.assertFromString(cid)): ScriptLedgerClient.CommandResult,
ScriptLedgerClient.ExerciseResult(
tplId,
None,
choice,
result.convertTo[Value](
LfValueCodec.apiValueJsonReader(choiceDef.returnType, damlLfTypeLookup(_))

View File

@ -32,6 +32,7 @@ object ScriptLedgerClient {
final case class CreateResult(contractId: ContractId) extends CommandResult
final case class ExerciseResult(
templateId: Identifier,
interfaceId: Option[Identifier],
choice: ChoiceName,
result: Value,
) extends CommandResult
@ -46,6 +47,7 @@ object ScriptLedgerClient {
sealed trait TreeEvent
final case class Exercised(
templateId: Identifier,
interfaceId: Option[Identifier],
contractId: ContractId,
choice: ChoiceName,
argument: Value,

View File

@ -314,7 +314,6 @@ abstract class AbstractFuncIT
}
}
}
"Exceptions:test" should {
"succeed" in {
for {
@ -329,7 +328,6 @@ abstract class AbstractFuncIT
}
}
}
"Interface:test" should {
"succeed" in {
for {
@ -344,7 +342,6 @@ abstract class AbstractFuncIT
}
}
}
"testMultiPartyQuery" should {
"should return contracts for all listed parties" in {
for {

View File

@ -227,6 +227,9 @@ object TransactionGenerator {
eventId <- nonEmptyId
contractId <- nonEmptyId
(scalaTemplateId, javaTemplateId) <- identifierGen
mbInterfaceId <- Gen.option(identifierGen)
scalaInterfaceId = mbInterfaceId.map(_._1)
javaInterfaceId = mbInterfaceId.map(_._2)
choice <- nonEmptyId
(scalaChoiceArgument, javaChoiceArgument) <- Gen.sized(valueGen)
actingParties <- Gen.listOf(nonEmptyId)
@ -240,6 +243,7 @@ object TransactionGenerator {
eventId,
contractId,
Some(scalaTemplateId),
scalaInterfaceId,
choice,
Some(scalaChoiceArgument),
actingParties,
@ -253,6 +257,7 @@ object TransactionGenerator {
witnessParties.asJava,
eventId,
javaTemplateId,
javaInterfaceId.orNull,
contractId,
choice,
javaChoiceArgument,

View File

@ -16,6 +16,8 @@ public class ExercisedEvent implements TreeEvent {
private final Identifier templateId;
private final Identifier interfaceId;
private final String contractId;
private final String choice;
@ -34,6 +36,7 @@ public class ExercisedEvent implements TreeEvent {
@NonNull List<@NonNull String> witnessParties,
@NonNull String eventId,
@NonNull Identifier templateId,
Identifier interfaceId,
@NonNull String contractId,
@NonNull String choice,
@NonNull Value choiceArgument,
@ -44,6 +47,7 @@ public class ExercisedEvent implements TreeEvent {
this.witnessParties = witnessParties;
this.eventId = eventId;
this.templateId = templateId;
this.interfaceId = interfaceId;
this.contractId = contractId;
this.choice = choice;
this.choiceArgument = choiceArgument;
@ -71,6 +75,14 @@ public class ExercisedEvent implements TreeEvent {
return templateId;
}
public boolean hasInterfaceId() {
return interfaceId == null;
}
public Identifier getInterfaceId() {
return interfaceId;
}
@NonNull
@Override
public String getContractId() {
@ -166,18 +178,19 @@ public class ExercisedEvent implements TreeEvent {
}
public EventOuterClass.@NonNull ExercisedEvent toProto() {
return EventOuterClass.ExercisedEvent.newBuilder()
.setEventId(getEventId())
.setChoice(getChoice())
.setChoiceArgument(getChoiceArgument().toProto())
.setConsuming(isConsuming())
.setContractId(getContractId())
.setTemplateId(getTemplateId().toProto())
.addAllActingParties(getActingParties())
.addAllWitnessParties(getWitnessParties())
.addAllChildEventIds(getChildEventIds())
.setExerciseResult(getExerciseResult().toProto())
.build();
EventOuterClass.ExercisedEvent.Builder builder = EventOuterClass.ExercisedEvent.newBuilder();
builder.setEventId(getEventId());
builder.setChoice(getChoice());
builder.setChoiceArgument(getChoiceArgument().toProto());
builder.setConsuming(isConsuming());
builder.setContractId(getContractId());
builder.setTemplateId(getTemplateId().toProto());
if (hasInterfaceId()) builder.setInterfaceId(getInterfaceId().toProto());
builder.addAllActingParties(getActingParties());
builder.addAllWitnessParties(getWitnessParties());
builder.addAllChildEventIds(getChildEventIds());
builder.setExerciseResult(getExerciseResult().toProto());
return builder.build();
}
public static ExercisedEvent fromProto(EventOuterClass.ExercisedEvent exercisedEvent) {
@ -185,6 +198,9 @@ public class ExercisedEvent implements TreeEvent {
exercisedEvent.getWitnessPartiesList(),
exercisedEvent.getEventId(),
Identifier.fromProto(exercisedEvent.getTemplateId()),
(exercisedEvent.hasInterfaceId()
? null
: Identifier.fromProto(exercisedEvent.getInterfaceId())),
exercisedEvent.getContractId(),
exercisedEvent.getChoice(),
Value.fromProto(exercisedEvent.getChoiceArgument()),

View File

@ -20,7 +20,8 @@ class Interfaces
behavior of "Generated Java code"
it should "contain all choices of an interface in templates implementing it" in withClient {
// TODO(#13668) Redesign the test once the issue is fixed
it should "contain all choices of an interface in templates implementing it" ignore withClient {
client =>
def checkTemplateId[T](
shouldBeId: Identifier,

View File

@ -129,6 +129,10 @@ message ExercisedEvent {
// Required
Identifier template_id = 3;
// The interface where the choice is defined if inherited
// Optional
Identifier interface_id = 13;
reserved 4; // removed field
// The choice that's been exercised on the target contract.

View File

@ -59,6 +59,7 @@ object MockMessages {
eventIdExercised,
contractId,
Some(templateId),
None,
choice,
None,
List(party),
@ -97,6 +98,7 @@ object MockMessages {
randomId("event-id"),
randomId("contract-id"),
Some(Identifier(randomId("package-id"), randomId("moduleName"), randomId("template-id"))),
None,
randomId("choice-id"),
None,
List(randomId("party")),

View File

@ -73,7 +73,8 @@ abstract class HttpServiceIntegrationTest
}: Future[Assertion]
}
"pick up new package's inherited interfaces" in withHttpService { (uri, encoder, _, _) =>
// TODO(#13668) Redesign the test once the issue is fixed
"pick up new package's inherited interfaces" ignore withHttpService { (uri, encoder, _, _) =>
import json.JsonProtocol._
def createIouAndExerciseTransfer(
initialTplId: domain.TemplateId.OptionalPkg,

View File

@ -151,7 +151,7 @@ final class CommandsValidator(ledgerId: LedgerId) {
value <- requirePresence(e.value.choiceArgument, "value")
validatedValue <- validateValue(value)
} yield ApiCommand.Exercise(
templateId = validatedTemplateId,
typeId = validatedTemplateId,
contractId = contractId,
choiceId = choice,
argument = validatedValue,

View File

@ -273,6 +273,7 @@ object LfEngineToApi {
eventId = eventId.toLedgerString,
contractId = node.targetCoid.coid,
templateId = Some(toApiIdentifier(node.templateId)),
interfaceId = node.interfaceId.map(toApiIdentifier),
choice = node.choiceId,
choiceArgument = Some(arg),
actingParties = node.actingParties.toSeq,

View File

@ -113,6 +113,7 @@ object domain {
eventId: EventId,
contractId: ContractId,
templateId: Ref.Identifier,
interfaceId: Option[Ref.Identifier],
choice: Ref.ChoiceName,
choiceArgument: Value,
actingParties: immutable.Set[Ref.Party],

View File

@ -0,0 +1,45 @@
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.store
import com.daml.lf.data.Ref
// TODO: https://github.com/digital-asset/daml/issues/12051
// Drop this workaround.
@deprecated
private object ChoiceCoder {
/*
Inherited choices are ambiguous: one needs the ID of the interface where
the choice is defined to disambiguate them.
Until interfaces are made stable, we do not want to change/migrate the
schema of the DBs, so we encode the choice identifier as follows:
- If the choice is defined in the template we let it such as.
This is backward compatible, simple, and cheap at runtime
- If the choice is inherited from an interface, we mangle the
tuple `(interfaceId, choiceName)` as follows:
"#" + interfaceId.toString + "#" + choiceName
Note that `#` is illegal within Identifiers and Names.
Once we have decided to make interfaces stable, we will plan on how to
handle more nicely interface IDs in the DBs.
*/
def encode(mbInterfaceId: Option[Ref.Identifier], choiceName: String): String =
mbInterfaceId match {
case None => choiceName
case Some(ifaceId) => "#" + ifaceId.toString + "#" + choiceName
}
def decode(s: String): (Option[Ref.Identifier], String) =
if (s.startsWith("#"))
s.split("#") match {
case Array(_, ifaceId, chName) =>
(Some(Ref.Identifier.assertFromString(ifaceId)), chName)
}
else
(None, s)
}

View File

@ -333,6 +333,7 @@ object EventStorageBackend {
eventId: EventId,
contractId: ContractId,
templateId: Option[Identifier],
interfaceId: Option[Identifier],
ledgerEffectiveTime: Option[Timestamp],
createSignatories: Option[Array[String]],
createObservers: Option[Array[String]],

View File

@ -15,9 +15,12 @@ import com.daml.lf.ledger.EventId
import com.daml.lf.transaction.Transaction.ChildrenRecursion
import com.daml.platform.{Create, Exercise, Key, Node, NodeId}
import com.daml.platform.index.index.StatusDetails
import com.daml.platform.store.ChoiceCoder
import com.daml.platform.store.dao.JdbcLedgerDao
import com.daml.platform.store.dao.events._
import scala.annotation.nowarn
object UpdateToDbDto {
def apply(
@ -209,7 +212,9 @@ object UpdateToDbDto {
blinding.disclosure.getOrElse(nodeId, Set.empty).map(_.toString),
create_key_value = createKeyValue
.map(compressionStrategy.createKeyValueCompression.compress),
exercise_choice = Some(exercise.choiceId),
exercise_choice = Some(
ChoiceCoder.encode(exercise.interfaceId, exercise.choiceId)
): @nowarn("msg=deprecated"),
exercise_argument = Some(exerciseArgument)
.map(compressionStrategy.exerciseArgumentCompression.compress),
exercise_result = exerciseResult

View File

@ -10,6 +10,7 @@ import com.daml.ledger.offset.Offset
import com.daml.lf.data.Ref
import com.daml.lf.data.Time.Timestamp
import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.store.ChoiceCoder
import com.daml.platform.store.backend.Conversions.{
contractId,
eventId,
@ -25,6 +26,7 @@ import com.daml.platform.store.backend.common.ComposableQuery.{CompositeSql, Sql
import com.daml.platform.store.cache.LedgerEndCache
import com.daml.platform.store.interning.StringInterning
import scala.annotation.nowarn
import scala.collection.immutable.ArraySeq
abstract class EventStorageBackendTemplate(
@ -277,6 +279,8 @@ abstract class EventStorageBackendTemplate(
): RowParser[EventStorageBackend.Entry[Raw.TreeEvent.Exercised]] =
exercisedEventRow map {
case eventOffset ~ transactionId ~ nodeIndex ~ eventSequentialId ~ eventId ~ contractId ~ ledgerEffectiveTime ~ templateId ~ commandId ~ workflowId ~ eventWitnesses ~ submitters ~ exerciseConsuming ~ exerciseChoice ~ exerciseArgument ~ exerciseArgumentCompression ~ exerciseResult ~ exerciseResultCompression ~ exerciseActors ~ exerciseChildEventIds =>
val (interfaceId, choiceName) =
ChoiceCoder.decode(exerciseChoice): @nowarn("msg=deprecated")
// ArraySeq.unsafeWrapArray is safe here
// since we get the Array from parsing and don't let it escape anywhere.
EventStorageBackend.Entry(
@ -295,8 +299,9 @@ abstract class EventStorageBackendTemplate(
eventId = eventId,
contractId = contractId,
templateId = stringInterning.templateId.externalize(templateId),
interfaceId = interfaceId,
exerciseConsuming = exerciseConsuming,
exerciseChoice = exerciseChoice,
exerciseChoice = choiceName,
exerciseArgument = exerciseArgument,
exerciseArgumentCompression = exerciseArgumentCompression,
exerciseResult = exerciseResult,
@ -779,6 +784,8 @@ abstract class EventStorageBackendTemplate(
createArgument ~ createArgumentCompression ~ treeEventWitnesses ~ flatEventWitnesses ~ submitters ~ exerciseChoice ~
exerciseArgument ~ exerciseArgumentCompression ~ exerciseResult ~ exerciseResultCompression ~ exerciseActors ~
exerciseChildEventIds ~ eventSequentialId ~ offset =>
val decodedExerciseChoice =
exerciseChoice.map(ChoiceCoder.decode): @nowarn("msg=deprecated")
RawTransactionEvent(
eventKind,
transactionId,
@ -788,6 +795,7 @@ abstract class EventStorageBackendTemplate(
eventId,
contractId,
templateId.map(stringInterning.templateId.externalize),
decodedExerciseChoice.flatMap(_._1),
ledgerEffectiveTime,
createSignatories.map(_.map(stringInterning.party.unsafe.externalize)),
createObservers.map(_.map(stringInterning.party.unsafe.externalize)),
@ -801,7 +809,7 @@ abstract class EventStorageBackendTemplate(
submitters
.map(_.view.map(stringInterning.party.unsafe.externalize).toSet)
.getOrElse(Set.empty),
exerciseChoice,
decodedExerciseChoice.map(_._2),
exerciseArgument,
exerciseArgumentCompression,
exerciseResult,

View File

@ -343,7 +343,9 @@ final class LfValueTranslation(
)
.assertExercise()
lazy val templateId: LfIdentifier = apiIdentifierToDamlLfIdentifier(raw.partial.templateId.get)
lazy val temlateId: LfIdentifier = apiIdentifierToDamlLfIdentifier(raw.partial.templateId.get)
lazy val interfaceId: Option[LfIdentifier] =
raw.partial.interfaceId.map(apiIdentifierToDamlLfIdentifier)
lazy val choiceName: LfChoiceName = LfChoiceName.assertFromString(raw.partial.choice)
// Convert Daml-LF values to ledger API values.
@ -353,7 +355,8 @@ final class LfValueTranslation(
value = exercise.argument,
verbose = verbose,
attribute = "exercise argument",
enrich = value => enricher.enrichChoiceArgument(templateId, choiceName, value.unversioned),
enrich = value =>
enricher.enrichChoiceArgument(temlateId, interfaceId, choiceName, value.unversioned),
)
exerciseResult <- exercise.result match {
case Some(result) =>
@ -361,7 +364,8 @@ final class LfValueTranslation(
value = result,
verbose = verbose,
attribute = "exercise result",
enrich = value => enricher.enrichChoiceResult(templateId, choiceName, value.unversioned),
enrich = value =>
enricher.enrichChoiceResult(temlateId, interfaceId, choiceName, value.unversioned),
).map(Some(_))
case None => Future.successful(None)
}

View File

@ -256,6 +256,7 @@ object Raw {
eventId: String,
contractId: String,
templateId: Identifier,
interfaceId: Option[Identifier],
exerciseConsuming: Boolean,
exerciseChoice: String,
exerciseArgument: Array[Byte],
@ -271,6 +272,7 @@ object Raw {
eventId = eventId,
contractId = contractId,
templateId = Some(LfEngineToApi.toApiIdentifier(templateId)),
interfaceId = interfaceId.map(LfEngineToApi.toApiIdentifier),
choice = exerciseChoice,
choiceArgument = null,
actingParties = exerciseActors,

View File

@ -280,6 +280,7 @@ private[events] object TransactionLogUpdatesConversions {
lfValueTranslation.enricher
.enrichChoiceArgument(
exercisedEvent.templateId,
exercisedEvent.interfaceId,
Ref.Name.assertFromString(exercisedEvent.choice),
value.unversioned,
)
@ -296,6 +297,7 @@ private[events] object TransactionLogUpdatesConversions {
val choiceResultEnricher = (value: Value) =>
lfValueTranslation.enricher.enrichChoiceResult(
exercisedEvent.templateId,
exercisedEvent.interfaceId,
Ref.Name.assertFromString(exercisedEvent.choice),
value.unversioned,
)

View File

@ -4,18 +4,24 @@
package com.daml.platform.store.dao.events
import java.io.ByteArrayInputStream
import com.daml.lf.data.Ref
import com.daml.platform.store.ChoiceCoder
import com.daml.platform.store.backend.EventStorageBackend.RawTransactionEvent
import com.daml.platform.store.interfaces.TransactionLogUpdate
import com.daml.platform.store.serialization.{Compression, ValueSerializer}
import scala.annotation.nowarn
object TransactionLogUpdatesReader {
def toTransactionEvent(
raw: RawTransactionEvent
): TransactionLogUpdate.Event =
raw.eventKind match {
case EventKind.NonConsumingExercise | EventKind.ConsumingExercise =>
val (interfaceId, choiceName) =
ChoiceCoder.decode(raw.exerciseChoice.mandatory("exercise_choice")): @nowarn(
"msg=deprecated"
)
TransactionLogUpdate.ExercisedEvent(
eventOffset = raw.offset,
transactionId = raw.transactionId,
@ -26,6 +32,7 @@ object TransactionLogUpdatesReader {
ledgerEffectiveTime = raw.ledgerEffectiveTime
.mandatory("ledgerEffectiveTime"),
templateId = raw.templateId.mandatory("template_id"),
interfaceId = interfaceId,
commandId = raw.commandId.getOrElse(""),
workflowId = raw.workflowId.getOrElse(""),
contractKey = raw.createKeyValue.map(
@ -37,7 +44,7 @@ object TransactionLogUpdatesReader {
treeEventWitnesses = raw.treeEventWitnesses,
flatEventWitnesses = raw.flatEventWitnesses,
submitters = raw.submitters,
choice = raw.exerciseChoice.mandatory("exercise_choice"),
choice = choiceName,
actingParties = raw.exerciseActors
.mandatory("exercise_actors")
.iterator

View File

@ -95,6 +95,7 @@ object TransactionLogUpdate {
contractId: ContractId,
ledgerEffectiveTime: Timestamp,
templateId: Identifier,
interfaceId: Option[Identifier],
commandId: String,
workflowId: String,
contractKey: Option[LfValue.VersionedValue],

View File

@ -221,6 +221,7 @@ final class BuffersUpdaterSpec
contractId = exercisedCid,
contractKey = Some(exercisedKey),
templateId = exercisedTemplateId,
interfaceId = None,
flatEventWitnesses = exercisedFlatEventWitnesses,
eventOffset = exercisedOffset,
eventSequentialId = exercisedEventSequentialId,

View File

@ -106,6 +106,7 @@ final class TransactionConversionSpec extends AnyWordSpec with Matchers {
eventId = s"#transactionId:$evId",
contractId = contractId,
templateId = Some(v.Identifier(defaultPackageId, "M", "T")),
interfaceId = None,
choice = "C",
choiceArgument = Some(v.Value(v.Value.Sum.Record(v.Record(None, Vector())))),
actingParties = Vector(party),

View File

@ -212,23 +212,23 @@
- ensure expression forms have the correct type: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L107)
- error on specifying both authCommonUri and authInternalUri/authExternalUri for the trigger service: [AuthorizationConfigTest.scala](triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala#L24)
- error on specifying only authInternalUri and no authExternalUri for the trigger service: [AuthorizationConfigTest.scala](triggers/service/src/test-suite/scala/com/daml/lf/engine/trigger/AuthorizationConfigTest.scala#L52)
- exercise-by-interface command is rejected for a: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L178)
- exercise-by-interface command is rejected for a: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L169)
- give a 'not found' response for a stop request on an unknown UUID in the trigger service: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L515)
- give a 'not found' response for a stop request with an unparseable UUID in the trigger service: [TriggerServiceTest.scala](triggers/service/src/test/scala/com/digitalasset/daml/lf/engine/trigger/TriggerServiceTest.scala#L500)
- ill-formed create API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L166)
- ill-formed create replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L108)
- ill-formed create-and-exercise API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L191)
- ill-formed create API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L157)
- ill-formed create replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L109)
- ill-formed create-and-exercise API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L182)
- ill-formed exception definitions are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L1614)
- ill-formed exercise API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L171)
- ill-formed exercise replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L113)
- ill-formed exercise-by-key API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L182)
- ill-formed exercise-by-key replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L120)
- ill-formed exercise API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L162)
- ill-formed exercise replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L114)
- ill-formed exercise-by-key API command is rejected: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L173)
- ill-formed exercise-by-key replay command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L121)
- ill-formed expressions are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L441)
- ill-formed fetch command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L167)
- ill-formed fetch-by-key command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L170)
- ill-formed fetch command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L168)
- ill-formed fetch-by-key command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L171)
- ill-formed interfaces are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L1353)
- ill-formed kinds are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L19)
- ill-formed lookup command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L175)
- ill-formed lookup command is rejected: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L176)
- ill-formed records are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L1756)
- ill-formed templates are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L981)
- ill-formed type synonyms applications are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L1735)
@ -237,16 +237,15 @@
- ill-formed variants are rejected: [TypingSpec.scala](daml-lf/validation/src/test/scala/com/digitalasset/daml/lf/validation/TypingSpec.scala#L1779)
- well formed create API command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L113)
- well formed create replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L80)
- well formed create-and-exercise API command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L147)
- well formed create-and-exercise API command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L139)
- well formed exercise API command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L118)
- well formed exercise replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L85)
- well formed exercise-by-interface command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L132)
- well formed exercise-by-interface via required interface command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L140)
- well formed exercise-by-key API command is accepted: [ApiCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ApiCommandPreprocessorSpec.scala#L125)
- well formed exercise-by-key command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L92)
- well formed fetch replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L144)
- well formed fetch-by-key replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L149)
- well formed lookup replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L154)
- well formed exercise-by-key command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L93)
- well formed fetch replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L145)
- well formed fetch-by-key replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L150)
- well formed lookup replay command is accepted: [ReplayCommandPreprocessorSpec.scala](daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/ReplayCommandPreprocessorSpec.scala#L155)
## Authentication:
- connect normally with tls on: [TlsTest.scala](ledger-service/http-json/src/it/scala/http/TlsTest.scala#L30)