Language server (#512)

This commit is contained in:
Dima 2022-05-17 15:05:25 +03:00 committed by GitHub
parent 2ff870dd9a
commit 16a802f5a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 350 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
language-server-npm/aqua-lsp-api.d.ts vendored Normal file
View File

@ -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<ErrorInfo[]>;
}
export var AquaLSP: Compiler;

13
language-server-npm/package-lock.json generated Normal file
View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import "run-builtins.aqua"
import "@fluencelabs/aqua-lib/builtin.aqua"
-- import "run-builtins.aqua"
data StructType:
numField: u32
@ -12,13 +13,32 @@ service OpNumber("op"):
service OpStruct("op"):
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:
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)

View File

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