LF: Structure Preprocessing Errors (#10013)

part of #9974.

CHANGELOG_BEGIN
CHANGELOG_END
This commit is contained in:
Remy 2021-06-17 09:20:27 +02:00 committed by GitHub
parent 1852830833
commit 3532460675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 69 deletions

View File

@ -19,6 +19,7 @@ import java.nio.file.Files
import com.daml.lf.language.{Interface, LanguageVersion}
import com.daml.lf.validation.Validation
import com.daml.lf.value.Value.ContractId
import com.daml.nameof.NameOf
/** Allows for evaluating [[Commands]] and validating [[Transaction]]s.
* <p>
@ -237,16 +238,20 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
@inline
private[lf] def runSafely[X](
handleMissingDependencies: => Result[Unit]
funcName: String
)(run: => Result[X]): Result[X] = {
def start: Result[X] =
try {
run
} catch {
case speedy.Compiler.PackageNotFound(_) =>
handleMissingDependencies.flatMap(_ => start)
// The two following error should be prevented by the type checking does by translateCommand
// so its an internal error.
case error: speedy.Compiler.PackageNotFound =>
ResultError(
Error.Preprocessing.Internal(funcName, s"CompilationError: ${error.getMessage}")
)
case speedy.Compiler.CompilationError(error) =>
ResultError(Error.Preprocessing.Generic(s"CompilationError: $error"))
ResultError(Error.Preprocessing.Internal(funcName, s"CompilationError: $error"))
}
start
}
@ -268,9 +273,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
seeding: speedy.InitialSeeding,
globalCids: Set[Value.ContractId],
): Result[(SubmittedTransaction, Tx.Metadata)] =
runSafely(
loadPackages(commands.foldLeft(Set.empty[PackageId])(_ + _.templateId.packageId).toList)
) {
runSafely(NameOf.qualifiedNameOfCurrentFunc) {
val sexpr = compiledPackages.compiler.unsafeCompile(commands)
val machine = Machine(
compiledPackages = compiledPackages,

View File

@ -5,7 +5,8 @@ package com.daml.lf
package engine
import com.daml.lf.data.Ref
import com.daml.lf.transaction.GlobalKey
import com.daml.lf.language.Ast
import com.daml.lf.transaction.{GlobalKey, NodeId}
import com.daml.lf.value.Value
sealed abstract class Error {
@ -68,16 +69,24 @@ object Error {
object Preprocessing {
sealed abstract class Error extends RuntimeException with scala.util.control.NoStackTrace {
sealed abstract class Error
extends RuntimeException
with scala.util.control.NoStackTrace
with Product {
def msg: String
override def toString: String =
productPrefix + productIterator.mkString("(", ",", ")")
}
// TODO https://github.com/digital-asset/daml/issues/9974
// get rid of Generic
final case class Generic(override val msg: String) extends Error
final case class Internal(
nameOfFunc: String,
override val msg: String,
detailMsg: String = "",
) extends Error
final case class Lookup(lookupError: language.LookupError) extends Error {
def msg: String = lookupError.pretty
override def msg: String = lookupError.pretty
}
private[engine] object MissingPackage {
@ -89,6 +98,30 @@ object Error {
case _ => None
}
}
final case class TypeMismatch(
typ: Ast.Type,
value: Value[Value.ContractId],
override val msg: String,
) extends Error
final case class ValueNesting(value: Value[Value.ContractId]) extends Error {
override def msg: String =
s"Provided value exceeds maximum nesting level of ${Value.MAXIMUM_NESTING}"
}
final case class RootNode(nodeId: NodeId, override val msg: String) extends Error
// TODO https://github.com/digital-asset/daml/issues/9974
// get ride of ContractIdFreshness
final case class ContractIdFreshness(
localContractIds: Set[Value.ContractId],
globalContractIds: Set[Value.ContractId],
) extends Error {
assert(localContractIds exists globalContractIds)
def msg: String = "Conflicting discriminators between a global and local contract ID."
}
}
// Error happening during interpretation

View File

@ -9,6 +9,7 @@ import com.daml.lf.data._
import com.daml.lf.language.Ast
import com.daml.lf.speedy.SValue
import com.daml.lf.value.Value
import com.daml.nameof.NameOf
import scala.annotation.tailrec
@ -53,7 +54,12 @@ private[lf] final class CommandPreprocessor(compiledPackages: CompiledPackages)
val (arg, argCids) = valueTranslator.unsafeTranslateValue(choiceArgType, argument)
val (key, keyCids) = valueTranslator.unsafeTranslateValue(ckTtype, contractKey)
keyCids.foreach { coid =>
fail(s"Contract IDs are not supported in contract key of $templateId: $coid")
// The type checking of contractKey done by unsafeTranslateValue should ensure
// keyCids is empty
throw Error.Preprocessing.Internal(
NameOf.qualifiedNameOfCurrentFunc,
s"Unexpected contract IDs in contract key of $templateId: $coid",
)
}
speedy.Command.ExerciseByKey(templateId, key, choiceId, arg) -> argCids
}
@ -87,7 +93,12 @@ private[lf] final class CommandPreprocessor(compiledPackages: CompiledPackages)
val ckTtype = handleLookup(interface.lookupTemplateKey(templateId)).typ
val (key, keyCids) = valueTranslator.unsafeTranslateValue(ckTtype, contractKey)
keyCids.foreach { coid =>
fail(s"Contract IDs are not supported in contract keys: $coid")
// The type checking of contractKey done by unsafeTranslateValue should ensure
// keyCids is empty
throw Error.Preprocessing.Internal(
NameOf.qualifiedNameOfCurrentFunc,
s"Unexpected contract IDs in contract key of $templateId: $coid",
)
}
speedy.Command.LookupByKey(templateId, key)
}

View File

@ -12,6 +12,7 @@ import com.daml.lf.language.{Ast, LookupError}
import com.daml.lf.speedy.SValue
import com.daml.lf.transaction.{GenTransaction, NodeId}
import com.daml.lf.value.Value
import com.daml.nameof.NameOf
import scala.annotation.tailrec
@ -88,7 +89,11 @@ private[engine] final class Preprocessor(compiledPackages: MutableCompiledPackag
case Ast.TTyCon(_) | Ast.TNat(_) | Ast.TBuiltin(_) | Ast.TVar(_) =>
go(typesToProcess, tmplToProcess0, tyConAlreadySeen0, tmplsAlreadySeen0)
case Ast.TSynApp(_, _) | Ast.TForall(_, _) | Ast.TStruct(_) =>
ResultError(Error.Preprocessing.Generic(s"unserializable type ${typ.pretty}"))
// We assume that getDependencies is always given serializable types
ResultError(
Error.Preprocessing
.Internal(NameOf.qualifiedNameOfCurrentFunc, s"unserializable type ${typ.pretty}")
)
}
case Nil =>
tmplToProcess0 match {
@ -160,10 +165,6 @@ private[preprocessing] object Preprocessor {
a
}
@throws[Error.Preprocessing.Error]
def fail(s: String): Nothing =
throw Error.Preprocessing.Generic(s)
@throws[Error.Preprocessing.Error]
def handleLookup[X](either: Either[LookupError, X]): X = either match {
case Right(v) => v

View File

@ -14,8 +14,6 @@ private[preprocessing] final class TransactionPreprocessor(
compiledPackages: MutableCompiledPackages
) {
import Preprocessor._
val commandPreprocessor = new CommandPreprocessor(compiledPackages)
// Accumulator used by unsafeTranslateTransactionRoots method.
@ -35,6 +33,9 @@ private[preprocessing] final class TransactionPreprocessor(
)
}
private[this] def invalidRootNode(nodeId: NodeId, message: String) =
throw Error.Preprocessing.RootNode(nodeId, message)
/*
* Translates a transaction tree into a sequence of Speedy commands
* and collects the global contract IDs.
@ -114,14 +115,14 @@ private[preprocessing] final class TransactionPreprocessor(
val newLocalCids = GenTransaction(tx.nodes, ImmArray(id)).localContracts.keys
acc.update(newCids, newLocalCids, cmd)
case _: Node.NodeFetch[_] =>
fail(s"Transaction contains a fetch root node $id")
invalidRootNode(id, s"Transaction contains a fetch root node $id")
case _: Node.NodeLookupByKey[_] =>
fail(s"Transaction contains a lookup by key root node $id")
invalidRootNode(id, s"Transaction contains a lookup by key root node $id")
}
case Some(_: Node.NodeRollback[NodeId]) =>
fail(s"invalid transaction, root refers to a rollback node $id")
invalidRootNode(id, s"invalid transaction, root refers to a rollback node $id")
case None =>
fail(s"invalid transaction, root refers to non-existing node $id")
invalidRootNode(id, s"invalid transaction, root refers to non-existing node $id")
}
}
@ -132,7 +133,7 @@ private[preprocessing] final class TransactionPreprocessor(
// - it catches obviously buggy transaction,
// - it is easier to reason about "soundness" of preprocessing under the disjointness assumption.
if (result.localCids exists result.globalCids)
fail("Conflicting discriminators between a global and local contract ID.")
throw Error.Preprocessing.ContractIdFreshness(result.localCids, result.globalCids)
result.commands.toImmArray -> result.globalCids
}

View File

@ -18,8 +18,6 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
import Preprocessor._
private[this] def fail(s: String) = throw Error.Preprocessing.Generic(s)
@throws[Error.Preprocessing.Error]
private def labeledRecordToMap(
fields: ImmArray[(Option[String], Value[ContractId])]
@ -50,18 +48,19 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
val cids = Set.newBuilder[Value.ContractId]
def go(ty0: Type, value: Value[ContractId], nesting: Int = 0): SValue =
def go(ty0: Type, value0: Value[ContractId], nesting: Int = 0): SValue =
if (nesting > Value.MAXIMUM_NESTING) {
fail(s"Provided value exceeds maximum nesting level of ${Value.MAXIMUM_NESTING}")
throw Error.Preprocessing.ValueNesting(value)
} else {
val newNesting = nesting + 1
def typeMismatch = fail(s"mismatching type: $ty and value: $value")
def typeError(msg: String = s"mismatching type: $ty and value: $value0") =
throw Error.Preprocessing.TypeMismatch(ty, value0, msg)
val (ty1, tyArgs) = AstUtil.destructApp(ty0)
ty1 match {
case TBuiltin(bt) =>
tyArgs match {
case Nil =>
(bt, value) match {
(bt, value0) match {
case (BTUnit, ValueUnit) =>
SValue.SUnit
case (BTBool, ValueBool(b)) =>
@ -77,16 +76,19 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
case (BTParty, ValueParty(p)) =>
SValue.SParty(p)
case _ =>
typeMismatch
typeError()
}
case typeArg0 :: Nil =>
(bt, value) match {
(bt, value0) match {
case (BTNumeric, ValueNumeric(d)) =>
typeArg0 match {
case TNat(s) =>
Numeric.fromBigDecimal(s, d).fold(fail, SValue.SNumeric(_))
Numeric.fromBigDecimal(s, d) match {
case Right(value) => SValue.SNumeric(value)
case Left(message) => typeError(message)
}
case _ =>
typeMismatch
typeError()
}
case (BTContractId, ValueContractId(c)) =>
cids += c
@ -117,10 +119,10 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
)
}
case _ =>
typeMismatch
typeError()
}
case typeArg0 :: typeArg1 :: Nil =>
(bt, value) match {
(bt, value0) match {
case (BTGenMap, ValueGenMap(entries)) =>
if (entries.isEmpty) {
SValue.SValue.EmptyGenMap
@ -133,18 +135,18 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
)
}
case _ =>
typeMismatch
typeError()
}
case _ =>
typeMismatch
typeError()
}
case TTyCon(tyCon) =>
value match {
value0 match {
// variant
case ValueVariant(mbId, constructorName, val0) =>
mbId.foreach(id =>
if (id != tyCon)
fail(
typeError(
s"Mismatching variant id, the type tells us $tyCon, but the value tells us $id"
)
)
@ -160,7 +162,7 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
case ValueRecord(mbId, flds) =>
mbId.foreach(id =>
if (id != tyCon)
fail(
typeError(
s"Mismatching record id, the type tells us $tyCon, but the value tells us $id"
)
)
@ -172,7 +174,7 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
// since in JavaScript / Scala / most languages (but _not_ JSON, interestingly)
// it's ok to do `{"a": 1, "a": 2}`, where the second occurrence would just win.
if (recordFlds.length != flds.length) {
fail(
typeError(
s"Expecting ${recordFlds.length} field for record $tyCon, but got ${flds.length}"
)
}
@ -182,7 +184,9 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
(recordFlds zip flds).map { case ((lbl, typ), (mbLbl, v)) =>
mbLbl.foreach(lbl_ =>
if (lbl_ != lbl)
fail(s"Mismatching record label $lbl_ (expecting $lbl) for record $tyCon")
typeError(
s"Mismatching record label $lbl_ (expecting $lbl) for record $tyCon"
)
)
val replacedTyp = AstUtil.substitute(typ, subst)
lbl -> go(replacedTyp, v, newNesting)
@ -191,7 +195,7 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
recordFlds.map { case (lbl, typ) =>
labeledRecords
.get(lbl)
.fold(fail(s"Missing record label $lbl for record $tyCon")) { v =>
.fold(typeError(s"Missing record label $lbl for record $tyCon")) { v =>
val replacedTyp = AstUtil.substitute(typ, subst)
lbl -> go(replacedTyp, v, newNesting)
}
@ -206,17 +210,17 @@ private[engine] final class ValueTranslator(interface: language.Interface) {
case ValueEnum(mbId, constructor) if tyArgs.isEmpty =>
mbId.foreach(id =>
if (id != tyCon)
fail(
typeError(
s"Mismatching enum id, the type tells us $tyCon, but the value tells us $id"
)
)
val rank = handleLookup(interface.lookupEnumConstructor(tyCon, constructor))
SValue.SEnum(tyCon, constructor, rank)
case _ =>
typeMismatch
typeError()
}
case _ =>
typeMismatch
typeError()
}
}

View File

@ -333,8 +333,8 @@ class ContractDiscriminatorFreshnessCheckSpec
.translateTransactionRoots(GenTransaction(newNodes, tx.roots))
.consume(_ => None, pkgs, _ => None, _ => VisibleByKey.Visible)
inside(result) { case Left(err) =>
err.msg should include("Conflicting discriminators")
inside(result) { case Left(Error.Preprocessing(err)) =>
err shouldBe a[Error.Preprocessing.ContractIdFreshness]
}
}

View File

@ -221,7 +221,9 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res shouldBe a[Left[_, _]]
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
"translate exercise commands argument including labels" in {
@ -298,7 +300,10 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res.left.value.msg should startWith("Missing record label n for record")
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
error.msg should startWith("Missing record label n for record")
}
}
"not translate exercise-by-key commands if the template specifies no key" in {
@ -313,9 +318,9 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res.left.value.msg should startWith(
s"template without contract key $templateId"
)
inside(res) { case Left(Error.Preprocessing(Error.Preprocessing.Lookup(error))) =>
error shouldBe a[language.LookupError.TemplateKey]
}
}
"not translate exercise-by-key commands if the given key does not match the type specified in the template" in {
@ -330,7 +335,9 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res.left.value.msg should startWith("mismatching type")
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
"translate create-and-exercise commands argument including labels" in {
@ -394,7 +401,9 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res shouldBe a[Left[_, _]]
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
"not translate create-and-exercise commands argument wrong label in choice arguments" in {
@ -413,7 +422,9 @@ class EngineTest
val res = preprocessor
.preprocessCommands(ImmArray(command))
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res shouldBe a[Left[_, _]]
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
"translate Optional values" in {
@ -451,12 +462,15 @@ class EngineTest
val id = Identifier(basicTestsPkgId, "BasicTests:MyRec")
val wrongRecord =
ValueRecord(Some(id), ImmArray(Some[Name]("wrongLbl") -> ValueText("foo")))
translator
val res = translator
.translateValue(
TTyConApp(id, ImmArray.empty),
wrongRecord,
)
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible) shouldBe a[Left[_, _]]
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
}
@ -1242,7 +1256,9 @@ class EngineTest
)
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res shouldBe a[Left[_, _]]
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
"work with fields without labels, in right order" in {
@ -1282,7 +1298,9 @@ class EngineTest
)
.consume(lookupContract, lookupPackage, lookupKey, allKeysVisible)
res shouldBe a[Left[_, _]]
inside(res) { case Left(Error.Preprocessing(error)) =>
error shouldBe a[Error.Preprocessing.TypeMismatch]
}
}
}

View File

@ -12,13 +12,18 @@ import com.daml.lf.speedy.SValue._
import com.daml.lf.testing.parser.Implicits._
import com.daml.lf.value.Value
import com.daml.lf.value.Value._
import org.scalatest.Inside
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.language.implicitConversions
class PreprocessorSpec extends AnyWordSpec with Matchers with TableDrivenPropertyChecks {
class PreprocessorSpec
extends AnyWordSpec
with Matchers
with TableDrivenPropertyChecks
with Inside {
import Preprocessor.ArrayList
@ -26,10 +31,18 @@ class PreprocessorSpec extends AnyWordSpec with Matchers with TableDrivenPropert
private implicit def toName(s: String): Ref.Name = Ref.Name.assertFromString(s)
val recordCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Record"))
val variantCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Variant"))
val enumCon = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Enum"))
val tricky = Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Tricky"))
private[this] val recordCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Record"))
private[this] val variantCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Variant"))
private[this] val enumCon =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Enum"))
private[this] val tricky =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:Tricky"))
private[this] val myListTyCons =
Ref.Identifier(pkgId, Ref.QualifiedName.assertFromString("Module:MyList"))
private[this] val myNilCons = Ref.Name.assertFromString("MyNil")
private[this] val myConsCons = Ref.Name.assertFromString("MyCons")
val pkg =
p"""
@ -41,6 +54,9 @@ class PreprocessorSpec extends AnyWordSpec with Matchers with TableDrivenPropert
record Tricky (b: * -> *) = { x : b Unit };
record MyCons = { head : Int64, tail: Module:MyList };
variant MyList = MyNil : Unit | MyCons: Module:MyCons ;
}
"""
@ -133,6 +149,28 @@ class PreprocessorSpec extends AnyWordSpec with Matchers with TableDrivenPropert
}
}
}
"fails on too deep values" in {
def mkMyList(n: Int) =
Iterator.range(0, n).foldLeft[Value[Nothing]](ValueVariant(None, myNilCons, ValueUnit)) {
case (v, n) =>
ValueVariant(
None,
myConsCons,
ValueRecord(None, ImmArray(None -> ValueInt64(n.toLong), None -> v)),
)
}
val notTooBig = mkMyList(49)
val tooBig = mkMyList(50)
translateValue(Ast.TTyCon(myListTyCons), notTooBig) shouldBe a[ResultDone[_]]
inside(translateValue(Ast.TTyCon(myListTyCons), tooBig)) {
case ResultError(Error.Preprocessing(err)) =>
err shouldBe Error.Preprocessing.ValueNesting(tooBig)
}
}
}
}