diff --git a/.github/workflows/test_branch.yml b/.github/workflows/test_branch.yml index 0777267a..2a08df71 100644 --- a/.github/workflows/test_branch.yml +++ b/.github/workflows/test_branch.yml @@ -51,13 +51,8 @@ jobs: run: | git clone https://github.com/fluencelabs/aqua-playground.git cd aqua-playground - rm -rf src/compiled/examples/* npm i cd .. - sbt "cli/run -i aqua-playground/aqua/examples -o aqua-playground/src/compiled/examples -m aqua-playground/node_modules -c \"UNIQUE_CONST = 1\" -c \"ANOTHER_CONST = \\\"ab\\\"\"" - cd aqua-playground - npm run examples - cd .. sbt "cliJS/fastOptJS" rm -rf aqua-playground/src/compiled/examples/* node cli/.js/target/scala-3.0.2/cli-fastopt.js -i aqua-playground/aqua/examples -o aqua-playground/src/compiled/examples -m aqua-playground/node_modules -c "UNIQUE_CONST = 1" -c "ANOTHER_CONST = \"ab\"" diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala deleted file mode 100644 index a5f30ba9..00000000 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala +++ /dev/null @@ -1,13 +0,0 @@ -package aqua.backend.js - -import aqua.backend.{Backend, Generated} -import aqua.model.transform.res.AquaRes -import cats.data.NonEmptyChain - -object JavaScriptBackend extends Backend { - - val ext = ".js" - - override def generate(res: AquaRes): Seq[Generated] = - if (res.isEmpty) Nil else Generated(ext, JavaScriptFile(res).generate) :: Nil -} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala deleted file mode 100644 index 0525ae2a..00000000 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptCommon.scala +++ /dev/null @@ -1,65 +0,0 @@ -package aqua.backend.js - -import aqua.backend.air.FuncAirGen -import aqua.model.transform.res.FuncRes -import aqua.types.* -import cats.syntax.show.* - -object JavaScriptCommon { - - // TODO: handle cases if there is already peer_ or config_ variable defined - def fixupArgName(arg: String): String = - if (arg == "peer" || arg == "config") { - arg + "_" - } else { - arg - } - - def callBackExprBody(at: ArrowType, callbackName: String): String = { - val arrowArgumentsToCallbackArgumentsList = - at.domain.toList.zipWithIndex - .map((`type`, idx) => { - val valueFromArg = s"req.args[$idx]" - `type` match { - case OptionType(t) => - s"${valueFromArg}.length === 0 ? null : ${valueFromArg}[0]" - case _ => valueFromArg - } - }) - .concat(List("callParams")) - .mkString(", ") - - val callCallbackStatement = s"$callbackName($arrowArgumentsToCallbackArgumentsList)" - - val callCallbackStatementAndReturn = - at.res.fold(s"${callCallbackStatement}; resp.result = {}")(`type` => - `type` match { - case OptionType(t) => s""" - | var respResult = ${callCallbackStatement}; - | resp.result = respResult === null ? [] : [respResult] - |""".stripMargin - case _ => s"resp.result = ${callCallbackStatement}" - } - ) - - val tetraplets = FuncRes - .arrowArgs(at) - .zipWithIndex - .map((x, idx) => { - s"${x.name}: req.tetraplets[${idx}]" - }) - .mkString(",") - - s""" - | const callParams = { - | ...req.particleContext, - | tetraplets: { - | ${tetraplets} - | }, - | }; - | resp.retCode = ResultCodes.success; - | ${callCallbackStatementAndReturn} - |""".stripMargin - } - -} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala deleted file mode 100644 index 0367ee75..00000000 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala +++ /dev/null @@ -1,46 +0,0 @@ -package aqua.backend.js - -import aqua.backend.Version -import aqua.model.transform.res.AquaRes - -case class JavaScriptFile(res: AquaRes) { - - import JavaScriptFile.Header - - def generate: String = - s"""${Header} - | - |function missingFields(obj, fields) { - | return fields.filter(f => !(f in obj)) - |} - | - |// Services - |${res.services.map(JavaScriptService(_)).map(_.generate).toList.mkString("\n\n")} - | - |// Functions - |${res.funcs.map(JavaScriptFunc(_)).map(_.generate).toList.mkString("\n\n")} - |""".stripMargin - -} - -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: ${Version.version} - | * - | */ - |import { Fluence, FluencePeer } from '@fluencelabs/fluence'; - |import { - | ResultCodes, - | RequestFlow, - | RequestFlowBuilder, - | CallParams, - |} from '@fluencelabs/fluence/dist/internal/compilerSupport/v1.js'; - |""".stripMargin - -} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala deleted file mode 100644 index 55ff527b..00000000 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala +++ /dev/null @@ -1,138 +0,0 @@ -package aqua.backend.js - -import aqua.backend.air.FuncAirGen -import aqua.model.transform.res.FuncRes -import aqua.types.* -import cats.syntax.show.* - -case class JavaScriptFunc(func: FuncRes) { - - import JavaScriptCommon._ - import FuncRes._ - import func._ - - private def returnCallback: String = - val respBody = func.returnType match { - case Some(x) => x match { - case OptionType(_) => - """ let [opt] = args; - | if (Array.isArray(opt)) { - | if (opt.length === 0) { resolve(null); } - | opt = opt[0]; - | } - | return resolve(opt);""".stripMargin - case pt: ProductType => - val unwrapOpts = pt.toList.zipWithIndex.collect { case (OptionType(_), i) => - s""" - | if(Array.isArray(opt[$i])) { - | if (opt[$i].length === 0) { opt[$i] = null; } - | else {opt[$i] = opt[$i][0]; } - | }""".stripMargin - }.mkString - - s""" let opt = args; - |$unwrapOpts - | return resolve(opt);""".stripMargin - case _ => - """ const [res] = args; - | resolve(res);""".stripMargin - } - case None => "" - } - s"""h.onEvent('$callbackServiceId', '$respFuncName', (args) => { - | $respBody - |}); - |""".stripMargin - - def generate: String = { - - val jsAir = FuncAirGen(func).generate - - val setCallbacks = func.args.collect { // Product types are not handled - case Arg(argName, OptionType(_)) => - s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)} === null ? [] : [${fixupArgName(argName)}];});""" - case Arg(argName, _: DataType) => - s"""h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)};});""" - case Arg(argName, at: ArrowType) => - s""" - | h.use((req, resp, next) => { - | if(req.serviceId === '${conf.callbackService}' && req.fnName === '$argName') { - | ${callBackExprBody(at, argName)} - | } - | next(); - | }); - """.stripMargin - } - .mkString("\n") - - val returnVal = - func.returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") - - val clientArgName = genArgName("client") - val configArgName = genArgName("config") - - val funcName = s"${func.funcName}" - - val argsLets = args.map(arg => s"let ${fixupArgName(arg.name)};").mkString("\n") - - val argsFormAssingment = args - .map(arg => fixupArgName(arg.name)) - .concat(List("config")) - .zipWithIndex - - // Argument unpacking has two forms: - // One starting from the first (by index) argument, - // One starting from zero - val argsAssignmentStartingFrom1 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix + 1}];").mkString("\n") - val argsAssignmentStartingFrom0 = argsFormAssingment.map((name, ix) => s"${name} = args[${ix}];").mkString("\n") - - s""" - | export function ${func.funcName}(...args) { - | let peer; - | ${argsLets} - | let config; - | if (FluencePeer.isInstance(args[0])) { - | peer = args[0]; - | ${argsAssignmentStartingFrom1} - | } else { - | peer = Fluence.getPeer(); - | ${argsAssignmentStartingFrom0} - | } - | - | let request; - | const promise = new Promise((resolve, reject) => { - | const r = new RequestFlowBuilder() - | .disableInjections() - | .withRawScript( - | ` - | ${jsAir.show} - | `, - | ) - | .configHandler((h) => { - | ${conf.relayVarName.fold("") { r => - s"""h.on('${conf.getDataService}', '$r', () => { - | return peer.getStatus().relayPeerId; - | });""".stripMargin }} - | $setCallbacks - | $returnCallback - | h.onEvent('${conf.errorHandlingService}', '${conf.errorFuncName}', (args) => { - | const [err] = args; - | reject(err); - | }); - | }) - | .handleScriptError(reject) - | .handleTimeout(() => { - | reject('Request timed out for ${func.funcName}'); - | }) - | if(${configArgName} && ${configArgName}.ttl) { - | r.withTTL(${configArgName}.ttl) - | } - | request = r.build(); - | }); - | peer.internals.initiateFlow(request); - | return ${returnVal}; - |} - """.stripMargin - } - -} diff --git a/backend/ts/src/main/scala/aqua/backend/Header.scala b/backend/ts/src/main/scala/aqua/backend/Header.scala new file mode 100644 index 00000000..077606d7 --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/Header.scala @@ -0,0 +1,22 @@ +package aqua.backend + +object Header { + + def header(isJs: Boolean): 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: ${Version.version} + | * + | */ + |import { Fluence, FluencePeer } from '@fluencelabs/fluence'; + |import { + | ResultCodes, + | RequestFlow, + | RequestFlowBuilder, + | CallParams, + |} from '@fluencelabs/fluence/dist/internal/compilerSupport/v1${if (isJs) ".js" else ""}'; + |""".stripMargin +} diff --git a/backend/ts/src/main/scala/aqua/backend/OutputFile.scala b/backend/ts/src/main/scala/aqua/backend/OutputFile.scala new file mode 100644 index 00000000..98df1f07 --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/OutputFile.scala @@ -0,0 +1,33 @@ +package aqua.backend + +import aqua.backend.ts.{TSFuncTypes, TSServiceTypes} +import aqua.backend.{Header, OutputService} +import aqua.model.transform.res.AquaRes + +case class OutputFile(res: AquaRes) { + + def generate(types: Types): String = { + import types.* + val services = res.services + .map(s => OutputService(s, types)) + .map(_.generate) + .toList + .mkString("\n\n") + val functions = + res.funcs.map(f => OutputFunc(f, types)).map(_.generate).toList.mkString("\n\n") + s"""${Header.header(false)} + | + |function ${typed( + s"""missingFields(${typed("obj", "any")}, ${typed("fields", "string[]")})""", + "string[]")} { + | return fields.filter(f => !(f in obj)) + |} + | + |// Services + |$services + |// Functions + |$functions + |""".stripMargin + } + +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala b/backend/ts/src/main/scala/aqua/backend/OutputFunc.scala similarity index 74% rename from backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala rename to backend/ts/src/main/scala/aqua/backend/OutputFunc.scala index 89215e3a..ec7117ff 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala +++ b/backend/ts/src/main/scala/aqua/backend/OutputFunc.scala @@ -1,17 +1,28 @@ -package aqua.backend.ts +package aqua.backend import aqua.backend.air.FuncAirGen +import aqua.backend.ts.TypeScriptCommon.{callBackExprBody, fixupArgName} +import aqua.backend.ts.{TSFuncTypes, TypeScriptCommon} import aqua.model.transform.res.FuncRes -import aqua.types.* +import aqua.model.transform.res.FuncRes.Arg +import aqua.types.{ArrowType, DataType, OptionType, ProductType} import cats.syntax.show.* -case class TypeScriptFunc(func: FuncRes) { +case class OutputFunc(func: FuncRes, types: Types) { - import TypeScriptCommon._ - import FuncRes._ - import func._ + import FuncRes.* + import TypeScriptCommon.* + import types.* + import func.* + val funcTypes = types.funcType(func) + import funcTypes.* - private def returnCallback: String = + val argsFormAssingment = args + .map(arg => fixupArgName(arg.name)) + .appended("config") + .zipWithIndex + + private def returnCallback: String = val respBody = func.returnType match { case Some(x) => x match { case OptionType(_) => @@ -29,7 +40,7 @@ case class TypeScriptFunc(func: FuncRes) { | }""".stripMargin }.mkString - s""" let opt: any = args; + s""" let ${typed("opt", "any")} = args; |$unwrapOpts | return resolve(opt);""".stripMargin case _ => @@ -46,9 +57,6 @@ case class TypeScriptFunc(func: FuncRes) { val tsAir = FuncAirGen(func).generate - val retTypeTs = func.returnType - .fold("void")(typeToTs) - val setCallbacks = func.args.collect { // Product types are not handled case Arg(argName, OptionType(_)) => s""" h.on('$dataServiceId', '$argName', () => {return ${fixupArgName(argName)} === null ? [] : [${fixupArgName(argName)}];});""" @@ -68,45 +76,23 @@ case class TypeScriptFunc(func: FuncRes) { val returnVal = func.returnType.fold("Promise.race([promise, Promise.resolve()])")(_ => "promise") - val clientArgName = genArgName("client") val configArgName = genArgName("config") - val configType = "{ttl?: number}" + val codeLeftSpace = " " * 20 - val funcName = s"${func.funcName}" + val argsLets = args.map(arg => s" let ${typed(fixupArgName(arg.name), "any")};").mkString("\n") - val argsTypescript = args - .map(arg => s"${fixupArgName(arg.name)}: " + typeToTs(arg.`type`)) - .concat(List(s"config?: $configType")) - - // defines different types for overloaded service registration function. - var funcTypeOverload1 = argsTypescript.mkString(", ") - var funcTypeOverload2 = ("peer: FluencePeer" :: argsTypescript).mkString(", ") - - val argsLets = args.map(arg => s" let ${fixupArgName(arg.name)}: any;").mkString("\n") - - val argsFormAssingment = args - .map(arg => fixupArgName(arg.name)) - .concat(List("config")) - .zipWithIndex - - // argument upnacking has two forms. + // argument upnacking has two forms. // One starting from the first (by index) argument, // One starting from zero val argsAssignmentStartingFrom1 = argsFormAssingment.map((name, ix) => s" ${name} = args[${ix + 1}];").mkString("\n") val argsAssignmentStartingFrom0 = argsFormAssingment.map((name, ix) => s" ${name} = args[${ix}];").mkString("\n") - val funcTypeRes = s"Promise<$retTypeTs>" - - val codeLeftSpace = " " * 20 - - s""" - |export function ${func.funcName}(${funcTypeOverload1}) : ${funcTypeRes}; - |export function ${func.funcName}(${funcTypeOverload2}) : ${funcTypeRes}; - |export function ${func.funcName}(...args: any) { - | let peer: FluencePeer; + s"""${funcTypes.generate} + |export function ${func.funcName}(${typed("...args", "any")}) { + | let ${typed("peer", "FluencePeer")}; |${argsLets} - | let config: any; + | let ${typed("config", "any")}; | if (FluencePeer.isInstance(args[0])) { | peer = args[0]; |${argsAssignmentStartingFrom1} @@ -115,8 +101,8 @@ case class TypeScriptFunc(func: FuncRes) { |${argsAssignmentStartingFrom0} | } | - | let request: RequestFlow; - | const promise = new Promise<$retTypeTs>((resolve, reject) => { + | let ${typed("request", "RequestFlow")}; + | const promise = new ${generic("Promise", retTypeTs._2)}((resolve, reject) => { | const r = new RequestFlowBuilder() | .disableInjections() | .withRawScript(` @@ -146,7 +132,7 @@ case class TypeScriptFunc(func: FuncRes) { | | request = r.build(); | }); - | peer.internals.initiateFlow(request!); + | peer.internals.initiateFlow(${bang("request")}); | return ${returnVal}; |}""".stripMargin } diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala b/backend/ts/src/main/scala/aqua/backend/OutputService.scala similarity index 63% rename from backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala rename to backend/ts/src/main/scala/aqua/backend/OutputService.scala index 6125d10a..f78ce1ac 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptService.scala +++ b/backend/ts/src/main/scala/aqua/backend/OutputService.scala @@ -1,21 +1,21 @@ -package aqua.backend.js +package aqua.backend -import aqua.backend.air.FuncAirGen -import aqua.model.transform.res.FuncRes -import aqua.types.* -import cats.syntax.show.* +import aqua.backend.ts.TypeScriptCommon.callBackExprBody +import aqua.backend.ts.TypeScriptCommon import aqua.model.transform.res.ServiceRes +import aqua.types.ArrowType -case class JavaScriptService(srv: ServiceRes) { +case class OutputService(srv: ServiceRes, types: Types) { - import JavaScriptCommon._ + import TypeScriptCommon.* + import types.* + val serviceTypes = types.serviceType(srv) + import serviceTypes.* def fnHandler(arrow: ArrowType, memberName: String) = { - s""" - | if (req.fnName === '${memberName}') { - | ${callBackExprBody(arrow, "service." + memberName)} - | } - """.stripMargin + s"""if (req.fnName === '${memberName}') { + |${callBackExprBody(arrow, "service." + memberName, 12)} + }""".stripMargin } def generate: String = @@ -25,22 +25,21 @@ case class JavaScriptService(srv: ServiceRes) { } .mkString("\n\n") - val registerName = s"register${srv.name}" - - val defaultServiceIdBranch = srv.defaultId.fold("")(x => - s""" - | else { - | serviceId = ${x} - |}""".stripMargin + val defaultServiceIdBranch = srv.defaultId.fold("")(x => + s"""else { + | serviceId = ${x} + | }""".stripMargin ) val membersNames = srv.members.map(_._1) s""" - |export function ${registerName}(...args) { - | let peer; - | let serviceId; - | let service; + |${serviceTypes.generate} + | + |export function register${srv.name}(${typed("...args", "any")}) { + | let ${typed("peer", "FluencePeer")}; + | let ${typed("serviceId", "any")}; + | let ${typed("service", "any")}; | if (FluencePeer.isInstance(args[0])) { | peer = args[0]; | } else { @@ -66,21 +65,21 @@ case class JavaScriptService(srv: ServiceRes) { | service = args[2]; | } | - | const incorrectServiceDefinitions = missingFields(service, [${membersNames.map { n => s"'$n'"}.mkString(", ")}]); - | if (!incorrectServiceDefinitions.length) { + | const incorrectServiceDefinitions = missingFields(service, [${membersNames.map { n => s"'$n'" }.mkString(", ")}]); + | if (!!incorrectServiceDefinitions.length) { | throw new Error("Error registering service ${srv.name}: missing functions: " + incorrectServiceDefinitions.map((d) => "'" + d + "'").join(", ")) | } | | peer.internals.callServiceHandler.use((req, resp, next) => { | if (req.serviceId !== serviceId) { | next(); - | return; - | } + | return; + | } | - |${fnHandlers} + | ${fnHandlers} | - | next(); - | }); - | } + | next(); + | }); + |} """.stripMargin } diff --git a/backend/ts/src/main/scala/aqua/backend/Types.scala b/backend/ts/src/main/scala/aqua/backend/Types.scala new file mode 100644 index 00000000..f64e514e --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/Types.scala @@ -0,0 +1,36 @@ +package aqua.backend + +import aqua.backend.ts.{TSFuncTypes, TSServiceTypes} +import aqua.model.transform.res.{FuncRes, ServiceRes} + +trait Types { + def typed(field: String, `type`: String): String + def generic(field: String, `type`: String): String + def bang(field: String): String + def funcType(f: FuncRes): FuncTypes + def serviceType(s: ServiceRes): ServiceTypes +} + +trait FuncTypes { + def retTypeTs: (Option[String], String) + def generate: String +} + +trait ServiceTypes { + def generate: String +} + +object EmptyTypes extends Types { + override def typed(field: String, `type`: String): String = field + override def generic(field: String, `type`: String): String = field + override def bang(field: String): String = field + override def funcType(f: FuncRes): FuncTypes = new FuncTypes { + override def retTypeTs: (Option[String], String) = (None, "") + override def generate: String = "" + } + override def serviceType(s: ServiceRes): ServiceTypes = new ServiceTypes { + override def generate: String = "" + } +} + + diff --git a/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala b/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala new file mode 100644 index 00000000..e48709aa --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/js/JavaScriptBackend.scala @@ -0,0 +1,38 @@ +package aqua.backend.js + +import aqua.backend.ts.TypeScriptTypes +import aqua.backend.{Backend, EmptyTypes, Generated, Header, OutputFile, OutputFunc, OutputService} +import aqua.model.transform.res.AquaRes + +object JavaScriptBackend extends Backend { + + val ext = ".js" + val tsExt = ".d.ts" + + def typesFile(res: AquaRes): Generated = { + val services = res.services + .map(s => TypeScriptTypes.serviceType(s)) + .map(_.generate) + .toList + .mkString("\n\n") + val functions = + res.funcs.map(f => TypeScriptTypes.funcType(f)).map(_.generate).toList.mkString("\n\n") + + val body = s"""${Header.header(false)} + | + |// Services + |$services + | + |// Functions + |$functions + |""".stripMargin + + Generated(tsExt, body) + } + + override def generate(res: AquaRes): Seq[Generated] = + if (res.isEmpty) Nil + else { + Generated(ext, OutputFile(res).generate(EmptyTypes)):: typesFile(res) :: Nil + } +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TSFuncTypes.scala b/backend/ts/src/main/scala/aqua/backend/ts/TSFuncTypes.scala new file mode 100644 index 00000000..c112bc3c --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TSFuncTypes.scala @@ -0,0 +1,37 @@ +package aqua.backend.ts + +import aqua.backend.FuncTypes +import aqua.backend.ts.TypeScriptCommon.{fixupArgName, typeToTs, genTypeName} +import aqua.model.transform.res.FuncRes +import aqua.types.* + +case class TSFuncTypes(func: FuncRes) extends FuncTypes { + import TypeScriptTypes._ + + override val retTypeTs = func.returnType + .fold((None, "void")) { t => genTypeName(t, func.funcName.capitalize + "Result") } + + override def generate = { + val configType = "?: {ttl?: number}" + + val argsTypescript = func.args + .map { arg => + val (typeDesc, t) = genTypeName(arg.`type`, func.funcName.capitalize + "Arg" + arg.name.capitalize) + (typeDesc, s"${typed(fixupArgName(arg.name), t)}") + } :+ (None, s"config$configType") + + val args = argsTypescript.map(_._2) + val argsDesc = argsTypescript.map(_._1).flatten + + // defines different types for overloaded service registration function. + var funcTypeOverload1 = args.mkString(", ") + var funcTypeOverload2 = (typed("peer", "FluencePeer") :: args).mkString(", ") + + val (resTypeDesc, resType) = retTypeTs + + s"""${argsDesc.mkString("\n")} + |${resTypeDesc.getOrElse("")} + |export function ${func.funcName}(${funcTypeOverload1}): ${generic("Promise", resType)}; + |export function ${func.funcName}(${funcTypeOverload2}): ${generic("Promise", resType)};""".stripMargin + } +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TSServiceTypes.scala b/backend/ts/src/main/scala/aqua/backend/ts/TSServiceTypes.scala new file mode 100644 index 00000000..3fb3d92c --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TSServiceTypes.scala @@ -0,0 +1,68 @@ +package aqua.backend.ts + +import aqua.backend.ServiceTypes +import aqua.backend.ts.TypeScriptCommon.fnDef +import aqua.model.transform.res.ServiceRes + +case class TSServiceTypes(srv: ServiceRes) extends ServiceTypes { + import TypeScriptTypes._ + + private val serviceTypeName = s"${srv.name}Def"; + + def registerServiceArgs = { + + // defined arguments used in overloads below + val peerDecl = s"${typed("peer", "FluencePeer")}"; + val serviceIdDecl = s"${typed("serviceId", "string")}"; + val serviceDecl = s"${typed("service", serviceTypeName)}" + + // Service registration functions has several overloads. + // Depending on whether the the service has the default id or not + // there would be different number of overloads + // This variable contain defines the list of lists where + // the outmost list describes the list of overloads + // and the innermost one defines the list of arguments in the overload + val registerServiceArgsSource = srv.defaultId.fold( + List( + List(serviceIdDecl, serviceDecl), + List(peerDecl, serviceIdDecl, serviceDecl) + ) + )(_ => + List( + List(serviceDecl), + List(serviceIdDecl, serviceDecl), + List(peerDecl, serviceDecl), + List(peerDecl, serviceIdDecl, serviceDecl) + ) + ) + + // Service registration functions has several overloads. + // Depending on whether the the service has the default id or not + // there would be different number of overloads + // This variable contain defines the list of lists where + // the outmost list describes the list of overloads + // and the innermost one defines the list of arguments in the overload + registerServiceArgsSource.map { x => + val args = x.mkString(", ") + s"export function register${srv.name}(${args}): void;" + } + .mkString("\n") + } + + def exportInterface = { + val fnDefs = srv.members.map { case (name, arrow) => + s"${typed(name, fnDef(arrow))};" + } + .mkString("\n") + + s"""export interface ${serviceTypeName} { + | ${fnDefs} + |}""".stripMargin + } + + def generate = { + s"""$exportInterface + |$registerServiceArgs + """ + } +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala index cfbdf74f..3cfe914d 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala @@ -1,6 +1,6 @@ package aqua.backend.ts -import aqua.backend.{Backend, Generated} +import aqua.backend.{Backend, Generated, OutputFile} import aqua.model.transform.res.AquaRes import cats.data.NonEmptyChain @@ -9,5 +9,5 @@ object TypeScriptBackend extends Backend { val ext = ".ts" override def generate(res: AquaRes): Seq[Generated] = - if (res.isEmpty) Nil else Generated(ext, TypeScriptFile(res).generate) :: Nil + if (res.isEmpty) Nil else Generated(ext, OutputFile(res).generate(TypeScriptTypes)) :: Nil } diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala index f1d3b4b6..dd66175e 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptCommon.scala @@ -7,6 +7,20 @@ import cats.syntax.show.* object TypeScriptCommon { + def genTypeName(t: Type, name: String): (Option[String], String) = { + val genType = typeToTs(t) + t match { + case tt: ProductType => + val gen = s"export type $name = $genType" + (Some(gen), name) + case tt: StructType => + val gen = s"export type $name = $genType" + (Some(gen), name) + case _ => (None, genType) + + } + } + def typeToTs(t: Type): String = t match { case OptionType(t) => typeToTs(t) + " | null" case ArrayType(t) => typeToTs(t) + "[]" @@ -14,7 +28,7 @@ object TypeScriptCommon { case pt: ProductType => "[" + pt.toList.map(typeToTs).mkString(", ") + "]" case st: StructType => - s"{ ${st.fields.map(typeToTs).toNel.map(kv => kv._1 + ": " + kv._2).toList.mkString("; ")} }" + s"{ ${st.fields.map(typeToTs).toNel.map(kv => kv._1 + ": " + kv._2 + ";").toList.mkString(" ")} }" case st: ScalarType if ScalarType.number(st) => "number" case ScalarType.bool => "boolean" case ScalarType.string => "string" @@ -35,10 +49,9 @@ object TypeScriptCommon { def returnType(at: ArrowType): String = at.res.fold("void")(typeToTs) - + def fnDef(at: ArrowType): String = - val args = argsToTs(at) - .concat(List(callParamsArg(at))) + val args = (argsToTs(at) :+ callParamsArg(at)) .mkString(", ") val retType = returnType(at) @@ -63,9 +76,6 @@ object TypeScriptCommon { } s"callParams: CallParams<${generic}>" - def argsCallToTs(at: ArrowType): List[String] = - FuncRes.arrowArgIndices(at).map(idx => s"args[$idx]") - def callBackExprBody(at: ArrowType, callbackName: String, leftSpace: Int): String = { val arrowArgumentsToCallbackArgumentsList = at.domain.toList diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala deleted file mode 100644 index fa5894e5..00000000 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala +++ /dev/null @@ -1,45 +0,0 @@ -package aqua.backend.ts - -import aqua.backend.Version -import aqua.model.transform.res.AquaRes - -case class TypeScriptFile(res: AquaRes) { - - import TypeScriptFile.Header - - def generate: String = - s"""${Header} - | - |function missingFields(obj: any, fields: string[]): string[] { - | return fields.filter(f => !(f in obj)) - |} - | - |// Services - |${res.services.map(TypeScriptService(_)).map(_.generate).toList.mkString("\n\n")} - |// Functions - |${res.funcs.map(TypeScriptFunc(_)).map(_.generate).toList.mkString("\n\n")} - |""".stripMargin - -} - -object TypeScriptFile { - - 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: ${Version.version} - | * - | */ - |import { Fluence, FluencePeer } from '@fluencelabs/fluence'; - |import { - | ResultCodes, - | RequestFlow, - | RequestFlowBuilder, - | CallParams, - |} from '@fluencelabs/fluence/dist/internal/compilerSupport/v1'; - |""".stripMargin - -} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala deleted file mode 100644 index c7dcbff7..00000000 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptService.scala +++ /dev/null @@ -1,133 +0,0 @@ -package aqua.backend.ts - -import aqua.backend.air.FuncAirGen -import aqua.model.transform.res.FuncRes -import aqua.types.* -import cats.syntax.show.* -import aqua.model.transform.res.ServiceRes - -case class TypeScriptService(srv: ServiceRes) { - - import TypeScriptCommon._ - - def fnHandler(arrow: ArrowType, memberName: String) = { - s"""if (req.fnName === '${memberName}') { - |${callBackExprBody(arrow, "service." + memberName, 12)} - }""".stripMargin - } - - def generate: String = - val fnHandlers = srv.members - .map{ case (name, arrow) => - fnHandler(arrow, name) - } - .mkString("\n\n") - - val fnDefs = srv.members - .map{ case (name, arrow) => - s"${name}: ${fnDef(arrow)};" - } - .mkString("\n") - - val serviceTypeName = s"${srv.name}Def"; - - val registerName = s"register${srv.name}" - - // defined arguments used in overloads below - val peerDecl = "peer: FluencePeer"; - val serviceIdDecl = "serviceId: string"; - val serviceDecl = s"service: ${serviceTypeName}" - - // Service registration functions has several overloads. - // Depending on whether the the service has the default id or not - // there would be different number of overloads - // This variable contain defines the list of lists where - // the outmost list describes the list of overloads - // and the innermost one defines the list of arguments in the overload - val registerServiceArgsSource = srv.defaultId.fold( - List( - List(serviceIdDecl, serviceDecl), - List(peerDecl, serviceIdDecl, serviceDecl) - ) - )(_ => - List( - List(serviceDecl), - List(serviceIdDecl, serviceDecl), - List(peerDecl, serviceDecl), - List(peerDecl, serviceIdDecl, serviceDecl), - ) - ) - - // Service registration functions has several overloads. - // Depending on whether the the service has the default id or not - // there would be different number of overloads - // This variable contain defines the list of lists where - // the outmost list describes the list of overloads - // and the innermost one defines the list of arguments in the overload - val registerServiceArgs = registerServiceArgsSource.map{ x => - val args = x.mkString(", ") - s"export function ${registerName}(${args}): void;" - } - .mkString("\n"); - - val defaultServiceIdBranch = srv.defaultId.fold("")(x => - s"""else { - | serviceId = ${x} - | }""".stripMargin - ) - - val membersNames = srv.members.map(_._1) - - s""" - |export interface ${serviceTypeName} { - | ${fnDefs} - |} - | - |$registerServiceArgs - |export function ${registerName}(...args: any) { - | let peer: FluencePeer; - | let serviceId: any; - | let service: any; - | if (FluencePeer.isInstance(args[0])) { - | peer = args[0]; - | } else { - | peer = Fluence.getPeer(); - | } - | - | if (typeof args[0] === 'string') { - | serviceId = args[0]; - | } else if (typeof args[1] === 'string') { - | serviceId = args[1]; - | } ${defaultServiceIdBranch} - | - | // Figuring out which overload is the service. - | // If the first argument is not Fluence Peer and it is an object, then it can only be the service def - | // If the first argument is peer, we are checking further. The second argument might either be - | // an object, that it must be the service object - | // or a string, which is the service id. In that case the service is the third argument - | if (!(FluencePeer.isInstance(args[0])) && typeof args[0] === 'object') { - | service = args[0]; - | } else if (typeof args[1] === 'object') { - | service = args[1]; - | } else { - | service = args[2]; - | } - | - | const incorrectServiceDefinitions = missingFields(service, [${membersNames.map { n => s"'$n'" }.mkString(", ")}]); - | if (!!incorrectServiceDefinitions.length) { - | throw new Error("Error registering service ${srv.name}: missing functions: " + incorrectServiceDefinitions.map((d) => "'" + d + "'").join(", ")) - | } - | - | peer.internals.callServiceHandler.use((req, resp, next) => { - | if (req.serviceId !== serviceId) { - | next(); - | return; - | } - | - | ${fnHandlers} - | - | next(); - | }); - |} - """.stripMargin -} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypes.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypes.scala new file mode 100644 index 00000000..122ce4b4 --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypes.scala @@ -0,0 +1,12 @@ +package aqua.backend.ts + +import aqua.backend.{FuncTypes, ServiceTypes, Types} +import aqua.model.transform.res.{FuncRes, ServiceRes} + +object TypeScriptTypes extends Types { + override def typed(field: String, t: String): String = s"$field: $t" + override def generic(field: String, t: String): String = s"$field<$t>" + override def bang(field: String): String = s"$field!" + def funcType(f: FuncRes): FuncTypes = TSFuncTypes(f) + def serviceType(s: ServiceRes): ServiceTypes = TSServiceTypes(s) +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypesFile.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypesFile.scala new file mode 100644 index 00000000..98d37558 --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptTypesFile.scala @@ -0,0 +1,16 @@ +package aqua.backend.ts + +import aqua.backend.Header +import aqua.model.transform.res.AquaRes + +case class TypeScriptTypesFile(res: AquaRes) { + def generate: String = + s"""${Header.header(false)} + | + |// Services + |${res.services.map(TSServiceTypes(_)).map(_.generate).toList.mkString("\n\n")} + | + |// Functions + |${res.funcs.map(TSFuncTypes(_)).map(_.generate).toList.mkString("\n\n")} + |""".stripMargin +} diff --git a/build.sbt b/build.sbt index c6e2e5e5..c2a39083 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,7 @@ lazy val cli = crossProject(JSPlatform, JVMPlatform) "co.fs2" %%% "fs2-io" % fs2V ) ) - .dependsOn(compiler, `backend-air`, `backend-ts`, `backend-js`) + .dependsOn(compiler, `backend-air`, `backend-ts`) lazy val cliJS = cli.js .settings( @@ -166,10 +166,3 @@ lazy val `backend-ts` = crossProject(JVMPlatform, JSPlatform) .in(file("backend/ts")) .settings(commons: _*) .dependsOn(`backend-air`) - -lazy val `backend-js` = crossProject(JVMPlatform, JSPlatform) - .withoutSuffixFor(JVMPlatform) - .crossType(CrossType.Pure) - .in(file("backend/js")) - .settings(commons: _*) - .dependsOn(`backend-air`)