From 5e1ef6e2278cd0f2318110982a249afbb7530168 Mon Sep 17 00:00:00 2001 From: Dima Date: Fri, 25 Jun 2021 10:25:27 +0300 Subject: [PATCH] Abstract backend (#182) --- .../scala/aqua/backend/air/AirBackend.scala | 17 ++ .../scala/aqua/backend/air/FuncAirGen.scala | 9 - .../aqua/backend/js/JavaScriptBackend.scala | 21 ++ .../aqua/backend/js/JavaScriptFunc.scala | 5 +- .../src/main/scala/aqua/backend/Backend.scala | 18 ++ .../aqua/backend/ts/TypeScriptBackend.scala | 21 ++ ...escriptFile.scala => TypeScriptFile.scala} | 10 +- ...escriptFunc.scala => TypeScriptFunc.scala} | 8 +- build.sbt | 10 +- cli/src/main/scala/aqua/AquaCompiler.scala | 244 +++++++----------- cli/src/main/scala/aqua/Prepared.scala | 57 ++++ cli/src/main/scala/aqua/io/AquaFile.scala | 38 +-- cli/src/main/scala/aqua/io/FileOps.scala | 47 ++++ cli/src/test/aqua/test.aqua | 18 ++ cli/src/test/scala/WriteFileSpec.scala | 60 +++++ .../test-kit}/src/main/scala/aqua/Node.scala | 0 .../aqua/model/topology/TopologySpec.scala | 33 +-- .../aqua/model/transform/TransformSpec.scala | 0 .../test/scala/aqua/parser/CoExprSpec.scala | 35 +++ 19 files changed, 422 insertions(+), 229 deletions(-) create mode 100644 backend/air/src/main/scala/aqua/backend/air/AirBackend.scala create mode 100644 backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala create mode 100644 backend/src/main/scala/aqua/backend/Backend.scala create mode 100644 backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala rename backend/ts/src/main/scala/aqua/backend/ts/{TypescriptFile.scala => TypeScriptFile.scala} (80%) rename backend/ts/src/main/scala/aqua/backend/ts/{TypescriptFunc.scala => TypeScriptFunc.scala} (96%) create mode 100644 cli/src/main/scala/aqua/Prepared.scala create mode 100644 cli/src/main/scala/aqua/io/FileOps.scala create mode 100644 cli/src/test/aqua/test.aqua create mode 100644 cli/src/test/scala/WriteFileSpec.scala rename {test-kit => model/test-kit}/src/main/scala/aqua/Node.scala (100%) rename {test-kit => model/test-kit}/src/test/scala/aqua/model/topology/TopologySpec.scala (85%) rename {test-kit => model/test-kit}/src/test/scala/aqua/model/transform/TransformSpec.scala (100%) create mode 100644 parser/src/test/scala/aqua/parser/CoExprSpec.scala diff --git a/backend/air/src/main/scala/aqua/backend/air/AirBackend.scala b/backend/air/src/main/scala/aqua/backend/air/AirBackend.scala new file mode 100644 index 00000000..d91e0fed --- /dev/null +++ b/backend/air/src/main/scala/aqua/backend/air/AirBackend.scala @@ -0,0 +1,17 @@ +package aqua.backend.air + +import aqua.backend.{Backend, Compiled} +import aqua.model.AquaContext +import aqua.model.transform.BodyConfig +import cats.implicits.toShow + +object AirBackend extends Backend { + + val ext = ".air" + + override def generate(context: AquaContext, bc: BodyConfig): Seq[Compiled] = { + context.funcs.values.toList.map(fc => + Compiled("." + fc.funcName + ext, FuncAirGen(fc).generateAir(bc).show) + ) + } +} diff --git a/backend/air/src/main/scala/aqua/backend/air/FuncAirGen.scala b/backend/air/src/main/scala/aqua/backend/air/FuncAirGen.scala index 0ca55886..0022f131 100644 --- a/backend/air/src/main/scala/aqua/backend/air/FuncAirGen.scala +++ b/backend/air/src/main/scala/aqua/backend/air/FuncAirGen.scala @@ -12,13 +12,4 @@ case class FuncAirGen(func: FuncCallable) { AirGen( Transform.forClient(func, conf) ).generate - - /** - * Generates AIR from the optimized function body, assuming client is behind a relay - * @return - */ - def generateClientAir(conf: BodyConfig = BodyConfig()): Air = - AirGen( - Transform.forClient(func, conf) - ).generate } diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala new file mode 100644 index 00000000..33cc2afb --- /dev/null +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptBackend.scala @@ -0,0 +1,21 @@ +package aqua.backend.js + +import aqua.backend.{Backend, Compiled} +import aqua.model.AquaContext +import aqua.model.transform.BodyConfig +import cats.data.Chain + +object JavaScriptBackend extends Backend { + + val ext = ".js" + + override def generate(context: AquaContext, bc: BodyConfig): Seq[Compiled] = { + val funcs = Chain.fromSeq(context.funcs.values.toSeq).map(JavaScriptFunc(_)) + Seq( + Compiled( + ext, + JavaScriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(bc)).toList.mkString("\n\n") + ) + ) + } +} diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala index 974aa174..80506f9a 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFunc.scala @@ -16,7 +16,7 @@ case class JavaScriptFunc(func: FuncCallable) { def generateTypescript(conf: BodyConfig = BodyConfig()): String = { - val tsAir = FuncAirGen(func).generateClientAir(conf) + val tsAir = FuncAirGen(func).generateAir(conf) val returnCallback = func.ret.as { s"""h.onEvent('${conf.callbackService}', '${conf.respFuncName}', (args) => { @@ -86,8 +86,7 @@ case class JavaScriptFunc(func: FuncCallable) { object JavaScriptFunc { def argsToTs(at: ArrowType): String = - at.args - .zipWithIndex + at.args.zipWithIndex .map(_.swap) .map(kv => "arg" + kv._1) .mkString(", ") diff --git a/backend/src/main/scala/aqua/backend/Backend.scala b/backend/src/main/scala/aqua/backend/Backend.scala new file mode 100644 index 00000000..7b08e220 --- /dev/null +++ b/backend/src/main/scala/aqua/backend/Backend.scala @@ -0,0 +1,18 @@ +package aqua.backend + +import aqua.model.AquaContext +import aqua.model.transform.BodyConfig + +/** + * Compilation result + * @param suffix extension or another info that will be added to a resulted file + * @param content a code that is used as an output + */ +case class Compiled(suffix: String, content: String) + +/** + * Describes how context can be finalized + */ +trait Backend { + def generate(context: AquaContext, bc: BodyConfig): Seq[Compiled] +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala new file mode 100644 index 00000000..73ff1feb --- /dev/null +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptBackend.scala @@ -0,0 +1,21 @@ +package aqua.backend.ts + +import aqua.backend.{Backend, Compiled} +import aqua.model.AquaContext +import aqua.model.transform.BodyConfig +import cats.data.Chain + +object TypeScriptBackend extends Backend { + + val ext = ".ts" + + override def generate(context: AquaContext, bc: BodyConfig): Seq[Compiled] = { + val funcs = Chain.fromSeq(context.funcs.values.toSeq).map(TypeScriptFunc(_)) + Seq( + Compiled( + ext, + TypeScriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(bc)).toList.mkString("\n\n") + ) + ) + } +} diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypescriptFile.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala similarity index 80% rename from backend/ts/src/main/scala/aqua/backend/ts/TypescriptFile.scala rename to backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala index 4d670de2..53f8447e 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypescriptFile.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala @@ -4,16 +4,16 @@ import aqua.model.AquaContext import aqua.model.transform.BodyConfig import cats.data.Chain -case class TypescriptFile(context: AquaContext) { +case class TypeScriptFile(context: AquaContext) { - def funcs: Chain[TypescriptFunc] = - Chain.fromSeq(context.funcs.values.toSeq).map(TypescriptFunc(_)) + def funcs: Chain[TypeScriptFunc] = + Chain.fromSeq(context.funcs.values.toSeq).map(TypeScriptFunc(_)) def generateTS(conf: BodyConfig = BodyConfig()): String = - TypescriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(conf)).toList.mkString("\n\n") + TypeScriptFile.Header + "\n\n" + funcs.map(_.generateTypescript(conf)).toList.mkString("\n\n") } -object TypescriptFile { +object TypeScriptFile { val Header: String = s"""/** diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypescriptFunc.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala similarity index 96% rename from backend/ts/src/main/scala/aqua/backend/ts/TypescriptFunc.scala rename to backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala index c2361108..05ad3cdf 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypescriptFunc.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFunc.scala @@ -7,9 +7,9 @@ import aqua.types._ import cats.syntax.functor._ import cats.syntax.show._ -case class TypescriptFunc(func: FuncCallable) { +case class TypeScriptFunc(func: FuncCallable) { - import TypescriptFunc._ + import TypeScriptFunc._ def argsTypescript: String = func.args.args.map(ad => s"${ad.name}: " + typeToTs(ad.`type`)).mkString(", ") @@ -25,7 +25,7 @@ case class TypescriptFunc(func: FuncCallable) { def generateTypescript(conf: BodyConfig = BodyConfig()): String = { - val tsAir = FuncAirGen(func).generateClientAir(conf) + val tsAir = FuncAirGen(func).generateAir(conf) val returnCallback = func.ret.as { s"""h.onEvent('${conf.callbackService}', '${conf.respFuncName}', (args) => { @@ -102,7 +102,7 @@ case class TypescriptFunc(func: FuncCallable) { } -object TypescriptFunc { +object TypeScriptFunc { def typeToTs(t: Type): String = t match { case OptionType(t) => typeToTs(t) + " | null" diff --git a/build.sbt b/build.sbt index fdaafadb..f7cf418c 100644 --- a/build.sbt +++ b/build.sbt @@ -55,7 +55,7 @@ lazy val cli = project "com.monovore" %% "decline-enumeratum" % declineEnumV ) ) - .dependsOn(semantics, `backend-air`, `backend-ts`, `backend-js`, linker) + .dependsOn(semantics, `backend-air`, `backend-ts`, `backend-js`, linker, backend) lazy val types = project .settings(commons) @@ -94,6 +94,7 @@ lazy val model = project .dependsOn(types) lazy val `test-kit` = project + .in(file("model/test-kit")) .settings(commons: _*) .dependsOn(model) @@ -107,10 +108,15 @@ lazy val semantics = project ) .dependsOn(model, `test-kit` % Test, parser) +lazy val backend = project + .in(file("backend")) + .settings(commons: _*) + .dependsOn(model) + lazy val `backend-air` = project .in(file("backend/air")) .settings(commons: _*) - .dependsOn(model) + .dependsOn(backend) lazy val `backend-ts` = project .in(file("backend/ts")) diff --git a/cli/src/main/scala/aqua/AquaCompiler.scala b/cli/src/main/scala/aqua/AquaCompiler.scala index 1f11b3a3..7f6f1c40 100644 --- a/cli/src/main/scala/aqua/AquaCompiler.scala +++ b/cli/src/main/scala/aqua/AquaCompiler.scala @@ -1,24 +1,22 @@ package aqua -import aqua.backend.air.FuncAirGen -import aqua.backend.js.JavaScriptFile -import aqua.backend.ts.TypescriptFile -import aqua.io.{AquaFileError, AquaFiles, FileModuleId, Unresolvable} +import aqua.backend.Backend +import aqua.backend.air.AirBackend +import aqua.backend.js.JavaScriptBackend +import aqua.backend.ts.TypeScriptBackend +import aqua.io._ import aqua.linker.Linker import aqua.model.AquaContext import aqua.model.transform.BodyConfig import aqua.parser.lift.FileSpan import aqua.semantics.{RulesViolated, SemanticError, Semantics} -import cats.Applicative -import cats.data.Validated.{Invalid, Valid} import cats.data._ import cats.effect.kernel.Concurrent import cats.kernel.Monoid import cats.syntax.flatMap._ import cats.syntax.functor._ -import cats.syntax.show._ +import cats.{Applicative, Monad} import fs2.io.file.Files -import fs2.text import wvlet.log.LogSupport import java.nio.file.Path @@ -29,36 +27,38 @@ object AquaCompiler extends LogSupport { case object JavaScriptTarget extends CompileTarget case object AirTarget extends CompileTarget - case class Prepared(modFile: Path, srcPath: Path, targetPath: Path, context: AquaContext) { + private def gatherPreparedFiles( + srcPath: Path, + targetPath: Path, + files: Map[FileModuleId, ValidatedNec[SemanticError[FileSpan.F], AquaContext]] + ): ValidatedNec[String, Chain[Prepared]] = { + val (errs, _, preps) = files.toSeq.foldLeft[(Chain[String], Set[String], Chain[Prepared])]( + (Chain.empty, Set.empty, Chain.empty) + ) { case ((errs, errsSet, preps), (modId, proc)) => + proc.fold( + es => { + val newErrs = showProcErrors(es.toChain).filterNot(errsSet.contains) + (errs ++ newErrs, errsSet ++ newErrs.iterator, preps) + }, + c => { + Prepared(modId.file, srcPath, targetPath, c) match { + case Validated.Valid(p) ⇒ + (errs, errsSet, preps :+ p) + case Validated.Invalid(err) ⇒ + (errs :+ err.getMessage, errsSet, preps) + } - def hasOutput(target: CompileTarget): Boolean = target match { - case _ => context.funcs.nonEmpty - } - - def targetPath(ext: String): Validated[Throwable, Path] = - Validated.catchNonFatal { - val srcDir = if (srcPath.toFile.isDirectory) srcPath else srcPath.getParent - val srcFilePath = srcDir.toAbsolutePath - .normalize() - .relativize(modFile.toAbsolutePath.normalize()) - - val targetAqua = - targetPath.toAbsolutePath - .normalize() - .resolve( - srcFilePath - ) - - val fileName = targetAqua.getFileName - if (fileName == null) { - throw new Exception(s"Unexpected: 'fileName' is null in path $targetAqua") - } else { - // rename `.aqua` file name to `.ext` - targetAqua.getParent.resolve(fileName.toString.stripSuffix(".aqua") + s".$ext") } - } + ) + } + NonEmptyChain + .fromChain(errs) + .fold(Validated.validNec[String, Chain[Prepared]](preps))(Validated.invalid) } + /** + * Create a structure that will be used to create output by a backend + */ def prepareFiles[F[_]: Files: Concurrent]( srcPath: Path, imports: LazyList[Path], @@ -81,21 +81,7 @@ object AquaCompiler extends LogSupport { ids => Unresolvable(ids.map(_.id.file.toString).mkString(" -> ")) ) match { case Validated.Valid(files) ⇒ - val (errs, _, preps) = - files.toSeq.foldLeft[(Chain[String], Set[String], Chain[Prepared])]( - (Chain.empty, Set.empty, Chain.empty) - ) { case ((errs, errsSet, preps), (modId, proc)) => - proc.fold( - es => { - val newErrs = showProcErrors(es.toChain).filterNot(errsSet.contains) - (errs ++ newErrs, errsSet ++ newErrs.iterator, preps) - }, - c => (errs, errsSet, preps :+ Prepared(modId.file, srcPath, targetPath, c)) - ) - } - NonEmptyChain - .fromChain(errs) - .fold(Validated.validNec[String, Chain[Prepared]](preps))(Validated.invalid) + gatherPreparedFiles(srcPath, targetPath, files) case Validated.Invalid(errs) ⇒ Validated.invalid( @@ -118,6 +104,38 @@ object AquaCompiler extends LogSupport { "Semantic error" } + def targetToBackend(target: CompileTarget): Backend = { + target match { + case TypescriptTarget => + TypeScriptBackend + case JavaScriptTarget => + JavaScriptBackend + case AirTarget => + AirBackend + } + } + + private def gatherResults[F[_]: Monad]( + results: List[EitherT[F, String, Unit]] + ): F[Validated[NonEmptyChain[String], Chain[String]]] = { + results + .foldLeft( + EitherT.rightT[F, NonEmptyChain[String]](Chain.empty[String]) + ) { case (accET, writeET) => + EitherT(for { + acc <- accET.value + writeResult <- writeET.value + } yield (acc, writeResult) match { + case (Left(errs), Left(err)) => Left(errs :+ err) + case (Right(res), Right(_)) => Right(res) + case (Left(errs), _) => Left(errs) + case (_, Left(err)) => Left(NonEmptyChain.of(err)) + }) + } + .value + .map(Validated.fromEither) + } + def compileFilesTo[F[_]: Files: Concurrent]( srcPath: Path, imports: LazyList[Path], @@ -129,116 +147,44 @@ object AquaCompiler extends LogSupport { prepareFiles(srcPath, imports, targetPath) .map(_.map(_.filter { p => val hasOutput = p.hasOutput(compileTo) - if (!hasOutput) info(s"Source ${p.modFile}: compilation OK (nothing to emit)") + if (!hasOutput) info(s"Source ${p.srcFile}: compilation OK (nothing to emit)") hasOutput })) .flatMap[ValidatedNec[String, Chain[String]]] { case Validated.Invalid(e) => Applicative[F].pure(Validated.invalid(e)) case Validated.Valid(preps) => - (compileTo match { - case TypescriptTarget => - preps.map { p => - p.targetPath("ts") match { - case Invalid(t) => - EitherT.pure(t.getMessage) - case Valid(tp) => - writeFile(tp, TypescriptFile(p.context).generateTS(bodyConfig)).flatTap { _ => - EitherT.pure( - Validated.catchNonFatal( - info( - s"Result ${tp.toAbsolutePath}: compilation OK (${p.context.funcs.size} functions)" - ) - ) - ) - } - } - - } - - 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 - .flatMap(p => - Chain - .fromSeq(p.context.funcs.values.toSeq) - .map(fc => fc.funcName -> FuncAirGen(fc).generateAir(bodyConfig).show) - .map { case (fnName, generated) => - val tpV = p.targetPath(fnName + ".air") - tpV match { - case Invalid(t) => - EitherT.pure(t.getMessage) - case Valid(tp) => - writeFile( - tp, - generated - ).flatTap { _ => - EitherT.pure( - Validated.catchNonFatal( - info( - s"Result ${tp.toAbsolutePath}: compilation OK (${p.context.funcs.size} functions)" - ) - ) - ) - } - } - } + val backend = targetToBackend(compileTo) + val results = preps.toList + .flatMap(p => + backend.generate(p.context, bodyConfig).map { compiled => + val targetPath = p.targetPath( + p.srcFile.getFileName.toString.stripSuffix(".aqua") + compiled.suffix ) - }).foldLeft( - EitherT.rightT[F, NonEmptyChain[String]](Chain.empty[String]) - ) { case (accET, writeET) => - EitherT(for { - a <- accET.value - w <- writeET.value - } yield (a, w) match { - case (Left(errs), Left(err)) => Left(errs :+ err) - case (Right(res), Right(_)) => Right(res) - case (Left(errs), _) => Left(errs) - case (_, Left(err)) => Left(NonEmptyChain.of(err)) - }) - }.value - .map(Validated.fromEither) + targetPath.fold( + t => EitherT.leftT[F, Unit](t.getMessage), + tp => + FileOps + .writeFile( + tp, + compiled.content + ) + .flatTap { _ => + EitherT.pure( + Validated.catchNonFatal( + info( + s"Result ${tp.toAbsolutePath}: compilation OK (${p.context.funcs.size} functions)" + ) + ) + ) + } + ) + } + ) + gatherResults(results) } } - def writeFile[F[_]: Files: Concurrent](file: Path, content: String): EitherT[F, String, Unit] = - EitherT.right[String](Files[F].deleteIfExists(file)) >> - EitherT[F, String, Unit]( - fs2.Stream - .emit( - content - ) - .through(text.utf8Encode) - .through(Files[F].writeAll(file)) - .attempt - .map { e => - e.left - .map(t => s"Error on writing file $file" + t) - } - .compile - .drain - .map(_ => Right(())) - ) - } diff --git a/cli/src/main/scala/aqua/Prepared.scala b/cli/src/main/scala/aqua/Prepared.scala new file mode 100644 index 00000000..219833bf --- /dev/null +++ b/cli/src/main/scala/aqua/Prepared.scala @@ -0,0 +1,57 @@ +package aqua + +import aqua.AquaCompiler.CompileTarget +import aqua.model.AquaContext +import cats.data.Validated + +import java.nio.file.Path + +object Prepared { + + /** + * @param srcFile aqua source + * @param srcPath a main source path with all aqua files + * @param targetPath a main path where all output files will be written + * @param context processed aqua code + * @return + */ + def apply( + srcFile: Path, + srcPath: Path, + targetPath: Path, + context: AquaContext + ): Validated[Throwable, Prepared] = + Validated.catchNonFatal { + val srcDir = if (srcPath.toFile.isDirectory) srcPath else srcPath.getParent + val srcFilePath = srcDir.toAbsolutePath + .normalize() + .relativize(srcFile.toAbsolutePath.normalize()) + + val targetDir = + targetPath.toAbsolutePath + .normalize() + .resolve( + srcFilePath + ) + + new Prepared(targetDir, srcFile, context) + } +} + +/** + * All info that can be used to write a final output. + * @param targetDir a directory to write to + * @param srcFile file with a source (aqua code) + * @param context processed code + */ +case class Prepared private (targetDir: Path, srcFile: Path, context: AquaContext) { + + def hasOutput(target: CompileTarget): Boolean = target match { + case _ => context.funcs.nonEmpty + } + + def targetPath(fileName: String): Validated[Throwable, Path] = + Validated.catchNonFatal { + targetDir.getParent.resolve(fileName) + } +} diff --git a/cli/src/main/scala/aqua/io/AquaFile.scala b/cli/src/main/scala/aqua/io/AquaFile.scala index 7c61510e..d4f1db2b 100644 --- a/cli/src/main/scala/aqua/io/AquaFile.scala +++ b/cli/src/main/scala/aqua/io/AquaFile.scala @@ -10,7 +10,6 @@ import cats.effect.Concurrent import cats.syntax.apply._ import cats.syntax.functor._ import fs2.io.file.Files -import fs2.text import java.nio.file.{Path, Paths} @@ -54,36 +53,25 @@ case class AquaFile( object AquaFile { - def readSourceText[F[_]: Files: Concurrent]( + def readAst[F[_]: Files: Concurrent]( file: Path - ): fs2.Stream[F, Either[AquaFileError, String]] = - Files[F] - .readAll(file, 4096) - .fold(Vector.empty[Byte])((acc, b) => acc :+ b) - // TODO fix for comment on last line in air - // TODO should be fixed by parser - .map(_.appendedAll("\n\r".getBytes)) - .flatMap(fs2.Stream.emits) - .through(text.utf8Decode) - .attempt + ): fs2.Stream[F, Either[AquaFileError, (String, Ast[FileSpan.F])]] = + FileOps + .readSourceText[F](file) .map { _.left .map(t => FileSystemError(t)) } - - def readAst[F[_]: Files: Concurrent]( - file: Path - ): fs2.Stream[F, Either[AquaFileError, (String, Ast[FileSpan.F])]] = - readSourceText[F](file).map( - _.flatMap(source => - Aqua - .parseFileString(file.toString, source) - .map(source -> _) - .toEither - .left - .map(AquaScriptErrors(_)) + .map( + _.flatMap(source => + Aqua + .parseFileString(file.toString, source) + .map(source -> _) + .toEither + .left + .map(AquaScriptErrors(_)) + ) ) - ) def read[F[_]: Files: Concurrent](file: Path): EitherT[F, AquaFileError, AquaFile] = EitherT(readAst[F](file).compile.last.map(_.getOrElse(Left(EmptyFileError(file))))).map { diff --git a/cli/src/main/scala/aqua/io/FileOps.scala b/cli/src/main/scala/aqua/io/FileOps.scala new file mode 100644 index 00000000..fd8f16be --- /dev/null +++ b/cli/src/main/scala/aqua/io/FileOps.scala @@ -0,0 +1,47 @@ +package aqua.io + +import cats.data.EitherT +import cats.effect.Concurrent +import cats.implicits.toFunctorOps +import fs2.io.file.Files +import fs2.text + +import java.nio.file.Path + +object FileOps { + + def writeFile[F[_]: Files: Concurrent](file: Path, content: String): EitherT[F, String, Unit] = + EitherT + .right[String](Files[F].deleteIfExists(file)) + .flatMap(_ => + EitherT[F, String, Unit]( + fs2.Stream + .emit( + content + ) + .through(text.utf8Encode) + .through(Files[F].writeAll(file)) + .attempt + .map { e => + e.left + .map(t => s"Error on writing file $file" + t) + } + .compile + .drain + .map(_ => Right(())) + ) + ) + + def readSourceText[F[_]: Files: Concurrent]( + file: Path + ): fs2.Stream[F, Either[Throwable, String]] = + Files[F] + .readAll(file, 4096) + .fold(Vector.empty[Byte])((acc, b) => acc :+ b) + // TODO fix for comment on last line in air + // TODO should be fixed by parser + .map(_.appendedAll("\n\r".getBytes)) + .flatMap(fs2.Stream.emits) + .through(text.utf8Decode) + .attempt +} diff --git a/cli/src/test/aqua/test.aqua b/cli/src/test/aqua/test.aqua new file mode 100644 index 00000000..57642e24 --- /dev/null +++ b/cli/src/test/aqua/test.aqua @@ -0,0 +1,18 @@ +service CustomId("cid"): + id() -> string + +func first(node_id: string, viaAr: []string) -> string: + on node_id via viaAr: + p <- CustomId.id() + <- p + + +func second(node_id: string, viaStr: *string) -> string: + on node_id via viaStr: + p <- CustomId.id() + <- p + +func third(relay: string, node_id: string, viaOpt: ?string) -> string: + on node_id via viaOpt: + p <- CustomId.id() + <- p \ No newline at end of file diff --git a/cli/src/test/scala/WriteFileSpec.scala b/cli/src/test/scala/WriteFileSpec.scala new file mode 100644 index 00000000..2497aa43 --- /dev/null +++ b/cli/src/test/scala/WriteFileSpec.scala @@ -0,0 +1,60 @@ +import aqua.AquaCompiler +import aqua.model.transform.BodyConfig +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.nio.file.{Files, Paths} + +class WriteFileSpec extends AnyFlatSpec with Matchers { + "cli" should "compile aqua code in js" in { + val src = Paths.get("./cli/src/test/aqua") + val targetTs = Files.createTempDirectory("ts") + val targetJs = Files.createTempDirectory("js") + val targetAir = Files.createTempDirectory("air") + + val bc = BodyConfig() + AquaCompiler + .compileFilesTo[IO](src, LazyList.empty, targetTs, AquaCompiler.TypescriptTarget, bc) + .unsafeRunSync() + .leftMap { err => + println(err) + err + } + .isValid should be(true) + val targetTsFile = targetTs.resolve("test.ts") + targetTsFile.toFile.exists() should be(true) + Files.deleteIfExists(targetTsFile) + + AquaCompiler + .compileFilesTo[IO](src, LazyList.empty, targetJs, AquaCompiler.JavaScriptTarget, bc) + .unsafeRunSync() + .leftMap { err => + println(err) + err + } + .isValid should be(true) + val targetJsFile = targetJs.resolve("test.js") + targetJsFile.toFile.exists() should be(true) + Files.deleteIfExists(targetJsFile) + + AquaCompiler + .compileFilesTo[IO](src, LazyList.empty, targetAir, AquaCompiler.AirTarget, bc) + .unsafeRunSync() + .leftMap { err => + println(err) + err + } + .isValid should be(true) + val targetAirFileFirst = targetAir.resolve("test.first.air") + val targetAirFileSecond = targetAir.resolve("test.second.air") + val targetAirFileThird = targetAir.resolve("test.third.air") + targetAirFileFirst.toFile.exists() should be(true) + targetAirFileSecond.toFile.exists() should be(true) + targetAirFileThird.toFile.exists() should be(true) + + Seq(targetAirFileFirst, targetAirFileSecond, targetAirFileThird).map(Files.deleteIfExists) + } + +} diff --git a/test-kit/src/main/scala/aqua/Node.scala b/model/test-kit/src/main/scala/aqua/Node.scala similarity index 100% rename from test-kit/src/main/scala/aqua/Node.scala rename to model/test-kit/src/main/scala/aqua/Node.scala diff --git a/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala b/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala similarity index 85% rename from test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala rename to model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala index b9dd31df..d0710d42 100644 --- a/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala +++ b/model/test-kit/src/test/scala/aqua/model/topology/TopologySpec.scala @@ -4,7 +4,7 @@ import aqua.Node import aqua.model.VarModel import aqua.model.func.Call import aqua.model.func.raw.FuncOps -import aqua.model.func.resolved.{MakeRes, ResolvedOp, SeqRes, XorRes} +import aqua.model.func.resolved.{MakeRes, ResolvedOp, XorRes} import aqua.types.ScalarType import cats.Eval import cats.data.Chain @@ -295,31 +295,10 @@ class TopologySpec extends AnyFlatSpec with Matchers { callRes(2, initPeer) ) -// println(Console.BLUE + init) -// println(Console.YELLOW + proc) -// println(Console.MAGENTA + expected) -// println(Console.RESET) - proc.equalsOrPrintDiff(expected) should be(true) } "topology resolver" should "not stackoverflow" in { - /* - OnTag(LiteralModel(%init_peer_id%,ScalarType(string)),Chain(VarModel(-relay-,ScalarType(string),Chain()))) { - SeqTag{ - CallServiceTag(LiteralModel("getDataSrv",ScalarType(string)),-relay-,Call(List(),Some(Export(-relay-,ScalarType(string)))),None) - CallServiceTag(LiteralModel("getDataSrv",ScalarType(string)),node_id,Call(List(),Some(Export(node_id,ScalarType(string)))),None) - CallServiceTag(LiteralModel("getDataSrv",ScalarType(string)),viaAr,Call(List(),Some(Export(viaAr,[]ScalarType(string)))),None) - OnTag(VarModel(node_id,ScalarType(string),Chain()),Chain(VarModel(viaAr,[]ScalarType(string),Chain()))) { - CallServiceTag(LiteralModel("cid",Literal(string)),ids,Call(List(),Some(Export(p,ScalarType(string)))),None) - } - OnTag(LiteralModel(%init_peer_id%,ScalarType(string)),Chain(VarModel(-relay-,ScalarType(string),Chain()))) { - CallServiceTag(LiteralModel("callbackSrv",ScalarType(string)),response,Call(List(VarModel(p,ScalarType(string),Chain())),None),None) - } - } - } - - */ val init = on( initPeer, relay :: Nil, @@ -394,11 +373,6 @@ class TopologySpec extends AnyFlatSpec with Matchers { callRes(3, initPeer) ) -// println(Console.BLUE + init) -// println(Console.YELLOW + proc) -// println(Console.MAGENTA + expected) -// println(Console.RESET) - proc.equalsOrPrintDiff(expected) should be(true) } @@ -453,11 +427,6 @@ class TopologySpec extends AnyFlatSpec with Matchers { callRes(4, initPeer) ) -// println(Console.BLUE + init) - println(Console.YELLOW + proc) - println(Console.MAGENTA + expected) - println(Console.RESET) - Node.equalsOrPrintDiff(proc, expected) should be(true) } diff --git a/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala b/model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala similarity index 100% rename from test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala rename to model/test-kit/src/test/scala/aqua/model/transform/TransformSpec.scala diff --git a/parser/src/test/scala/aqua/parser/CoExprSpec.scala b/parser/src/test/scala/aqua/parser/CoExprSpec.scala new file mode 100644 index 00000000..f3deff9c --- /dev/null +++ b/parser/src/test/scala/aqua/parser/CoExprSpec.scala @@ -0,0 +1,35 @@ +package aqua.parser + +import aqua.AquaSpec +import aqua.parser.expr.{CallArrowExpr, CoExpr} +import aqua.parser.lexer.Token +import aqua.parser.lift.LiftParser.Implicits.idLiftParser +import cats.data.Chain +import cats.free.Cofree +import cats.{Eval, Id} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class CoExprSpec extends AnyFlatSpec with Matchers with AquaSpec { + + "co" should "be parsed" in { + CoExpr.readLine[Id].parseAll("co x <- y()").value should be( + Cofree[Chain, Expr[Id]]( + CoExpr[Id](Token.lift[Id, Unit](())), + Eval.now( + Chain( + Cofree[Chain, Expr[Id]]( + CallArrowExpr( + Some(AquaSpec.toName("x")), + None, + AquaSpec.toName("y"), + Nil + ), + Eval.now(Chain.empty) + ) + ) + ) + ) + ) + } +}