Javascript backend (#161)

Add javascript backend
This commit is contained in:
Pavel 2021-06-10 13:43:46 +03:00 committed by GitHub
parent 4696e95129
commit 6522deccb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 3 deletions

View File

@ -0,0 +1,32 @@
package aqua.backend.js
import aqua.model.AquaContext
import aqua.model.transform.BodyConfig
import cats.data.Chain
case class JavaScriptFile(context: AquaContext) {
def funcs: Chain[JavaScriptFunc] =
Chain.fromSeq(context.funcs.values.toSeq).map(JavaScriptFunc(_))
def generateJS(conf: BodyConfig = BodyConfig()): String =
JavaScriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(conf)).toList.mkString("\n\n")
}
object JavaScriptFile {
val Header: String =
s"""/**
| *
| * This file is auto-generated. Do not edit manually: changes may be erased.
| * Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
| * If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
| * Aqua version: ${Option(getClass.getPackage.getImplementationVersion)
.filter(_.nonEmpty)
.getOrElse("Unknown")}
| *
| */
|import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable';
|""".stripMargin
}

View File

@ -0,0 +1,98 @@
package aqua.backend.js
import aqua.backend.air.FuncAirGen
import aqua.model.func.{ArgDef, FuncCallable}
import aqua.model.transform.BodyConfig
import aqua.types._
import cats.syntax.functor._
import cats.syntax.show._
case class JavaScriptFunc(func: FuncCallable) {
import JavaScriptFunc._
def argsJavaScript: String =
func.args.args.map(ad => s"${ad.name}").mkString(", ")
def generateTypescript(conf: BodyConfig = BodyConfig()): String = {
val tsAir = FuncAirGen(func).generateClientAir(conf)
val returnCallback = func.ret.as {
s"""h.onEvent('${conf.callbackService}', '${conf.respFuncName}', (args) => {
| const [res] = args;
| resolve(res);
|});
|""".stripMargin
}
val setCallbacks = func.args.args.map {
case ArgDef.Data(argName, OptionType(_)) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName === null ? [] : [$argName];});"""
case ArgDef.Data(argName, _) =>
s"""h.on('${conf.getDataService}', '$argName', () => {return $argName;});"""
case ArgDef.Arrow(argName, at) =>
val value = s"$argName(${argsCallToJs(
at
)})"
val expr = at.res.fold(s"$value; return {}")(_ => s"return $value")
s"""h.on('${conf.callbackService}', '$argName', (args) => {$expr;});"""
}.mkString("\n")
val returnVal =
func.ret.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise")
s"""
|export async function ${func.funcName}(client${if (func.args.isEmpty) ""
else ", "}${argsJavaScript}) {
| let request;
| const promise = new Promise((resolve, reject) => {
| request = new RequestFlowBuilder()
| .disableInjections()
| .withRawScript(
| `
|${tsAir.show}
| `,
| )
| .configHandler((h) => {
| ${conf.relayVarName.fold("") { r =>
s"""h.on('${conf.getDataService}', '$r', () => {
| return client.relayPeerId;
| });""".stripMargin
}}
| $setCallbacks
| ${returnCallback.getOrElse("")}
| h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => {
| // assuming error is the single argument
| const [err] = args;
| reject(err);
| });
| })
| .handleScriptError(reject)
| .handleTimeout(() => {
| reject('Request timed out for ${func.funcName}');
| })
| .build();
| });
| await client.initiateFlow(request);
| return ${returnVal};
|}
""".stripMargin
}
}
object JavaScriptFunc {
def argsToTs(at: ArrowType): String =
at.args
.zipWithIndex
.map(_.swap)
.map(kv => "arg" + kv._1)
.mkString(", ")
def argsCallToJs(at: ArrowType): String =
at.args.zipWithIndex.map(_._2).map(idx => s"args[$idx]").mkString(", ")
}

View File

@ -55,7 +55,7 @@ lazy val cli = project
"com.monovore" %% "decline-enumeratum" % declineEnumV
)
)
.dependsOn(semantics, `backend-air`, `backend-ts`, linker)
.dependsOn(semantics, `backend-air`, `backend-ts`, `backend-js`, linker)
lazy val types = project
.settings(commons)
@ -112,3 +112,8 @@ lazy val `backend-ts` = project
.in(file("backend/ts"))
.settings(commons: _*)
.dependsOn(`backend-air`)
lazy val `backend-js` = project
.in(file("backend/js"))
.settings(commons: _*)
.dependsOn(`backend-air`)

View File

@ -97,6 +97,12 @@ object AppOps {
.map(_ => true)
.withDefault(false)
val compileToJs: Opts[Boolean] =
Opts
.flag("js", "Generate .js file instead of typescript")
.map(_ => true)
.withDefault(false)
val noRelay: Opts[Boolean] =
Opts
.flag("no-relay", "Do not generate a pass through the relay node")

View File

@ -37,19 +37,20 @@ object AquaCli extends IOApp with LogSupport {
importOpts,
outputOpts,
compileToAir,
compileToJs,
noRelay,
noXorWrapper,
wrapWithOption(helpOpt),
wrapWithOption(versionOpt),
logLevelOpt
).mapN { case (input, imports, output, toAir, noRelay, noXor, h, v, logLevel) =>
).mapN { case (input, imports, output, toAir, toJs, noRelay, noXor, h, v, logLevel) =>
WLogger.setDefaultLogLevel(LogLevel.toLogLevel(logLevel))
WLogger.setDefaultFormatter(CustomLogFormatter)
// if there is `--help` or `--version` flag - show help and version
// otherwise continue program execution
h.map(_ => helpAndExit) orElse v.map(_ => versionAndExit) getOrElse {
val target = if (toAir) AquaCompiler.AirTarget else AquaCompiler.TypescriptTarget
val target = if (toAir) AquaCompiler.AirTarget else if (toJs) AquaCompiler.JavaScriptTarget else AquaCompiler.TypescriptTarget
val bc = {
val bc = BodyConfig(wrapWithXor = !noXor)
bc.copy(relayVarName = bc.relayVarName.filterNot(_ => noRelay))

View File

@ -2,6 +2,7 @@ package aqua
import aqua.backend.air.FuncAirGen
import aqua.backend.ts.TypescriptFile
import aqua.backend.js.JavaScriptFile
import aqua.io.{AquaFileError, AquaFiles, FileModuleId, Unresolvable}
import aqua.linker.Linker
import aqua.model.AquaContext
@ -24,6 +25,7 @@ import java.nio.file.Path
object AquaCompiler extends LogSupport {
sealed trait CompileTarget
case object TypescriptTarget extends CompileTarget
case object JavaScriptTarget extends CompileTarget
case object AirTarget extends CompileTarget
case class Prepared(modFile: Path, srcPath: Path, targetPath: Path, context: AquaContext) {
@ -152,6 +154,25 @@ object AquaCompiler extends LogSupport {
}
case JavaScriptTarget =>
preps.map { p =>
p.targetPath("js") match {
case Invalid(t) =>
EitherT.pure(t.getMessage)
case Valid(tp) =>
writeFile(tp, JavaScriptFile(p.context).generateJS(bodyConfig)).flatTap { _ =>
EitherT.pure(
Validated.catchNonFatal(
info(
s"Result ${tp.toAbsolutePath}: compilation OK (${p.context.funcs.size} functions)"
)
)
)
}
}
}
// TODO add function name to AirTarget class
case AirTarget =>
preps