Skip to content

Commit

Permalink
LF: Structure Preprocessing Errors
Browse files Browse the repository at this point in the history
part of #9974.

CHANGELOG_BEGIN
CHANGELOG_END
  • Loading branch information
remyhaemmerle-da committed Jun 15, 2021
1 parent af144e8 commit de011f7
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -237,7 +238,8 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV

@inline
private[lf] def runSafely[X](
handleMissingDependencies: => Result[Unit]
funcName: String,
handleMissingDependencies: => Result[Unit],
)(run: => Result[X]): Result[X] = {
def start: Result[X] =
try {
Expand All @@ -246,7 +248,7 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
case speedy.Compiler.PackageNotFound(_) =>
handleMissingDependencies.flatMap(_ => start)
case speedy.Compiler.CompilationError(error) =>
ResultError(Error.Preprocessing.Generic(s"CompilationError: $error"))
ResultError(Error.Preprocessing.Internal(funcName, s"CompilationError: $error"))
}
start
}
Expand All @@ -269,7 +271,8 @@ class Engine(val config: EngineConfig = new EngineConfig(LanguageVersion.StableV
globalCids: Set[Value.ContractId],
): Result[(SubmittedTransaction, Tx.Metadata)] =
runSafely(
loadPackages(commands.foldLeft(Set.empty[PackageId])(_ + _.templateId.packageId).toList)
NameOf.qualifiedNameOfCurrentFunc,
loadPackages(commands.foldLeft(Set.empty[PackageId])(_ + _.templateId.packageId).toList),
) {
val sexpr = compiledPackages.compiler.unsafeCompile(commands)
val machine = Machine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -68,16 +69,31 @@ 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

this match {
case _: ValueNesting | _: ContractIdInContractKey | _: RootNode =>
remy.log(s"*** $this ***")
remy.stackTrace()
case _ =>
}

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 {
Expand All @@ -89,6 +105,37 @@ object Error {
case _ => None
}
}

final case class Type(
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 ContractIdInContractKey(
templateId: Ref.TypeConName,
key: Value[Value.ContractId],
contractId: Value.ContractId,
) extends Error {
override def msg: String =
s"Contract IDs are not supported in contract key of $templateId: $contractId"
}

final case class RootNode(nodeId: NodeId, override val msg: String) extends Error

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ 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")
throw Error.Preprocessing.ContractIdInContractKey(templateId, contractKey, coid)
}
speedy.Command.ExerciseByKey(templateId, key, choiceId, arg) -> argCids
}
Expand Down Expand Up @@ -87,7 +87,7 @@ 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")
throw Error.Preprocessing.ContractIdInContractKey(templateId, contractKey, coid)
}
speedy.Command.LookupByKey(templateId, key)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ private[preprocessing] final class TransactionPreprocessor(
compiledPackages: MutableCompiledPackages
) {

import Preprocessor._

val commandPreprocessor = new CommandPreprocessor(compiledPackages)

// Accumulator used by unsafeTranslateTransactionRoots method.
Expand All @@ -35,6 +33,9 @@ private[preprocessing] final class TransactionPreprocessor(
)
}

private[this] def invalideRootNode(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.
Expand Down Expand Up @@ -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")
invalideRootNode(id, s"Transaction contains a fetch root node $id")
case _: Node.NodeLookupByKey[_] =>
fail(s"Transaction contains a lookup by key root node $id")
invalideRootNode(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")
invalideRootNode(id, s"invalid transaction, root refers to a rollback node $id")
case None =>
fail(s"invalid transaction, root refers to non-existing node $id")
invalideRootNode(id, s"invalid transaction, root refers to non-existing node $id")
}
}

Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])]
Expand Down Expand Up @@ -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.Type(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)) =>
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
)
)
Expand All @@ -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"
)
)
Expand All @@ -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}"
)
}
Expand All @@ -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)
Expand All @@ -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)
}
Expand All @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

}
Expand Down
Loading

0 comments on commit de011f7

Please sign in to comment.