Abstract backend (#182)

This commit is contained in:
Dima 2021-06-25 10:25:27 +03:00 committed by GitHub
parent bbf47628c6
commit 5e1ef6e227
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 422 additions and 229 deletions

View File

@ -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)
)
}
}

View File

@ -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
}

View File

@ -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")
)
)
}
}

View File

@ -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(", ")

View File

@ -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]
}

View File

@ -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")
)
)
}
}

View File

@ -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"""/**

View File

@ -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"

View File

@ -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"))

View File

@ -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) {
def hasOutput(target: CompileTarget): Boolean = target match {
case _ => context.funcs.nonEmpty
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 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,69 +147,30 @@ 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
val backend = targetToBackend(compileTo)
val results = preps.toList
.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(
backend.generate(p.context, bodyConfig).map { compiled =>
val targetPath = p.targetPath(
p.srcFile.getFileName.toString.stripSuffix(".aqua") + compiled.suffix
)
targetPath.fold(
t => EitherT.leftT[F, Unit](t.getMessage),
tp =>
FileOps
.writeFile(
tp,
generated
).flatTap { _ =>
compiled.content
)
.flatTap { _ =>
EitherT.pure(
Validated.catchNonFatal(
info(
@ -200,45 +179,12 @@ object AquaCompiler extends LogSupport {
)
)
}
}
)
}
)
}).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)
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(()))
)
}

View File

@ -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)
}
}

View File

@ -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,27 +53,16 @@ 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(
.map(
_.flatMap(source =>
Aqua
.parseFileString(file.toString, source)

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
)
)
)
)
)
}
}