From e2da2e90d7408cc5c4433006200787bbbad32373 Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 11 Aug 2021 19:53:36 +0300 Subject: [PATCH] Crossprojects for JVM and JS platforms (#237) --- .jvmopts | 6 + .../src/main/scala/aqua/backend/Version.scala | 7 ++ .../src/main/scala/aqua/backend/Version.scala | 8 ++ .../main/scala/aqua/backend/air/AirGen.scala | 8 +- .../aqua/backend/js/JavaScriptFile.scala | 5 +- .../aqua/backend/ts/TypeScriptFile.scala | 5 +- build.sbt | 103 +++++++++------- cli/.js/src/main/scala/aqua/JsApp.scala | 48 ++++++++ .../src/main/scala/aqua/AppOps.scala | 89 ++++++++------ .../src/main/scala/aqua/AquaCli.scala | 34 +++--- .../main/scala/aqua/AquaPathCompiler.scala | 17 ++- .../src/main/scala/aqua/ErrorRendering.scala | 2 +- cli/{ => .jvm}/src/main/scala/aqua/Test.scala | 13 +- .../scala/aqua/files/AquaFileSources.scala | 93 +++++++++------ .../main/scala/aqua/files/FileModuleId.scala | 4 +- cli/{ => .jvm}/src/test/aqua/test.aqua | 0 .../src/test/scala/SourcesSpec.scala | 35 +++--- .../src/test/scala/WriteFileSpec.scala | 28 +++-- .../test/test-dir/broken-import/broken.aqua | 0 .../src/test/test-dir/importNear.aqua | 0 .../src/test/test-dir/imports/import.aqua | 0 cli/{ => .jvm}/src/test/test-dir/index.aqua | 0 .../test/test-dir/path-test/importNear.aqua | 0 .../src/test/test-dir/path-test/index.aqua | 0 cli/src/main/scala/aqua/AquaIO.scala | 2 +- .../main/scala/aqua/CustomLogFormatter.scala | 13 -- cli/src/main/scala/aqua/LogFormatter.scala | 7 ++ cli/src/main/scala/aqua/LogLevel.scala | 18 +-- .../main/scala/aqua/files/AquaFilesIO.scala | 111 ++++++++++-------- .../main/scala/aqua/io/AquaFileError.scala | 2 +- .../src/main/scala/aqua/linker/Linker.scala | 18 +-- .../main/scala/aqua/model/AquaContext.scala | 8 +- .../main/scala/aqua/model/ValueModel.scala | 7 +- .../scala/aqua/model/func/FuncCallable.scala | 11 +- .../aqua/model/func/raw/PathFinder.scala | 38 +++--- .../scala/aqua/model/func/raw/RawCursor.scala | 5 +- .../scala/aqua/model/topology/Topology.scala | 18 +-- .../aqua/model/transform/Transform.scala | 4 +- .../test/scala/aqua/parser/FuncExprSpec.scala | 6 +- project/plugins.sbt | 4 +- .../main/scala/aqua/semantics/Semantics.scala | 4 +- 41 files changed, 454 insertions(+), 327 deletions(-) create mode 100644 .jvmopts create mode 100644 backend/.js/src/main/scala/aqua/backend/Version.scala create mode 100644 backend/.jvm/src/main/scala/aqua/backend/Version.scala create mode 100644 cli/.js/src/main/scala/aqua/JsApp.scala rename cli/{ => .jvm}/src/main/scala/aqua/AppOps.scala (64%) rename cli/{ => .jvm}/src/main/scala/aqua/AquaCli.scala (80%) rename cli/{ => .jvm}/src/main/scala/aqua/AquaPathCompiler.scala (77%) rename cli/{ => .jvm}/src/main/scala/aqua/ErrorRendering.scala (96%) rename cli/{ => .jvm}/src/main/scala/aqua/Test.scala (65%) rename cli/{ => .jvm}/src/main/scala/aqua/files/AquaFileSources.scala (59%) rename cli/{ => .jvm}/src/main/scala/aqua/files/FileModuleId.scala (62%) rename cli/{ => .jvm}/src/test/aqua/test.aqua (100%) rename cli/{ => .jvm}/src/test/scala/SourcesSpec.scala (81%) rename cli/{ => .jvm}/src/test/scala/WriteFileSpec.scala (65%) rename cli/{ => .jvm}/src/test/test-dir/broken-import/broken.aqua (100%) rename cli/{ => .jvm}/src/test/test-dir/importNear.aqua (100%) rename cli/{ => .jvm}/src/test/test-dir/imports/import.aqua (100%) rename cli/{ => .jvm}/src/test/test-dir/index.aqua (100%) rename cli/{ => .jvm}/src/test/test-dir/path-test/importNear.aqua (100%) rename cli/{ => .jvm}/src/test/test-dir/path-test/index.aqua (100%) delete mode 100644 cli/src/main/scala/aqua/CustomLogFormatter.scala create mode 100644 cli/src/main/scala/aqua/LogFormatter.scala diff --git a/.jvmopts b/.jvmopts new file mode 100644 index 00000000..1321950e --- /dev/null +++ b/.jvmopts @@ -0,0 +1,6 @@ +-Dfile.encoding=UTF8 +-Xms1G +-Xmx5G +-XX:ReservedCodeCacheSize=500M +-XX:+TieredCompilation +-XX:+UseParallelGC \ No newline at end of file diff --git a/backend/.js/src/main/scala/aqua/backend/Version.scala b/backend/.js/src/main/scala/aqua/backend/Version.scala new file mode 100644 index 00000000..ebe28fc3 --- /dev/null +++ b/backend/.js/src/main/scala/aqua/backend/Version.scala @@ -0,0 +1,7 @@ +package aqua.backend + +object Version { + + // TODO: get version for JS compiler + lazy val version = "Unknown (JS)" +} diff --git a/backend/.jvm/src/main/scala/aqua/backend/Version.scala b/backend/.jvm/src/main/scala/aqua/backend/Version.scala new file mode 100644 index 00000000..0468e0a8 --- /dev/null +++ b/backend/.jvm/src/main/scala/aqua/backend/Version.scala @@ -0,0 +1,8 @@ +package aqua.backend + +object Version { + + lazy val version = Option(getClass.getPackage.getImplementationVersion) + .filter(_.nonEmpty) + .getOrElse("Unknown") +} diff --git a/backend/air/src/main/scala/aqua/backend/air/AirGen.scala b/backend/air/src/main/scala/aqua/backend/air/AirGen.scala index cae4ffb4..e7d49dd1 100644 --- a/backend/air/src/main/scala/aqua/backend/air/AirGen.scala +++ b/backend/air/src/main/scala/aqua/backend/air/AirGen.scala @@ -7,14 +7,14 @@ import aqua.types.StreamType import cats.Eval import cats.data.Chain import cats.free.Cofree -import wvlet.log.LogSupport +import scribe.Logging sealed trait AirGen { def generate: Air } -object AirGen extends LogSupport { +object AirGen extends Logging { def lambdaToString(ls: List[LambdaModel]): String = ls match { case Nil => "" @@ -54,7 +54,7 @@ object AirGen extends LogSupport { case o :: Nil => ParGen(o, NullGen) case _ => ops.toList.reduceLeftOption(ParGen(_, _)).getOrElse { - warn("ParRes with no children converted to Null") + logger.warn("ParRes with no children converted to Null") NullGen } }) @@ -63,7 +63,7 @@ object AirGen extends LogSupport { case o :: Nil => XorGen(o, NullGen) case _ => ops.toList.reduceLeftOption(XorGen(_, _)).getOrElse { - warn("XorRes with no children converted to Null") + logger.warn("XorRes with no children converted to Null") NullGen } }) diff --git a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala index d6e05884..4293e22c 100644 --- a/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala +++ b/backend/js/src/main/scala/aqua/backend/js/JavaScriptFile.scala @@ -1,5 +1,6 @@ package aqua.backend.js +import aqua.backend.Version import aqua.model.AquaContext import aqua.model.transform.GenerationConfig import cats.data.Chain @@ -21,9 +22,7 @@ object JavaScriptFile { | * 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")} + | * Aqua version: ${Version.version} | * | */ |import { RequestFlowBuilder } from '@fluencelabs/fluence/dist/api.unstable'; diff --git a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala index 39af2908..b441727c 100644 --- a/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala +++ b/backend/ts/src/main/scala/aqua/backend/ts/TypeScriptFile.scala @@ -1,5 +1,6 @@ package aqua.backend.ts +import aqua.backend.Version import aqua.model.AquaContext import aqua.model.transform.GenerationConfig import cats.data.Chain @@ -21,9 +22,7 @@ object TypeScriptFile { | * 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")} + | * Aqua version: ${Version.version} | * | */ |import { FluenceClient, PeerIdB58 } from '@fluencelabs/fluence'; diff --git a/build.sbt b/build.sbt index 55b14a4a..7d76c3af 100644 --- a/build.sbt +++ b/build.sbt @@ -8,19 +8,12 @@ val catsV = "2.6.1" val catsParseV = "0.3.4" val monocleV = "3.0.0-M6" val scalaTestV = "3.2.9" -val fs2V = "3.0.6" +val fs2V = "3.1.0" val catsEffectV = "3.2.1" -val airframeLogV = "21.5.4" val log4catsV = "2.1.1" val slf4jV = "1.7.30" val declineV = "2.1.0" -val airframeLog = "org.wvlet.airframe" %% "airframe-log" % airframeLogV -val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectV -val fs2Io = "co.fs2" %% "fs2-io" % fs2V -val catsFree = "org.typelevel" %% "cats-free" % catsV -val cats = "org.typelevel" %% "cats-core" % catsV - name := "aqua-hll" val commons = Seq( @@ -28,9 +21,8 @@ val commons = Seq( version := baseAquaVersion.value + "-" + sys.env.getOrElse("BUILD_NUMBER", "SNAPSHOT"), scalaVersion := dottyVersion, libraryDependencies ++= Seq( - "org.typelevel" %% "log4cats-core" % log4catsV, - airframeLog, - "org.scalatest" %% "scalatest" % scalaTestV % Test + "com.outr" %%% "scribe" % "3.5.5", + "org.scalatest" %%% "scalatest" % scalaTestV % Test ), scalacOptions ++= { Seq( @@ -47,95 +39,124 @@ val commons = Seq( commons -lazy val cli = project +lazy val cli = crossProject(JSPlatform, JVMPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) + .in(file("cli")) .settings(commons: _*) + .settings( + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectV, + "com.monovore" %%% "decline" % declineV, + "com.monovore" %%% "decline-effect" % declineV, + "co.fs2" %%% "fs2-io" % fs2V + ) + ) + .dependsOn(compiler, `backend-air`, `backend-ts`, `backend-js`) + +lazy val cliJS = cli.js + .settings( + scalaJSUseMainModuleInitializer := true + ) + +lazy val cliJVM = cli.jvm .settings( Compile / run / mainClass := Some("aqua.AquaCli"), assembly / mainClass := Some("aqua.AquaCli"), assembly / assemblyJarName := "aqua-cli-" + version.value + ".jar", libraryDependencies ++= Seq( - "com.monovore" %% "decline" % declineV, - "com.monovore" %% "decline-effect" % declineV, - catsEffect, - fs2Io, - "org.typelevel" %% "log4cats-slf4j" % log4catsV, - "org.slf4j" % "slf4j-jdk14" % slf4jV ) ) - .dependsOn(compiler, `backend-air`, `backend-ts`, `backend-js`) -lazy val types = project +lazy val types = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .settings(commons) .settings( libraryDependencies ++= Seq( - cats + "org.typelevel" %%% "cats-core" % catsV ) ) -lazy val parser = project +lazy val parser = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .settings(commons: _*) .settings( libraryDependencies ++= Seq( - "org.typelevel" %% "cats-parse" % catsParseV, - catsFree + "org.typelevel" %%% "cats-parse" % catsParseV, + "org.typelevel" %%% "cats-free" % catsV ) ) .dependsOn(types) -lazy val linker = project +lazy val linker = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .settings(commons: _*) - .settings( - libraryDependencies ++= Seq( - airframeLog - ) - ) .dependsOn(parser) -lazy val model = project +lazy val model = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .settings(commons: _*) .settings( libraryDependencies ++= Seq( - catsFree + "org.typelevel" %%% "cats-free" % catsV ) ) .dependsOn(types) -lazy val `test-kit` = project +lazy val `test-kit` = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("model/test-kit")) .settings(commons: _*) .dependsOn(model) -lazy val semantics = project +lazy val semantics = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .settings(commons: _*) .settings( libraryDependencies ++= Seq( - "com.github.julien-truffaut" %% "monocle-core" % monocleV, - "com.github.julien-truffaut" %% "monocle-macro" % monocleV + "com.github.julien-truffaut" %%% "monocle-core" % monocleV, + "com.github.julien-truffaut" %%% "monocle-macro" % monocleV ) ) .dependsOn(model, `test-kit` % Test, parser) -lazy val compiler = project +lazy val compiler = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("compiler")) .settings(commons: _*) .dependsOn(semantics, linker, backend) -lazy val backend = project +lazy val backend = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("backend")) .settings(commons: _*) .dependsOn(model) -lazy val `backend-air` = project +lazy val `backend-air` = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("backend/air")) .settings(commons: _*) .dependsOn(backend) -lazy val `backend-ts` = project +lazy val `backend-ts` = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("backend/ts")) .settings(commons: _*) .dependsOn(`backend-air`) -lazy val `backend-js` = project +lazy val `backend-js` = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) .in(file("backend/js")) .settings(commons: _*) .dependsOn(`backend-air`) diff --git a/cli/.js/src/main/scala/aqua/JsApp.scala b/cli/.js/src/main/scala/aqua/JsApp.scala new file mode 100644 index 00000000..76f9be26 --- /dev/null +++ b/cli/.js/src/main/scala/aqua/JsApp.scala @@ -0,0 +1,48 @@ +package aqua + +import aqua.backend.Backend +import aqua.backend.ts.TypeScriptBackend +import aqua.compiler.{AquaCompiler, AquaError, AquaSources} +import aqua.model.transform.GenerationConfig +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import cats.data.* +import cats.syntax.functor.* +import cats.syntax.applicative.* +import cats.{Applicative, Monad, Show} +import aqua.parser.lift.FileSpan + +object JsApp { + + trait AquaJsError {} + + case class FileId(name: String) + + class Sources[F[_]: Applicative] extends AquaSources[F, AquaJsError, FileId] { + + def sources: F[ValidatedNec[AquaJsError, Chain[(FileId, String)]]] = + Validated.Valid(Chain.empty).pure[F] + + // Resolve id of the imported imp string from I file + def resolveImport(from: FileId, imp: String): F[ValidatedNec[AquaJsError, FileId]] = + Validated.Valid(FileId("")).pure[F] + + // Load file by its resolved I + def load(file: FileId): F[ValidatedNec[AquaJsError, String]] = Validated.Valid("").pure[F] + } + + def main(args: Array[String]): Unit = { + // test code +// val sources = new Sources[IO]() +// val bodyConfig = GenerationConfig() +// val b = AquaCompiler +// .compileTo[IO, AquaJsError, FileId, FileSpan.F, String]( +// sources, +// (fmid, src) => FileSpan.fileSpanLiftParser(fmid.name, src), +// TypeScriptBackend, +// bodyConfig, +// (a) => ??? +// ) + println("Hello world!") + } +} diff --git a/cli/src/main/scala/aqua/AppOps.scala b/cli/.jvm/src/main/scala/aqua/AppOps.scala similarity index 64% rename from cli/src/main/scala/aqua/AppOps.scala rename to cli/.jvm/src/main/scala/aqua/AppOps.scala index b2cf6fdf..c4633607 100644 --- a/cli/src/main/scala/aqua/AppOps.scala +++ b/cli/.jvm/src/main/scala/aqua/AppOps.scala @@ -6,16 +6,15 @@ import aqua.parser.expr.ConstantExpr import aqua.parser.lift.LiftParser import cats.data.Validated.{Invalid, Valid} import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.effect.ExitCode +import cats.effect.{unsafe, ExitCode, IO} import cats.effect.std.Console import cats.syntax.functor.* import cats.syntax.traverse.* import cats.{Comonad, Functor} import com.monovore.decline.Opts.help import com.monovore.decline.{Opts, Visibility} -import wvlet.log.{LogLevel => WLogLevel} - -import java.nio.file.Path +import scribe.Level +import fs2.io.file.{Files, Path} object AppOps { @@ -25,13 +24,13 @@ object AppOps { val versionOpt: Opts[Unit] = Opts.flag("version", help = "Show version", "v", Visibility.Partial) - val logLevelOpt: Opts[WLogLevel] = + val logLevelOpt: Opts[Level] = Opts.option[String]("log-level", help = "Set log level").withDefault("info").mapValidated { str => Validated.fromEither(toLogLevel(str)) } - def toLogLevel(logLevel: String): Either[NonEmptyList[String], WLogLevel] = { + def toLogLevel(logLevel: String): Either[NonEmptyList[String], Level] = { LogLevel.stringToLogLevel .get(logLevel.toLowerCase) .toRight( @@ -42,56 +41,68 @@ object AppOps { ) } - def checkPath: Path => ValidatedNel[String, Path] = { p => - Validated - .fromEither(Validated.catchNonFatal { - val f = p.toFile - if (f.exists()) { - if (f.isFile) { - val filename = f.getName - val ext = Option(filename) - .filter(_.contains(".")) - .map(f => f.substring(f.lastIndexOf(".") + 1)) - .getOrElse("") - if (ext != "aqua") Left("File must be with 'aqua' extension") - else Right(p) - } else Right(p) - } else { - Left(s"There is no path '${p.toString}'") - } - }.toEither.left.map(t => s"An error occurred on imports reading: ${t.getMessage}").flatten) - .toValidatedNel + def checkPath(implicit runtime: unsafe.IORuntime): String => ValidatedNel[String, Path] = { + pathStr => + Validated + .fromEither(Validated.catchNonFatal { + val p = Path(pathStr) + Files[IO] + .exists(p) + .flatMap { exists => + if (exists) + Files[IO].isRegularFile(p).map { isFile => + if (isFile) { + val filename = p.fileName.toString + val ext = Option(filename) + .filter(_.contains(".")) + .map(f => f.substring(f.lastIndexOf(".") + 1)) + .getOrElse("") + if (ext != "aqua") Left("File must be with 'aqua' extension") + else Right(p) + } else + Right(p) + } + else + IO(Left(s"There is no path '${p.toString}'")) + } + // TODO: make it with correct effects + .unsafeRunSync() + }.toEither.left.map(t => s"An error occurred on imports reading: ${t.getMessage}").flatten) + .toValidatedNel } - val inputOpts: Opts[Path] = + def inputOpts(implicit runtime: unsafe.IORuntime): Opts[Path] = Opts - .option[Path]( + .option[String]( "input", "Path to an aqua file or an input directory that contains your .aqua files", "i" ) .mapValidated(checkPath) - val outputOpts: Opts[Path] = - Opts.option[Path]("output", "Path to the output directory", "o").mapValidated(checkPath) + def outputOpts(implicit runtime: unsafe.IORuntime): Opts[Path] = + Opts.option[String]("output", "Path to the output directory", "o").mapValidated(checkPath) - val importOpts: Opts[List[Path]] = + def importOpts(implicit runtime: unsafe.IORuntime): Opts[List[Path]] = Opts - .options[Path]("import", "Path to the directory to import from", "m") + .options[String]("import", "Path to the directory to import from", "m") .mapValidated { ps => val checked = ps - .map(p => { + .map(pStr => { Validated.catchNonFatal { - val f = p.toFile - if (f.exists() && f.isDirectory) { - Right(p) - } else { - Left(s"There is no path ${p.toString} or it is not a directory") - } + val p = Path(pStr) + (for { + exists <- Files[IO].exists(p) + isDir <- Files[IO].isDirectory(p) + } yield { + if (exists && isDir) Right(p) + else Left(s"There is no path ${p.toString} or it is not a directory") + }) + // TODO: make it with correct effects + .unsafeRunSync() } }) .toList - checked.map { case Validated.Valid(pE) => pE match { diff --git a/cli/src/main/scala/aqua/AquaCli.scala b/cli/.jvm/src/main/scala/aqua/AquaCli.scala similarity index 80% rename from cli/src/main/scala/aqua/AquaCli.scala rename to cli/.jvm/src/main/scala/aqua/AquaCli.scala index 699748d9..4bab5013 100644 --- a/cli/src/main/scala/aqua/AquaCli.scala +++ b/cli/.jvm/src/main/scala/aqua/AquaCli.scala @@ -9,18 +9,16 @@ import aqua.model.transform.GenerationConfig import aqua.parser.lift.LiftParser.Implicits.idLiftParser import cats.Id import cats.data.Validated -import cats.effect._ -import cats.effect.std.{Console => ConsoleEff} -import cats.syntax.apply._ -import cats.syntax.functor._ +import cats.effect.* +import cats.effect.std.Console as ConsoleEff +import cats.syntax.apply.* +import cats.syntax.functor.* import com.monovore.decline.Opts import com.monovore.decline.effect.CommandIOApp import fs2.io.file.Files -import org.typelevel.log4cats.slf4j.Slf4jLogger -import org.typelevel.log4cats.{Logger, SelfAwareStructuredLogger} -import wvlet.log.{LogSupport, Logger => WLogger} +import scribe.Logging -object AquaCli extends IOApp with LogSupport { +object AquaCli extends IOApp with Logging { import AppOps._ sealed trait CompileTarget @@ -39,7 +37,8 @@ object AquaCli extends IOApp with LogSupport { } } - def main[F[_]: Concurrent: Files: ConsoleEff: Logger]: Opts[F[ExitCode]] = { + def main[F[_]: Concurrent: Files: ConsoleEff](runtime: unsafe.IORuntime): Opts[F[ExitCode]] = { + implicit val r = runtime versionOpt .as( versionAndExit @@ -59,8 +58,11 @@ object AquaCli extends IOApp with LogSupport { constantOpts[Id] ).mapN { case (input, imports, output, toAir, toJs, noRelay, noXor, h, v, logLevel, constants) => - WLogger.setDefaultLogLevel(logLevel) - WLogger.setDefaultFormatter(CustomLogFormatter) + scribe.Logger.root + .clearHandlers() + .clearModifiers() + .withHandler(formatter = LogFormatter.formatter, minimumLevel = Some(logLevel)) + .replace() implicit val aio: AquaIO[F] = new AquaFilesIO[F] @@ -75,7 +77,7 @@ object AquaCli extends IOApp with LogSupport { val bc = GenerationConfig(wrapWithXor = !noXor, constants = constants) bc.copy(relayVarName = bc.relayVarName.filterNot(_ => noRelay)) } - info(s"Aqua Compiler ${versionStr}") + logger.info(s"Aqua Compiler ${versionStr}") AquaPathCompiler .compileFilesTo[F]( input, @@ -89,7 +91,7 @@ object AquaCli extends IOApp with LogSupport { errs.map(System.out.println) ExitCode.Error case Validated.Valid(results) => - results.map(info(_)) + results.map(logger.info(_)) ExitCode.Success } } @@ -97,17 +99,13 @@ object AquaCli extends IOApp with LogSupport { } override def run(args: List[String]): IO[ExitCode] = { - - implicit def logger[F[_]: Sync]: SelfAwareStructuredLogger[F] = - Slf4jLogger.getLogger[F] - CommandIOApp.run[IO]( "aqua-c", "Aquamarine compiler", helpFlag = false, None )( - main[IO], + main[IO](runtime), // Weird ugly hack: in case version flag or help flag is present, ignore other options, // be it correct or not args match { diff --git a/cli/src/main/scala/aqua/AquaPathCompiler.scala b/cli/.jvm/src/main/scala/aqua/AquaPathCompiler.scala similarity index 77% rename from cli/src/main/scala/aqua/AquaPathCompiler.scala rename to cli/.jvm/src/main/scala/aqua/AquaPathCompiler.scala index c895f54b..160223e2 100644 --- a/cli/src/main/scala/aqua/AquaPathCompiler.scala +++ b/cli/.jvm/src/main/scala/aqua/AquaPathCompiler.scala @@ -3,20 +3,19 @@ package aqua import aqua.backend.Backend import aqua.compiler.{AquaCompiler, AquaError} import aqua.files.{AquaFileSources, FileModuleId} -import aqua.io._ +import aqua.io.* import aqua.model.transform.GenerationConfig import aqua.parser.lift.FileSpan -import cats.data._ -import cats.syntax.functor._ -import cats.syntax.show._ +import cats.data.* +import cats.syntax.functor.* +import cats.syntax.show.* import cats.{Monad, Show} -import wvlet.log.LogSupport +import scribe.Logging +import fs2.io.file.{Files, Path} -import java.nio.file.Path +object AquaPathCompiler extends Logging { -object AquaPathCompiler extends LogSupport { - - def compileFilesTo[F[_]: AquaIO: Monad]( + def compileFilesTo[F[_]: AquaIO: Monad: Files]( srcPath: Path, imports: List[Path], targetPath: Path, diff --git a/cli/src/main/scala/aqua/ErrorRendering.scala b/cli/.jvm/src/main/scala/aqua/ErrorRendering.scala similarity index 96% rename from cli/src/main/scala/aqua/ErrorRendering.scala rename to cli/.jvm/src/main/scala/aqua/ErrorRendering.scala index 1ecc5b29..19973f1a 100644 --- a/cli/src/main/scala/aqua/ErrorRendering.scala +++ b/cli/.jvm/src/main/scala/aqua/ErrorRendering.scala @@ -53,7 +53,7 @@ object ErrorRendering { val span = token.unit._1 showForConsole(span, s"Cannot resolve import") case CycleError(modules) => - s"Cycle loops detected in imports: ${modules.map(_.file.getFileName)}" + s"Cycle loops detected in imports: ${modules.map(_.file.fileName)}" case CompileError(err) => err match { case RulesViolated(token, message) => diff --git a/cli/src/main/scala/aqua/Test.scala b/cli/.jvm/src/main/scala/aqua/Test.scala similarity index 65% rename from cli/src/main/scala/aqua/Test.scala rename to cli/.jvm/src/main/scala/aqua/Test.scala index 3b09b375..3bdd6bb3 100644 --- a/cli/src/main/scala/aqua/Test.scala +++ b/cli/.jvm/src/main/scala/aqua/Test.scala @@ -5,24 +5,19 @@ import aqua.files.AquaFilesIO import aqua.model.transform.GenerationConfig import cats.data.Validated import cats.effect.{IO, IOApp, Sync} -import org.typelevel.log4cats.SelfAwareStructuredLogger -import org.typelevel.log4cats.slf4j.Slf4jLogger -import java.nio.file.Paths +import fs2.io.file.Path object Test extends IOApp.Simple { - implicit def logger[F[_]: Sync]: SelfAwareStructuredLogger[F] = - Slf4jLogger.getLogger[F] - implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] override def run: IO[Unit] = AquaPathCompiler .compileFilesTo[IO]( - Paths.get("./aqua-src"), - List(Paths.get("./aqua")), - Paths.get("./target"), + Path("./aqua-src"), + List(Path("./aqua")), + Path("./target"), TypeScriptBackend, GenerationConfig() ) diff --git a/cli/src/main/scala/aqua/files/AquaFileSources.scala b/cli/.jvm/src/main/scala/aqua/files/AquaFileSources.scala similarity index 59% rename from cli/src/main/scala/aqua/files/AquaFileSources.scala rename to cli/.jvm/src/main/scala/aqua/files/AquaFileSources.scala index bdf8d0e3..7d3d3de4 100644 --- a/cli/src/main/scala/aqua/files/AquaFileSources.scala +++ b/cli/.jvm/src/main/scala/aqua/files/AquaFileSources.scala @@ -3,19 +3,22 @@ package aqua.files import aqua.AquaIO import aqua.compiler.{AquaCompiled, AquaSources} import aqua.io.{AquaFileError, FileSystemError, ListAquaErrors} -import cats.Monad +import cats.{Functor, Monad} import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec} import cats.implicits.catsSyntaxApplicativeId -import cats.syntax.either._ -import cats.syntax.flatMap._ -import cats.syntax.functor._ -import cats.syntax.traverse._ +import cats.syntax.either.* +import cats.syntax.flatMap.* +import cats.syntax.functor.* +import cats.syntax.monad.* +import cats.syntax.traverse.* +import fs2.io.file.{Files, Path} -import java.nio.file.{Path, Paths} import scala.util.Try -class AquaFileSources[F[_]: AquaIO: Monad](sourcesPath: Path, importFrom: List[Path]) - extends AquaSources[F, AquaFileError, FileModuleId] { +class AquaFileSources[F[_]: AquaIO: Monad: Files: Functor]( + sourcesPath: Path, + importFrom: List[Path] +) extends AquaSources[F, AquaFileError, FileModuleId] { private val filesIO = implicitly[AquaIO[F]] override def sources: F[ValidatedNec[AquaFileError, Chain[(FileModuleId, String)]]] = @@ -49,10 +52,11 @@ class AquaFileSources[F[_]: AquaIO: Monad](sourcesPath: Path, importFrom: List[P from: FileModuleId, imp: String ): F[ValidatedNec[AquaFileError, FileModuleId]] = { - Validated.fromEither(Try(Paths.get(imp)).toEither.leftMap(FileSystemError.apply)) match { + val validatedPath = Validated.fromEither(Try(Path(imp)).toEither.leftMap(FileSystemError.apply)) + validatedPath match { case Validated.Valid(importP) => filesIO - .resolve(importP, from.file.getParent +: importFrom) + .resolve(importP, importFrom.prependedAll(from.file.parent)) .bimap(NonEmptyChain.one, FileModuleId(_)) .value .map(Validated.fromEither) @@ -64,6 +68,15 @@ class AquaFileSources[F[_]: AquaIO: Monad](sourcesPath: Path, importFrom: List[P override def load(file: FileModuleId): F[ValidatedNec[AquaFileError, String]] = filesIO.readFile(file.file).leftMap(NonEmptyChain.one).value.map(Validated.fromEither) + // Get a directory of a file, or this file if it is a directory itself + private def getDir(path: Path): F[Path] = { + Files[F] + .isDirectory(path) + .map { res => + if (res) path else path.parent.getOrElse(path) + } + } + /** * @param srcFile aqua source * @param targetPath a main path where all output files will be written @@ -74,23 +87,36 @@ class AquaFileSources[F[_]: AquaIO: Monad](sourcesPath: Path, importFrom: List[P srcFile: Path, targetPath: Path, suffix: String - ): Validated[Throwable, Path] = - Validated.catchNonFatal { - val srcDir = if (sourcesPath.toFile.isDirectory) sourcesPath else sourcesPath.getParent - val srcFilePath = srcDir.toAbsolutePath - .normalize() - .relativize(srcFile.toAbsolutePath.normalize()) + ): F[Validated[Throwable, Path]] = + getDir(sourcesPath).map { srcDir => + Validated.catchNonFatal { + val srcFilePath = srcDir.absolute.normalize + .relativize(srcFile.absolute.normalize) - val targetDir = - targetPath.toAbsolutePath - .normalize() - .resolve( - srcFilePath - ) + val targetDir = + targetPath.absolute.normalize + .resolve( + srcFilePath + ) - targetDir.getParent.resolve(srcFile.getFileName.toString.stripSuffix(".aqua") + suffix) + targetDir.parent + .getOrElse(targetDir) + .resolve(srcFile.fileName.toString.stripSuffix(".aqua") + suffix) + } } + // Write content to a file and return a success message + private def writeWithResult(target: Path, content: String, size: Int) = { + filesIO + .writeFile( + target, + content + ) + .as(s"Result $target: compilation OK ($size functions)") + .value + .map(Validated.fromEither) + } + def write( targetPath: Path )(ac: AquaCompiled[FileModuleId]): F[Seq[Validated[AquaFileError, String]]] = @@ -106,19 +132,14 @@ class AquaFileSources[F[_]: AquaIO: Monad](sourcesPath: Path, importFrom: List[P ac.sourceId.file, targetPath, compiled.suffix - ).leftMap(FileSystemError.apply) - .map { target => - filesIO - .writeFile( - target, - compiled.content - ) - .as(s"Result $target: compilation OK (${ac.compiled.size} functions)") - .value - .map(Validated.fromEither) - } - // TODO: we use both EitherT and F[Validated] to handle errors, that's why so weird - .traverse(identity) + ).flatMap { result => + result + .leftMap(FileSystemError.apply) + .map { target => + writeWithResult(target, compiled.content, ac.compiled.size) + } + .traverse(identity) + } }.traverse(identity) .map(_.map(_.andThen(identity))) } diff --git a/cli/src/main/scala/aqua/files/FileModuleId.scala b/cli/.jvm/src/main/scala/aqua/files/FileModuleId.scala similarity index 62% rename from cli/src/main/scala/aqua/files/FileModuleId.scala rename to cli/.jvm/src/main/scala/aqua/files/FileModuleId.scala index dbb882d7..b20c0c13 100644 --- a/cli/src/main/scala/aqua/files/FileModuleId.scala +++ b/cli/.jvm/src/main/scala/aqua/files/FileModuleId.scala @@ -1,11 +1,11 @@ package aqua.files -import java.nio.file.Path +import fs2.io.file.Path case class FileModuleId private (file: Path) object FileModuleId { def apply(file: Path): FileModuleId = - new FileModuleId(file.toAbsolutePath.normalize()) + new FileModuleId(file.absolute.normalize) } diff --git a/cli/src/test/aqua/test.aqua b/cli/.jvm/src/test/aqua/test.aqua similarity index 100% rename from cli/src/test/aqua/test.aqua rename to cli/.jvm/src/test/aqua/test.aqua diff --git a/cli/src/test/scala/SourcesSpec.scala b/cli/.jvm/src/test/scala/SourcesSpec.scala similarity index 81% rename from cli/src/test/scala/SourcesSpec.scala rename to cli/.jvm/src/test/scala/SourcesSpec.scala index bea13f00..4304390b 100644 --- a/cli/src/test/scala/SourcesSpec.scala +++ b/cli/.jvm/src/test/scala/SourcesSpec.scala @@ -5,18 +5,16 @@ import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} import cats.data.Chain import cats.effect.IO import cats.effect.unsafe.implicits.global -import fs2.io.file.Files import fs2.text import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers - -import java.nio.file.Paths +import fs2.io.file.{Files, Path} class SourcesSpec extends AnyFlatSpec with Matchers { implicit val aquaIO: AquaIO[IO] = AquaFilesIO.summon[IO] "AquaFileSources" should "generate correct fileId with imports" in { - val path = Paths.get("cli/src/test/test-dir/path-test") + val path = Path("cli/.jvm/src/test/test-dir/path-test") val importPath = path.resolve("imports") val sourceGen = new AquaFileSources[IO](path, importPath :: Nil) @@ -42,7 +40,7 @@ class SourcesSpec extends AnyFlatSpec with Matchers { } "AquaFileSources" should "throw an error if a source file doesn't exist" in { - val path = Paths.get("some/random/path") + val path = Path("some/random/path") val sourceGen = new AquaFileSources[IO](path, Nil) @@ -51,7 +49,7 @@ class SourcesSpec extends AnyFlatSpec with Matchers { } "AquaFileSources" should "throw an error if there is no import that is indicated in a source" in { - val path = Paths.get("cli/src/test/test-dir") + val path = Path("cli/.jvm/src/test/test-dir") val importPath = path.resolve("random/import/path") val sourceGen = new AquaFileSources[IO](path, importPath :: Nil) @@ -61,7 +59,7 @@ class SourcesSpec extends AnyFlatSpec with Matchers { } "AquaFileSources" should "find correct imports" in { - val srcPath = Paths.get("cli/src/test/test-dir/index.aqua") + val srcPath = Path("cli/.jvm/src/test/test-dir/index.aqua") val importPath = srcPath.resolve("imports") val sourceGen = new AquaFileSources[IO](srcPath, importPath :: Nil) @@ -73,7 +71,8 @@ class SourcesSpec extends AnyFlatSpec with Matchers { .unsafeRunSync() result.isValid shouldBe true - result.getOrElse(FileModuleId(Paths.get("/some/random"))).file.toFile.exists() shouldBe true + val file = result.getOrElse(FileModuleId(Path("/some/random"))).file + Files[IO].exists(file).unsafeRunSync() shouldBe true // should be found near src file val result2 = @@ -82,7 +81,8 @@ class SourcesSpec extends AnyFlatSpec with Matchers { .unsafeRunSync() result2.isValid shouldBe true - result2.getOrElse(FileModuleId(Paths.get("/some/random"))).file.toFile.exists() shouldBe true + val file2 = result2.getOrElse(FileModuleId(Path("/some/random"))).file + Files[IO].exists(file2).unsafeRunSync() shouldBe true // near src file but in another directory val sourceGen2 = new AquaFileSources[IO](srcPath, Nil) @@ -92,20 +92,21 @@ class SourcesSpec extends AnyFlatSpec with Matchers { .unsafeRunSync() result3.isValid shouldBe true - result3.getOrElse(FileModuleId(Paths.get("/some/random"))).file.toFile.exists() shouldBe true + val file3 = result3.getOrElse(FileModuleId(Path("/some/random"))).file + Files[IO].exists(file3).unsafeRunSync() shouldBe true } "AquaFileSources" should "resolve correct path for target" in { - val path = Paths.get("cli/src/test/test-dir") + val path = Path("cli/.jvm/src/test/test-dir") val filePath = path.resolve("some-dir/file.aqua") - val targetPath = Paths.get("/target/dir/") + val targetPath = Path("/target/dir/") val sourceGen = new AquaFileSources[IO](path, Nil) val suffix = "_custom.super" - val resolved = sourceGen.resolveTargetPath(filePath, targetPath, suffix) + val resolved = sourceGen.resolveTargetPath(filePath, targetPath, suffix).unsafeRunSync() resolved.isValid shouldBe true val targetFilePath = resolved.toOption.get @@ -113,13 +114,13 @@ class SourcesSpec extends AnyFlatSpec with Matchers { } "AquaFileSources" should "write correct file with correct path" in { - val path = Paths.get("cli/src/test/test-dir") + val path = Path("cli/.jvm/src/test/test-dir") val filePath = path.resolve("imports/import.aqua") val targetPath = path.resolve("target/") // clean up - val resultPath = Paths.get("cli/src/test/test-dir/target/imports/import_hey.custom") + val resultPath = Path("cli/.jvm/src/test/test-dir/target/imports/import_hey.custom") Files[IO].deleteIfExists(resultPath).unsafeRunSync() val sourceGen = new AquaFileSources[IO](path, Nil) @@ -136,13 +137,13 @@ class SourcesSpec extends AnyFlatSpec with Matchers { Files[IO].exists(resultPath).unsafeRunSync() shouldBe true val resultText = Files[IO] - .readAll(resultPath, 1000) + .readAll(resultPath) .fold( Vector .empty[Byte] )((acc, b) => acc :+ b) .flatMap(fs2.Stream.emits) - .through(text.utf8Decode) + .through(text.utf8.decode) .attempt .compile .last diff --git a/cli/src/test/scala/WriteFileSpec.scala b/cli/.jvm/src/test/scala/WriteFileSpec.scala similarity index 65% rename from cli/src/test/scala/WriteFileSpec.scala rename to cli/.jvm/src/test/scala/WriteFileSpec.scala index 6aefc227..f0134d32 100644 --- a/cli/src/test/scala/WriteFileSpec.scala +++ b/cli/.jvm/src/test/scala/WriteFileSpec.scala @@ -8,14 +8,14 @@ import cats.effect.unsafe.implicits.global import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import java.nio.file.{Files, Paths} +import fs2.io.file.{Files, Path} 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 src = Path("./cli/.jvm/src/test/aqua") + val targetTs = Files[IO].createTempDirectory.unsafeRunSync() + val targetJs = Files[IO].createTempDirectory.unsafeRunSync() + val targetAir = Files[IO].createTempDirectory.unsafeRunSync() import aqua.files.AquaFilesIO.summon @@ -29,8 +29,8 @@ class WriteFileSpec extends AnyFlatSpec with Matchers { } .isValid should be(true) val targetTsFile = targetTs.resolve("test.ts") - targetTsFile.toFile.exists() should be(true) - Files.deleteIfExists(targetTsFile) + Files[IO].exists(targetTsFile).unsafeRunSync() should be(true) + Files[IO].deleteIfExists(targetTsFile).unsafeRunSync() AquaPathCompiler .compileFilesTo[IO](src, List.empty, targetJs, JavaScriptBackend, bc) @@ -41,8 +41,8 @@ class WriteFileSpec extends AnyFlatSpec with Matchers { } .isValid should be(true) val targetJsFile = targetJs.resolve("test.js") - targetJsFile.toFile.exists() should be(true) - Files.deleteIfExists(targetJsFile) + Files[IO].exists(targetJsFile).unsafeRunSync() should be(true) + Files[IO].deleteIfExists(targetJsFile).unsafeRunSync() AquaPathCompiler .compileFilesTo[IO](src, List.empty, targetAir, AirBackend, bc) @@ -55,11 +55,13 @@ class WriteFileSpec extends AnyFlatSpec with Matchers { 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) + Files[IO].exists(targetAirFileFirst).unsafeRunSync() should be(true) + Files[IO].exists(targetAirFileSecond).unsafeRunSync() should be(true) + Files[IO].exists(targetAirFileThird).unsafeRunSync() should be(true) - Seq(targetAirFileFirst, targetAirFileSecond, targetAirFileThird).map(Files.deleteIfExists) + Seq(targetAirFileFirst, targetAirFileSecond, targetAirFileThird).map(f => + Files[IO].deleteIfExists(f).unsafeRunSync() + ) } } diff --git a/cli/src/test/test-dir/broken-import/broken.aqua b/cli/.jvm/src/test/test-dir/broken-import/broken.aqua similarity index 100% rename from cli/src/test/test-dir/broken-import/broken.aqua rename to cli/.jvm/src/test/test-dir/broken-import/broken.aqua diff --git a/cli/src/test/test-dir/importNear.aqua b/cli/.jvm/src/test/test-dir/importNear.aqua similarity index 100% rename from cli/src/test/test-dir/importNear.aqua rename to cli/.jvm/src/test/test-dir/importNear.aqua diff --git a/cli/src/test/test-dir/imports/import.aqua b/cli/.jvm/src/test/test-dir/imports/import.aqua similarity index 100% rename from cli/src/test/test-dir/imports/import.aqua rename to cli/.jvm/src/test/test-dir/imports/import.aqua diff --git a/cli/src/test/test-dir/index.aqua b/cli/.jvm/src/test/test-dir/index.aqua similarity index 100% rename from cli/src/test/test-dir/index.aqua rename to cli/.jvm/src/test/test-dir/index.aqua diff --git a/cli/src/test/test-dir/path-test/importNear.aqua b/cli/.jvm/src/test/test-dir/path-test/importNear.aqua similarity index 100% rename from cli/src/test/test-dir/path-test/importNear.aqua rename to cli/.jvm/src/test/test-dir/path-test/importNear.aqua diff --git a/cli/src/test/test-dir/path-test/index.aqua b/cli/.jvm/src/test/test-dir/path-test/index.aqua similarity index 100% rename from cli/src/test/test-dir/path-test/index.aqua rename to cli/.jvm/src/test/test-dir/path-test/index.aqua diff --git a/cli/src/main/scala/aqua/AquaIO.scala b/cli/src/main/scala/aqua/AquaIO.scala index a92681e6..5d723716 100644 --- a/cli/src/main/scala/aqua/AquaIO.scala +++ b/cli/src/main/scala/aqua/AquaIO.scala @@ -3,7 +3,7 @@ package aqua import aqua.io.AquaFileError import cats.data.{Chain, EitherT, ValidatedNec} -import java.nio.file.Path +import fs2.io.file.Path trait AquaIO[F[_]] { def readFile(file: Path): EitherT[F, AquaFileError, String] diff --git a/cli/src/main/scala/aqua/CustomLogFormatter.scala b/cli/src/main/scala/aqua/CustomLogFormatter.scala deleted file mode 100644 index 793ed777..00000000 --- a/cli/src/main/scala/aqua/CustomLogFormatter.scala +++ /dev/null @@ -1,13 +0,0 @@ -package aqua - -import wvlet.log.LogFormatter.{appendStackTrace, highlightLog} -import wvlet.log.{LogFormatter, LogRecord} - -object CustomLogFormatter extends LogFormatter { - - override def formatLog(r: LogRecord): String = { - val log = - s"[${highlightLog(r.level, r.level.name)}] ${highlightLog(r.level, r.getMessage)}" - appendStackTrace(log, r) - } -} diff --git a/cli/src/main/scala/aqua/LogFormatter.scala b/cli/src/main/scala/aqua/LogFormatter.scala new file mode 100644 index 00000000..0eeed63f --- /dev/null +++ b/cli/src/main/scala/aqua/LogFormatter.scala @@ -0,0 +1,7 @@ +package aqua + +import scribe.format._ + +object LogFormatter { + val formatter: Formatter = formatter"$date ${string("[")}$levelColored${string("]")} $message$mdc" +} diff --git a/cli/src/main/scala/aqua/LogLevel.scala b/cli/src/main/scala/aqua/LogLevel.scala index 0ddae668..56caf63e 100644 --- a/cli/src/main/scala/aqua/LogLevel.scala +++ b/cli/src/main/scala/aqua/LogLevel.scala @@ -1,16 +1,16 @@ package aqua -import wvlet.log.{LogLevel => WLogLevel} +import scribe.Level object LogLevel { - val stringToLogLevel: Map[String, WLogLevel] = Map( - ("debug" -> WLogLevel.DEBUG), - ("trace" -> WLogLevel.TRACE), - ("info" -> WLogLevel.INFO), - ("off" -> WLogLevel.OFF), - ("warn" -> WLogLevel.WARN), - ("error" -> WLogLevel.ERROR), - ("all" -> WLogLevel.ALL) + val stringToLogLevel: Map[String, Level] = Map( + ("debug" -> Level.Debug), + ("trace" -> Level.Trace), + ("info" -> Level.Info), + ("off" -> Level.Fatal), + ("warn" -> Level.Warn), + ("error" -> Level.Error), + ("all" -> Level.Trace) ) } diff --git a/cli/src/main/scala/aqua/files/AquaFilesIO.scala b/cli/src/main/scala/aqua/files/AquaFilesIO.scala index e7e86d4f..4a49ab0c 100644 --- a/cli/src/main/scala/aqua/files/AquaFilesIO.scala +++ b/cli/src/main/scala/aqua/files/AquaFilesIO.scala @@ -1,20 +1,21 @@ package aqua.files import aqua.AquaIO -import aqua.io._ +import aqua.io.* +import cats.data.* import cats.data.Validated.{Invalid, Valid} -import cats.data._ import cats.effect.kernel.Concurrent -import cats.syntax.applicative._ -import cats.syntax.apply._ -import cats.syntax.either._ -import cats.syntax.flatMap._ -import cats.syntax.functor._ -import cats.syntax.applicativeError._ -import fs2.io.file.Files +import cats.syntax.applicative.* +import cats.syntax.applicativeError.* +import cats.syntax.apply.* +import cats.syntax.either.* +import cats.syntax.flatMap.* +import cats.syntax.functor.* +import cats.syntax.traverse.* +import cats.syntax.foldable.* +import fs2.io.file.{Files, Path} import fs2.text -import java.nio.file.Path import scala.util.Try class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { @@ -22,13 +23,13 @@ class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { override def readFile(file: Path): EitherT[F, AquaFileError, String] = EitherT( Files[F] - .readAll(file, 4096) + .readAll(file) .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) + .through(text.utf8.decode) .attempt .compile .last @@ -49,14 +50,14 @@ class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { ): EitherT[F, AquaFileError, Path] = in.headOption.fold(notFound)(p => EitherT( - Concurrent[F].attempt(p.toFile.isFile.pure[F]) + Concurrent[F].attempt(Files[F].isRegularFile(p)) ) .leftMap[AquaFileError](FileSystemError.apply) .recover({ case _ => false }) .flatMap { case true => EitherT( - Concurrent[F].attempt(p.toAbsolutePath.normalize().pure[F]) + Concurrent[F].attempt(p.absolute.normalize.pure[F]) ).leftMap[AquaFileError](FileSystemError.apply) case false => findFirstF(in.tail, notFound) @@ -76,56 +77,72 @@ class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { EitherT.leftT(FileNotFound(src, imports)) ) - override def listAqua(folder: Path): F[ValidatedNec[AquaFileError, Chain[Path]]] = - Validated - .fromEither( - Try { - val f = folder.toFile - if (!f.exists()) { - Left(FileNotFound(folder, Nil)) - } else if (f.isDirectory) { - Right(f.listFiles().toList) - } else { - Right(f :: Nil) + // Get all files for every path if the path in the list is a directory or this path otherwise + private def gatherFiles(files: List[Path], listFunction: (f: Path) => F[ValidatedNec[AquaFileError, Chain[Path]]]): List[F[ValidatedNec[AquaFileError, Chain[Path]]]] = { + files.map(f => gatherFile(f, listFunction)) + } + + // Get all files if the path is a directory or this path otherwise + private def gatherFile(f: Path, listFunction: (f: Path) => F[ValidatedNec[AquaFileError, Chain[Path]]]): F[ValidatedNec[AquaFileError, Chain[Path]]] = { + Files[F].isDirectory(f).flatMap { isDir => + if (isDir) + listFunction(f) + else + Files[F].isRegularFile(f).map { isFile => + if (isFile) + Validated.validNec(Chain.one(f.absolute.normalize)) + else + Validated.invalidNec(FileNotFound(f, Nil)) + } + } + } + + // Get all files if the path is a directory or this path otherwise + override def listAqua(folder: Path): F[ValidatedNec[AquaFileError, Chain[Path]]] = { + Files[F] + .exists(folder) + .flatMap { exists => + if (!exists) { + Left(FileNotFound(folder, Nil): AquaFileError).pure[F] + } else { + Files[F].isDirectory(folder).flatMap { isDir => + if (isDir) { + Files[F].list(folder).compile.toList.map(Right(_)) + } else { + Right(folder :: Nil).pure[F] + } } - }.toEither.leftMap[AquaFileError](FileSystemError.apply).flatMap(identity) - ) - .leftMap(NonEmptyChain.one) - .pure[F] + } + } + .map(Validated.fromEither) + .map(_.leftMap(NonEmptyChain.one)) .flatMap { case Valid(files) => - files.collect { - case f if f.isFile && f.getName.endsWith(".aqua") => - Validated - .fromTry( - Try(Chain.one(f.toPath.toAbsolutePath.normalize())) - ) - .leftMap(FileSystemError.apply) - .leftMap(NonEmptyChain.one) - .pure[F] - case f if f.isDirectory => - listAqua(f.toPath) - }.foldLeft(Validated.validNec[AquaFileError, Chain[Path]](Chain.nil).pure[F]) { - case (acc, v) => - (acc, v).mapN(_ combine _) + gatherFiles(files, listAqua).foldLeft( + Validated.validNec[AquaFileError, Chain[Path]](Chain.nil).pure[F] + ) { case (acc, v) => + (acc, v).mapN(_ combine _) } case Invalid(errs) => Validated.invalid[NonEmptyChain[AquaFileError], Chain[Path]](errs).pure[F] } + } private def deleteIfExists(file: Path): EitherT[F, AquaFileError, Boolean] = Files[F].deleteIfExists(file).attemptT.leftMap(FileSystemError.apply) - private def createDirectories(path: Path): EitherT[F, AquaFileError, Path] = - Files[F].createDirectories(path).attemptT.leftMap(FileSystemError.apply) + private def createDirectories(path: Path): EitherT[F, AquaFileError, Unit] = + Files[F].createDirectories(path).attemptT.leftMap(FileSystemError.apply(_): AquaFileError) // Writes to a file, creates directories if they do not exist override def writeFile(file: Path, content: String): EitherT[F, AquaFileError, Unit] = - deleteIfExists(file) >> createDirectories(file.getParent) >> + deleteIfExists(file) >> file.parent + .map(createDirectories) + .getOrElse(EitherT.liftF(().pure[F])) >> EitherT( fs2.Stream .emit(content) - .through(text.utf8Encode) + .through(text.utf8.encode) .through(Files[F].writeAll(file)) .attempt .compile diff --git a/cli/src/main/scala/aqua/io/AquaFileError.scala b/cli/src/main/scala/aqua/io/AquaFileError.scala index 1a93d6ed..5805bac9 100644 --- a/cli/src/main/scala/aqua/io/AquaFileError.scala +++ b/cli/src/main/scala/aqua/io/AquaFileError.scala @@ -2,7 +2,7 @@ package aqua.io import cats.data.NonEmptyChain -import java.nio.file.Path +import fs2.io.file.Path sealed trait AquaFileError { def showForConsole: String diff --git a/linker/src/main/scala/aqua/linker/Linker.scala b/linker/src/main/scala/aqua/linker/Linker.scala index dd7ed2ac..516ee541 100644 --- a/linker/src/main/scala/aqua/linker/Linker.scala +++ b/linker/src/main/scala/aqua/linker/Linker.scala @@ -3,11 +3,11 @@ package aqua.linker import cats.data.{NonEmptyChain, Validated, ValidatedNec} import cats.kernel.{Monoid, Semigroup} import cats.syntax.monoid._ -import wvlet.log.LogSupport +import scribe.Logging import scala.annotation.tailrec -object Linker extends LogSupport { +object Linker extends Logging { @tailrec def iter[I, E, T: Semigroup]( @@ -20,22 +20,22 @@ object Linker extends LogSupport { Right(proc) case _ => val (canHandle, postpone) = mods.partition(_.dependsOn.keySet.forall(proc.contains)) - debug("ITERATE, can handle: " + canHandle.map(_.id)) - debug(s"dependsOn = ${mods.map(_.dependsOn.keySet)}") - debug(s"postpone = ${postpone.map(_.id)}") - debug(s"proc = ${proc.keySet}") + logger.debug("ITERATE, can handle: " + canHandle.map(_.id)) + logger.debug(s"dependsOn = ${mods.map(_.dependsOn.keySet)}") + logger.debug(s"postpone = ${postpone.map(_.id)}") + logger.debug(s"proc = ${proc.keySet}") if (canHandle.isEmpty && postpone.nonEmpty) Left(cycleError(postpone)) else { val folded = canHandle.foldLeft(proc) { case (acc, m) => val importKeys = m.dependsOn.keySet - debug(s"${m.id} dependsOn $importKeys") + logger.debug(s"${m.id} dependsOn $importKeys") val deps: T => T = importKeys.map(acc).foldLeft[T => T](identity) { case (fAcc, f) => - debug("COMBINING ONE TIME ") + logger.debug("COMBINING ONE TIME ") t => { - debug(s"call combine $t") + logger.debug(s"call combine $t") fAcc(t) |+| f(t) } } diff --git a/model/src/main/scala/aqua/model/AquaContext.scala b/model/src/main/scala/aqua/model/AquaContext.scala index cc611779..f83f592f 100644 --- a/model/src/main/scala/aqua/model/AquaContext.scala +++ b/model/src/main/scala/aqua/model/AquaContext.scala @@ -7,7 +7,7 @@ import cats.Monoid import cats.data.NonEmptyMap import cats.syntax.functor.* import cats.syntax.monoid.* -import wvlet.log.LogSupport +import scribe.Logging import scala.collection.immutable.SortedMap @@ -19,7 +19,7 @@ case class AquaContext( // TODO: merge this with abilities, when have ability resolution variance services: Map[String, ServiceModel] ) { - + private def prefixFirst[T](prefix: String, pair: (String, T)): (String, T) = (prefix + pair._1, pair._2) @@ -28,7 +28,7 @@ case class AquaContext( .foldLeft(types) { case (ts, (k, v)) => ts ++ v.allTypes(k + ".") } - .map(prefixFirst(prefix, _)) + .map(prefixFirst(prefix, _)) def allFuncs(prefix: String = ""): Map[String, FuncCallable] = abilities @@ -67,7 +67,7 @@ case class AquaContext( .map(StructType(name, _)) } -object AquaContext extends LogSupport { +object AquaContext extends Logging { trait Implicits { implicit val aquaContextMonoid: Monoid[AquaContext] diff --git a/model/src/main/scala/aqua/model/ValueModel.scala b/model/src/main/scala/aqua/model/ValueModel.scala index dcd93f69..7423e89c 100644 --- a/model/src/main/scala/aqua/model/ValueModel.scala +++ b/model/src/main/scala/aqua/model/ValueModel.scala @@ -3,7 +3,7 @@ package aqua.model import aqua.types._ import cats.Eq import cats.data.{Chain, NonEmptyMap} -import wvlet.log.LogSupport +import scribe.Logging sealed trait ValueModel { def `type`: Type @@ -46,7 +46,8 @@ case class IntoFieldModel(field: String, `type`: Type) extends LambdaModel case class IntoIndexModel(idx: Int, `type`: Type) extends LambdaModel case class VarModel(name: String, `type`: Type, lambda: Chain[LambdaModel] = Chain.empty) - extends ValueModel with LogSupport { + extends ValueModel with Logging { + def deriveFrom(vm: VarModel): VarModel = vm.copy(lambda = vm.lambda ++ lambda) override val lastType: Type = lambda.lastOption.map(_.`type`).getOrElse(`type`) @@ -81,7 +82,7 @@ case class VarModel(name: String, `type`: Type, lambda: Chain[LambdaModel] = Cha deriveFrom(nvm) case valueModel => if (lambda.nonEmpty) - error( + logger.error( s"Var $name derived from scalar $valueModel, but lambda is lost: $lambda" ) valueModel diff --git a/model/src/main/scala/aqua/model/func/FuncCallable.scala b/model/src/main/scala/aqua/model/func/FuncCallable.scala index fd0e8154..4cc11474 100644 --- a/model/src/main/scala/aqua/model/func/FuncCallable.scala +++ b/model/src/main/scala/aqua/model/func/FuncCallable.scala @@ -7,7 +7,7 @@ import aqua.types.{ArrowType, ProductType, StreamType, Type} import cats.Eval import cats.data.Chain import cats.free.Cofree -import wvlet.log.Logger +import scribe.Logging case class FuncCallable( funcName: String, @@ -16,10 +16,7 @@ case class FuncCallable( ret: List[ValueModel], capturedArrows: Map[String, FuncCallable], capturedValues: Map[String, ValueModel] -) extends Model { - - private val logger = Logger.of[FuncCallable] - import logger._ +) extends Model with Logging { lazy val args: List[(String, Type)] = arrowType.domain.toLabelledList() lazy val argNames: List[String] = args.map(_._1) @@ -48,7 +45,7 @@ case class FuncCallable( forbiddenNames: Set[String] ): Eval[(FuncOp, List[ValueModel])] = { - debug("Call: " + call) + logger.debug("Call: " + call) // Collect all arguments: what names are used inside the function, what values are received val argsFull = ArgsCall(arrowType.domain, call.args) @@ -140,7 +137,7 @@ case class FuncCallable( case (acc @ (_, resolvedExports), tag) => tag match { case CallArrowTag(fn, _) if !allArrows.contains(fn) => - error(s"UNRESOLVED $fn in $funcName, skipping, will become (null) in AIR!") + logger.error(s"UNRESOLVED $fn in $funcName, skipping, will become (null) in AIR!") case _ => } diff --git a/model/src/main/scala/aqua/model/func/raw/PathFinder.scala b/model/src/main/scala/aqua/model/func/raw/PathFinder.scala index b81e9478..728564b2 100644 --- a/model/src/main/scala/aqua/model/func/raw/PathFinder.scala +++ b/model/src/main/scala/aqua/model/func/raw/PathFinder.scala @@ -3,11 +3,11 @@ package aqua.model.func.raw import aqua.model.ValueModel import cats.data.Chain import cats.data.Chain.{:==, ==:, nil} -import wvlet.log.LogSupport +import scribe.Logging import scala.annotation.tailrec -object PathFinder extends LogSupport { +object PathFinder extends Logging { def find(from: RawCursor, to: RawCursor, isExit: Boolean = false): Chain[ValueModel] = { @@ -21,14 +21,14 @@ object PathFinder extends LogSupport { !to.parentTag.exists(_.isInstanceOf[ParGroupTag]) if (wasHandled) { - debug("Was handled") - debug(" :: " + from) - debug(" -> " + to) + logger.debug("Was handled") + logger.debug(" :: " + from) + logger.debug(" -> " + to) Chain.empty } else { - debug("Find path") - debug(" :: " + from) - debug(" -> " + to) + logger.debug("Find path") + logger.debug(" :: " + from) + logger.debug(" -> " + to) findPath( fromOn, toOn, @@ -49,10 +49,10 @@ object PathFinder extends LogSupport { case (acc, p) if acc.contains(p) => acc.takeWhile(_ != p) :+ p case (acc, p) => acc :+ p } - trace(s"PEER IDS: $optimized") - trace(s"PREFIX: $prefix") - trace(s"SUFFIX: $suffix") - trace(s"OPTIMIZED WITH PREFIX AND SUFFIX: $optimized") + logger.trace(s"PEER IDS: $optimized") + logger.trace(s"PREFIX: $prefix") + logger.trace(s"SUFFIX: $suffix") + logger.trace(s"OPTIMIZED WITH PREFIX AND SUFFIX: $optimized") val noPrefix = skipPrefix(optimized, prefix, optimized) skipSuffix(noPrefix, suffix, noPrefix) } @@ -63,28 +63,28 @@ object PathFinder extends LogSupport { fromPeer: Option[ValueModel], toPeer: Option[ValueModel] ): Chain[ValueModel] = { - trace(s"FROM ON: $fromOn") - trace(s"TO ON: $toOn") + logger.trace(s"FROM ON: $fromOn") + logger.trace(s"TO ON: $toOn") val (from, to) = skipCommonPrefix(fromOn, toOn) val fromFix = if (from.isEmpty && fromPeer != toPeer) Chain.fromOption(fromOn.lastOption) else from val toFix = if (to.isEmpty && fromPeer != toPeer) Chain.fromOption(toOn.lastOption) else to - trace("FIND PATH FROM | " + fromFix) - trace(" TO | " + toFix) + logger.trace("FIND PATH FROM | " + fromFix) + logger.trace(" TO | " + toFix) val fromTo = fromFix.reverse.flatMap(_.via.reverse) ++ toFix.flatMap(_.via) - trace(s"FROM TO: $fromTo") + logger.trace(s"FROM TO: $fromTo") val fromPeerCh = Chain.fromOption(fromPeer) val toPeerCh = Chain.fromOption(toPeer) val optimized = optimizePath(fromPeerCh ++ fromTo ++ toPeerCh, fromPeerCh, toPeerCh) - trace( + logger.trace( s"FROM PEER '${fromPeer.map(_.toString).getOrElse("None")}' TO PEER '${toPeer.map(_.toString).getOrElse("None")}'" ) - trace(" Optimized: " + optimized) + logger.trace(" Optimized: " + optimized) optimized } diff --git a/model/src/main/scala/aqua/model/func/raw/RawCursor.scala b/model/src/main/scala/aqua/model/func/raw/RawCursor.scala index b9086ab6..b276434c 100644 --- a/model/src/main/scala/aqua/model/func/raw/RawCursor.scala +++ b/model/src/main/scala/aqua/model/func/raw/RawCursor.scala @@ -6,11 +6,12 @@ import cats.Eval import cats.data.{Chain, NonEmptyList, OptionT} import cats.free.Cofree import cats.syntax.traverse._ -import wvlet.log.LogSupport +import scribe.Logging // Can be heavily optimized by caching parent cursors, not just list of zippers case class RawCursor(tree: NonEmptyList[ChainZipper[FuncOp.Tree]]) - extends ChainCursor[RawCursor, FuncOp.Tree](RawCursor.apply) with LogSupport { + extends ChainCursor[RawCursor, FuncOp.Tree](RawCursor.apply) with Logging { + def tag: RawTag = current.head def parentTag: Option[RawTag] = parent.map(_.head) diff --git a/model/src/main/scala/aqua/model/topology/Topology.scala b/model/src/main/scala/aqua/model/topology/Topology.scala index a4818a27..528907b3 100644 --- a/model/src/main/scala/aqua/model/topology/Topology.scala +++ b/model/src/main/scala/aqua/model/topology/Topology.scala @@ -10,9 +10,9 @@ import cats.data.Chain.nil import cats.data.{Chain, NonEmptyChain, NonEmptyList, OptionT} import cats.free.Cofree import cats.syntax.traverse._ -import wvlet.log.LogSupport +import scribe.Logging -object Topology extends LogSupport { +object Topology extends Logging { type Tree = Cofree[Chain, RawTag] type Res = Cofree[Chain, ResolvedOp] @@ -50,7 +50,7 @@ object Topology extends LogSupport { val cursor = RawCursor(NonEmptyList.one(ChainZipper.one(op))) val resolvedCofree = cursor .cata(wrap) { rc => - debug(s"<:> $rc") + logger.debug(s"<:> $rc") val resolved = MakeRes .resolve(rc.currentPeerId) .lift @@ -64,12 +64,12 @@ object Topology extends LogSupport { through(rc.pathToNext) ) if (cz.next.nonEmpty || cz.prev.nonEmpty) { - debug(s"Resolved $rc -> $cofree") + logger.debug(s"Resolved $rc -> $cofree") if (cz.prev.nonEmpty) - trace("From prev: " + cz.prev.map(_.head).toList.mkString(" -> ")) + logger.trace("From prev: " + cz.prev.map(_.head).toList.mkString(" -> ")) if (cz.next.nonEmpty) - trace("To next: " + cz.next.map(_.head).toList.mkString(" -> ")) - } else debug(s"EMPTY $rc -> $cofree") + logger.trace("To next: " + cz.next.map(_.head).toList.mkString(" -> ")) + } else logger.debug(s"EMPTY $rc -> $cofree") cz } ) @@ -78,11 +78,11 @@ object Topology extends LogSupport { resolvedCofree.map(NonEmptyChain.fromChain(_).map(_.uncons)).map { case None => - error("Topology emitted nothing") + logger.error("Topology emitted nothing") Cofree(SeqRes, MakeRes.nilTail) case Some((el, `nil`)) => el case Some((el, tail)) => - warn("Topology emitted many nodes, that's unusual") + logger.warn("Topology emitted many nodes, that's unusual") Cofree(SeqRes, Eval.now(el +: tail)) } } diff --git a/model/src/main/scala/aqua/model/transform/Transform.scala b/model/src/main/scala/aqua/model/transform/Transform.scala index 30871d12..e2cc51ee 100644 --- a/model/src/main/scala/aqua/model/transform/Transform.scala +++ b/model/src/main/scala/aqua/model/transform/Transform.scala @@ -7,9 +7,9 @@ import aqua.model.topology.Topology import aqua.types.ScalarType import cats.data.Chain import cats.free.Cofree -import wvlet.log.LogSupport +import scribe.Logging -object Transform extends LogSupport { +object Transform extends Logging { def defaultFilter(t: ResolvedOp): Boolean = t match { case _: NoAir => false diff --git a/parser/src/test/scala/aqua/parser/FuncExprSpec.scala b/parser/src/test/scala/aqua/parser/FuncExprSpec.scala index e9f39ba9..f95adf69 100644 --- a/parser/src/test/scala/aqua/parser/FuncExprSpec.scala +++ b/parser/src/test/scala/aqua/parser/FuncExprSpec.scala @@ -113,7 +113,7 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec { | <- v |""".stripMargin - parser[Id]().parseAll(script).value.toEither shouldBe Symbol("left") + parser[Id]().parseAll(script).value.toEither.isLeft shouldBe true } "function with multiline definitions" should "parse without error" in { @@ -121,7 +121,7 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec { """func tryGen(a: string, | b: string)""".stripMargin - FuncExpr.p[Id].parseAll(script) shouldBe Symbol("right") + FuncExpr.p[Id].parseAll(script).isRight shouldBe true } "function with root expression without children" should "parse with error" in { @@ -131,7 +131,7 @@ class FuncExprSpec extends AnyFlatSpec with Matchers with AquaSpec { | <- v |""".stripMargin - parser[Id]().parseAll(script).value.toEither shouldBe Symbol("left") + parser[Id]().parseAll(script).value.toEither.isLeft shouldBe true } "multi function expression" should "parse" in { diff --git a/project/plugins.sbt b/project/plugins.sbt index 72477a2f..ffb6c88d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1 +1,3 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") diff --git a/semantics/src/main/scala/aqua/semantics/Semantics.scala b/semantics/src/main/scala/aqua/semantics/Semantics.scala index 5e7fcb3c..83a91879 100644 --- a/semantics/src/main/scala/aqua/semantics/Semantics.scala +++ b/semantics/src/main/scala/aqua/semantics/Semantics.scala @@ -23,9 +23,9 @@ import cats.syntax.apply._ import cats.syntax.semigroup._ import monocle.Lens import monocle.macros.GenLens -import wvlet.log.LogSupport +import scribe.Logging -object Semantics extends LogSupport { +object Semantics extends Logging { def folder[F[_], G[_]](implicit A: AbilitiesAlgebra[F, G],