From 42adee12bead80fb98e09a5b88031449d9db0391 Mon Sep 17 00:00:00 2001 From: Remy Haemmerle Date: Wed, 23 Jun 2021 12:32:12 +0200 Subject: [PATCH] LF: Move Speedy Interpretation Error to transaction package to be accessible from any package part of #9974 CHANGELOG_BEGIN CHANGELOG_END --- .../daml/lf/scenario/Conversions.scala | 148 +++++++++--------- .../digitalasset/daml/lf/engine/Engine.scala | 15 +- .../digitalasset/daml/lf/engine/Error.scala | 41 +++-- .../digitalasset/daml/lf/engine/Result.scala | 12 +- .../daml/lf/engine/EngineTest.scala | 57 +++---- .../digitalasset/daml/lf/speedy/Pretty.scala | 66 ++++---- .../daml/lf/speedy/SBuiltin.scala | 44 +++--- .../digitalasset/daml/lf/speedy/SError.scala | 103 ++---------- .../digitalasset/daml/lf/speedy/SExpr.scala | 7 +- .../digitalasset/daml/lf/speedy/SValue.scala | 13 +- .../digitalasset/daml/lf/speedy/Speedy.scala | 8 +- .../lf/transaction/PartialTransaction.scala | 5 +- .../daml/lf/speedy/ExceptionTest.scala | 72 ++++----- .../daml/lf/speedy/SBuiltinTest.scala | 10 +- .../daml/lf/speedy/ScenarioRunner.scala | 5 +- .../src/main/scala/com/daml/lf/Error.scala | 98 ++++++++++++ .../digitalasset/daml/lf/value/Value.scala | 23 +++ .../daml/lf/engine/script/Runner.scala | 18 ++- .../daml/lf/engine/script/ScriptF.scala | 10 +- .../lf/engine/script/test/JsonApiIt.scala | 4 +- .../services/ApiSubmissionService.scala | 8 +- .../scala/platform/store/ErrorCause.scala | 2 +- .../services/ApiSubmissionServiceSpec.scala | 32 +++- 23 files changed, 435 insertions(+), 366 deletions(-) create mode 100644 daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala diff --git a/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala b/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala index e2d4479ef0ae..5aaea7cdbf71 100644 --- a/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala +++ b/compiler/scenario-service/server/src/main/scala/com/digitalasset/daml/lf/scenario/Conversions.scala @@ -1,7 +1,8 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.scenario +package com.daml.lf +package scenario import com.daml.lf.data.{ImmArray, Numeric, Ref} import com.daml.lf.ledger.EventId @@ -88,58 +89,78 @@ final class Conversions( case SError.SRequiresOnLedger(operation) => setCrash(operation) - case SError.DamlEMatchError(reason) => setCrash(reason) - - case SError.DamlEUnhandledException(excep) => - builder.setUnhandledException(convertValue(excep.value.toValue)) - - case SError.DamlEUserError(msg) => builder.setUserError(msg) - - case SError.DamlETransactionError(reason) => - setCrash(reason) - - case SError.DamlETemplatePreconditionViolated(tid, optLoc, arg) => - val uepvBuilder = proto.ScenarioError.TemplatePreconditionViolated.newBuilder - optLoc.map(convertLocation).foreach(uepvBuilder.setLocation) - builder.setTemplatePrecondViolated( - uepvBuilder - .setTemplateId(convertIdentifier(tid)) - .setArg(convertValue(arg)) - .build - ) - case SError.DamlELocalContractNotActive(coid, tid, consumedBy) => - builder.setUpdateLocalContractNotActive( - proto.ScenarioError.ContractNotActive.newBuilder - .setContractRef(mkContractRef(coid, tid)) - .setConsumedBy(proto.NodeId.newBuilder.setId(consumedBy.toString).build) - .build - ) - case SError.DamlEDuplicateContractKey(key) => - builder.setScenarioCommitError( - proto.CommitError.newBuilder.setUniqueKeyViolation(convertGlobalKey(key)).build - ) - case SError.DamlEFailedAuthorization(nid, fa) => - builder.setScenarioCommitError( - proto.CommitError.newBuilder - .setFailedAuthorizations(convertFailedAuthorization(nid, fa)) - .build - ) - - case SError.DamlECreateEmptyContractKeyMaintainers(tid, arg, key) => - builder.setCreateEmptyContractKeyMaintainers( - proto.ScenarioError.CreateEmptyContractKeyMaintainers.newBuilder - .setArg(convertValue(arg)) - .setTemplateId(convertIdentifier(tid)) - .setKey(convertValue(key)) - ) - - case SError.DamlEFetchEmptyContractKeyMaintainers(tid, key) => - builder.setFetchEmptyContractKeyMaintainers( - proto.ScenarioError.FetchEmptyContractKeyMaintainers.newBuilder - .setTemplateId(convertIdentifier(tid)) - .setKey(convertValue(key)) - ) + case SError.SErrorDamlException(interpretationError) => + import interpretation.Error._ + interpretationError match { + case UnhandledException(_, value) => + builder.setUnhandledException(convertValue(value)) + case UserError(msg) => + builder.setUserError(msg) + case ContractNotFound(cid) => + // TODO https://github.com/digital-asset/daml/issues/9974 + // we should probably use ${cid.coid} instead of $cid + setCrash(s"contract $cid not found") + case TemplatePreconditionViolated(tid, optLoc, arg) => + val uepvBuilder = proto.ScenarioError.TemplatePreconditionViolated.newBuilder + optLoc.map(convertLocation).foreach(uepvBuilder.setLocation) + builder.setTemplatePrecondViolated( + uepvBuilder + .setTemplateId(convertIdentifier(tid)) + .setArg(convertValue(arg)) + .build + ) + case LocalContractNotActive(coid, tid, consumedBy) => + builder.setUpdateLocalContractNotActive( + proto.ScenarioError.ContractNotActive.newBuilder + .setContractRef(mkContractRef(coid, tid)) + .setConsumedBy(proto.NodeId.newBuilder.setId(consumedBy.toString).build) + .build + ) + case LocalContractKeyNotVisible(coid, gk, actAs, readAs, stakeholders) => + builder.setScenarioContractKeyNotVisible( + proto.ScenarioError.ContractKeyNotVisible.newBuilder + .setContractRef(mkContractRef(coid, gk.templateId)) + .addAllActAs(actAs.map(convertParty(_)).asJava) + .addAllReadAs(readAs.map(convertParty(_)).asJava) + .addAllStakeholders(stakeholders.map(convertParty).asJava) + .build + ) + case ContractKeyNotFound(gk) => + builder.setScenarioContractKeyNotFound( + proto.ScenarioError.ContractKeyNotFound.newBuilder + .setTemplateId(convertIdentifier(gk.templateId)) + .setKey(convertValue(gk.key)) + .build + ) + case DuplicateContractKey(key) => + builder.setScenarioCommitError( + proto.CommitError.newBuilder.setUniqueKeyViolation(convertGlobalKey(key)).build + ) + case CreateEmptyContractKeyMaintainers(tid, arg, key) => + builder.setCreateEmptyContractKeyMaintainers( + proto.ScenarioError.CreateEmptyContractKeyMaintainers.newBuilder + .setArg(convertValue(arg)) + .setTemplateId(convertIdentifier(tid)) + .setKey(convertValue(key)) + ) + case FetchEmptyContractKeyMaintainers(tid, key) => + builder.setFetchEmptyContractKeyMaintainers( + proto.ScenarioError.FetchEmptyContractKeyMaintainers.newBuilder + .setTemplateId(convertIdentifier(tid)) + .setKey(convertValue(key)) + ) + case wtc: WronglyTypedContract => + sys.error( + s"Got unexpected DamlEWronglyTypedContract error in scenario service: $wtc. Note that in the scenario service this error should never surface since contract fetches are all type checked." + ) + case FailedAuthorization(nid, fa) => + builder.setScenarioCommitError( + proto.CommitError.newBuilder + .setFailedAuthorizations(convertFailedAuthorization(nid, fa)) + .build + ) + } case SError.ScenarioErrorContractNotEffective(coid, tid, effectiveAt) => builder.setScenarioContractNotEffective( proto.ScenarioError.ContractNotEffective.newBuilder @@ -166,16 +187,6 @@ final class Conversions( .build ) - case SError.DamlELocalContractKeyNotVisible(coid, gk, actAs, readAs, stakeholders) => - builder.setScenarioContractKeyNotVisible( - proto.ScenarioError.ContractKeyNotVisible.newBuilder - .setContractRef(mkContractRef(coid, gk.templateId)) - .addAllActAs(actAs.map(convertParty(_)).asJava) - .addAllReadAs(readAs.map(convertParty(_)).asJava) - .addAllStakeholders(stakeholders.map(convertParty).asJava) - .build - ) - case SError.ScenarioErrorContractKeyNotVisible(coid, gk, actAs, readAs, stakeholders) => builder.setScenarioContractKeyNotVisible( proto.ScenarioError.ContractKeyNotVisible.newBuilder @@ -187,14 +198,6 @@ final class Conversions( .build ) - case SError.DamlEContractKeyNotFound(gk) => - builder.setScenarioContractKeyNotFound( - proto.ScenarioError.ContractKeyNotFound.newBuilder - .setTemplateId(convertIdentifier(gk.templateId)) - .setKey(convertValue(gk.key)) - .build - ) - case SError.ScenarioErrorCommitError(commitError) => builder.setScenarioCommitError( convertCommitError(commitError) @@ -207,11 +210,6 @@ final class Conversions( case SError.ScenarioErrorPartyAlreadyExists(party) => builder.setScenarioPartyAlreadyExists(party) - - case wtc: SError.DamlEWronglyTypedContract => - sys.error( - s"Got unexpected DamlEWronglyTypedContract error in scenario service: $wtc. Note that in the scenario service this error should never surface since contract fetches are all type checked." - ) } builder.build } diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala index 82cfc6f38091..667d59dda7a6 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Engine.scala @@ -321,24 +321,23 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV time: Time.Timestamp, ): Result[(SubmittedTransaction, Tx.Metadata)] = machine.withOnLedger("Daml Engine") { onLedger => var finished: Boolean = false + def detailMsg = + s"Last location: ${Pretty.prettyLoc(machine.lastLocation).render(80)}, partial transaction: ${onLedger.ptxInternal.nodesToString}" while (!finished) { machine.run() match { case SResultFinalValue(_) => finished = true - case SResultError(SError.DamlEDuplicateContractKey(key)) => + case SResultError(SError.SErrorDamlException(error)) => // Special-cased because duplicate key errors // produce a different gRPC error code. - return ResultError(Error.Interpretation(Error.Interpretation.DuplicateContractKey(key))) - - case SResultError(SError.DamlEContractKeyNotFound(key)) => - return ResultError(Error.Interpretation.ContractKeyNotFound(key)) + return ResultError(Error.Interpretation.DamlException(error), detailMsg) case SResultError(err) => return ResultError( Error.Interpretation.Generic( - s"Interpretation error: ${Pretty.prettyError(err, onLedger.ptxInternal).render(80)}", - s"Last location: ${Pretty.prettyLoc(machine.lastLocation).render(80)}, partial transaction: ${onLedger.ptxInternal.nodesToString}", - ) + s"Interpretation error: ${Pretty.prettyError(err, onLedger.ptxInternal).render(80)}" + ), + detailMsg, ) case SResultNeedPackage(pkgId, callback) => diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Error.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Error.scala index 3aca33c513a3..3c7a597a3025 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Error.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Error.scala @@ -6,7 +6,7 @@ package engine import com.daml.lf.data.Ref import com.daml.lf.language.Ast -import com.daml.lf.transaction.{GlobalKey, NodeId} +import com.daml.lf.transaction.NodeId import com.daml.lf.value.Value sealed abstract class Error { @@ -125,7 +125,8 @@ object Error { } // Error happening during interpretation - final case class Interpretation(interpretationError: Interpretation.Error) extends Error { + final case class Interpretation(interpretationError: Interpretation.Error, detailMsg: String) + extends Error { def msg: String = interpretationError.msg } @@ -137,28 +138,24 @@ object Error { // TODO https://github.com/digital-asset/daml/issues/9974 // get rid of Generic - final case class Generic(override val msg: String, detailMsg: String) extends Error - object Generic { - def apply(msg: String): Generic = Generic(msg, msg) + final case class Generic(override val msg: String) extends Error + + final case class DamlException(error: interpretation.Error) extends Error { + // TODO https://github.com/digital-asset/daml/issues/9974 + // For now we try to preserve the exact same message (for the ledger API) + // Review once all the errors are properly structured + override def msg: String = error match { + case interpretation.Error.ContractNotFound(cid) => + // TODO https://github.com/digital-asset/daml/issues/9974 + // we should probably use ${cid.coid} instead of $cid + s"Contract could not be found with id $cid" + case interpretation.Error.ContractKeyNotFound(key) => + s"dependency error: couldn't find key: $key" + case _ => + s"Interpretation error: Error: ${speedy.Pretty.prettyDamlException(error).render(80)}" + } } - final case class ContractNotFound(ci: Value.ContractId) extends Error { - override def msg = s"Contract could not be found with id $ci" - } - - final case class ContractKeyNotFound(key: GlobalKey) extends Error { - override def msg = s"dependency error: couldn't find key: $key" - } - - /** See com.daml.lf.transaction.Transaction.DuplicateContractKey - * for more information. - */ - final case class DuplicateContractKey(key: GlobalKey) extends Error { - override def msg = s"Duplicate contract key $key" - } - - final case class Authorization(override val msg: String) extends Error - } // Error happening during transaction validation diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala index 91efd05e0ea8..dba1d693c55d 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/Result.scala @@ -1,7 +1,8 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.engine +package com.daml.lf +package engine import com.daml.lf.data.Ref._ import com.daml.lf.data.{BackStack, ImmArray, ImmArrayCons} @@ -75,8 +76,8 @@ object ResultError { ResultError(Error.Package(packageError)) def apply(preprocessingError: Error.Preprocessing.Error): ResultError = ResultError(Error.Preprocessing(preprocessingError)) - def apply(interpretationError: Error.Interpretation.Error): ResultError = - ResultError(Error.Interpretation(interpretationError)) + def apply(interpretationError: Error.Interpretation.Error, details: String = "N/A"): ResultError = + ResultError(Error.Interpretation(interpretationError, details)) def apply(validationError: Error.Validation.Error): ResultError = ResultError(Error.Validation(validationError)) } @@ -165,7 +166,10 @@ object Result { ResultNeedContract( acoid, { - case None => ResultError(Error.Interpretation.ContractNotFound(acoid)) + case None => + ResultError( + Error.Interpretation.DamlException(interpretation.Error.ContractNotFound(acoid)) + ) case Some(contract) => resume(contract) }, ) diff --git a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala index f23662724413..c2b022efa78a 100644 --- a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala +++ b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala @@ -31,6 +31,7 @@ import Value._ import com.daml.lf.speedy.{InitialSeeding, SValue, svalue} import com.daml.lf.speedy.SValue._ import com.daml.lf.command._ +import com.daml.lf.engine.Error.Interpretation import com.daml.lf.transaction.Node.{GenActionNode, GenNode} import com.daml.lf.transaction.test.TransactionBuilder.assertAsVersionedValue import org.scalactic.Equality @@ -754,9 +755,9 @@ class EngineTest val submitResult = engine .submit(submitters, Commands(ImmArray(command), let, "test"), participant, submissionSeed) .consume(lookupContract, lookupPackage, lookupKey, VisibleByKey.fromSubmitters(submitters)) - inside(submitResult) { case Left(err) => - err shouldBe Error.Interpretation( - Error.Interpretation.ContractKeyNotFound( + inside(submitResult) { case Left(Error.Interpretation(err, _)) => + err shouldBe Interpretation.DamlException( + interpretation.Error.ContractKeyNotFound( GlobalKey.assertBuild( BasicTests_WithKey, ValueRecord( @@ -968,21 +969,22 @@ class EngineTest ) .consume(_ => None, lookupPackage, lookupKey, VisibleByKey.fromSubmitters(submitters)) - inside(result) { case Left(err) => - err shouldBe Error.Interpretation( - Error.Interpretation.ContractKeyNotFound( - GlobalKey.assertBuild( - BasicTests_WithKey, - ValueRecord( - Some(BasicTests_WithKey), - ImmArray( - (Some[Ref.Name]("p"), ValueParty(alice)), - (Some[Ref.Name]("k"), ValueInt64(43)), + inside(result) { case Left(Error.Interpretation(err, _)) => + err shouldBe + Interpretation.DamlException( + interpretation.Error.ContractKeyNotFound( + GlobalKey.assertBuild( + BasicTests_WithKey, + ValueRecord( + Some(BasicTests_WithKey), + ImmArray( + (Some[Ref.Name]("p"), ValueParty(alice)), + (Some[Ref.Name]("k"), ValueInt64(43)), + ), ), - ), + ) ) ) - ) } } "error if Speedy fails to find the key" in { @@ -1013,21 +1015,22 @@ class EngineTest ) .consume(_ => None, lookupPackage, lookupKey, VisibleByKey.fromSubmitters(submitters)) - inside(result) { case Left(err) => - err shouldBe Error.Interpretation( - Error.Interpretation.ContractKeyNotFound( - GlobalKey.assertBuild( - BasicTests_WithKey, - ValueRecord( - Some(BasicTests_WithKey), - ImmArray( - (Some[Ref.Name]("p"), ValueParty(alice)), - (Some[Ref.Name]("k"), ValueInt64(43)), + inside(result) { case Left(Error.Interpretation(err, _)) => + err shouldBe + Interpretation.DamlException( + interpretation.Error.ContractKeyNotFound( + GlobalKey.assertBuild( + BasicTests_WithKey, + ValueRecord( + Some(BasicTests_WithKey), + ImmArray( + (Some[Ref.Name]("p"), ValueParty(alice)), + (Some[Ref.Name]("k"), ValueInt64(43)), + ), ), - ), + ) ) ) - ) } } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala index 057a62396158..f7c84d6c3e0b 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Pretty.scala @@ -31,10 +31,17 @@ import com.daml.lf.speedy.SBuiltin._ private[lf] object Pretty { - def prettyError(err: SError, ptx: PartialTransaction): Doc = + private[this] val dummyTx = PartialTransaction.initial( + (_ => TransactionVersion.minVersion), + ContractKeyUniquenessMode.On, + submissionTime = Time.Timestamp.MinValue, + initialSeeds = InitialSeeding.NoSeed, + ) + + def prettyError(err: SError, ptx: PartialTransaction = dummyTx): Doc = text("Error:") & (err match { case ex: SErrorDamlException => - prettyDamlException(ex, ptx) + prettyDamlException(ex.error, ptx) case SErrorCrash(reason) => text(s"CRASH: $reason") case SRequiresOnLedger(operation) => @@ -44,37 +51,23 @@ private[lf] object Pretty { prettyScenarioError(serr) }) - def prettyError(err: SError): Doc = { - val ptx = PartialTransaction.initial( - (_ => TransactionVersion.minVersion), - ContractKeyUniquenessMode.On, - submissionTime = Time.Timestamp.MinValue, - initialSeeds = InitialSeeding.NoSeed, - ) - prettyError(err, ptx) - } - def prettyParty(p: Party): Doc = char('\'') + text(p) + char('\'') - private def prettyDamlException(ex: SErrorDamlException, ptx: PartialTransaction): Doc = - ex match { - case DamlEFailedAuthorization(nid, fa) => + def prettyDamlException(error: interpretation.Error, ptx: PartialTransaction = dummyTx): Doc = { + import interpretation.Error._ + error match { + case FailedAuthorization(nid, fa) => text(prettyFailedAuthorization(nid, fa)) - case DamlEUnhandledException(SAny(_, value)) => - text(s"Unhandled exception:") & prettyValue(true)(value.toValue) - case DamlEUserError(message) => + case UnhandledException(_, value) => + text(s"Unhandled exception:") & prettyValue(true)(value) + case UserError(message) => text(s"User abort: $message") - case DamlETransactionError(reason) => - text(s"Transaction error: $reason") - case DamlEMatchError(reason) => - text(reason) - case DamlETemplatePreconditionViolated(tid, optLoc @ _, arg) => + case TemplatePreconditionViolated(templateId, loc @ _, arg) => text("Update failed due to precondition violation when creating") & - prettyTypeConName(tid) & + prettyTypeConName(templateId) & text("with") & prettyValue(true)(arg) - - case DamlELocalContractNotActive(coid, tid, consumedBy) => + case LocalContractNotActive(coid, tid, consumedBy) => text("Update failed due to fetch of an inactive contract") & prettyContractId(coid) & char('(') + (prettyTypeConName(tid)) + text(").") / text(s"The contract had been consumed in sub-transaction #$consumedBy:") + @@ -87,14 +80,12 @@ private[lf] object Pretty { case Some(node) => (line + prettyPartialTransactionNode(node)).nested(4) }) - - case DamlEContractKeyNotFound(gk) => + case ContractKeyNotFound(gk) => text( "Update failed due to fetch-by-key or exercise-by-key which did not find a contract with key" ) & prettyValue(false)(gk.key) & char('(') + prettyIdentifier(gk.templateId) + char(')') - - case DamlELocalContractKeyNotVisible(coid, gk, actAs, readAs, stakeholders) => + case LocalContractKeyNotVisible(coid, gk, actAs, readAs, stakeholders) => text( "Update failed due to a fetch, lookup or exercise by key of contract not visible to the reading parties" ) & prettyContractId(coid) & @@ -109,31 +100,30 @@ private[lf] object Pretty { comma + space, stakeholders.map(prettyParty), ) + char('.') - - case DamlEDuplicateContractKey(key) => + case DuplicateContractKey(key) => text("Update failed due to a duplicate contract key") & prettyValue(false)(key.key) - - case DamlEWronglyTypedContract(coid, expected, actual) => + case WronglyTypedContract(coid, expected, actual) => text("Update failed due to wrongly typed contract id") & prettyContractId(coid) / text("Expected contract of type") & prettyTypeConName(expected) & text( "but got" ) & prettyTypeConName( actual ) - - case DamlECreateEmptyContractKeyMaintainers(tid, arg, key) => + case CreateEmptyContractKeyMaintainers(tid, arg, key) => text("Update failed due to a contract key with an empty sey of maintainers when creating") & prettyTypeConName(tid) & text("with") & prettyValue(true)(arg) / text("The computed key is") & prettyValue(true)(key) - - case DamlEFetchEmptyContractKeyMaintainers(tid, key) => + case FetchEmptyContractKeyMaintainers(tid, key) => text( "Update failed due to a contract key with an empty sey of maintainers when fetching or looking up by key" ) & prettyTypeConName(tid) / text("The provided key is") & prettyValue(true)(key) + case ContractNotFound(cid) => + text("Update failed du to a unknown contract") & prettyContractId(cid) } + } // A minimal pretty-print of an update transaction node, without recursing into child nodes.. def prettyPartialTransactionNode(node: PartialTransaction.Node): Doc = diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala index bc795842fefe..53f775fce703 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltin.scala @@ -10,6 +10,7 @@ import java.util.regex.Pattern import com.daml.lf.data.Ref._ import com.daml.lf.data._ import com.daml.lf.data.Numeric.Scale +import com.daml.lf.interpretation.{Error => IE} import com.daml.lf.language.{Ast, Util => AstUtil} import com.daml.lf.speedy.SError._ import com.daml.lf.speedy.SExpr._ @@ -20,6 +21,7 @@ import com.daml.lf.speedy.SValue.{SValue => SV} import com.daml.lf.transaction.{Transaction => Tx} import com.daml.lf.value.{Value => V} import com.daml.lf.transaction.{GlobalKey, GlobalKeyWithMaintainers, Node} +import com.daml.lf.value.Value.ValueArithmeticError import com.daml.scalautil.Statement.discard import scala.jdk.CollectionConverters._ @@ -37,7 +39,7 @@ import scala.collection.immutable.TreeSet private[speedy] sealed abstract class SBuiltin(val arity: Int) { // Helper for constructing expressions applying this builtin. // E.g. SBCons(SEVar(1), SEVar(2)) - private[speedy] def apply(args: SExpr*): SExpr = + private[lf] def apply(args: SExpr*): SExpr = SEApp(SEBuiltin(this), args.toArray) /** Execute the builtin with 'arity' number of arguments in 'args'. @@ -878,10 +880,12 @@ private[lf] object SBuiltin { final case class SBCheckPrecond(templateId: TypeConName) extends SBuiltinPure(2) { override private[speedy] def executePure(args: util.ArrayList[SValue]): SUnit.type = { if (!getSBool(args, 1)) - throw DamlETemplatePreconditionViolated( - templateId = templateId, - optLocation = None, - arg = args.get(0).toValue, + throw SErrorDamlException( + IE.TemplatePreconditionViolated( + templateId = templateId, + optLocation = None, + arg = args.get(0).toValue, + ) ) SUnit } @@ -909,7 +913,9 @@ private[lf] object SBuiltin { val mbKey = extractOptionalKeyWithMaintainers(args.get(4)) mbKey.foreach { case Node.KeyWithMaintainers(key, maintainers) => if (maintainers.isEmpty) - throw DamlECreateEmptyContractKeyMaintainers(templateId, createArg.toValue, key) + throw SErrorDamlException( + IE.CreateEmptyContractKeyMaintainers(templateId, createArg.toValue, key) + ) } val auth = machine.auth val (coid, newPtx) = onLedger.ptx @@ -923,7 +929,7 @@ private[lf] object SBuiltin { stakeholders = sigs union obs, key = mbKey, ) - .fold(err => throw DamlETransactionError(err), identity) + .fold(err => crash(err), identity) machine.addLocalContract(coid, templateId, createArg, sigs, obs, mbKey) onLedger.ptx = newPtx @@ -983,7 +989,7 @@ private[lf] object SBuiltin { byKey = byKey, chosenValue = arg, ) - .fold(err => throw DamlETransactionError(err), identity) + .fold(err => crash(err), identity) checkAborted(onLedger.ptx) machine.returnValue = SUnit } @@ -1012,7 +1018,7 @@ private[lf] object SBuiltin { } else { // This is a user-error. machine.ctrl = SEDamlException( - DamlEWronglyTypedContract(coid, templateId, cached.templateId) + IE.WronglyTypedContract(coid, templateId, cached.templateId) ) } } else { @@ -1027,7 +1033,7 @@ private[lf] object SBuiltin { { case V.ContractInst(actualTmplId, V.VersionedValue(_, arg), _) => if (actualTmplId != templateId) { machine.ctrl = - SEDamlException(DamlEWronglyTypedContract(coid, templateId, actualTmplId)) + SEDamlException(IE.WronglyTypedContract(coid, templateId, actualTmplId)) } else { val keyExpr = args.get(1) match { // No by-key operation, we have to recompute. @@ -1163,7 +1169,7 @@ private[lf] object SBuiltin { override def handleInputKeyFound(machine: Machine, cid: V.ContractId): Unit = machine.ctrl = importCid(cid) override def handleKeyNotFound(machine: Machine, gkey: GlobalKey): Boolean = { - machine.ctrl = SEDamlException(DamlEContractKeyNotFound(gkey)) + machine.ctrl = SEDamlException(IE.ContractKeyNotFound(gkey)) false } @@ -1206,7 +1212,9 @@ private[lf] object SBuiltin { import PartialTransaction.{KeyActive, KeyInactive} val keyWithMaintainers = extractKeyWithMaintainers(args.get(0)) if (keyWithMaintainers.maintainers.isEmpty) - throw DamlEFetchEmptyContractKeyMaintainers(operation.templateId, keyWithMaintainers.key) + throw SErrorDamlException( + IE.FetchEmptyContractKeyMaintainers(operation.templateId, keyWithMaintainers.key) + ) val gkey = GlobalKey(operation.templateId, keyWithMaintainers.key) // check if we find it locally onLedger.ptx.keys.get(gkey) match { @@ -1224,7 +1232,7 @@ private[lf] object SBuiltin { operation.handleActiveKey(machine, coid) case SVisibleByKey.NotVisible(actAs, readAs) => machine.ctrl = SEDamlException( - DamlELocalContractKeyNotVisible(coid, gkey, actAs, readAs, stakeholders) + IE.LocalContractKeyNotVisible(coid, gkey, actAs, readAs, stakeholders) ) }, ) @@ -1359,7 +1367,7 @@ private[lf] object SBuiltin { /** $error :: Text -> a */ final case object SBError extends SBuiltinPure(1) { override private[speedy] def executePure(args: util.ArrayList[SValue]): Nothing = - throw DamlEUserError(getSText(args, 0)) + throw SErrorDamlException(IE.UserError(getSText(args, 0))) } /** $throw :: AnyException -> a */ @@ -1397,7 +1405,7 @@ private[lf] object SBuiltin { override private[speedy] def execute(args: util.ArrayList[SValue], machine: Machine): Unit = { val exception = getSAnyException(args, 0) val tyCon = exception.id - if (tyCon == SArithmeticError.tyCon) + if (tyCon == ValueArithmeticError.tyCon) machine.returnValue = exception.values.get(0) else if (!machine.compiledPackages.packageIds.contains(exception.id.packageId)) throw SpeedyHungry( @@ -1581,11 +1589,11 @@ private[lf] object SBuiltin { private[speedy] def checkAborted(ptx: PartialTransaction): Unit = ptx.aborted match { case Some(Tx.AuthFailureDuringExecution(nid, fa)) => - throw DamlEFailedAuthorization(nid, fa) + throw SErrorDamlException(IE.FailedAuthorization(nid, fa)) case Some(Tx.ContractNotActive(coid, tid, consumedBy)) => - throw DamlELocalContractNotActive(coid, tid, consumedBy) + throw SErrorDamlException(IE.LocalContractNotActive(coid, tid, consumedBy)) case Some(Tx.DuplicateContractKey(key)) => - throw DamlEDuplicateContractKey(key) + throw SErrorDamlException(IE.DuplicateContractKey(key)) case None => () } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SError.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SError.scala index 7337aba06a72..dd4337f476d1 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SError.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SError.scala @@ -1,14 +1,13 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.speedy +package com.daml.lf +package speedy import com.daml.lf.data.Ref._ import com.daml.lf.data.Time import com.daml.lf.ledger.EventId -import com.daml.lf.ledger.FailedAuthorization -import com.daml.lf.transaction.{GlobalKey, NodeId, Transaction => Tx} -import com.daml.lf.value.Value +import com.daml.lf.transaction.{GlobalKey, Transaction => Tx} import com.daml.lf.value.Value.ContractId import com.daml.lf.scenario.ScenarioLedger @@ -35,82 +34,15 @@ object SError { def crash[A](reason: String): A = throw SErrorCrash(reason) - /** Daml exceptions that can be caught. These include - * arithmetic errors, call to error builtin or update - * errors. - */ - sealed abstract class SErrorDamlException extends SError - - /** Unhandled exceptions */ - final case class DamlEUnhandledException(exception: SValue.SAny) extends SErrorDamlException { - override def toString: String = s"Unhandled exception: $exception" - } - - /** User initiated error, via e.g. 'abort' or 'assert' */ - final case class DamlEUserError(message: String) extends SErrorDamlException - - /** An inexhaustive pattern match */ - final case class DamlEMatchError(reason: String) extends SErrorDamlException - - /** Template pre-condition (ensure) evaluated to false and the transaction - * was aborted. - */ - final case class DamlETemplatePreconditionViolated( - templateId: TypeConName, - optLocation: Option[Location], - arg: Value[ContractId], - ) extends SErrorDamlException - - /** A fetch or an exercise on a transaction-local contract that has already - * been consumed. - */ - final case class DamlELocalContractNotActive( - coid: ContractId, - templateId: TypeConName, - consumedBy: NodeId, - ) extends SErrorDamlException - - final case class DamlELocalContractKeyNotVisible( - coid: ContractId, - key: GlobalKey, - actAs: Set[Party], - readAs: Set[Party], - stakeholders: Set[Party], - ) extends SErrorDamlException + /** Daml exceptions that should be reported to the user. */ + final case class SErrorDamlException(error: interpretation.Error) extends SError - /** Fetch-by-key failed + /** Errors from scenario in + * /** Unhandled exceptions */ + * final case class DamlEUnhandledException(exception: SValue.SAny) extends SErrorDamlException { + * override def toString: String = s"Unhandled exception: $exception" + * }terpretation. */ - final case class DamlEContractKeyNotFound( - key: GlobalKey - ) extends SErrorDamlException - - /** Two contracts with the same key were active at the same time. - * See com.daml.lf.transaction.Transaction.DuplicateContractKey - * for more details. - */ - final case class DamlEDuplicateContractKey( - key: GlobalKey - ) extends SErrorDamlException - - /** Error during an operation on the update transaction. */ - final case class DamlETransactionError( - reason: String - ) extends SErrorDamlException - - /** A create a contract key without maintainers */ - final case class DamlECreateEmptyContractKeyMaintainers( - templateId: TypeConName, - arg: Value[ContractId], - key: Value[Nothing], - ) extends SErrorDamlException - - /** A fetch or lookup a contract key without maintainers */ - final case class DamlEFetchEmptyContractKeyMaintainers( - templateId: TypeConName, - key: Value[Nothing], - ) extends SErrorDamlException - - /** Errors from scenario interpretation. */ sealed trait SErrorScenario extends SError final case class ScenarioErrorContractNotEffective( @@ -125,21 +57,6 @@ object SError { consumedBy: EventId, ) extends SErrorScenario - /** We tried to fetch / exercise a contract of the wrong type -- - * see . - */ - final case class DamlEWronglyTypedContract( - coid: ContractId, - expected: TypeConName, - actual: TypeConName, - ) extends SErrorDamlException - - /** There was an authorization failure during execution. */ - final case class DamlEFailedAuthorization( - nid: NodeId, - fa: FailedAuthorization, - ) extends SErrorDamlException - /** A fetch or exercise was being made against a contract that has not * been disclosed to 'committer'. */ diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SExpr.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SExpr.scala index c91842414290..b4ae3c1d6444 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SExpr.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SExpr.scala @@ -1,7 +1,8 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.speedy +package com.daml.lf +package speedy /** The simplified AST for the speedy interpreter. * @@ -377,9 +378,9 @@ object SExpr { /** We cannot crash in the engine call back. * Rather, we set the control to this expression and then crash when executing. */ - final case class SEDamlException(error: SErrorDamlException) extends SExpr { + final case class SEDamlException(error: interpretation.Error) extends SExpr { def execute(machine: Machine): Unit = { - throw error + throw SErrorDamlException(error) } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala index 457403d24246..dbc478895891 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SValue.scala @@ -10,6 +10,7 @@ import com.daml.lf.data._ import com.daml.lf.data.Ref._ import com.daml.lf.language.Ast._ import com.daml.lf.speedy.SError.SErrorCrash +import com.daml.lf.value.Value.ValueArithmeticError import com.daml.lf.value.{Value => V} import scala.jdk.CollectionConverters._ @@ -214,24 +215,18 @@ object SValue { } object SArithmeticError { - // The package ID should match the ID of the stable package daml-prim-DA-Exception-ArithmeticError - // See test compiler/damlc/tests/src/stable-packages.sh - val tyCon: Ref.TypeConName = Ref.Identifier.assertFromString( - "cb0552debf219cc909f51cbb5c3b41e9981d39f8f645b1f35e2ef5be2e0b858a:DA.Exception.ArithmeticError:ArithmeticError" - ) - val typ: Type = TTyCon(tyCon) - val fields: ImmArray[Ref.Name] = ImmArray(Ref.Name.assertFromString("message")) + val fields: ImmArray[Ref.Name] = ImmArray(ValueArithmeticError.fieldName) def apply(builtinName: String, args: ImmArray[String]): SAny = { val array = new util.ArrayList[SValue](1) array.add( SText(s"ArithmeticError while evaluating ($builtinName ${args.iterator.mkString(" ")}).") ) - SAny(typ, SRecord(tyCon, fields, array)) + SAny(ValueArithmeticError.typ, SRecord(ValueArithmeticError.tyCon, fields, array)) } // Assumes excep is properly typed def unapply(excep: SAny): Option[SValue] = excep match { - case SAnyException(SRecord(`tyCon`, _, args)) => Some(args.get(0)) + case SAnyException(SRecord(ValueArithmeticError.tyCon, _, args)) => Some(args.get(0)) case _ => None } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala index 9d4570e6e545..2a01f3472aaf 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Speedy.scala @@ -18,8 +18,8 @@ import com.daml.lf.speedy.SResult._ import com.daml.lf.speedy.SBuiltin.checkAborted import com.daml.lf.transaction.{ ContractKeyUniquenessMode, - Node, IncompleteTransaction, + Node, TransactionVersion, } import com.daml.lf.value.{Value => V} @@ -1066,7 +1066,7 @@ private[lf] object Speedy { } machine.ctrl = altOpt - .getOrElse(throw DamlEMatchError(s"No match for $v in ${alts.toList}")) + .getOrElse(crash(s"No match for $v in ${alts.toList}")) .body } @@ -1329,7 +1329,9 @@ private[lf] object Speedy { machine.kontStack.clear() machine.env.clear() machine.envBase = 0 - throw DamlEUnhandledException(excep) + throw SErrorDamlException( + interpretation.Error.UnhandledException(excep.ty, excep.value.toValue) + ) } } diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala index c2f1f986cd6f..5054ae527989 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/transaction/PartialTransaction.scala @@ -7,7 +7,6 @@ package speedy import com.daml.lf.data.Ref.{ChoiceName, Location, Party, TypeConName} import com.daml.lf.data.{BackStack, ImmArray, Ref, Time} import com.daml.lf.ledger.{Authorize, FailedAuthorization} -import com.daml.lf.speedy.SError.DamlEUnhandledException import com.daml.lf.transaction.{ ContractKeyUniquenessMode, GenTransaction, @@ -696,7 +695,9 @@ private[lf] case class PartialTransaction( def rollbackTry(ex: SValue.SAny): PartialTransaction = { // we must never create a rollback containing a node with a version pre-dating exceptions if (context.minChildVersion < TxVersion.minExceptions) { - throw DamlEUnhandledException(ex) + throw SError.SErrorDamlException( + interpretation.Error.UnhandledException(ex.ty, ex.value.toValue) + ) } context.info match { case info: TryContextInfo => diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala index d4bed71083a9..0a9246f87927 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala @@ -4,19 +4,19 @@ package com.daml.lf package speedy -import java.util - import com.daml.lf.data.Ref import com.daml.lf.data.Ref.{PackageId, Party} +import com.daml.lf.interpretation.{Error => IE} import com.daml.lf.language.Ast._ import com.daml.lf.language.{LanguageVersion, Interface} import com.daml.lf.speedy.Compiler.FullStackTrace import com.daml.lf.speedy.SResult.{SResultError, SResultFinalValue} -import com.daml.lf.speedy.SError.DamlEUnhandledException +import com.daml.lf.speedy.SError.SErrorDamlException import com.daml.lf.speedy.SValue.SUnit import com.daml.lf.testing.parser.Implicits._ import com.daml.lf.testing.parser.ParserParameters import com.daml.lf.validation.Validation +import com.daml.lf.value.Value.{ValueRecord, ValueText} import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -53,39 +53,34 @@ class ExceptionTest extends AnyWordSpec with Matchers with TableDrivenPropertyCh } """) - val List(e1, e2) = + val List((t1, e1), (t2, e2)) = List("M:E1", "M:E2") .map(id => data.Ref.Identifier.assertFromString(s"${defaultParserParameters.defaultPackageId}:$id") ) - .map(tyCon => - SValue.SAny( - TTyCon(tyCon), - SValue.SRecord(tyCon, data.ImmArray.empty, new util.ArrayList()), - ) - ) + .map(tyCon => TTyCon(tyCon) -> ValueRecord(Some(tyCon), data.ImmArray.empty)) val arithmeticCon = data.Ref.Identifier.assertFromString( "cb0552debf219cc909f51cbb5c3b41e9981d39f8f645b1f35e2ef5be2e0b858a:DA.Exception.ArithmeticError:ArithmeticError" ) - val fields = new util.ArrayList[SValue]() - fields.add(SValue.SText("ArithmeticError while evaluating (DIV_INT64 1 0).")) val divZeroE = - SValue.SAny( - TTyCon(arithmeticCon), - SValue.SRecord( - arithmeticCon, - data.ImmArray(data.Ref.Name.assertFromString("message")), - fields, + ValueRecord( + Some(arithmeticCon), + data.ImmArray( + Some(data.Ref.Name.assertFromString("message")) -> + ValueText("ArithmeticError while evaluating (DIV_INT64 1 0).") ), ) val testCases = Table[String, SResult]( ("expression", "expected"), - ("M:unhandled1", SResultError(DamlEUnhandledException(e1))), - ("M:unhandled2", SResultError(DamlEUnhandledException(e1))), - ("M:unhandled3", SResultError(DamlEUnhandledException(e1))), - ("M:unhandled4", SResultError(DamlEUnhandledException(e2))), - ("M:divZero", SResultError(DamlEUnhandledException(divZeroE))), + ("M:unhandled1", SResultError(SErrorDamlException(IE.UnhandledException(t1, e1)))), + ("M:unhandled2", SResultError(SErrorDamlException(IE.UnhandledException(t1, e1)))), + ("M:unhandled3", SResultError(SErrorDamlException(IE.UnhandledException(t1, e1)))), + ("M:unhandled4", SResultError(SErrorDamlException(IE.UnhandledException(t2, e2)))), + ( + "M:divZero", + SResultError(SErrorDamlException(IE.UnhandledException(TTyCon(arithmeticCon), divZeroE))), + ), ) forEvery(testCases) { (exp: String, expected: SResult) => @@ -471,16 +466,6 @@ class ExceptionTest extends AnyWordSpec with Matchers with TableDrivenPropertyCh val example: Expr = EApp(e"M:causeRollback", EPrimLit(PLParty(party))) def transactionSeed: crypto.Hash = crypto.Hash.hashPrivateKey("transactionSeed") - val anException = { - val id = "M:AnException" - val tyCon = - data.Ref.Identifier.assertFromString(s"${defaultParserParameters.defaultPackageId}:$id") - SValue.SAny( - TTyCon(tyCon), - SValue.SRecord(tyCon, data.ImmArray.empty, new util.ArrayList()), - ) - } - "works as expected for a contract version POST-dating exceptions" in { val pkgs = mkPackagesAtVersion(LanguageVersion.v1_dev) val res = Speedy.Machine.fromUpdateExpr(pkgs, transactionSeed, example, party).run() @@ -488,9 +473,16 @@ class ExceptionTest extends AnyWordSpec with Matchers with TableDrivenPropertyCh } "causes an uncatchable exception to be thrown for a contract version PRE-dating exceptions" in { + + val id = "M:AnException" + val tyCon = + data.Ref.Identifier.assertFromString(s"${defaultParserParameters.defaultPackageId}:$id") + val anException = + IE.UnhandledException(TTyCon(tyCon), ValueRecord(Some(tyCon), data.ImmArray.empty)) + val pkgs = mkPackagesAtVersion(LanguageVersion.v1_11) val res = Speedy.Machine.fromUpdateExpr(pkgs, transactionSeed, example, party).run() - res shouldBe SResultError(DamlEUnhandledException(anException)) + res shouldBe SResultError(SErrorDamlException(anException)) } def mkPackagesAtVersion(languageVersion: LanguageVersion): PureCompiledPackages = { @@ -692,12 +684,8 @@ class ExceptionTest extends AnyWordSpec with Matchers with TableDrivenPropertyCh val anException = { val id = "NewM:AnException" - val tyCon = - data.Ref.Identifier.assertFromString(s"$newPid:$id") - SValue.SAny( - TTyCon(tyCon), - SValue.SRecord(tyCon, data.ImmArray.empty, new util.ArrayList()), - ) + val tyCon = data.Ref.Identifier.assertFromString(s"$newPid:$id") + IE.UnhandledException(TTyCon(tyCon), ValueRecord(Some(tyCon), data.ImmArray.empty)) } def transactionSeed: crypto.Hash = crypto.Hash.hashPrivateKey("transactionSeed") @@ -713,12 +701,12 @@ class ExceptionTest extends AnyWordSpec with Matchers with TableDrivenPropertyCh "causes uncatchable exception when an old contract is within a new-exercise within a try-catch" in { val res = Speedy.Machine.fromUpdateExpr(pkgs, transactionSeed, causeUncatchable, party).run() - res shouldBe SResultError(DamlEUnhandledException(anException)) + res shouldBe SResultError(SErrorDamlException(anException)) } "causes uncatchable exception when an old contract is within a new-exercise which aborts" in { val res = Speedy.Machine.fromUpdateExpr(pkgs, transactionSeed, causeUncatchable2, party).run() - res shouldBe SResultError(DamlEUnhandledException(anException)) + res shouldBe SResultError(SErrorDamlException(anException)) } } diff --git a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala index 118cc36071e8..df796f311e75 100644 --- a/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala +++ b/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/SBuiltinTest.scala @@ -7,13 +7,15 @@ package speedy import java.util import com.daml.lf.data._ +import com.daml.lf.interpretation.{Error => IE} import com.daml.lf.language.Ast._ -import com.daml.lf.speedy.SError.{DamlEUnhandledException, SError, SErrorCrash} +import com.daml.lf.speedy.SError.{SError, SErrorCrash} import com.daml.lf.speedy.SExpr._ import com.daml.lf.speedy.SResult.{SResultError, SResultFinalValue, SResultNeedPackage} import com.daml.lf.speedy.SValue.{SValue => _, _} import com.daml.lf.testing.parser.Implicits._ import com.daml.lf.value.Value +import com.daml.lf.value.Value.ValueArithmeticError import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.matchers.should.Matchers import org.scalatest.freespec.AnyFreeSpec @@ -1485,7 +1487,11 @@ class SBuiltinTest extends AnyFreeSpec with Matchers with TableDrivenPropertyChe inside( evalSExpr(SEAppAtomicSaturatedBuiltin(builtin, args.map(SEValue(_)).toArray), false) ) { - case Left(DamlEUnhandledException(SArithmeticError(SText(msg)))) + case Left( + SError.SErrorDamlException( + IE.UnhandledException(ValueArithmeticError.typ, ValueArithmeticError(msg)) + ) + ) if msg == s"ArithmeticError while evaluating ($name ${args.iterator.map(lit2string).mkString(" ")})." => } } diff --git a/daml-lf/scenario-interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/ScenarioRunner.scala b/daml-lf/scenario-interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/ScenarioRunner.scala index 4022b85d9411..8cdddf678217 100644 --- a/daml-lf/scenario-interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/ScenarioRunner.scala +++ b/daml-lf/scenario-interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/ScenarioRunner.scala @@ -1,7 +1,8 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.speedy +package com.daml.lf +package speedy import com.daml.lf.CompiledPackages import com.daml.lf.crypto @@ -317,7 +318,7 @@ object ScenarioRunner { ledger.ledgerData.activeKeys.get(gk) match { case None => - missingWith(DamlEContractKeyNotFound(gk)) + missingWith(SErrorDamlException(interpretation.Error.ContractKeyNotFound(gk))) case Some(acoid) => ledger.lookupGlobalContract( view = ScenarioLedger.ParticipantView(actAs, readAs), diff --git a/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala b/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala new file mode 100644 index 000000000000..108ed8e10b70 --- /dev/null +++ b/daml-lf/transaction/src/main/scala/com/daml/lf/Error.scala @@ -0,0 +1,98 @@ +// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf +package interpretation + +import com.daml.lf.data.Ref.{Location, Party, TypeConName} +import com.daml.lf.transaction.{GlobalKey, NodeId} +import com.daml.lf.language.Ast +import com.daml.lf.value.Value +import com.daml.lf.value.Value.ContractId + +/** Daml exceptions that should be reported to the user + */ +sealed abstract class Error extends Serializable with Product { + override def toString: String = s"$productPrefix(${productIterator.mkString("\n")}" +} + +object Error { + + /** Unhandled exceptions */ + final case class UnhandledException(exceptionType: Ast.Type, value: Value[ContractId]) + extends Error + + /** User initiated error, via e.g. 'abort' or 'assert' */ + final case class UserError(message: String) extends Error + + final case class ContractNotFound(cid: Value.ContractId) extends Error + + /** Template pre-condition (ensure) evaluated to false and the transaction + * was aborted. + */ + final case class TemplatePreconditionViolated( + templateId: TypeConName, + optLocation: Option[Location], + arg: Value[ContractId], + ) extends Error + + /** A fetch or an exercise on a transaction-local contract that has already + * been consumed. + */ + final case class LocalContractNotActive( + coid: ContractId, + templateId: TypeConName, + consumedBy: NodeId, + ) extends Error + + final case class LocalContractKeyNotVisible( + coid: ContractId, + key: GlobalKey, + actAs: Set[Party], + readAs: Set[Party], + stakeholders: Set[Party], + ) extends Error + + /** Fetch-by-key failed + */ + final case class ContractKeyNotFound( + key: GlobalKey + ) extends Error + + /** Two contracts with the same key were active at the same time. + * See com.daml.lf.transaction.Transaction.DuplicateContractKey + * for more details. + */ + final case class DuplicateContractKey( + key: GlobalKey + ) extends Error + + /** A create a contract key without maintainers */ + final case class CreateEmptyContractKeyMaintainers( + templateId: TypeConName, + arg: Value[ContractId], + key: Value[Nothing], + ) extends Error + + /** A fetch or lookup a contract key without maintainers */ + final case class FetchEmptyContractKeyMaintainers( + templateId: TypeConName, + key: Value[Nothing], + ) extends Error + + /** We tried to fetch / exercise a contract of the wrong type -- + * see . + */ + final case class WronglyTypedContract( + coid: ContractId, + expected: TypeConName, + actual: TypeConName, + ) extends Error + + /** There was an authorization failure during execution. */ + final case class FailedAuthorization( + nid: NodeId, + fa: ledger.FailedAuthorization, + ) extends Error + +} diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala index 11a669f32cd6..d94e59c12055 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/value/Value.scala @@ -7,6 +7,7 @@ package value import com.daml.lf.crypto.Hash import com.daml.lf.data.Ref.{Identifier, Name} import com.daml.lf.data._ +import com.daml.lf.language.Ast import com.daml.lf.transaction.TransactionVersion import data.ScalazEqual._ @@ -249,6 +250,28 @@ object Value extends CidContainer1[Value] { tycon: Option[Identifier], fields: ImmArray[(Option[Name], Value[Cid])], ) extends Value[Cid] + + object ValueArithmeticError { + // The package ID should match the ID of the stable package daml-prim-DA-Exception-ArithmeticError + // See test compiler/damlc/tests/src/stable-packages.sh + val tyCon: Ref.TypeConName = Ref.Identifier.assertFromString( + "cb0552debf219cc909f51cbb5c3b41e9981d39f8f645b1f35e2ef5be2e0b858a:DA.Exception.ArithmeticError:ArithmeticError" + ) + val typ: Ast.Type = Ast.TTyCon(tyCon) + private val someTyCon = Some(tyCon) + val fieldName: Ref.Name = Ref.Name.assertFromString("message") + private val someFieldName = Some(fieldName) + def apply(message: String): ValueRecord[Nothing] = + ValueRecord(someTyCon, ImmArray(someFieldName -> ValueText(message))) + def unapply(excep: ValueRecord[_]): Option[String] = + excep match { + case ValueRecord(id, ImmArray((field, ValueText(message)))) + if id.forall(_ == tyCon) && field.forall(_ == fieldName) => + Some(message) + case _ => None + } + } + final case class ValueVariant[+Cid](tycon: Option[Identifier], variant: Name, value: Value[Cid]) extends Value[Cid] final case class ValueEnum(tycon: Option[Identifier], value: Name) extends ValueCidlessLeaf diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Runner.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Runner.scala index e3f203227b6c..67a81cadc7c4 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Runner.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Runner.scala @@ -31,6 +31,8 @@ import com.daml.lf.iface.EnvironmentInterface import com.daml.lf.iface.reader.InterfaceReader import com.daml.lf.language.Ast._ import com.daml.lf.language.{Interface, LanguageVersion} +import com.daml.lf.interpretation.{Error => IE} +import com.daml.lf.speedy.SBuiltin.SBToAny import com.daml.lf.speedy.SExpr._ import com.daml.lf.speedy.SResult._ import com.daml.lf.speedy.SValue._ @@ -394,13 +396,17 @@ private[lf] class Runner( case ScriptF.Catch(act, handle) => run(SEApp(SEValue(act), Array(SEValue(SUnit)))).transformWith { case Success(v) => Future.successful(SEValue(v)) - case Failure(SError.DamlEUnhandledException(exc)) => - machine.setExpressionToEvaluate(SEApp(SEValue(handle), Array(SEValue(exc)))) + case Failure( + exce @ SError.SErrorDamlException(IE.UnhandledException(typ, value)) + ) => + machine.setExpressionToEvaluate( + SEApp(SEValue(handle), Array(SBToAny(typ)(SEImportValue(typ, value)))) + ) stepToValue() .fold(Future.failed, Future.successful) .flatMap { case SOptional(None) => - Future.failed(SError.DamlEUnhandledException(exc)) + Future.failed(exce) case SOptional(Some(free)) => Future.successful(SEValue(free)) case e => Future.failed( @@ -409,8 +415,10 @@ private[lf] class Runner( } case Failure(e) => Future.failed(e) } - case ScriptF.Throw(exc) => - Future.failed(SError.DamlEUnhandledException(exc)) + case ScriptF.Throw(SAny(ty, value)) => + Future.failed( + SError.SErrorDamlException(IE.UnhandledException(ty, value.toValue)) + ) case cmd: ScriptF.Cmd => cmd.execute(env).transform { case Failure(exception) => diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala index 7cc18a9d2c0c..02e7b9edc054 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala @@ -1,7 +1,8 @@ // Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -package com.daml.lf.engine.script +package com.daml.lf +package engine.script import java.time.Clock @@ -14,9 +15,8 @@ import com.daml.lf.data.Ref.{Identifier, Name, PackageId, Party} import com.daml.lf.data.Time.Timestamp import com.daml.lf.engine.script.ledgerinteraction.{ScriptLedgerClient, ScriptTimeMode} import com.daml.lf.language.Ast -import com.daml.lf.speedy.SError.DamlEUserError import com.daml.lf.speedy.SExpr.{SEApp, SEValue} -import com.daml.lf.speedy.{SExpr, SValue} +import com.daml.lf.speedy.{SError, SExpr, SValue} import com.daml.lf.speedy.SValue._ import com.daml.lf.speedy.Speedy.Machine import com.daml.lf.value.Value @@ -161,7 +161,9 @@ object ScriptF { Future.successful(SEApp(SEValue(data.continue), Array(SEValue(SUnit)))) case Left(()) => Future.failed( - new DamlEUserError("Expected submit to fail but it succeeded") + SError.SErrorDamlException( + interpretation.Error.UserError("Expected submit to fail but it succeeded") + ) ) } } yield v diff --git a/daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/JsonApiIt.scala b/daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/JsonApiIt.scala index 759a73fa1aab..02e6cbd90914 100644 --- a/daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/JsonApiIt.scala +++ b/daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/JsonApiIt.scala @@ -374,10 +374,10 @@ final class JsonApiIt run(clients, QualifiedName.assertFromString("ScriptTest:jsonFailingCreateAndExercise")) ) } yield { - exception.cause.getMessage should include("Error: User abort: Assertion failed.") + exception.cause.getMessage should include("User abort: Assertion failed.") } } - "submitMustFail succeeds on assertion falure" in { + "submitMustFail succeeds on assertion failure" in { for { clients <- getClients() result <- run( diff --git a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala index 5e77e79a6164..14ad930772e9 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/apiserver/services/ApiSubmissionService.scala @@ -27,6 +27,7 @@ import com.daml.ledger.participant.state.v1.{ import com.daml.lf.crypto import com.daml.lf.data.Ref.Party import com.daml.lf.engine.{Error => LfError} +import com.daml.lf.interpretation.{Error => InterpretationError} import com.daml.lf.transaction.SubmittedTransaction import com.daml.logging.LoggingContext.withEnrichedLoggingContext import com.daml.logging.{ContextualizedLogger, LoggingContext} @@ -289,8 +290,11 @@ private[apiserver] final class ApiSubmissionService private[services] ( // TODO https://github.com/digital-asset/daml/issues/9974 // Review once LF errors are properly structured case LfError.Interpretation( - LfError.Interpretation.ContractNotFound(_) | - LfError.Interpretation.DuplicateContractKey(_) + LfError.Interpretation.DamlException( + InterpretationError.ContractNotFound(_) | + InterpretationError.DuplicateContractKey(_) + ), + _, ) | LfError.Validation(LfError.Validation.ReplayMismatch(_)) => Status.ABORTED.withDescription(cause.explain) case _ => Status.INVALID_ARGUMENT.withDescription(cause.explain) diff --git a/ledger/participant-integration-api/src/main/scala/platform/store/ErrorCause.scala b/ledger/participant-integration-api/src/main/scala/platform/store/ErrorCause.scala index bb9f3b41fa08..cdf52d55abc1 100644 --- a/ledger/participant-integration-api/src/main/scala/platform/store/ErrorCause.scala +++ b/ledger/participant-integration-api/src/main/scala/platform/store/ErrorCause.scala @@ -18,7 +18,7 @@ private[platform] object ErrorCause { override def explain: String = { val details = { error match { - case LfError.Interpretation(LfError.Interpretation.Generic(_, detailMsg)) => + case LfError.Interpretation(_, detailMsg) => detailMsg case _ => "N/A" diff --git a/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala b/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala index 8482067deb29..93912b2395eb 100644 --- a/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala +++ b/ledger/participant-integration-api/src/test/suite/scala/platform/apiserver/services/ApiSubmissionServiceSpec.scala @@ -28,8 +28,10 @@ import com.daml.lf.crypto.Hash import com.daml.lf.data.Time.Timestamp import com.daml.lf.data.{ImmArray, Ref} import com.daml.lf.engine.{Error => LfError} +import com.daml.lf.interpretation.{Error => LfInterpretationError} import com.daml.lf.language.LookupError -import com.daml.lf.transaction.ReplayNodeMismatch +import com.daml.lf +import com.daml.lf.transaction.{GlobalKey, NodeId, ReplayNodeMismatch} import com.daml.lf.transaction.test.TransactionBuilder import com.daml.lf.value.Value import com.daml.logging.LoggingContext @@ -192,12 +194,26 @@ class ApiSubmissionServiceSpec behavior of "submit" it should "return proper gRPC status codes for DamlLf errors" in { + val tmplId = Ref.Identifier.assertFromString("pkgId:M:T") + val errorsToStatuses = List( ErrorCause.DamlLf( - LfError.Interpretation(LfError.Interpretation.ContractNotFound(null)) + LfError.Interpretation( + LfError.Interpretation.DamlException( + LfInterpretationError.ContractNotFound(Value.ContractId.assertFromString("#cid")) + ), + "N/A", + ) ) -> Status.ABORTED, ErrorCause.DamlLf( - LfError.Interpretation(LfError.Interpretation.DuplicateContractKey(null)) + LfError.Interpretation( + LfError.Interpretation.DamlException( + LfInterpretationError.DuplicateContractKey( + GlobalKey.assertBuild(tmplId, Value.ValueUnit) + ) + ), + "N/A", + ) ) -> Status.ABORTED, ErrorCause.DamlLf( LfError.Validation( @@ -212,7 +228,15 @@ class ApiSubmissionServiceSpec ) ) -> Status.INVALID_ARGUMENT, ErrorCause.DamlLf( - LfError.Interpretation(LfError.Interpretation.Authorization("")) + LfError.Interpretation( + LfError.Interpretation.DamlException( + LfInterpretationError.FailedAuthorization( + NodeId(1), + lf.ledger.FailedAuthorization.NoSignatories(tmplId, None), + ) + ), + "N/A", + ) ) -> Status.INVALID_ARGUMENT, ErrorCause.LedgerTime(0) -> Status.ABORTED, )