From f34cd3a4e20a5d5a32050d20ef1f54fe96d7786f Mon Sep 17 00:00:00 2001 From: Dima Date: Mon, 31 May 2021 12:50:31 +0300 Subject: [PATCH] 120 improve output (#137) --- cli/src/main/scala/aqua/Aqua.scala | 27 ++++--- cli/src/main/scala/aqua/AquaCli.scala | 21 +++-- cli/src/main/scala/aqua/AquaCompiler.scala | 81 ++++++++++++++----- cli/src/main/scala/aqua/AquaError.scala | 33 +++----- cli/src/main/scala/aqua/Test.scala | 4 +- cli/src/main/scala/aqua/io/AquaFile.scala | 6 +- .../main/scala/aqua/io/AquaFileError.scala | 5 +- cli/src/main/scala/aqua/io/FileModuleId.scala | 11 +-- .../scala/aqua/parser/lift/FileSpan.scala | 14 ++-- .../main/scala/aqua/parser/lift/Span.scala | 29 ++++--- 10 files changed, 138 insertions(+), 93 deletions(-) diff --git a/cli/src/main/scala/aqua/Aqua.scala b/cli/src/main/scala/aqua/Aqua.scala index a4c8d0d3..164a9585 100644 --- a/cli/src/main/scala/aqua/Aqua.scala +++ b/cli/src/main/scala/aqua/Aqua.scala @@ -1,28 +1,29 @@ package aqua -import aqua.parser.{Ast, BlockIndentError, FuncReturnError, LexerError} import aqua.parser.lift.{FileSpan, LiftParser, Span} +import aqua.parser.{Ast, BlockIndentError, FuncReturnError, LexerError} +import cats.Eval import cats.data.ValidatedNec +import cats.parse.LocationMap object Aqua { - def parseString(input: String): ValidatedNec[AquaError, Ast[Span.F]] = - Ast - .fromString[Span.F](input) - .leftMap(_.map { - case BlockIndentError(indent, message) => CustomSyntaxError(indent._1, message) - case FuncReturnError(point, message) => CustomSyntaxError(point._1, message) - case LexerError(pe) => SyntaxError(pe.failedAtOffset, pe.expected) - }) - def parseFileString(name: String, input: String): ValidatedNec[AquaError, Ast[FileSpan.F]] = { implicit val fileLift: LiftParser[FileSpan.F] = FileSpan.fileSpanLiftParser(name, input) Ast .fromString[FileSpan.F](input) .leftMap(_.map { - case BlockIndentError(indent, message) => CustomSyntaxError(indent._1.span, message) - case FuncReturnError(point, message) => CustomSyntaxError(point._1.span, message) - case LexerError(pe) => SyntaxError(pe.failedAtOffset, pe.expected) + case BlockIndentError(indent, message) => CustomSyntaxError(indent._1, message) + case FuncReturnError(point, message) => CustomSyntaxError(point._1, message) + case LexerError(pe) => + val fileSpan = + FileSpan( + name, + input, + Eval.later(LocationMap(input)), + Span(pe.failedAtOffset, pe.failedAtOffset + 1) + ) + SyntaxError(fileSpan, pe.expected) }) } diff --git a/cli/src/main/scala/aqua/AquaCli.scala b/cli/src/main/scala/aqua/AquaCli.scala index 03419d0c..ed68d188 100644 --- a/cli/src/main/scala/aqua/AquaCli.scala +++ b/cli/src/main/scala/aqua/AquaCli.scala @@ -3,7 +3,7 @@ package aqua import aqua.model.transform.BodyConfig import cats.data.Validated import cats.effect._ -import cats.effect.std.Console +import cats.effect.std.{Console => ConsoleEff} import cats.syntax.apply._ import cats.syntax.functor._ import com.monovore.decline.Opts @@ -11,12 +11,22 @@ 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 wvlet.log.LogFormatter.{appendStackTrace, highlightLog} +import wvlet.log.{LogFormatter, LogRecord, LogSupport, Logger => WLogger} + +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) + } +} object AquaCli extends IOApp with LogSupport { import AppOps._ - def main[F[_]: Concurrent: Files: Console: Logger]: Opts[F[ExitCode]] = { + def main[F[_]: Concurrent: Files: ConsoleEff: Logger]: Opts[F[ExitCode]] = { versionOpt .as( versionAndExit @@ -34,6 +44,7 @@ object AquaCli extends IOApp with LogSupport { logLevelOpt ).mapN { case (input, imports, output, toAir, noRelay, noXor, h, v, logLevel) => WLogger.setDefaultLogLevel(LogLevel.toLogLevel(logLevel)) + WLogger.setDefaultFormatter(CustomLogFormatter) // if there is `--help` or `--version` flag - show help and version // otherwise continue program execution @@ -52,8 +63,8 @@ object AquaCli extends IOApp with LogSupport { case Validated.Invalid(errs) => errs.map(println) ExitCode.Error - case Validated.Valid(res) => - res.map(println) + case Validated.Valid(results) => + results.map(println) ExitCode.Success } } diff --git a/cli/src/main/scala/aqua/AquaCompiler.scala b/cli/src/main/scala/aqua/AquaCompiler.scala index 65f1a45d..b0c40a19 100644 --- a/cli/src/main/scala/aqua/AquaCompiler.scala +++ b/cli/src/main/scala/aqua/AquaCompiler.scala @@ -10,6 +10,7 @@ import aqua.parser.lexer.Token import aqua.parser.lift.FileSpan import aqua.semantics.{CompilerState, Semantics} import cats.Applicative +import cats.data.Validated.{Invalid, Valid} import cats.data._ import cats.effect.kernel.Concurrent import cats.syntax.flatMap._ @@ -18,19 +19,30 @@ import cats.syntax.monoid._ import cats.syntax.show._ import fs2.io.file.Files import fs2.text +import wvlet.log.LogSupport import java.nio.file.Path -object AquaCompiler { +object AquaCompiler extends LogSupport { sealed trait CompileTarget case object TypescriptTarget extends CompileTarget case object AirTarget extends CompileTarget - case class Prepared(target: String => Path, model: ScriptModel) { + case class Prepared(srcFile: Path, srcDir: Path, model: ScriptModel) { def hasOutput(target: CompileTarget): Boolean = target match { case _ => model.funcs.nonEmpty } + + def targetPath(ext: String): Validated[Throwable, Path] = + Validated.catchNonFatal { + val fileName = srcFile.getFileName + if (fileName == null) { + throw new Exception(s"Unexpected: 'fileName' is null in path $srcFile") + } else { + srcDir.resolve(fileName.toString.stripSuffix(".aqua") + s".$ext") + } + } } def prepareFiles[F[_]: Files: Concurrent]( @@ -69,7 +81,19 @@ object AquaCompiler { (errs ++ showProcErrors(proc.errors), preps) case (_, model: ScriptModel) => - (errs, preps :+ Prepared(modId.targetPath(srcPath, targetPath, _), model)) + val src = Validated.catchNonFatal { + targetPath.toAbsolutePath + .normalize() + .resolve( + srcPath.toAbsolutePath + .normalize() + .relativize(modId.file.toAbsolutePath.normalize()) + ) + } + src match { + case Validated.Invalid(t) => (errs :+ t.getMessage, preps) + case Validated.Valid(s) => (errs, preps :+ Prepared(s, srcPath, model)) + } case (_, model) => ( @@ -95,7 +119,7 @@ object AquaCompiler { ): Chain[String] = errors.map(err => err._1.unit._1 - .focus(1) + .focus(2) .map(_.toConsoleStr(err._2, Console.CYAN)) .getOrElse("(Dup error, but offset is beyond the script)") + "\n" ) @@ -108,28 +132,47 @@ object AquaCompiler { bodyConfig: BodyConfig ): F[ValidatedNec[String, Chain[String]]] = prepareFiles(srcPath, imports, targetPath) - .map(_.map(_.filter(_.hasOutput(compileTo)))) + .map(_.map(_.filter { p => + val hasOutput = p.hasOutput(compileTo) + if (!hasOutput) info(s"Source ${p.srcFile}: compilation OK (nothing to emit)") + else info(s"Source ${p.srcFile}: compilation OK (${p.model.funcs.length} functions)") + 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 => writeFile(p.target("ts"), TypescriptFile(p.model).generateTS(bodyConfig))) + preps.map { p => + val tpV = p.targetPath("ts") + tpV match { + case Invalid(t) => + EitherT.pure(t.getMessage) + case Valid(tp) => + writeFile(tp, TypescriptFile(p.model).generateTS(bodyConfig)) + } + + } // TODO add function name to AirTarget class case AirTarget => preps .flatMap(p => - p.model.resolveFunctions.map { fc => - fc.funcName -> FuncAirGen(fc).generateAir(bodyConfig).show - }.map { case (n, g) => - writeFile( - p.target(n + ".air"), - g - ) - } + p.model.resolveFunctions + .map(fc => FuncAirGen(fc).generateAir(bodyConfig).show) + .map { generated => + val tpV = p.targetPath("ts") + tpV match { + case Invalid(t) => + EitherT.pure(t.getMessage) + case Valid(tp) => + writeFile( + tp, + generated + ) + } + } ) }).foldLeft( @@ -140,7 +183,7 @@ object AquaCompiler { w <- writeET.value } yield (a, w) match { case (Left(errs), Left(err)) => Left(errs :+ err) - case (Right(res), Right(r)) => Right(res :+ r) + case (Right(res), Right(_)) => Right(res) case (Left(errs), _) => Left(errs) case (_, Left(err)) => Left(NonEmptyChain.of(err)) }) @@ -149,9 +192,9 @@ object AquaCompiler { } - def writeFile[F[_]: Files: Concurrent](file: Path, content: String): EitherT[F, String, String] = + def writeFile[F[_]: Files: Concurrent](file: Path, content: String): EitherT[F, String, Unit] = EitherT.right[String](Files[F].deleteIfExists(file)) >> - EitherT[F, String, String]( + EitherT[F, String, Unit]( fs2.Stream .emit( content @@ -165,7 +208,7 @@ object AquaCompiler { } .compile .drain - .map(_ => Right(s"Compiled $file")) + .map(_ => Right(())) ) } diff --git a/cli/src/main/scala/aqua/AquaError.scala b/cli/src/main/scala/aqua/AquaError.scala index d5dd67d9..353a97c3 100644 --- a/cli/src/main/scala/aqua/AquaError.scala +++ b/cli/src/main/scala/aqua/AquaError.scala @@ -1,20 +1,18 @@ package aqua -import aqua.parser.lift.Span -import cats.Eval +import aqua.parser.lift.FileSpan import cats.data.NonEmptyList -import cats.parse.LocationMap import cats.parse.Parser.Expectation sealed trait AquaError { - def showForConsole(script: String): String + def showForConsole: String } -case class CustomSyntaxError(span: Span, message: String) extends AquaError { +case class CustomSyntaxError(span: FileSpan, message: String) extends AquaError { - override def showForConsole(script: String): String = + override def showForConsole: String = span - .focus(Eval.later(LocationMap(script)), 2) + .focus(3) .map( _.toConsoleStr( message, @@ -27,13 +25,13 @@ case class CustomSyntaxError(span: Span, message: String) extends AquaError { ) + Console.RESET + "\n" } -case class SyntaxError(offset: Int, expectations: NonEmptyList[Expectation]) extends AquaError { +case class SyntaxError(span: FileSpan, expectations: NonEmptyList[Expectation]) extends AquaError { - override def showForConsole(script: String): String = - Span(offset, offset + 1) - .focus(Eval.later(LocationMap(script)), 2) - .map( - _.toConsoleStr( + override def showForConsole: String = + span + .focus(3) + .map(spanFocus => + spanFocus.toConsoleStr( s"Syntax error, expected: ${expectations.toList.mkString(", ")}", Console.RED ) @@ -43,12 +41,3 @@ case class SyntaxError(offset: Int, expectations: NonEmptyList[Expectation]) ext .mkString(", ") ) + Console.RESET + "\n" } - -case class CompilerError(span: Span, hint: String) extends AquaError { - - override def showForConsole(script: String): String = - span - .focus(Eval.later(LocationMap(script)), 1) - .map(_.toConsoleStr(hint, Console.CYAN)) - .getOrElse("(Dup error, but offset is beyond the script)") + "\n" -} diff --git a/cli/src/main/scala/aqua/Test.scala b/cli/src/main/scala/aqua/Test.scala index 135374e7..15a5065e 100644 --- a/cli/src/main/scala/aqua/Test.scala +++ b/cli/src/main/scala/aqua/Test.scala @@ -25,8 +25,8 @@ object Test extends IOApp.Simple { .map { case Validated.Invalid(errs) => errs.map(println) - case Validated.Valid(res) => - res.map(println) + case Validated.Valid(_) => + } } diff --git a/cli/src/main/scala/aqua/io/AquaFile.scala b/cli/src/main/scala/aqua/io/AquaFile.scala index 9d99ef14..99730ad6 100644 --- a/cli/src/main/scala/aqua/io/AquaFile.scala +++ b/cli/src/main/scala/aqua/io/AquaFile.scala @@ -7,10 +7,10 @@ import aqua.parser.head.ImportExpr import aqua.parser.lift.FileSpan import cats.data.{EitherT, NonEmptyChain} import cats.effect.Concurrent +import cats.syntax.apply._ +import cats.syntax.functor._ import fs2.io.file.Files import fs2.text -import cats.syntax.functor._ -import cats.syntax.apply._ import java.nio.file.{Path, Paths} @@ -78,7 +78,7 @@ object AquaFile { .map(source -> _) .toEither .left - .map(AquaScriptErrors(file.toString, source, _)) + .map(AquaScriptErrors(_)) ) ) diff --git a/cli/src/main/scala/aqua/io/AquaFileError.scala b/cli/src/main/scala/aqua/io/AquaFileError.scala index 6cbce04e..40404e87 100644 --- a/cli/src/main/scala/aqua/io/AquaFileError.scala +++ b/cli/src/main/scala/aqua/io/AquaFileError.scala @@ -34,9 +34,8 @@ case class Unresolvable(msg: String) extends AquaFileError { } // TODO there should be no AquaErrors, as they does not fit -case class AquaScriptErrors(name: String, script: String, errors: NonEmptyChain[AquaError]) - extends AquaFileError { +case class AquaScriptErrors(errors: NonEmptyChain[AquaError]) extends AquaFileError { override def showForConsole: String = - errors.map(_.showForConsole(script)).toChain.toList.mkString("\n") + errors.map(_.showForConsole).toChain.toList.mkString("\n") } diff --git a/cli/src/main/scala/aqua/io/FileModuleId.scala b/cli/src/main/scala/aqua/io/FileModuleId.scala index 9d4a8f03..2e52b253 100644 --- a/cli/src/main/scala/aqua/io/FileModuleId.scala +++ b/cli/src/main/scala/aqua/io/FileModuleId.scala @@ -7,16 +7,7 @@ import cats.syntax.applicative._ import java.nio.file.Path -case class FileModuleId(file: Path) { - - def targetPath(src: Path, target: Path, ext: String): Path = { - val aqua = - target.toAbsolutePath - .normalize() - .resolve(src.toAbsolutePath.normalize().relativize(file.toAbsolutePath.normalize())) - aqua.getParent.resolve(aqua.getFileName.toString.stripSuffix(".aqua") + s".$ext") - } -} +case class FileModuleId(file: Path) {} object FileModuleId { diff --git a/parser/src/main/scala/aqua/parser/lift/FileSpan.scala b/parser/src/main/scala/aqua/parser/lift/FileSpan.scala index 4a312cff..be7edd27 100644 --- a/parser/src/main/scala/aqua/parser/lift/FileSpan.scala +++ b/parser/src/main/scala/aqua/parser/lift/FileSpan.scala @@ -1,10 +1,11 @@ package aqua.parser.lift -import cats.{Comonad, Eval} import cats.parse.{LocationMap, Parser => P} +import cats.{Comonad, Eval} import scala.language.implicitConversions +// TODO: rewrite FileSpan and Span under one trait case class FileSpan(name: String, source: String, locationMap: Eval[LocationMap], span: Span) { def focus(ctx: Int): Option[FileSpan.Focus] = @@ -16,11 +17,12 @@ object FileSpan { case class Focus(name: String, locationMap: Eval[LocationMap], ctx: Int, spanFocus: Span.Focus) { def toConsoleStr(msg: String, onLeft: String, onRight: String = Console.RESET): String = - s"$name:${spanFocus.line._1 + 1}:${spanFocus.column + 1}\n" + spanFocus.toConsoleStr( - msg, - onLeft, - onRight - ) + s"$name:${spanFocus.line._1 + 1}:${spanFocus.column + 1}\n" + + spanFocus.toConsoleStr( + msg, + onLeft, + onRight + ) } type F[T] = (FileSpan, T) diff --git a/parser/src/main/scala/aqua/parser/lift/Span.scala b/parser/src/main/scala/aqua/parser/lift/Span.scala index 31b72c47..a0f120b9 100644 --- a/parser/src/main/scala/aqua/parser/lift/Span.scala +++ b/parser/src/main/scala/aqua/parser/lift/Span.scala @@ -12,18 +12,23 @@ case class Span(startIndex: Int, endIndex: Int) { map.toLineCol(startIndex).flatMap { case (line, column) => map .getLine(line) - .map(l => + .map { l => + val pre = + (Math.max(0, line - ctx) until line).map(i => map.getLine(i).map(i -> _)).toList.flatten + val linePos = { + val (l1, l2) = l.splitAt(column) + val (lc, l3) = l2.splitAt(endIndex - startIndex) + (line, l1, lc, l3) + } + val post = + ((line + 1) to (line + ctx)).map(i => map.getLine(i).map(i -> _)).toList.flatten Span.Focus( - (Math - .max(0, line - ctx) until line).map(i => map.getLine(i).map(i -> _)).toList.flatten, { - val (l1, l2) = l.splitAt(column) - val (lc, l3) = l2.splitAt(endIndex - startIndex) - (line, l1, lc, l3) - }, - ((line + 1) to (line + ctx)).map(i => map.getLine(i).map(i -> _)).toList.flatten, + pre, + linePos, + post, column ) - ) + } } } } @@ -48,7 +53,11 @@ object Span { onLeft + s + (" " * (lastNSize - s.length)) + onRight + " " } - def toConsoleStr(msg: String, onLeft: String, onRight: String = Console.RESET): String = { + def toConsoleStr( + msg: String, + onLeft: String, + onRight: String = Console.RESET + ): String = { val line3Length = line._3.length val line3Mult = if (line3Length == 0) 1 else line3Length pre.map(formatLine(_, onLeft, onRight)).mkString("\n") +