diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b967f513..e9736eb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,6 +27,11 @@ jobs: env: BUILD_NUMBER: ${{ github.run_number }} + - name: JS language server API build + run: sbt language-server-api/fullLinkJS + env: + BUILD_NUMBER: ${{ github.run_number }} + - name: Get project version # In CI sbt appends a new line after its output, so we need `tail -n3 | head -n2` to get last two non-empty lines run: | @@ -56,6 +61,13 @@ jobs: stat "$JS" echo "JS=$JS" >> $GITHUB_ENV + - name: Check API .js exists + run: | + JSAPI="language-server-api/target/scala-3.1.0/language-server-api-opt/aqua-${{ env.VERSION }}.js" + mv language-server-api/target/scala-3.1.0/language-server-api-opt/main.js "$JSAPI" + stat "$JSAPI" + echo "JSAPI=$JSAPI" >> $GITHUB_ENV + ### Publish to NPM registry - uses: actions/setup-node@v1 with: @@ -63,6 +75,7 @@ jobs: registry-url: "https://registry.npmjs.org" - run: cp ${{ env.JS }} ./npm/aqua.js + - run: cp ${{ env.JSAPI }} ./language-server-npm/aqua-lsp-api.js - run: npm version ${{ env.VERSION }} working-directory: ./npm @@ -111,3 +124,15 @@ jobs: ${{ env.JS }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - run: npm version ${{ env.VERSION }} + working-directory: ./language-server-npm + + - name: Publish aqua LSP API to NPM + run: | + npm i + npm publish --access public + working-directory: ./language-server-npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + diff --git a/build.sbt b/build.sbt index ddc46a8d..0e0bd9d7 100644 --- a/build.sbt +++ b/build.sbt @@ -46,13 +46,11 @@ lazy val cli = crossProject(JSPlatform, JVMPlatform) .settings(commons: _*) .settings( libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-effect" % catsEffectV, - "com.monovore" %%% "decline" % declineV, - "com.monovore" %%% "decline-effect" % declineV, - "co.fs2" %%% "fs2-io" % fs2V + "com.monovore" %%% "decline" % declineV, + "com.monovore" %%% "decline-effect" % declineV ) ) - .dependsOn(compiler, `backend-air`, `backend-ts`) + .dependsOn(compiler, `backend-air`, `backend-ts`, io) lazy val cliJS = cli.js .settings( @@ -69,6 +67,34 @@ lazy val cliJVM = cli.jvm ) ) +lazy val io = crossProject(JVMPlatform, JSPlatform) + .withoutSuffixFor(JVMPlatform) + .crossType(CrossType.Pure) + .settings(commons: _*) + .settings( + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectV, + "co.fs2" %%% "fs2-io" % fs2V + ) + ) + .dependsOn(compiler, parser) + +lazy val `language-server-api` = project + .in(file("language-server-api")) + .enablePlugins(ScalaJSPlugin) + .settings(commons: _*) + .settings( + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), + scalaJSUseMainModuleInitializer := true + ) + .settings( + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectV, + "co.fs2" %%% "fs2-io" % fs2V + ) + ) + .dependsOn(compiler.js, io.js) + lazy val types = crossProject(JVMPlatform, JSPlatform) .withoutSuffixFor(JVMPlatform) .crossType(CrossType.Pure) diff --git a/cli/.js/src/main/scala/aqua/builder/Finisher.scala b/cli/.js/src/main/scala/aqua/builder/Finisher.scala index 1efde747..ae3052ac 100644 --- a/cli/.js/src/main/scala/aqua/builder/Finisher.scala +++ b/cli/.js/src/main/scala/aqua/builder/Finisher.scala @@ -1,7 +1,6 @@ package aqua.builder import aqua.backend.* -import aqua.io.OutputPrinter import aqua.js.{CallJsFunction, FluencePeer, ServiceHandler} import aqua.model.{LiteralModel, VarModel} import aqua.raw.ops.{Call, CallArrowRawTag} diff --git a/cli/.js/src/main/scala/aqua/builder/Service.scala b/cli/.js/src/main/scala/aqua/builder/Service.scala index 7c75e707..277af0b1 100644 --- a/cli/.js/src/main/scala/aqua/builder/Service.scala +++ b/cli/.js/src/main/scala/aqua/builder/Service.scala @@ -1,7 +1,6 @@ package aqua.builder import aqua.backend.* -import aqua.io.OutputPrinter import aqua.js.{CallJsFunction, CallServiceHandler, FluencePeer, ServiceHandler} import cats.data.NonEmptyList import scribe.Logging diff --git a/cli/.js/src/main/scala/aqua/ipfs/IpfsOpts.scala b/cli/.js/src/main/scala/aqua/ipfs/IpfsOpts.scala index d7b7c39b..87b49b43 100644 --- a/cli/.js/src/main/scala/aqua/ipfs/IpfsOpts.scala +++ b/cli/.js/src/main/scala/aqua/ipfs/IpfsOpts.scala @@ -13,13 +13,11 @@ import aqua.{ RunInfo, SubCommandBuilder } -import aqua.io.OutputPrinter import aqua.js.{Fluence, PeerConfig} import aqua.keypair.KeyPairShow.show import cats.data.{NonEmptyChain, NonEmptyList, Validated, ValidatedNec, ValidatedNel} import Validated.{invalid, invalidNec, valid, validNec, validNel} import aqua.builder.IPFSUploader -import aqua.files.AquaFilesIO import aqua.ipfs.js.IpfsApi import aqua.model.LiteralModel import aqua.raw.value.LiteralRaw diff --git a/cli/.js/src/main/scala/aqua/remote/RemoteInfoOpts.scala b/cli/.js/src/main/scala/aqua/remote/RemoteInfoOpts.scala index 6eb4cc02..24ed4ab9 100644 --- a/cli/.js/src/main/scala/aqua/remote/RemoteInfoOpts.scala +++ b/cli/.js/src/main/scala/aqua/remote/RemoteInfoOpts.scala @@ -2,7 +2,6 @@ package aqua.remote import aqua.builder.IPFSUploader import DistOpts.* -import aqua.files.AquaFilesIO import aqua.ipfs.IpfsOpts.{pathOpt, UploadFuncName} import aqua.js.FluenceEnvironment import aqua.model.{LiteralModel, ValueModel} diff --git a/cli/.js/src/main/scala/aqua/run/RunOpts.scala b/cli/.js/src/main/scala/aqua/run/RunOpts.scala index 4eb0b691..36a90f92 100644 --- a/cli/.js/src/main/scala/aqua/run/RunOpts.scala +++ b/cli/.js/src/main/scala/aqua/run/RunOpts.scala @@ -2,7 +2,6 @@ package aqua.run import aqua.ArgOpts.checkDataGetServices import aqua.builder.{ArgumentGetter, Service} -import aqua.files.AquaFilesIO import aqua.model.transform.TransformConfig import aqua.model.{LiteralModel, ValueModel, VarModel} import aqua.parser.expr.func.CallArrowExpr diff --git a/cli/.js/src/main/scala/aqua/script/ScriptOpts.scala b/cli/.js/src/main/scala/aqua/script/ScriptOpts.scala index 29c68a6f..2f74a873 100644 --- a/cli/.js/src/main/scala/aqua/script/ScriptOpts.scala +++ b/cli/.js/src/main/scala/aqua/script/ScriptOpts.scala @@ -6,8 +6,6 @@ import aqua.backend.Generated import aqua.backend.air.{AirBackend, AirGen, FuncAirGen} import aqua.builder.ArgumentGetter import aqua.compiler.AquaCompiler -import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} -import aqua.io.{AquaFileError, OutputPrinter} import aqua.ipfs.js.IpfsApi import aqua.js.{Config, Fluence, PeerConfig} import aqua.keypair.KeyPairShow.show diff --git a/cli/.jvm/src/test/scala/WriteFileSpec.scala b/cli/.jvm/src/test/scala/WriteFileSpec.scala index 076e0a0e..6b08d528 100644 --- a/cli/.jvm/src/test/scala/WriteFileSpec.scala +++ b/cli/.jvm/src/test/scala/WriteFileSpec.scala @@ -10,6 +10,7 @@ import org.scalatest.matchers.should.Matchers import fs2.io.file.{Files, Path} class WriteFileSpec extends AnyFlatSpec with Matchers { + "cli" should "compile aqua code in js" in { val src = Path("./cli/.jvm/src/test/aqua") val targetTs = Files[IO].createTempDirectory.unsafeRunSync() diff --git a/cli/src/main/scala/aqua/ErrorRendering.scala b/cli/src/main/scala/aqua/ErrorRendering.scala index 271a2d5b..89e34700 100644 --- a/cli/src/main/scala/aqua/ErrorRendering.scala +++ b/cli/src/main/scala/aqua/ErrorRendering.scala @@ -4,7 +4,7 @@ import aqua.compiler.* import aqua.files.FileModuleId import aqua.io.AquaFileError import aqua.parser.lift.{FileSpan, Span} -import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError} +import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError} import aqua.semantics.{HeaderError, RulesViolated, WrongAST} import cats.parse.LocationMap import cats.parse.Parser.Expectation @@ -13,32 +13,6 @@ import cats.{Eval, Show} object ErrorRendering { - def betterSymbol(symbol: Char): String = { - symbol match { - case ' ' => "whitespace" - case '\t' => "tabulation" - case c => c.toString - } - } - - def expectationToString(expectation: Expectation, acc: List[String] = Nil): List[String] = { - // TODO: match all expectations - expectation match { - // get the deepest context - case WithContext(str, exp: WithContext) => expectationToString(exp, List(str)) - case WithContext(str, exp) => s"$str (${expectationToString(exp)})" +: acc - case FailWith(_, message) => message +: acc - case InRange(offset, lower, upper) => - if (lower == upper) - s"Expected symbol '${betterSymbol(lower)}'" +: acc - else - s"Expected symbols from '${betterSymbol(lower)}' to '${betterSymbol(upper)}'" +: acc - case OneOfStr(offset, strs) => - s"Expected one of these strings: ${strs.map(s => s"'$s'").mkString(", ")}" +: acc - case e => ("Expected: " + e.toString) +: acc - } - } - def showForConsole(errorType: String, span: FileSpan, messages: List[String]): String = span .focus(3) @@ -70,7 +44,7 @@ object ErrorRendering { val msg = FileSpan(span.name, span.locationMap, localSpan) .focus(0) .map { spanFocus => - val errorMessages = exps.flatMap(exp => expectationToString(exp)) + val errorMessages = exps.flatMap(exp => ParserError.expectationToString(exp)) spanFocus.toConsoleStr( "Syntax error", s"${errorMessages.head}" :: errorMessages.tail.map(t => "OR " + t), @@ -113,7 +87,7 @@ object ErrorRendering { .map(_.toConsoleStr("Header error", message :: Nil, Console.CYAN)) .getOrElse("(Dup error, but offset is beyond the script)") case WrongAST(ast) => - s"Semantic error: wrong AST" + "Semantic error: wrong AST" } diff --git a/cli/src/main/scala/aqua/AquaIO.scala b/io/src/main/scala/aqua/AquaIO.scala similarity index 100% rename from cli/src/main/scala/aqua/AquaIO.scala rename to io/src/main/scala/aqua/AquaIO.scala diff --git a/cli/src/main/scala/aqua/SpanParser.scala b/io/src/main/scala/aqua/SpanParser.scala similarity index 96% rename from cli/src/main/scala/aqua/SpanParser.scala rename to io/src/main/scala/aqua/SpanParser.scala index f7823e06..5d962931 100644 --- a/cli/src/main/scala/aqua/SpanParser.scala +++ b/io/src/main/scala/aqua/SpanParser.scala @@ -22,7 +22,6 @@ object SpanParser extends scribe.Logging { ) } } - import Span.spanLiftParser val parser = Parser.natParser(Parser.spanParser, nat)(source) logger.trace("parser created") parser diff --git a/cli/src/main/scala/aqua/files/AquaFileSources.scala b/io/src/main/scala/aqua/files/AquaFileSources.scala similarity index 97% rename from cli/src/main/scala/aqua/files/AquaFileSources.scala rename to io/src/main/scala/aqua/files/AquaFileSources.scala index 1720472d..facb5934 100644 --- a/cli/src/main/scala/aqua/files/AquaFileSources.scala +++ b/io/src/main/scala/aqua/files/AquaFileSources.scala @@ -118,7 +118,12 @@ class AquaFileSources[F[_]: AquaIO: Monad: Files: Functor]( } // Write content to a file and return a success message - private def writeWithResult(target: Path, content: String, funcsCount: Int, servicesCount: Int) = { + private def writeWithResult( + target: Path, + content: String, + funcsCount: Int, + servicesCount: Int + ) = { filesIO .writeFile( target, diff --git a/cli/src/main/scala/aqua/files/AquaFilesIO.scala b/io/src/main/scala/aqua/files/AquaFilesIO.scala similarity index 87% rename from cli/src/main/scala/aqua/files/AquaFilesIO.scala rename to io/src/main/scala/aqua/files/AquaFilesIO.scala index d40d9a6b..9a97c4d0 100644 --- a/cli/src/main/scala/aqua/files/AquaFilesIO.scala +++ b/io/src/main/scala/aqua/files/AquaFilesIO.scala @@ -78,12 +78,18 @@ class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { ) // 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]]]] = { + 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]]] = { + 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) @@ -107,8 +113,15 @@ class AquaFilesIO[F[_]: Files: Concurrent] extends AquaIO[F] { } else { Files[F].isDirectory(folder).flatMap { isDir => if (isDir) { - Files[F].list(folder).evalFilter(p => if (p.extName == ".aqua") true.pure[F] else Files[F].isDirectory(p)) - .compile.toList.map(Right(_)) + Files[F] + .list(folder) + .evalFilter(p => + if (p.extName == ".aqua") true.pure[F] + else Files[F].isDirectory(p) + ) + .compile + .toList + .map(Right(_)) } else { Right(folder :: Nil).pure[F] } diff --git a/cli/src/main/scala/aqua/files/FileModuleId.scala b/io/src/main/scala/aqua/files/FileModuleId.scala similarity index 100% rename from cli/src/main/scala/aqua/files/FileModuleId.scala rename to io/src/main/scala/aqua/files/FileModuleId.scala diff --git a/cli/src/main/scala/aqua/io/AquaFileError.scala b/io/src/main/scala/aqua/io/AquaFileError.scala similarity index 100% rename from cli/src/main/scala/aqua/io/AquaFileError.scala rename to io/src/main/scala/aqua/io/AquaFileError.scala diff --git a/cli/src/main/scala/aqua/io/OutputPrinter.scala b/io/src/main/scala/aqua/io/OutputPrinter.scala similarity index 100% rename from cli/src/main/scala/aqua/io/OutputPrinter.scala rename to io/src/main/scala/aqua/io/OutputPrinter.scala diff --git a/language-server-api/src/main/scala/aqua/lsp/AquaLSP.scala b/language-server-api/src/main/scala/aqua/lsp/AquaLSP.scala new file mode 100644 index 00000000..c526b0e1 --- /dev/null +++ b/language-server-api/src/main/scala/aqua/lsp/AquaLSP.scala @@ -0,0 +1,131 @@ +package aqua.lsp + +import aqua.compiler.* +import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId} +import aqua.io.* +import aqua.model.transform.TransformConfig +import aqua.parser.lift.{FileSpan, Span} +import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError} +import aqua.semantics.{HeaderError, RulesViolated, WrongAST} +import aqua.{AquaIO, SpanParser} +import cats.data.NonEmptyChain +import cats.data.Validated.{Invalid, Valid} +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import fs2.io.file.{Files, Path} +import scribe.Logging + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.scalajs.js.JSConverters.* +import scala.scalajs.js.annotation.* +import scala.scalajs.js.{undefined, UndefOr} + +@JSExportAll +case class ErrorInfo(start: Int, end: Int, message: String, location: UndefOr[String]) + +object ErrorInfo { + + def apply(fileSpan: FileSpan, message: String): ErrorInfo = { + val start = fileSpan.span.startIndex + val end = fileSpan.span.endIndex + ErrorInfo(start, end, message, fileSpan.name) + } + + def applyOp(start: Int, end: Int, message: String, location: Option[String]): ErrorInfo = { + ErrorInfo(start, end, message, location.getOrElse(undefined)) + } +} + +@JSExportTopLevel("AquaLSP") +object AquaLSP extends App with Logging { + + def errorToInfo(error: AquaError[FileModuleId, AquaFileError, FileSpan.F]): List[ErrorInfo] = { + error match { + case ParserErr(err) => + err match { + case BlockIndentError(indent, message) => + ErrorInfo(indent._1, message) :: Nil + case ArrowReturnError(point, message) => + ErrorInfo(point._1, message) :: Nil + case LexerError((span, e)) => + e.expected.toList + .groupBy(_.offset) + .map { case (offset, exps) => + val localSpan = Span(offset, offset + 1) + val fSpan = FileSpan(span.name, span.locationMap, localSpan) + val errorMessages = exps.flatMap(exp => ParserError.expectationToString(exp)) + val msg = s"${errorMessages.head}" :: errorMessages.tail.map(t => "OR " + t) + (offset, ErrorInfo(fSpan, msg.mkString("\n"))) + } + .toList + .sortBy(_._1) + .map(_._2) + .reverse + } + case SourcesErr(err) => + ErrorInfo.applyOp(0, 0, err.showForConsole, None) :: Nil + case ResolveImportsErr(_, token, err) => + ErrorInfo(token.unit._1, err.showForConsole) :: Nil + case ImportErr(token) => + ErrorInfo(token.unit._1, "Cannot resolve import") :: Nil + case CycleError(modules) => + ErrorInfo.applyOp( + 0, + 0, + s"Cycle loops detected in imports: ${modules.map(_.file.fileName)}", + None + ) :: Nil + case CompileError(err) => + err match { + case RulesViolated(token, messages) => + ErrorInfo(token.unit._1, messages.mkString("\n")) :: Nil + case HeaderError(token, message) => + ErrorInfo(token.unit._1, message) :: Nil + case WrongAST(ast) => + ErrorInfo.applyOp(0, 0, "Semantic error: wrong AST", None) :: Nil + + } + case OutputError(_, err) => + ErrorInfo.applyOp(0, 0, err.showForConsole, None) :: Nil + } + } + + @JSExport + def compile( + pathStr: String, + imports: scalajs.js.Array[String] + ): scalajs.js.Promise[scalajs.js.Array[ErrorInfo]] = { + + logger.debug(s"Compiling '$pathStr' with imports: $imports") + + implicit val aio: AquaIO[IO] = new AquaFilesIO[IO] + + val sources = new AquaFileSources[IO](Path(pathStr), imports.toList.map(Path.apply)) + val config = TransformConfig() + + val proc = for { + res <- AquaCompiler + .compileToContext[IO, AquaFileError, FileModuleId, FileSpan.F]( + sources, + SpanParser.parser, + config + ) + } yield { + logger.debug("Compilation done.") + val result = res match { + case Valid(_) => + logger.debug("No errors on compilation.") + List.empty.toJSArray + case Invalid(e: NonEmptyChain[AquaError[FileModuleId, AquaFileError, FileSpan.F]]) => + val errors = e.toNonEmptyList.toList.flatMap(errorToInfo) + logger.debug("Errors: " + errors.mkString("\n")) + errors.toJSArray + } + result + } + + proc.unsafeToFuture().toJSPromise + + } +} diff --git a/language-server-npm/aqua-lsp-api.d.ts b/language-server-npm/aqua-lsp-api.d.ts new file mode 100644 index 00000000..6543831e --- /dev/null +++ b/language-server-npm/aqua-lsp-api.d.ts @@ -0,0 +1,12 @@ +export interface ErrorInfo { + start: number, + end: number, + message: string, + location: string | null +} + +export class Compiler { + compile(path: string, imports: string[]): Promise; +} + +export var AquaLSP: Compiler; diff --git a/language-server-npm/package-lock.json b/language-server-npm/package-lock.json new file mode 100644 index 00000000..fe9db9d1 --- /dev/null +++ b/language-server-npm/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "@fluencelabs/aqua-language-server-api", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@fluencelabs/aqua-language-server-api", + "version": "0.0.0", + "license": "Apache-2.0" + } + } +} diff --git a/language-server-npm/package.json b/language-server-npm/package.json new file mode 100644 index 00000000..e5d1339c --- /dev/null +++ b/language-server-npm/package.json @@ -0,0 +1,27 @@ +{ + "name": "@fluencelabs/aqua-language-server-api", + "version": "0.0.3", + "description": "Aqua Language Server API", + "type": "commonjs", + "files": [ + "aqua-lsp-api.js", + "aqua-lsp-api.d.ts" + ], + "scripts": { + "move:scalajs": "cp ../language-server-api/target/scala-3.1.0/language-server-opt/main.js ./aqua-lsp-api.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fluencelabs/aqua.git" + }, + "keywords": [ + "aqua", + "fluence" + ], + "author": "Fluence Labs", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/fluencelabs/aqua/issues" + }, + "homepage": "https://github.com/fluencelabs/aqua#readme" +} diff --git a/npm/test/data.json b/npm/test/data.json index ab6cafd2..2a051b31 100644 --- a/npm/test/data.json +++ b/npm/test/data.json @@ -1,8 +1,35 @@ { + "target": "12D3KooWMhVpgfQxBLkQkJed8VFNvgN4iE6MD7xCybb1ZYWW2Gtz", + "validators": [ + "12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", + "12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", + "12D3KooWJbJFaZ3k5sNd8DjQgg3aERoKtBAnirEvPV8yp76kEXHB", + "12D3KooWCKCeqLPSgMnDjyFsJuWqREDtKNHx1JEBiwaMXhCLNTRb", + "12D3KooWKnRcsTpYx9axkJ6d69LPfpPXrkVLe96skuPTAo76LLVH", + "12D3KooWBSdm6TkqnEFrgBuSkpVE3dR1kr6952DsWQRNwJZjFZBv", + "12D3KooWGzNvhSDsgFoHwpWHAyPf1kcTYCGeRBPfznL8J6qdyu2H", + "12D3KooWF7gjXhQ4LaKj6j7ntxsPpGk34psdQicN2KNfBi9bFKXg", + "12D3KooWB9P1xmV3c7ZPpBemovbwCiRRTKd3Kq2jsVPQN4ZukDfy" + ], + "timeout": 5000, "stringField": "some string", "numberField": 123, "structField": { "numField": 42, - "arrField": ["str1", "str2"] + "arrField": ["str1", "str2", "r43r34", "ferer"], + "arr2": [{ + "a": "fef", + "b": [1,2,3,4], + "c": "erfer", + "d": "frefe" + },{ + "b": [1,2,3,4], + "c": "erfer", + "d": "frefe" + }, { + "a": "as", + "c": "erfer", + "d": "gerrt" + }] } } diff --git a/npm/test/sample.aqua b/npm/test/sample.aqua index 14ea5bc0..1b0ace41 100644 --- a/npm/test/sample.aqua +++ b/npm/test/sample.aqua @@ -1,4 +1,5 @@ -import "run-builtins.aqua" +import "@fluencelabs/aqua-lib/builtin.aqua" +-- import "run-builtins.aqua" data StructType: numField: u32 @@ -11,14 +12,33 @@ service OpNumber("op"): identity(n: u32) -> u32 service OpStruct("op"): - identity(st: StructType) -> StructType + identity(st: StructType) -> StructType + noop() +func parseBug(): + stream: *string + if stream[0] != "FOO": + Op.noop() -func identityArgsAndReturn(structArg: StructType, stringArg: string, numberArg: u32) -> string, u32, StructType: +func identityArgsAndReturn (structArg: StructType, stringArg: string, numberArg: u32) -> string, u32, StructType: on HOST_PEER_ID: sArg <- OpString.identity(stringArg) - nArg <- OpNumber.identity(numberArg) + nArg = OpNumber.identity (numberArg) + OpNumber.identity (numberArg) stArg <- OpStruct.identity(structArg) -- it could be used only on init_peer_id - Console.print("hello") <- sArg, nArg, stArg + +service Ssss("ss"): + foo4: u64 -> u16 + +func aaa(a: u64) -> u16: + res <- Ssss.foo4(a) + <- res + +func bar(callback: u32 -> u32): + callback(1) + +func baz(): + bar(aaa) + + diff --git a/parser/src/main/scala/aqua/parser/ParserError.scala b/parser/src/main/scala/aqua/parser/ParserError.scala index df7427fa..b6f585dc 100644 --- a/parser/src/main/scala/aqua/parser/ParserError.scala +++ b/parser/src/main/scala/aqua/parser/ParserError.scala @@ -1,6 +1,8 @@ package aqua.parser import cats.parse.Parser +import cats.parse.Parser.Expectation +import cats.parse.Parser.Expectation.{FailWith, InRange, OneOfStr, WithContext} import cats.~> trait ParserError[F[_]] { @@ -22,3 +24,32 @@ case class ArrowReturnError[F[_]](point: F[Unit], message: String) extends Parse def mapK[K[_]](fk: F ~> K): ArrowReturnError[K] = copy(fk(point)) } + +object ParserError { + + def betterSymbol(symbol: Char): String = { + symbol match { + case ' ' => "whitespace" + case '\t' => "tabulation" + case c => c.toString + } + } + + def expectationToString(expectation: Expectation, acc: List[String] = Nil): List[String] = { + // TODO: match all expectations + expectation match { + // get the deepest context + case WithContext(str, exp: WithContext) => expectationToString(exp, List(str)) + case WithContext(str, exp) => s"$str (${expectationToString(exp)})" +: acc + case FailWith(_, message) => message +: acc + case InRange(offset, lower, upper) => + if (lower == upper) + s"Expected symbol '${betterSymbol(lower)}'" +: acc + else + s"Expected symbols from '${betterSymbol(lower)}' to '${betterSymbol(upper)}'" +: acc + case OneOfStr(offset, strs) => + s"Expected one of these strings: ${strs.map(s => s"'$s'").mkString(", ")}" +: acc + case e => ("Expected: " + e.toString) +: acc + } + } +}