Updating compiler backends (#243)

* Updating compiler backends: add FuncRes

* TypeScriptService

* ServiceRes
This commit is contained in:
Dmitry Kurinskiy 2021-08-16 16:59:36 +03:00 committed by GitHub
parent 38fb824b68
commit 6c498b029b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 236 additions and 179 deletions

View File

@ -1,17 +1,15 @@
package aqua.backend.air
import aqua.backend.{Backend, Generated}
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import cats.implicits.toShow
import aqua.model.res.AquaRes
import cats.syntax.show.*
object AirBackend extends Backend {
val ext = ".air"
override def generate(context: AquaContext, genConf: GenerationConfig): Seq[Generated] = {
context.funcs.values.toList.map(fc =>
Generated("." + fc.funcName + ext, FuncAirGen(fc).generateAir(genConf).show)
)
override def generate(aqua: AquaRes): Seq[Generated] = {
aqua.funcs.toList
.map(fr => Generated("." + fr.funcName + ext, FuncAirGen(fr).generate.show))
}
}

View File

@ -1,15 +1,14 @@
package aqua.backend.air
import aqua.model.func.FuncCallable
import aqua.model.transform.{GenerationConfig, Transform}
import aqua.model.res.FuncRes
case class FuncAirGen(func: FuncCallable) {
case class FuncAirGen(func: FuncRes) {
/**
* Generates AIR from the function body
*/
def generateAir(conf: GenerationConfig = GenerationConfig()): Air =
def generate: Air =
AirGen(
Transform.forClient(func, conf)
func.body
).generate
}

View File

@ -1,23 +1,25 @@
package aqua.backend.js
import aqua.backend.{Backend, Generated}
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import aqua.model.res.AquaRes
import cats.data.NonEmptyChain
object JavaScriptBackend extends Backend {
val ext = ".js"
override def generate(context: AquaContext, genConf: GenerationConfig): Seq[Generated] = {
val funcs = NonEmptyChain.fromSeq(context.funcs.values.toSeq).map(_.map(JavaScriptFunc(_)))
override def generate(aqua: AquaRes): Seq[Generated] = {
val funcs = NonEmptyChain.fromChain(
aqua.funcs
.map(JavaScriptFunc(_))
)
funcs
.map(fs =>
Seq(
Generated(
ext,
JavaScriptFile.Header + "\n\n" + fs
.map(_.generateJavascript(genConf))
.map(_.generate)
.toChain
.toList
.mkString("\n\n")

View File

@ -1,18 +1,6 @@
package aqua.backend.js
import aqua.backend.Version
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import cats.data.Chain
case class JavaScriptFile(context: AquaContext) {
def funcs: Chain[JavaScriptFunc] =
Chain.fromSeq(context.funcs.values.toSeq).map(JavaScriptFunc(_))
def generateJS(conf: GenerationConfig = GenerationConfig()): String =
JavaScriptFile.Header + "\n\n" + funcs.map(_.generateJavascript(conf)).toList.mkString("\n\n")
}
object JavaScriptFile {

View File

@ -1,25 +1,22 @@
package aqua.backend.js
import aqua.backend.air.FuncAirGen
import aqua.model.func.FuncCallable
import aqua.model.transform.GenerationConfig
import aqua.types._
import cats.syntax.show._
import aqua.model.res.FuncRes
import aqua.types.*
import cats.syntax.show.*
case class JavaScriptFunc(func: FuncCallable) {
case class JavaScriptFunc(func: FuncRes) {
import JavaScriptFunc._
import FuncRes._
import func._
def argsJavaScript: String =
func.argNames.mkString(", ")
argNames.mkString(", ")
// TODO: use common functions between TypeScript and JavaScript backends
private def genReturnCallback(
retType: Type,
callbackService: String,
respFuncName: String
): String = {
val body = retType match {
private def returnCallback: String = returnType.fold("") { retType =>
val respBody = retType match {
case OptionType(_) =>
""" let [opt] = args;
| if (Array.isArray(opt)) {
@ -44,42 +41,38 @@ case class JavaScriptFunc(func: FuncCallable) {
| resolve(res);""".stripMargin
}
s"""h.onEvent('$callbackService', '$respFuncName', (args) => {
| $body
s"""h.onEvent('$callbackServiceId', '$respFuncName', (args) => {
| $respBody
|});
|""".stripMargin
}
def generateJavascript(conf: GenerationConfig = GenerationConfig()): String = {
def generate: String = {
val tsAir = FuncAirGen(func).generateAir(conf)
val jsAir = FuncAirGen(func).generate
val setCallbacks = func.args.collect {
case (argName, OptionType(_)) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName === null ? [] : [$argName];});"""
case (argName, _: DataType) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName;});"""
case (argName, at: ArrowType) =>
case Arg(argName, OptionType(_)) =>
s"""h.on('$dataServiceId', '$argName', () => {return $argName === null ? [] : [$argName];});"""
case Arg(argName, _: DataType) =>
s"""h.on('$dataServiceId, '$argName', () => {return $argName;});"""
case Arg(argName, at: ArrowType) =>
val value = s"$argName(${argsCallToJs(
at
)})"
val expr = at.codomain.uncons.fold(s"$value; return {}")(_ => s"return $value")
s"""h.on('${conf.callbackService}', '$argName', (args) => {$expr;});"""
val expr = arrowToRes(at).fold(s"$value; return {}")(_ => s"return $value")
s"""h.on('$callbackServiceId', '$argName', (args) => {$expr;});"""
}
.mkString("\n")
val returnCallback = func.arrowType.codomain.uncons
.map(_ => genReturnCallback(func.arrowType.codomain, conf.callbackService, conf.respFuncName))
.getOrElse("")
val returnVal =
func.ret.headOption.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise")
returnType.headOption.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise")
// TODO: it could be non-unique
val configArgName = "config"
val clientArgName = genArgName("client")
val configArgName = genArgName("config")
s"""
|export async function ${func.funcName}(client${if (func.args.isEmpty) ""
|export async function ${func.funcName}(${clientArgName}${if (func.args.isEmpty) ""
else ", "}${argsJavaScript}, $configArgName) {
| let request;
| $configArgName = $configArgName || {};
@ -88,18 +81,18 @@ case class JavaScriptFunc(func: FuncCallable) {
| .disableInjections()
| .withRawScript(
| `
|${tsAir.show}
|${jsAir.show}
| `,
| )
| .configHandler((h) => {
| ${conf.relayVarName.fold("") { r =>
s"""h.on('${conf.getDataService}', '$r', () => {
| ${relayVarName.fold("") { r =>
s"""h.on('$dataServiceId', '$r', () => {
| return client.relayPeerId;
| });""".stripMargin
}}
| $setCallbacks
| $returnCallback
| h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => {
| h.onEvent('$errorHandlerId', '$errorFuncName', (args) => {
| // assuming error is the single argument
| const [err] = args;
| reject(err);
@ -107,14 +100,14 @@ case class JavaScriptFunc(func: FuncCallable) {
| })
| .handleScriptError(reject)
| .handleTimeout(() => {
| reject('Request timed out for ${func.funcName}');
| reject('Request timed out for ${funcName}');
| })
| if(${configArgName}.ttl) {
| r.withTTL(${configArgName}.ttl)
| }
| request = r.build();
| });
| await client.initiateFlow(request);
| await ${clientArgName}.initiateFlow(request);
| return ${returnVal};
|}
""".stripMargin
@ -124,10 +117,7 @@ case class JavaScriptFunc(func: FuncCallable) {
object JavaScriptFunc {
def argsToTs(at: ArrowType): String =
at.domain.toLabelledList().map(_._1).mkString(", ")
def argsCallToJs(at: ArrowType): String =
at.domain.toList.zipWithIndex.map(_._2).map(idx => s"args[$idx]").mkString(", ")
FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]").mkString(", ")
}

View File

@ -1,7 +1,6 @@
package aqua.backend
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import aqua.model.res.AquaRes
/**
* Compiler backend generates output based on the processed model
@ -9,11 +8,10 @@ import aqua.model.transform.GenerationConfig
trait Backend {
/**
* Generate the result based on the given [[AquaContext]] and [[GenerationConfig]]
* Generate the result based on the given [[AquaRes]]
*
* @param context Source file context, processed, transformed
* @param genConf Generation configuration
* @param aqua Source file context, processed, transformed
* @return Zero or more [[Generated]] objects, based on arguments
*/
def generate(context: AquaContext, genConf: GenerationConfig): Seq[Generated]
def generate(aqua: AquaRes): Seq[Generated]
}

View File

@ -1,29 +1,13 @@
package aqua.backend.ts
import aqua.backend.{Backend, Generated}
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import aqua.model.res.AquaRes
import cats.data.NonEmptyChain
object TypeScriptBackend extends Backend {
val ext = ".ts"
override def generate(context: AquaContext, genConf: GenerationConfig): Seq[Generated] = {
val funcs = NonEmptyChain.fromSeq(context.funcs.values.toSeq).map(_.map(TypeScriptFunc(_)))
funcs
.map(fs =>
Seq(
Generated(
ext,
TypeScriptFile.Header + "\n\n" + fs
.map(_.generateTypescript(genConf))
.toChain
.toList
.mkString("\n\n")
)
)
)
.getOrElse(Seq.empty)
}
override def generate(res: AquaRes): Seq[Generated] =
if (res.isEmpty) Nil else Generated(ext, TypeScriptFile(res).generate) :: Nil
}

View File

@ -1,17 +1,22 @@
package aqua.backend.ts
import aqua.backend.Version
import aqua.model.AquaContext
import aqua.model.transform.GenerationConfig
import cats.data.Chain
import aqua.model.res.AquaRes
case class TypeScriptFile(context: AquaContext) {
case class TypeScriptFile(res: AquaRes) {
def funcs: Chain[TypeScriptFunc] =
Chain.fromSeq(context.funcs.values.toSeq).map(TypeScriptFunc(_))
import TypeScriptFile.Header
def generate: String =
s"""${Header}
|
|// Services
|${res.services.map(TypeScriptService(_)).map(_.generate).toList.mkString("\n\n")}
|
|// Functions
|${res.funcs.map(TypeScriptFunc(_)).map(_.generate).toList.mkString("\n\n")}
|""".stripMargin
def generateTS(conf: GenerationConfig = GenerationConfig()): String =
TypeScriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(conf)).toList.mkString("\n\n")
}
object TypeScriptFile {

View File

@ -1,33 +1,21 @@
package aqua.backend.ts
import aqua.backend.air.FuncAirGen
import aqua.model.func.FuncCallable
import aqua.model.transform.GenerationConfig
import aqua.types._
import cats.syntax.show._
import aqua.model.res.FuncRes
import aqua.types.*
import cats.syntax.show.*
case class TypeScriptFunc(func: FuncCallable) {
case class TypeScriptFunc(func: FuncRes) {
import TypeScriptFunc._
import FuncRes._
import func._
def argsTypescript: String =
func.arrowType.domain.toLabelledList().map(ad => s"${ad._1}: " + typeToTs(ad._2)).mkString(", ")
args.map(ad => s"${ad.name}: " + typeToTs(ad.`type`)).mkString(", ")
def generateUniqueArgName(args: List[String], basis: String, attempt: Int): String = {
val name = if (attempt == 0) {
basis
} else {
basis + attempt
}
args.find(_ == name).map(_ => generateUniqueArgName(args, basis, attempt + 1)).getOrElse(name)
}
private def genReturnCallback(
retType: Type,
callbackService: String,
respFuncName: String
): String = {
val body = retType match {
private def returnCallback: String = returnType.fold("") { retType =>
val respBody = retType match {
case OptionType(_) =>
""" let [opt] = args;
| if (Array.isArray(opt)) {
@ -52,52 +40,43 @@ case class TypeScriptFunc(func: FuncCallable) {
| resolve(res);""".stripMargin
}
s"""h.onEvent('$callbackService', '$respFuncName', (args) => {
| $body
s"""h.onEvent('$callbackServiceId', '$respFuncName', (args) => {
| $respBody
|});
|""".stripMargin
}
def generateTypescript(conf: GenerationConfig = GenerationConfig()): String = {
def generate: String = {
val tsAir = FuncAirGen(func).generateAir(conf)
val tsAir = FuncAirGen(func).generate
// TODO: support multi return
val retType =
if (func.arrowType.codomain.length > 1) Some(func.arrowType.codomain)
else func.arrowType.codomain.uncons.map(_._1)
val retTypeTs = retType
val retTypeTs = func.returnType
.fold("void")(typeToTs)
val returnCallback = retType
.map(t => genReturnCallback(t, conf.callbackService, conf.respFuncName))
.getOrElse("")
val setCallbacks = func.args.collect { // Product types are not handled
case (argName, OptionType(_)) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName === null ? [] : [$argName];});"""
case (argName, _: DataType) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName;});"""
case (argName, at: ArrowType) =>
case Arg(argName, OptionType(_)) =>
s"""h.on('$dataServiceId', '$argName', () => {return $argName === null ? [] : [$argName];});"""
case Arg(argName, _: DataType) =>
s"""h.on('$dataServiceId', '$argName', () => {return $argName;});"""
case Arg(argName, at: ArrowType) =>
val value = s"$argName(${argsCallToTs(
at
)})"
val expr = at.res.fold(s"$value; return {}")(_ => s"return $value")
s"""h.on('${conf.callbackService}', '$argName', (args) => {$expr;});"""
val expr = arrowToRes(at).fold(s"$value; return {}")(_ => s"return $value")
s"""h.on('$callbackServiceId', '$argName', (args) => {$expr;});"""
}
.mkString("\n")
// TODO support multi return
val returnVal =
func.ret.headOption.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise")
returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise")
val clientArgName = generateUniqueArgName(func.argNames, "client", 0)
val configArgName = generateUniqueArgName(func.argNames, "config", 0)
val clientArgName = genArgName("client")
val configArgName = genArgName("config")
val configType = "{ttl?: number}"
s"""
|export async function ${func.funcName}($clientArgName: FluenceClient${if (func.args.isEmpty)
|export async function ${funcName}($clientArgName: FluenceClient${if (args.isEmpty)
""
else ", "}${argsTypescript}, $configArgName?: $configType): Promise<$retTypeTs> {
| let request: RequestFlow;
@ -110,14 +89,14 @@ case class TypeScriptFunc(func: FuncCallable) {
| `,
| )
| .configHandler((h) => {
| ${conf.relayVarName.fold("") { r =>
s"""h.on('${conf.getDataService}', '$r', () => {
| ${relayVarName.fold("") { r =>
s"""h.on('$dataServiceId', '$r', () => {
| return $clientArgName.relayPeerId!;
| });""".stripMargin
}}
| $setCallbacks
| $returnCallback
| h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => {
| h.onEvent('$errorHandlerId', '$errorFuncName', (args) => {
| // assuming error is the single argument
| const [err] = args;
| reject(err);
@ -125,7 +104,7 @@ case class TypeScriptFunc(func: FuncCallable) {
| })
| .handleScriptError(reject)
| .handleTimeout(() => {
| reject('Request timed out for ${func.funcName}');
| reject('Request timed out for ${funcName}');
| })
| if(${configArgName} && ${configArgName}.ttl) {
| r.withTTL(${configArgName}.ttl)
@ -158,20 +137,18 @@ object TypeScriptFunc {
case lt: LiteralType if lt.oneOf(ScalarType.string) => "string"
case _: DataType => "any"
case at: ArrowType =>
s"(${argsToTs(at)}) => ${at.res
s"(${argsToTs(at)}) => ${FuncRes
.arrowToRes(at)
.fold("void")(typeToTs)}"
case _ =>
// TODO: handle product types in returns
"any"
}
def argsToTs(at: ArrowType): String =
at.domain
.toLabelledList()
.map(nt => nt._1 + ": " + typeToTs(nt._2))
FuncRes
.arrowArgs(at)
.map(nt => nt.name + ": " + typeToTs(nt.`type`))
.mkString(", ")
def argsCallToTs(at: ArrowType): String =
at.domain.toList.zipWithIndex.map(_._2).map(idx => s"args[$idx]").mkString(", ")
FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]").mkString(", ")
}

View File

@ -0,0 +1,20 @@
package aqua.backend.ts
import aqua.model.res.ServiceRes
case class TypeScriptService(srv: ServiceRes) {
import TypeScriptFunc.typeToTs
def generate: String =
s"""
|//${srv.name}
|//defaultId = ${srv.defaultId.getOrElse("undefined")}
|
|${srv.members.map { case (n, v) =>
s"//${n}: ${typeToTs(v)}"
}.mkString("\n")}
|//END ${srv.name}
|
|""".stripMargin
}

View File

@ -3,15 +3,16 @@ package aqua.compiler
import aqua.backend.Backend
import aqua.linker.Linker
import aqua.model.AquaContext
import aqua.model.res.AquaRes
import aqua.model.transform.GenerationConfig
import aqua.parser.lift.LiftParser
import aqua.semantics.Semantics
import cats.data.Validated.{validNec, Invalid, Valid}
import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec}
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.syntax.traverse._
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.traverse.*
import cats.{Comonad, Monad}
object AquaCompiler {
@ -47,7 +48,7 @@ object AquaCompiler {
}
.map(
_.map { ap =>
val compiled = backend.generate(ap.context, config)
val compiled = backend.generate(AquaRes.fromContext(ap.context, config))
AquaCompiled(ap.id, compiled)
}
)

View File

@ -0,0 +1,18 @@
package aqua.model.res
import aqua.model.AquaContext
import aqua.model.transform.{GenerationConfig, Transform}
import cats.data.Chain
case class AquaRes(funcs: Chain[FuncRes], services: Chain[ServiceRes]) {
def isEmpty: Boolean = funcs.isEmpty && services.isEmpty
}
object AquaRes {
def fromContext(ctx: AquaContext, conf: GenerationConfig): AquaRes =
AquaRes(
funcs = Chain.fromSeq(ctx.funcs.values.toSeq).map(Transform.fn(_, conf)),
services = Chain.fromSeq(ctx.services.values.toSeq).map(ServiceRes.fromModel(_))
)
}

View File

@ -0,0 +1,51 @@
package aqua.model.res
import aqua.model.func.FuncCallable
import aqua.model.func.resolved.ResolvedOp
import aqua.model.transform.GenerationConfig
import aqua.types.{ArrowType, Type}
import cats.data.Chain
import cats.free.Cofree
case class FuncRes(
source: FuncCallable,
conf: GenerationConfig,
body: Cofree[Chain, ResolvedOp]
) {
import FuncRes._
lazy val funcName = source.funcName
lazy val args: List[Arg] = arrowArgs(source.arrowType)
def argNames: List[String] = source.argNames
def relayVarName: Option[String] = conf.relayVarName
def dataServiceId: String = conf.getDataService
def callbackServiceId: String = conf.callbackService
def respFuncName: String = conf.respFuncName
def errorHandlerId: String = conf.errorHandlingService
def errorFuncName: String = conf.errorFuncName
def genArgName(basis: String): String = {
val forbidden = args.map(_._1).toSet
def genIter(i: Int): String = {
val n = if (i < 0) basis else basis + i
if (forbidden(n)) genIter(i + 1) else n
}
genIter(-1)
}
def returnType: Option[Type] = arrowToRes(source.arrowType)
}
object FuncRes {
case class Arg(name: String, `type`: Type)
def arrowArgs(at: ArrowType): List[Arg] = at.domain.toLabelledList().map(Arg(_, _))
def arrowArgIndices(at: ArrowType): List[Int] =
LazyList.from(0).take(at.domain.length).toList
def arrowToRes(at: ArrowType): Option[Type] =
if (at.codomain.length > 1) Some(at.codomain)
else at.codomain.uncons.map(_._1)
}

View File

@ -0,0 +1,20 @@
package aqua.model.res
import aqua.model.ServiceModel
import aqua.types.{ArrowType, ScalarType}
import aqua.model.LiteralModel
case class ServiceRes(name: String, members: List[(String, ArrowType)], defaultId: Option[String])
object ServiceRes {
def fromModel(sm: ServiceModel): ServiceRes =
ServiceRes(
name = sm.name,
members = sm.arrows.toNel.toList,
defaultId = sm.defaultId.collect {
case LiteralModel(value, t) if ScalarType.string.acceptsValueOf(t) =>
value
}
)
}

View File

@ -3,6 +3,7 @@ package aqua.model.transform
import aqua.model.func.FuncCallable
import aqua.model.VarModel
import aqua.model.func.resolved.{NoAir, ResolvedOp}
import aqua.model.res.FuncRes
import aqua.model.topology.Topology
import aqua.types.ScalarType
import cats.data.Chain
@ -22,7 +23,7 @@ object Transform extends Logging {
): Cofree[Chain, ResolvedOp] =
tree.copy(tail = tree.tail.map(_.filter(t => filter(t.head)).map(clear(_, filter))))
def forClient(func: FuncCallable, conf: GenerationConfig): Cofree[Chain, ResolvedOp] = {
def fn(func: FuncCallable, conf: GenerationConfig): FuncRes = {
val initCallable: InitPeerCallable = InitViaRelayCallable(
Chain.fromOption(conf.relayVarName).map(VarModel(_, ScalarType.string))
)
@ -48,6 +49,10 @@ object Transform extends Logging {
callback,
conf.respFuncName
)
FuncRes(
func,
conf,
clear(
Topology.resolve(
errorsCatcher
@ -57,5 +62,6 @@ object Transform extends Logging {
.tree
)
)
)
}
}

View File

@ -30,9 +30,9 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val bc = GenerationConfig()
val fc = Transform.forClient(func, bc)
val fc = Transform.fn(func, bc)
val procFC: Node.Res = fc
val procFC: Node.Res = fc.body
val expectedFC: Node.Res =
MakeRes.xor(
@ -80,9 +80,9 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val bc = GenerationConfig(wrapWithXor = false)
val fc = Transform.forClient(func, bc)
val fc = Transform.fn(func, bc)
val procFC: Res = fc
val procFC: Res = fc.body
val expectedFC: Res =
MakeRes.seq(
@ -141,7 +141,7 @@ class TransformSpec extends AnyFlatSpec with Matchers {
val bc = GenerationConfig(wrapWithXor = false)
val res = Transform.forClient(f2, bc): Node.Res
val res = Transform.fn(f2, bc).body: Node.Res
res.equalsOrPrintDiff(
MakeRes.seq(