Skip to content

Commit

Permalink
Support starting a DAML script from a JSON input value (digital-asset…
Browse files Browse the repository at this point in the history
…#3490)

This uses the format for LF values that we already use elsewhere.

There is one annoying part in this PR where I had to duplicate the
logic for converting to the types used in the interface reader since
it is not exposed but hopefully we can get rid of this soon in a
separate PR.

fixes digital-asset#3470
  • Loading branch information
cocreature authored Nov 18, 2019
1 parent 5f8bf41 commit 6a09ea7
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 5 deletions.
1 change: 1 addition & 0 deletions daml-lf/interface/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ da_scala_library(
tags = ["maven_coordinates=com.digitalasset:daml-lf-interface:__VERSION__"],
visibility = [
"//daml-lf:__subpackages__",
"//daml-script:__subpackages__",
"//extractor:__subpackages__",
"//language-support:__subpackages__",
"//ledger-service:__subpackages__",
Expand Down
3 changes: 3 additions & 0 deletions daml-script/runner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ da_scala_library(
"//daml-lf/archive:daml_lf_dev_archive_java_proto",
"//daml-lf/data",
"//daml-lf/engine",
"//daml-lf/interface",
"//daml-lf/interpreter",
"//daml-lf/language",
"//daml-lf/transaction",
"//language-support/scala/bindings",
"//language-support/scala/bindings-akka",
"//ledger-api/rs-grpc-bridge",
"//ledger-service/lf-value-json",
"//ledger/ledger-api-client",
"//ledger/ledger-api-common",
"@maven//:com_github_scopt_scopt_2_12",
"@maven//:com_typesafe_akka_akka_stream_2_12",
"@maven//:io_spray_spray_json_2_12",
"@maven//:org_scalaz_scalaz_core_2_12",
"@maven//:org_typelevel_paiges_core_2_12",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import java.util
import scala.annotation.tailrec
import scala.collection.JavaConverters._

import com.digitalasset.daml.lf.data.{FrontStack}
import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq
import com.digitalasset.daml.lf.data.Ref._
import com.digitalasset.daml.lf.engine.{ResultDone, ValueTranslator}
import com.digitalasset.daml.lf.iface
import com.digitalasset.daml.lf.language.Ast
import com.digitalasset.daml.lf.language.Ast._
import com.digitalasset.daml.lf.language.{Util => AstUtil}
import com.digitalasset.daml.lf.speedy.SBuiltin._
import com.digitalasset.daml.lf.speedy.SExpr._
import com.digitalasset.daml.lf.speedy.Speedy
Expand Down Expand Up @@ -294,4 +299,63 @@ object Converter {
}
} yield record(anyTemplateTyCon, ("getAnyTemplate", SAny(TTyCon(tyCon), argSValue)))
}

// TODO This duplicates the type_ method from com.digitalasset.daml.lf.iface.reader.InterfaceReader
// since it is not exposed. We should find some place to expose that method or a way to
// avoid duplicating this logic.
def toIfaceType(
a: Ast.Type,
args: FrontStack[iface.Type] = FrontStack.empty
): Either[String, iface.Type] =
a match {
case Ast.TVar(x) =>
if (args.isEmpty)
Right(iface.TypeVar(x))
else
Left("arguments passed to a type parameter")
case Ast.TTyCon(c) =>
Right(iface.TypeCon(iface.TypeConName(c), args.toImmArray.toSeq))
case AstUtil.TNumeric(Ast.TNat(n)) if args.isEmpty =>
Right(iface.TypeNumeric(n))
case Ast.TBuiltin(bt) =>
primitiveType(bt, args.toImmArray.toSeq)
case Ast.TApp(tyfun, arg) =>
toIfaceType(arg, FrontStack.empty).flatMap(tArg => toIfaceType(tyfun, tArg +: args))
case Ast.TForall(_, _) | Ast.TTuple(_) | Ast.TNat(_) =>
Left(s"unserializable data type: ${a.pretty}")
}

def primitiveType(
a: Ast.BuiltinType,
args: ImmArraySeq[iface.Type]
): Either[String, iface.TypePrim] =
for {
ab <- a match {
case Ast.BTUnit => Right((0, iface.PrimType.Unit))
case Ast.BTBool => Right((0, iface.PrimType.Bool))
case Ast.BTInt64 => Right((0, iface.PrimType.Int64))
case Ast.BTText => Right((0, iface.PrimType.Text))
case Ast.BTDate => Right((0, iface.PrimType.Date))
case Ast.BTTimestamp => Right((0, iface.PrimType.Timestamp))
case Ast.BTParty => Right((0, iface.PrimType.Party))
case Ast.BTContractId => Right((1, iface.PrimType.ContractId))
case Ast.BTList => Right((1, iface.PrimType.List))
case Ast.BTOptional => Right((1, iface.PrimType.Optional))
case Ast.BTMap => Right((1, iface.PrimType.Map))
case Ast.BTGenMap =>
// FIXME https://github.com/digital-asset/daml/issues/2256
Left(s"Unsupported primitive type: $a")
case Ast.BTNumeric =>
Left(s"Unserializable primitive type: $a must be applied to one and only one TNat")
case Ast.BTUpdate | Ast.BTScenario | Ast.BTArrow | Ast.BTAny | Ast.BTTypeRep =>
Left(s"Unserializable primitive type: $a")
}
(arity, primType) = ab
typ <- {
if (args.length != arity)
Left(s"$a requires $arity arguments, but got ${args.length}")
else
Right(iface.TypePrim(primType, args))
}
} yield typ
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@ import akka.stream.scaladsl.Sink
import com.typesafe.scalalogging.StrictLogging
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}
import scalaz.{\/-}
import scalaz.std.either._
import scalaz.syntax.tag._
import scalaz.syntax.traverse._
import spray.json._

import com.digitalasset.daml.lf.PureCompiledPackages
import com.digitalasset.daml.lf.archive.Dar
import com.digitalasset.daml.lf.data.FrontStack
import com.digitalasset.daml.lf.data.Ref._
import com.digitalasset.daml.lf.engine.ValueTranslator
import com.digitalasset.daml.lf.iface
import com.digitalasset.daml.lf.iface.EnvironmentInterface
import com.digitalasset.daml.lf.iface.reader.InterfaceReader
import com.digitalasset.daml.lf.language.Ast._
import com.digitalasset.daml.lf.speedy.{Compiler, Pretty, Speedy, SValue, TraceLog}
import com.digitalasset.daml.lf.speedy.SExpr._
import com.digitalasset.daml.lf.speedy.SResult._
import com.digitalasset.daml.lf.speedy.SValue._
import com.digitalasset.daml.lf.value.Value
import com.digitalasset.daml.lf.value.Value.AbsoluteContractId
import com.digitalasset.daml.lf.value.json.ApiCodecCompressed
import com.digitalasset.ledger.api.domain.LedgerId
import com.digitalasset.ledger.api.refinements.ApiTypes.ApplicationId
import com.digitalasset.ledger.api.v1.command_service.SubmitAndWaitRequest
Expand All @@ -34,12 +42,28 @@ import com.digitalasset.ledger.api.v1.transaction_filter.{
import com.digitalasset.ledger.client.LedgerClient
import com.digitalasset.ledger.client.services.commands.CommandUpdater

object LfValueCodec extends ApiCodecCompressed[AbsoluteContractId](false, false) {
override final def apiContractIdToJsValue(obj: AbsoluteContractId) =
JsString(obj.coid)
override final def jsValueToApiContractId(json: JsValue) = json match {
case JsString(s) =>
ContractIdString.fromString(s).fold(deserializationError(_), AbsoluteContractId)
case _ => deserializationError("ContractId must be a string")
}
}

class Runner(
dar: Dar[(PackageId, Package)],
applicationId: ApplicationId,
commandUpdater: CommandUpdater)
extends StrictLogging {

val ifaceDar = dar.map(pkg => InterfaceReader.readInterface(() => \/-(pkg))._2)

val envIface = EnvironmentInterface.fromReaderInterfaces(ifaceDar)
def damlLfTypeLookup(id: Identifier): Option[iface.DefDataType.FWT] =
envIface.typeDecls.get(id).map(_.`type`)

val darMap: Map[PackageId, Package] = dar.all.toMap
val compiler = Compiler(darMap)
val scriptModuleName = DottedName.assertFromString("Daml.Script")
Expand All @@ -49,6 +73,9 @@ class Runner(
}
.get
._1
val scriptTyCon = Identifier(
scriptPackageId,
QualifiedName(scriptModuleName, DottedName.assertFromString("Script")))
val stdlibPackageId =
dar.all
.find {
Expand Down Expand Up @@ -106,12 +133,48 @@ class Runner(
SubmitAndWaitRequest(Some(commandUpdater.applyOverrides(commands)))
}

def run(client: LedgerClient, scriptId: Identifier)(
def run(client: LedgerClient, scriptId: Identifier, inputValue: Option[JsValue])(
implicit ec: ExecutionContext,
mat: ActorMaterializer): Future[SValue] = {
val scriptExpr = EVal(scriptId)
val scriptTy = darMap
.get(scriptId.packageId)
.flatMap(_.lookupIdentifier(scriptId.qualifiedName).toOption) match {
case Some(DValue(ty, _, _, _)) => ty
case Some(d @ DDataType(_, _, _)) =>
throw new RuntimeException(s"Expected DAML script but got datatype $d")
case None => throw new RuntimeException(s"Could not find DAML script $scriptId")
}
def assertScriptTy(ty: Type) = {
ty match {
case TApp(TTyCon(tyCon), _) if tyCon == scriptTyCon => {}
case _ => throw new RuntimeException(s"Expected type 'Script a' but got $ty")
}
}
val scriptExpr = inputValue match {
case None => {
assertScriptTy(scriptTy)
SEVal(LfDefRef(scriptId), None)
}
case Some(inputJson) =>
scriptTy match {
case TApp(TApp(TBuiltin(BTArrow), param), result) => {
assertScriptTy(result)
val paramIface = Converter.toIfaceType(param) match {
case Left(s) => throw new ConverterException(s"Failed to convert $result: $s")
case Right(ty) => ty
}
val inputLfVal = inputJson.convertTo[Value[AbsoluteContractId]](
LfValueCodec.apiValueJsonReader(paramIface, damlLfTypeLookup(_)))
SEApp(SEVal(LfDefRef(scriptId), None), Array(SEValue(SValue.fromValue(inputLfVal))))
}
case _ =>
throw new RuntimeException(
s"Expected $scriptId to have function type but got $scriptTy")
}

}
var machine =
Speedy.Machine.fromSExpr(compiler.compile(scriptExpr), false, compiledPackages)
Speedy.Machine.fromSExpr(scriptExpr, false, compiledPackages)

def stepToValue() = {
while (!machine.isFinal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ case class RunnerConfig(
ledgerPort: Int,
timeProviderType: TimeProviderType,
commandTtl: Duration,
inputFile: Option[File],
)

object RunnerConfig {
Expand Down Expand Up @@ -52,6 +53,12 @@ object RunnerConfig {
c.copy(commandTtl = Duration.ofSeconds(t))
}
.text("TTL in seconds used for commands emitted by the trigger. Defaults to 30s.")

opt[File]("input-file")
.action { (t, c) =>
c.copy(inputFile = Some(t))
}
.text("Path to a file containing the input value for the script in JSON format.")
}
def parse(args: Array[String]): Option[RunnerConfig] =
parser.parse(
Expand All @@ -63,6 +70,7 @@ object RunnerConfig {
ledgerPort = 0,
timeProviderType = TimeProviderType.Static,
commandTtl = Duration.ofSeconds(30L),
inputFile = None,
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import akka.stream._
import java.time.Instant
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.Duration
import scala.io.Source
import scalaz.syntax.traverse._
import spray.json._

import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.archive.{Dar, DarReader}
Expand Down Expand Up @@ -67,10 +69,20 @@ object RunnerMain {
implicit val ec: ExecutionContext = system.dispatcher
implicit val materializer: ActorMaterializer = ActorMaterializer()(system)

val inputValue = config.inputFile.map(file => {
val source = Source.fromFile(file)
val fileContent = try {
source.mkString
} finally {
source.close()
}
fileContent.parseJson
})

val runner = new Runner(dar, applicationId, commandUpdater)
val flow: Future[Unit] = for {
client <- LedgerClient.singleHost(config.ledgerHost, config.ledgerPort, clientConfig)
_ <- runner.run(client, scriptId)
_ <- runner.run(client, scriptId, inputValue)
} yield ()

flow.onComplete(_ => system.terminate())
Expand Down
1 change: 1 addition & 0 deletions daml-script/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ da_scala_binary(
"//ledger/ledger-api-common",
"@maven//:com_github_scopt_scopt_2_12",
"@maven//:com_typesafe_akka_akka_stream_2_12",
"@maven//:io_spray_spray_json_2_12",
"@maven//:org_scalaz_scalaz_core_2_12",
],
)
Expand Down
3 changes: 3 additions & 0 deletions daml-script/test/daml/ScriptTest.daml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,6 @@ test1 = do
_ -> error $ "Expected exactly one NumericTpl but got " <> show ts
v' <- submit alice $ exerciseCmd cid GetV
pure (v + v')

test2 : C -> Script Int
test2 (C _ i) = pure i
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import scala.concurrent.duration.Duration
import scala.util.{Success, Failure}
import scalaz.syntax.tag._
import scalaz.syntax.traverse._
import spray.json._

import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.archive.Dar
Expand Down Expand Up @@ -84,6 +85,7 @@ class TestRunner(val config: Config) extends StrictLogging {
dar: Dar[(PackageId, Package)],
// Identifier of the script value
scriptId: Identifier,
inputValue: Option[JsValue],
assertResult: SValue => Either[String, Unit]) = {

println(s"---\n$name:")
Expand All @@ -100,7 +102,7 @@ class TestRunner(val config: Config) extends StrictLogging {

val testFlow: Future[Unit] = for {
client <- clientF
result <- runner.run(client, scriptId)
result <- runner.run(client, scriptId, inputValue)
_ <- assertResult(result) match {
case Left(err) =>
Future.failed(new RuntimeException(s"Assertion on script result failed: $err"))
Expand Down Expand Up @@ -128,6 +130,7 @@ case class Test0(dar: Dar[(PackageId, Package)], runner: TestRunner) {
"test0",
dar,
scriptId,
None,
result =>
result match {
case SRecord(_, _, vals) if vals.size == 5 => {
Expand Down Expand Up @@ -194,6 +197,7 @@ case class Test1(dar: Dar[(PackageId, Package)], runner: TestRunner) {
"test1",
dar,
scriptId,
None,
result =>
result match {
case SNumeric(n) =>
Expand All @@ -204,6 +208,23 @@ case class Test1(dar: Dar[(PackageId, Package)], runner: TestRunner) {
}
}

case class Test2(dar: Dar[(PackageId, Package)], runner: TestRunner) {
val scriptId = Identifier(dar.main._1, QualifiedName.assertFromString("ScriptTest:test2"))
def runTests() = {
runner.genericTest(
"test2",
dar,
scriptId,
Some(JsObject(("p", JsString("Alice")), ("v", JsNumber(42)))),
result =>
result match {
case SInt64(i) => TestRunner.assertEqual(i, 42, "Numeric")
case v => Left(s"Expected SInt but got $v")
}
)
}
}

object TestMain {

private val configParser = new scopt.OptionParser[Config]("daml_script_test") {
Expand Down Expand Up @@ -240,6 +261,7 @@ object TestMain {
val runner = new TestRunner(config)
Test0(dar, runner).runTests()
Test1(dar, runner).runTests()
Test2(dar, runner).runTests()
}
}
}

0 comments on commit 6a09ea7

Please sign in to comment.