From 3cd31c5827dfc8208a5cbed0722e3ddeabc7e31f Mon Sep 17 00:00:00 2001 From: Dima Date: Tue, 20 Feb 2024 14:02:55 +0300 Subject: [PATCH] feat(language-server): Add types for types in LSP API (#1078) --- integration-tests/lsp-aqua/types.aqua | 29 ++++ integration-tests/package.json | 6 +- .../src/__test__/lsp-types.spec.ts | 103 ++++++++++++ .../src/main/scala/aqua/lsp/OutputTypes.scala | 4 +- .../main/scala/aqua/lsp/ResultHelper.scala | 4 +- .../.js/src/main/scala/aqua/lsp/TypeJs.scala | 105 +++++++++++++ .../language-server-npm/aqua-lsp-api.d.ts | 147 ++++++++++++++---- 7 files changed, 363 insertions(+), 35 deletions(-) create mode 100644 integration-tests/lsp-aqua/types.aqua create mode 100644 integration-tests/src/__test__/lsp-types.spec.ts create mode 100644 language-server/language-server-api/.js/src/main/scala/aqua/lsp/TypeJs.scala diff --git a/integration-tests/lsp-aqua/types.aqua b/integration-tests/lsp-aqua/types.aqua new file mode 100644 index 000000000..c15fd6867 --- /dev/null +++ b/integration-tests/lsp-aqua/types.aqua @@ -0,0 +1,29 @@ +aqua Types + +alias Top: ⊤ +alias Bottom: ⊥ + +alias Number: u32 +alias String: string +alias Array: []string +alias Stream: *string +alias Option: ?string + +data Struct: + a: Number + b: String + c: Array + d: Option + +service Srv("srv"): + noop(srvArg: string) + +ability Ability: + a: Number + b: String + +func nilArrow(): + Srv.noop("") + +func fullArrow(a: string, b: u32) -> string, u32: + <- a, b diff --git a/integration-tests/package.json b/integration-tests/package.json index 8c33965ec..f6b22bc8e 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -18,16 +18,14 @@ "scripts": { "build": "tsc & npm run compile-aqua", "test": "NODE_OPTIONS=--experimental-vm-modules jest --detectOpenHandles", + "test:lsp": "NODE_OPTIONS=--experimental-vm-modules jest src/__test__/lsp-types.spec.ts --detectOpenHandles", "examples": "jest", - "pubsub": "node -r ts-node/register src/pubsub.ts", "exec": "npm run compile-aqua && npm run prettify-compiled && node -r ts-node/register src/index.ts", "run": "node -r ts-node/register src/index.ts", "compile-aqua": "node --loader ts-node/esm ./src/compile.ts", "compile-aqua:air": "aqua -i ./aqua/ -o ./compiled-air -a", "prettify-compiled": "prettier --write src/compiled", - "prettify": "prettier --write src", - "aqua": "aqua", - "do": "aqua dist deploy --addr /dns4/kras-04.fluence.dev/tcp/19001/wss/p2p/12D3KooWFEwNWcHqi9rtsmDhsYcDbRUCDXH84RC4FW6UfsFWaoHi --config-path deploy.json --service tsOracle" + "prettify": "prettier --write src" }, "prettier": {}, "devDependencies": { diff --git a/integration-tests/src/__test__/lsp-types.spec.ts b/integration-tests/src/__test__/lsp-types.spec.ts new file mode 100644 index 000000000..be7b1bd84 --- /dev/null +++ b/integration-tests/src/__test__/lsp-types.spec.ts @@ -0,0 +1,103 @@ +describe("Testing LSP types", () => { + it("dummy", async () => { + expect(true).toBeTruthy() + }) +}) + +// FIXME: these tests work only with LSP ESM build (ModuleKind.ESModule in build.sbt) +/* +import { + AbilityType, + AquaLSP, ArrayType, BottomType, + OptionType, + ScalarType, StreamType, StructType, + TopType, + Type, ServiceType, ArrowType +} from '@fluencelabs/aqua-language-server-api/aqua-lsp-api'; + +describe("Testing LSP types", () => { + it("check types in aqua file", async () => { + const compiled = await AquaLSP.compile("lsp-aqua/types.aqua", {}) + const types = compiled.tokens.map(ti => ti.type) + + const isTop = (type: Type): type is TopType => type.tag === "top"; + const tops = types.filter(isTop) + expect(tops).toHaveLength(1) + + const isBottom = (type: Type): type is BottomType => type.tag === "bottom"; + const bottom = types.filter(isBottom) + expect(bottom).toHaveLength(1) + + const isScalar = (type: Type): type is ScalarType => type.tag === "scalar"; + const scalars = types.filter(isScalar) + expect(scalars).toHaveLength(8) + scalars.forEach(sc => expect(sc.name).toBeDefined()) + + const isArray = (type: Type): type is ArrayType => type.tag === "array"; + const arrays = types.filter(isArray) + expect(arrays).toHaveLength(2) + arrays.forEach(sc => expect(sc.element).toBeDefined()) + + const isOption = (type: Type): type is OptionType => type.tag === "option"; + const options = types.filter(isOption) + expect(options).toHaveLength(2) + options.forEach(sc => expect(sc.element).toBeDefined()) + + const isStream = (type: Type): type is StreamType => type.tag === "stream"; + const streams = types.filter(isStream) + expect(streams).toHaveLength(1) + streams.forEach(sc => expect(sc.element).toBeDefined()) + + const isAbility = (type: Type): type is AbilityType => type.tag === "ability"; + const abilities = types.filter(isAbility) + expect(abilities).toHaveLength(1) + abilities.forEach((sc) => { + expect(sc.name).toBeDefined() + expect(sc.fields).toBeDefined() + expect(Object.entries(sc.fields)).toHaveLength(2) + }) + + const isStruct = (type: Type): type is StructType => type.tag === "struct"; + const structs = types.filter(isStruct) + expect(structs).toHaveLength(1) + structs.forEach((sc) => { + expect(sc.name).toBeDefined() + expect(sc.fields).toBeDefined() + expect(Object.entries(sc.fields)).toHaveLength(4) + }) + + const isService = (type: Type): type is ServiceType => type.tag === "service"; + const services = types.filter(isService) + expect(services).toHaveLength(1) + services.forEach((sc) => { + expect(sc.name).toBeDefined() + expect(sc.fields).toBeDefined() + expect(Object.entries(sc.fields)).toHaveLength(1) + }) + + const isArrow = (type: Type): type is ArrowType => type.tag === "arrow"; + const arrows = types.filter(isArrow) + expect(arrows).toHaveLength(3) + + const fullArrow = arrows[0] + const argA = fullArrow.domain.args["a"] + expect(argA).toEqual({name: "string", tag: "scalar"}) + + const argB = fullArrow.domain.args["b"] + expect(argB).toEqual({name: "u32", tag: "scalar"}) + + const codomain = fullArrow.codomain.types + expect(codomain[0]).toEqual({name: "string", tag: "scalar"}) + expect(codomain[1]).toEqual({name: "u32", tag: "scalar"}) + + const nilArrow = arrows[1] + expect(nilArrow.domain.args).toEqual({}) + expect(nilArrow.codomain.types).toEqual([]) + + const srvNoopArrow = arrows[2] + expect(srvNoopArrow.domain.args).toEqual({srvArg: {name: "string", tag: "scalar"}}) + expect(srvNoopArrow.codomain.types).toEqual([]) + + }) + +})*/ diff --git a/language-server/language-server-api/.js/src/main/scala/aqua/lsp/OutputTypes.scala b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/OutputTypes.scala index 63c6ed129..44ad43931 100644 --- a/language-server/language-server-api/.js/src/main/scala/aqua/lsp/OutputTypes.scala +++ b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/OutputTypes.scala @@ -4,7 +4,7 @@ import aqua.parser.lift.FileSpan import scala.scalajs.js import scala.scalajs.js.annotation.JSExportAll -import scala.scalajs.js.{UndefOr, undefined} +import scala.scalajs.js.{undefined, UndefOr} @JSExportAll case class CompilationResult( @@ -16,7 +16,7 @@ case class CompilationResult( ) @JSExportAll -case class ExprInfoJs(location: TokenLocation, `type`: String) +case class ExprInfoJs(location: TokenLocation, `type`: TypeJs) @JSExportAll case class TokenLocation(name: String, startLine: Int, startCol: Int, endLine: Int, endCol: Int) diff --git a/language-server/language-server-api/.js/src/main/scala/aqua/lsp/ResultHelper.scala b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/ResultHelper.scala index 17c51b20f..1fbd735c3 100644 --- a/language-server/language-server-api/.js/src/main/scala/aqua/lsp/ResultHelper.scala +++ b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/ResultHelper.scala @@ -84,8 +84,8 @@ object ResultHelper extends Logging { private def tokensToJs(tokens: List[DefinitionInfo[FileSpan.F]]): js.Array[ExprInfoJs] = tokens.flatMap { ti => TokenLocation.fromSpan(ti.token.unit._1).map { tl => - val typeName = ti.`type`.show - ExprInfoJs(tl, typeName) + val typeDef = TypeJs.fromType(ti.`type`) + ExprInfoJs(tl, typeDef) } }.toJSArray diff --git a/language-server/language-server-api/.js/src/main/scala/aqua/lsp/TypeJs.scala b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/TypeJs.scala new file mode 100644 index 000000000..53b48dea2 --- /dev/null +++ b/language-server/language-server-api/.js/src/main/scala/aqua/lsp/TypeJs.scala @@ -0,0 +1,105 @@ +package aqua.lsp + +import aqua.types.* + +import scala.scalajs.js.Dictionary +import scala.scalajs.js.JSConverters.* +import scalajs.js + +sealed trait TypeJs extends js.Object { + val tag: String +} + +class ScalarTypeJs(val name: String) extends TypeJs { + val tag: String = "scalar" +} + +class ArrayTypeJs(val element: TypeJs) extends TypeJs { + val tag: String = "array" +} + +class OptionTypeJs(val element: TypeJs) extends TypeJs { + val tag: String = "option" +} + +class StreamTypeJs(val element: TypeJs) extends TypeJs { + val tag: String = "stream" +} + +class StreamMapTypeJs(val element: TypeJs) extends TypeJs { + val tag: String = "streammap" +} + +class CanonStreamTypeJs(val element: TypeJs) extends TypeJs { + val tag: String = "canon" +} + +class AbilityTypeJs(val name: String, val fields: js.Dictionary[TypeJs]) extends TypeJs { + val tag: String = "ability" +} + +class StructTypeJs(val name: String, val fields: js.Dictionary[TypeJs]) extends TypeJs { + val tag: String = "struct" +} + +class ServiceTypeJs(val name: String, val fields: js.Dictionary[TypeJs]) extends TypeJs { + val tag: String = "service" +} + +trait ProductType extends TypeJs + +class LabeledConsTypeJs(val args: js.Dictionary[TypeJs]) extends TypeJs { + val tag: String = "labeled" +} + +class UnlabeledConsTypeJs(val types: js.Array[TypeJs]) extends TypeJs { + val tag: String = "unlabeled" +} + +class ArrowTypeJs(val domain: LabeledConsTypeJs, val codomain: UnlabeledConsTypeJs) extends TypeJs { + val tag: String = "arrow" +} + +class NilTypeJs extends TypeJs { + val tag: String = "nil" +} + +class BottomTypeJs extends TypeJs { + val tag: String = "bottom" +} + +class TopTypeJs extends TypeJs { + val tag: String = "top" +} + +object TypeJs { + + def typeList(types: Iterable[(String, Type)]): Dictionary[TypeJs] = + js.Dictionary(types.map { case (n, t) => + (n, TypeJs.fromType(t)) + }.toSeq: _*) + + def fromType(t: Type): TypeJs = { + t match + case ScalarType(name) => new ScalarTypeJs(name) + case LiteralType(_, name) => new ScalarTypeJs(name) + case ArrayType(el) => new ArrayTypeJs(fromType(el)) + case OptionType(el) => new OptionTypeJs(fromType(el)) + case StreamType(el) => new StreamTypeJs(fromType(el)) + case StreamMapType(el) => new StreamMapTypeJs(fromType(el)) + case CanonStreamType(el) => new CanonStreamTypeJs(fromType(el)) + case StructType(name, fields) => new StructTypeJs(name, typeList(fields.toSortedMap)) + case AbilityType(name, fields) => new AbilityTypeJs(name, typeList(fields.toSortedMap)) + case ServiceType(name, fields) => new ServiceTypeJs(name, typeList(fields.toSortedMap)) + case lct: LabeledConsType => new LabeledConsTypeJs(typeList(lct.toLabelledList())) + case uct: UnlabeledConsType => new UnlabeledConsTypeJs(uct.toList.map(fromType).toJSArray) + case ArrowType(domain, codomain) => + ArrowTypeJs( + new LabeledConsTypeJs(typeList(domain.toLabelledList())), + new UnlabeledConsTypeJs(codomain.toList.map(fromType).toJSArray) + ) + case TopType => new TopTypeJs() + case BottomType => new BottomTypeJs() + case NilType => new NilTypeJs() + } +} diff --git a/language-server/language-server-npm/aqua-lsp-api.d.ts b/language-server/language-server-npm/aqua-lsp-api.d.ts index d8dd6317e..02ec0e30e 100644 --- a/language-server/language-server-npm/aqua-lsp-api.d.ts +++ b/language-server/language-server-npm/aqua-lsp-api.d.ts @@ -1,48 +1,141 @@ +export interface ScalarType { + name: string, + tag: "scalar" +} + +export interface ArrayType { + element: Type, + tag: "array" +} + +export interface OptionType { + element: Type, + tag: "option" +} + +export interface StreamType { + element: Type, + tag: "stream" +} + +export interface StreamMapType { + element: Type, + tag: "streammap" +} + +export interface CanonStreamType { + element: Type, + tag: "canon" +} + +export interface AbilityType { + name: string, + fields: Record, + tag: "ability" +} + +export interface StructType { + name: string, + fields: Record, + tag: "struct" +} + +export interface ServiceType { + name: string, + fields: Record, + tag: "service" +} + +export interface LabeledConsType { + args: Record, + tag: "labeled" +} + +export interface UnlabeledConsType { + types: Type[], + tag: "unlabeled" +} + +export interface ArrowType { + domain: LabeledConsType + codomain: UnlabeledConsType, + tag: "arrow" +} + +export interface NilType { + tag: "nil" +} + +export interface BottomType { + tag: "bottom" +} + +export interface TopType { + tag: "top" +} + +export type Type = + ScalarType + | ArrayType + | OptionType + | StreamType + | StreamMapType + | CanonStreamType + | AbilityType + | StructType + | ServiceType + | LabeledConsType + | UnlabeledConsType + | ArrowType + | NilType + | TopType + | BottomType + export interface TokenLocation { - name: string; - startLine: number; - startCol: number; - endLine: number; - endCol: number; + name: string; + startLine: number; + startCol: number; + endLine: number; + endCol: number; } export interface TokenInfo { - location: TokenLocation; - type: string; + location: TokenLocation; + type: Type; } export interface TokenLink { - current: TokenLocation; - definition: TokenLocation; + current: TokenLocation; + definition: TokenLocation; } export interface TokenImport { - current: TokenLocation; - path: string; + current: TokenLocation; + path: string; } export interface ErrorInfo { - infoType: "error"; - start: number; - end: number; - message: string; - location: string | null; + infoType: "error"; + start: number; + end: number; + message: string; + location: string | null; } export interface WarningInfo { - infoType: "warning"; - start: number; - end: number; - message: string; - location: string | null; + infoType: "warning"; + start: number; + end: number; + message: string; + location: string | null; } export interface CompilationResult { - errors: ErrorInfo[]; - warnings: WarningInfo[]; - locations: TokenLink[]; - importLocations: TokenImport[]; - tokens: TokenInfo[]; + errors: ErrorInfo[]; + warnings: WarningInfo[]; + locations: TokenLink[]; + importLocations: TokenImport[]; + tokens: TokenInfo[]; } /* @@ -59,7 +152,7 @@ export interface CompilationResult { export type Imports = Record>; export class Compiler { - compile(path: string, imports: Imports): Promise; + compile(path: string, imports: Imports): Promise; } export var AquaLSP: Compiler;