feat(language-server): Pass token types to LSP [LNG-285] (#999)

This commit is contained in:
Dima 2023-12-07 19:06:51 +07:00 committed by GitHub
parent 4cecab1a26
commit 74d02e1f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 771 additions and 420 deletions

View File

@ -78,12 +78,12 @@ jobs:
registry-url: "https://npm.fluence.dev"
cache: "pnpm"
- run: pnpm -r i
- run: pnpm --filter='!integration-tests' -r i
- name: Set package version
run: node ci.cjs bump-version ${{ steps.version.outputs.id }}
- run: pnpm -r build
- run: pnpm --filter='!integration-tests' -r build
- name: Publish snapshot
id: snapshot

View File

@ -1,34 +1,24 @@
package aqua.run
import aqua.Rendering.given
import aqua.compiler.{AquaCompiler, AquaCompilerConf, CompileResult, CompilerAPI}
import aqua.compiler.{AquaCompilerConf, CompileResult, CompilerAPI}
import aqua.files.{AquaFileSources, FileModuleId}
import aqua.{AquaIO, SpanParser}
import aqua.io.{AquaFileError, AquaPath, PackagePath, Prelude}
import aqua.io.{AquaFileError, AquaPath, PackagePath}
import aqua.model.transform.TransformConfig
import aqua.model.{AquaContext, FuncArrow}
import aqua.parser.lift.FileSpan
import aqua.run.CliFunc
import aqua.{AquaIO, SpanParser}
import cats.data.Validated.{invalidNec, validNec}
import cats.data.{Chain, NonEmptyList, Validated, ValidatedNec}
import cats.effect.IO
import cats.data.{Chain, ValidatedNec}
import cats.effect.kernel.{Async, Clock}
import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.monad.*
import cats.syntax.show.*
import cats.syntax.traverse.*
import cats.syntax.option.*
import cats.syntax.either.*
import cats.syntax.validated.*
import cats.syntax.apply.*
import cats.syntax.traverse.*
import fs2.io.file.{Files, Path}
import scribe.Logging
import scala.concurrent.duration.Duration
class FuncCompiler[F[_]: Files: AquaIO: Async](
input: Option[AquaPath],
imports: List[Path],

View File

@ -1,20 +1,3 @@
aqua StreamArgs
export lng280BugWithForEmptyStreamFunc
service StreamService("test-service"):
store(numbers: []u32, n: u32)
func callService(stream: *u32, n: u32):
stream <<- 1
StreamService.store(stream, n)
func returnEmptyStream() -> *u32:
<- *[]
func lng280BugWithForEmptyStreamFunc():
arr = [1,2,3,4,5]
for a <- arr:
str <- returnEmptyStream()
-- passing the function directly won't work, see LNG-290
callService(str, a)
func arr() -> string:
n = "str"
<- n

View File

@ -80,7 +80,7 @@ lazy val `language-server-api` = crossProject(JSPlatform, JVMPlatform)
lazy val `language-server-apiJS` = `language-server-api`.js
.settings(
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)),
scalaJSUseMainModuleInitializer := true
scalaJSUseMainModuleInitializer := false
)
.settings(addBundleJS("../../language-server-npm/aqua-lsp-api.js"))
.enablePlugins(ScalaJSPlugin)

View File

@ -22,7 +22,7 @@
"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": "ts-node ./src/compile.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",

View File

@ -1,16 +1,12 @@
package aqua.lsp
import aqua.compiler.*
import aqua.compiler.AquaError.{ParserError as AquaParserError, *}
import aqua.compiler.AquaWarning.*
import aqua.compiler.AquaError.SourcesError
import aqua.files.{AquaFileSources, AquaFilesIO, FileModuleId}
import aqua.io.*
import aqua.parser.lexer.{LiteralToken, Token}
import aqua.parser.lift.FileSpan
import aqua.parser.lift.FileSpan.F
import aqua.parser.lift.{FileSpan, Span}
import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError}
import aqua.raw.ConstantRaw
import aqua.semantics.*
import aqua.{AquaIO, SpanParser}
import cats.data.Validated
@ -23,148 +19,12 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.scalajs.js
import scala.scalajs.js.JSConverters.*
import scala.scalajs.js.annotation.*
import scala.scalajs.js.{UndefOr, undefined}
import scribe.Logging
@JSExportAll
case class CompilationResult(
errors: js.Array[ErrorInfo],
warnings: js.Array[WarningInfo] = js.Array(),
locations: js.Array[TokenLink] = js.Array(),
importLocations: js.Array[TokenImport] = js.Array()
)
@JSExportAll
case class TokenLocation(name: String, startLine: Int, startCol: Int, endLine: Int, endCol: Int)
@JSExportAll
case class TokenLink(current: TokenLocation, definition: TokenLocation)
@JSExportAll
case class TokenImport(current: TokenLocation, path: String)
object TokenLocation {
def fromSpan(span: FileSpan): Option[TokenLocation] = {
val start = span.locationMap.value.toLineCol(span.span.startIndex)
val end = span.locationMap.value.toLineCol(span.span.endIndex)
for {
startLC <- start
endLC <- end
} yield {
TokenLocation(span.name, startLC._1, startLC._2, endLC._1, endLC._2)
}
}
}
@JSExportAll
case class ErrorInfo(
start: Int,
end: Int,
message: String,
location: UndefOr[String]
) {
// Used to distinguish from WarningInfo in TS
val infoType: String = "error"
}
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))
}
}
@JSExportAll
case class WarningInfo(
start: Int,
end: Int,
message: String,
location: UndefOr[String]
) {
// Used to distinguish from ErrorInfo in TS
val infoType: String = "warning"
}
object WarningInfo {
def apply(fileSpan: FileSpan, message: String): WarningInfo = {
val start = fileSpan.span.startIndex
val end = fileSpan.span.endIndex
WarningInfo(start, end, message, fileSpan.name)
}
}
@JSExportTopLevel("AquaLSP")
object AquaLSP extends App with Logging {
object AquaLSP extends Logging {
private def errorToInfo(
error: AquaError[FileModuleId, AquaFileError, FileSpan.F]
): List[ErrorInfo] = error match {
case AquaParserError(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 SourcesError(err) =>
ErrorInfo.applyOp(0, 0, err.showForConsole, None) :: Nil
case ResolveImportsError(_, token, err) =>
ErrorInfo(token.unit._1, err.showForConsole) :: Nil
case ImportError(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
case AirValidationError(errors) =>
errors.toChain.toList.map(ErrorInfo.applyOp(0, 0, _, None))
}
private def warningToInfo(
warning: AquaWarning[FileSpan.F]
): List[WarningInfo] = warning match {
case CompileWarning(SemanticWarning(token, messages)) =>
WarningInfo(token.unit._1, messages.mkString("\n")) :: Nil
}
import ResultHelper.*
@JSExport
def compile(
@ -195,54 +55,14 @@ object AquaLSP extends App with Logging {
logger.debug("Compilation done.")
def locationsToJs(
locations: List[(Token[FileSpan.F], Token[FileSpan.F])]
): js.Array[TokenLink] = {
locations.flatMap { case (from, to) =>
val fromOp = TokenLocation.fromSpan(from.unit._1)
val toOp = TokenLocation.fromSpan(to.unit._1)
val link = for {
from <- fromOp
to <- toOp
} yield TokenLink(from, to)
if (link.isEmpty)
logger.warn(s"Incorrect coordinates for token '${from.unit._1.name}'")
link.toList
}.toJSArray
}
def importsToTokenImport(imports: List[LiteralToken[FileSpan.F]]): js.Array[TokenImport] =
imports.flatMap { lt =>
val (span, str) = lt.valueToken
val unquoted = str.substring(1, str.length - 1)
TokenLocation.fromSpan(span).map(l => TokenImport(l, unquoted))
}.toJSArray
val result = fileRes match {
fileRes match {
case Valid(lsp) =>
val errors = lsp.errors.map(CompileError.apply).flatMap(errorToInfo)
val warnings = lsp.warnings.map(CompileWarning.apply).flatMap(warningToInfo)
errors match
case Nil =>
logger.debug("No errors on compilation.")
case errs =>
logger.debug("Errors: " + errs.mkString("\n"))
CompilationResult(
errors.toJSArray,
warnings.toJSArray,
locationsToJs(lsp.locations),
importsToTokenImport(lsp.importTokens)
)
lspToCompilationResult(lsp)
case Invalid(e) =>
val errors = e.toChain.toList.flatMap(errorToInfo)
logger.debug("Errors: " + errors.mkString("\n"))
CompilationResult(errors.toJSArray)
}
result
}
proc.unsafeToFuture().toJSPromise

View File

@ -0,0 +1,86 @@
package aqua.lsp
import aqua.parser.lift.FileSpan
import scala.scalajs.js
import scala.scalajs.js.annotation.JSExportAll
import scala.scalajs.js.{UndefOr, undefined}
@JSExportAll
case class CompilationResult(
errors: js.Array[ErrorInfo],
warnings: js.Array[WarningInfo] = js.Array(),
locations: js.Array[TokenLink] = js.Array(),
importLocations: js.Array[TokenImport] = js.Array(),
tokens: js.Array[ExprInfoJs] = js.Array()
)
@JSExportAll
case class ExprInfoJs(location: TokenLocation, `type`: String)
@JSExportAll
case class TokenLocation(name: String, startLine: Int, startCol: Int, endLine: Int, endCol: Int)
@JSExportAll
case class TokenLink(current: TokenLocation, definition: TokenLocation)
@JSExportAll
case class TokenImport(current: TokenLocation, path: String)
object TokenLocation {
def fromSpan(span: FileSpan): Option[TokenLocation] = {
val start = span.locationMap.value.toLineCol(span.span.startIndex)
val end = span.locationMap.value.toLineCol(span.span.endIndex)
for {
startLC <- start
endLC <- end
} yield TokenLocation(span.name, startLC._1, startLC._2, endLC._1, endLC._2)
}
}
@JSExportAll
case class ErrorInfo(
start: Int,
end: Int,
message: String,
location: UndefOr[String]
) {
// Used to distinguish from WarningInfo in TS
val infoType: String = "error"
}
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))
}
}
@JSExportAll
case class WarningInfo(
start: Int,
end: Int,
message: String,
location: UndefOr[String]
) {
// Used to distinguish from ErrorInfo in TS
val infoType: String = "warning"
}
object WarningInfo {
def apply(fileSpan: FileSpan, message: String): WarningInfo = {
val start = fileSpan.span.startIndex
val end = fileSpan.span.endIndex
WarningInfo(start, end, message, fileSpan.name)
}
}

View File

@ -0,0 +1,135 @@
package aqua.lsp
import aqua.compiler.AquaError.{ParserError as AquaParserError, *}
import aqua.compiler.AquaWarning.CompileWarning
import aqua.compiler.{AquaError, AquaWarning}
import aqua.files.FileModuleId
import aqua.io.AquaFileError
import aqua.lsp.AquaLSP.logger
import aqua.parser.lexer.LiteralToken
import aqua.parser.lift.{FileSpan, Span}
import aqua.parser.{ArrowReturnError, BlockIndentError, LexerError, ParserError}
import aqua.semantics.rules.locations.{DefinitionInfo, TokenLocation as TokenLoc}
import aqua.semantics.{HeaderError, RulesViolated, SemanticWarning, WrongAST}
import cats.syntax.show.*
import scala.scalajs.js
import scala.scalajs.js.JSConverters.*
import scribe.Logging
object ResultHelper extends Logging {
import aqua.types.Type.given
def warningToInfo(
warning: AquaWarning[FileSpan.F]
): List[WarningInfo] = warning match {
case CompileWarning(SemanticWarning(token, messages)) =>
WarningInfo(token.unit._1, messages.mkString("\n")) :: Nil
}
def errorToInfo(
error: AquaError[FileModuleId, AquaFileError, FileSpan.F]
): List[ErrorInfo] = error match {
case AquaParserError(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 SourcesError(err) =>
ErrorInfo.applyOp(0, 0, err.showForConsole, None) :: Nil
case ResolveImportsError(_, token, err) =>
ErrorInfo(token.unit._1, err.showForConsole) :: Nil
case ImportError(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
case AirValidationError(errors) =>
errors.toChain.toList.map(ErrorInfo.applyOp(0, 0, _, None))
}
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)
}
}.toJSArray
private def locationsToJs(
locations: List[TokenLoc[FileSpan.F]]
): js.Array[TokenLink] =
locations.flatMap { case TokenLoc(from, to) =>
val fromOp = TokenLocation.fromSpan(from.unit._1)
val toOp = TokenLocation.fromSpan(to.unit._1)
val link = for {
from <- fromOp
to <- toOp
} yield TokenLink(from, to)
if (link.isEmpty)
logger.warn(s"Incorrect coordinates for token '${from.unit._1.name}'")
link.toList
}.toJSArray
private def importsToTokenImport(imports: List[LiteralToken[FileSpan.F]]): js.Array[TokenImport] =
imports.flatMap { lt =>
val (span, str) = lt.valueToken
val unquoted = str.substring(1, str.length - 1)
TokenLocation.fromSpan(span).map(l => TokenImport(l, unquoted))
}.toJSArray
def lspToCompilationResult(lsp: LspContext[FileSpan.F]): CompilationResult = {
val errors = lsp.errors.map(CompileError.apply).flatMap(errorToInfo)
val warnings = lsp.warnings.map(CompileWarning.apply).flatMap(warningToInfo)
errors match
case Nil =>
logger.debug("No errors on compilation.")
case errs =>
logger.debug("Errors: " + errs.mkString("\n"))
CompilationResult(
errors.toJSArray,
warnings.toJSArray,
locationsToJs(lsp.variables.flatMap(v => v.allLocations)),
importsToTokenImport(lsp.importTokens),
tokensToJs(lsp.variables.map(_.definition))
)
}
}

View File

@ -6,15 +6,12 @@ import aqua.raw.RawContext
import aqua.semantics.header.{HeaderHandler, HeaderSem}
import cats.data.Validated.validNec
import cats.syntax.semigroup.*
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.data.{Chain, Validated, ValidatedNec}
import cats.syntax.either.*
import cats.syntax.functor.*
import cats.syntax.monoid.*
import cats.syntax.traverse.*
import cats.syntax.either.*
import cats.syntax.semigroup.*
import cats.{Comonad, Monad, Monoid, Order}
import cats.data.{Chain, NonEmptyChain, Validated, ValidatedNec}
object LSPCompiler {

View File

@ -1,12 +1,10 @@
package aqua.lsp
import aqua.parser.lexer.Token
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.locations.{LocationsAlgebra, LocationsState}
import aqua.semantics.rules.locations.{DefinitionInfo, LocationsAlgebra, LocationsState}
import aqua.types.AbilityType
import cats.data.State
import monocle.Lens
import monocle.macros.GenLens
import scribe.Logging
class LocationsInterpreter[S[_], X](using
@ -15,30 +13,25 @@ class LocationsInterpreter[S[_], X](using
type SX[A] = State[X, A]
val stack = new StackInterpreter[S, X, LocationsState[S], LocationsState[S]](
GenLens[LocationsState[S]](_.stack)
)
import stack.*
override def addToken(name: String, token: Token[S]): State[X, Unit] = modify { st =>
st.copy(tokens = st.tokens.updated(name, token))
override def addDefinition(definition: DefinitionInfo[S]): State[X, Unit] = modify { st =>
st.addDefinition(definition)
}
private def combineFieldName(name: String, field: String): String = name + "." + field
override def addTokenWithFields(
name: String,
token: Token[S],
fields: List[(String, Token[S])]
): State[X, Unit] = modify { st =>
st.copy(tokens =
st.tokens ++ ((name, token) +: fields.map(kv => (combineFieldName(name, kv._1), kv._2))).toMap
)
override def addDefinitionWithFields(
definition: DefinitionInfo[S],
fields: List[DefinitionInfo[S]]
): State[X, Unit] = {
val allTokens =
definition +: fields.map { fieldDef =>
fieldDef.copy(name = AbilityType.fullName(definition.name, fieldDef.name))
}
modify { st =>
st.addDefinitions(allTokens)
}
}
def pointFieldLocation(typeName: String, fieldName: String, token: Token[S]): State[X, Unit] =
pointLocation(combineFieldName(typeName, fieldName), token)
pointLocation(AbilityType.fullName(typeName, fieldName), token)
def pointTokenWithFieldLocation(
typeName: String,
@ -48,37 +41,21 @@ class LocationsInterpreter[S[_], X](using
): State[X, Unit] = {
for {
_ <- pointLocation(typeName, typeToken)
_ <- pointLocation(combineFieldName(typeName, fieldName), token)
_ <- pointLocation(AbilityType.fullName(typeName, fieldName), token)
} yield {}
}
override def pointLocation(name: String, token: Token[S]): State[X, Unit] = {
modify { st =>
val newLoc: Option[Token[S]] = st.stack.collectFirst {
case frame if frame.tokens.contains(name) => frame.tokens(name)
} orElse st.tokens.get(name)
st.copy(locations = st.locations ++ newLoc.map(token -> _).toList)
st.addLocation(name, token)
}
}
def pointLocations(locations: List[(String, Token[S])]): State[X, Unit] = {
def pointLocations(locations: List[(String, Token[S])]): State[X, Unit] =
modify { st =>
val newLocs = locations.flatMap { case (name, token) =>
(st.stack.collectFirst {
case frame if frame.tokens.contains(name) => frame.tokens(name)
} orElse st.tokens.get(name)).map(token -> _)
}
st.copy(locations = st.locations ++ newLocs)
}
st.addLocations(locations)
}
private def modify(f: LocationsState[S] => LocationsState[S]): SX[Unit] =
State.modify(lens.modify(f))
override def beginScope(): SX[Unit] =
stack.beginScope(LocationsState[S]())
override def endScope(): SX[Unit] = stack.endScope
}

View File

@ -2,10 +2,10 @@ package aqua.lsp
import aqua.parser.lexer.{LiteralToken, NamedTypeToken, Token}
import aqua.raw.{RawContext, RawPart}
import aqua.semantics.{SemanticError, SemanticWarning}
import aqua.semantics.header.Picker
import aqua.types.{ArrowType, Type}
import aqua.semantics.rules.locations.{TokenLocation, VariableInfo}
import aqua.semantics.{SemanticError, SemanticWarning}
import aqua.types.{AbilityType, ArrowType, Type}
import cats.syntax.monoid.*
import cats.{Monoid, Semigroup}
@ -15,12 +15,13 @@ case class LspContext[S[_]](
abDefinitions: Map[String, NamedTypeToken[S]] = Map.empty[String, NamedTypeToken[S]],
rootArrows: Map[String, ArrowType] = Map.empty[String, ArrowType],
constants: Map[String, Type] = Map.empty[String, Type],
tokens: Map[String, Token[S]] = Map.empty[String, Token[S]],
locations: List[(Token[S], Token[S])] = Nil,
variables: List[VariableInfo[S]] = Nil,
importTokens: List[LiteralToken[S]] = Nil,
errors: List[SemanticError[S]] = Nil,
warnings: List[SemanticWarning[S]] = Nil
)
) {
lazy val allLocations: List[TokenLocation[S]] = variables.flatMap(_.allLocations)
}
object LspContext {
@ -33,8 +34,8 @@ object LspContext {
abDefinitions = x.abDefinitions ++ y.abDefinitions,
rootArrows = x.rootArrows ++ y.rootArrows,
constants = x.constants ++ y.constants,
locations = x.locations ++ y.locations,
tokens = x.tokens ++ y.tokens,
importTokens = x.importTokens ++ y.importTokens,
variables = x.variables ++ y.variables,
errors = x.errors ++ y.errors,
warnings = x.warnings ++ y.warnings
)
@ -87,10 +88,13 @@ object LspContext {
override def declares(ctx: LspContext[S]): Set[String] = ctx.raw.declares
override def setAbility(ctx: LspContext[S], name: String, ctxAb: LspContext[S]): LspContext[S] =
val prefix = name + "."
ctx.copy(
raw = ctx.raw.setAbility(name, ctxAb.raw),
tokens = ctx.tokens ++ ctxAb.tokens.map(kv => (prefix + kv._1) -> kv._2)
variables = ctx.variables ++ ctxAb.variables.map(v =>
v.copy(definition =
v.definition.copy(name = AbilityType.fullName(name, v.definition.name))
)
)
)
override def setModule(
@ -113,13 +117,16 @@ object LspContext {
declared: Boolean
): Option[LspContext[S]] =
// rename tokens from one context with prefix addition
val newTokens = rename.map { renameStr =>
ctx.tokens.map {
case (tokenName, token) if tokenName.startsWith(name) =>
tokenName.replaceFirst(name, renameStr) -> token
val newVariables = rename.map { renameStr =>
ctx.variables.map {
case v if v.definition.name.startsWith(name) =>
v.copy(definition =
v.definition.copy(name = v.definition.name.replaceFirst(v.definition.name, renameStr))
)
case kv => kv
}
}.getOrElse(ctx.tokens)
}.getOrElse(ctx.variables)
ctx.raw
.pick(name, rename, declared)
@ -132,7 +139,7 @@ object LspContext {
ctx.rootArrows.get(name).fold(Map.empty)(t => Map(rename.getOrElse(name) -> t)),
constants =
ctx.constants.get(name).fold(Map.empty)(t => Map(rename.getOrElse(name) -> t)),
tokens = newTokens
variables = newVariables
)
)

View File

@ -49,7 +49,7 @@ class LspSemantics[S[_]] extends Semantics[S, LspContext[S]] {
definitions = rawState.abilities.definitions ++ init.abDefinitions
),
locations = rawState.locations.copy(
tokens = rawState.locations.tokens ++ init.tokens
variables = rawState.locations.variables ++ init.variables
)
)
@ -69,9 +69,8 @@ class LspSemantics[S[_]] extends Semantics[S, LspContext[S]] {
rootArrows = state.names.rootArrows,
constants = state.names.constants,
abDefinitions = state.abilities.definitions,
locations = state.locations.allLocations,
importTokens = importTokens,
tokens = state.locations.tokens,
variables = state.locations.variables,
errors = state.errors.toList,
warnings = state.warnings.toList
).pure[Result]

View File

@ -0,0 +1,281 @@
package aqua.lsp
import aqua.compiler.{AquaCompilerConf, AquaError, AquaSources}
import aqua.parser.Parser
import aqua.parser.lift.Span
import aqua.parser.lift.Span.S
import aqua.raw.ConstantRaw
import aqua.semantics.rules.locations.{DefinitionInfo, TokenLocation, VariableInfo}
import aqua.types.*
import cats.Id
import cats.data.*
import cats.instances.string.*
import org.scalatest.Inside
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class AquaLSPSpec extends AnyFlatSpec with Matchers with Inside {
private def getByPosition(code: String, str: String, position: Int): Option[(Int, Int)] = {
str.r.findAllMatchIn(code).toList.lift(position).map(r => (r.start, r.end))
}
extension (c: LspContext[Span.S]) {
def checkLocations(
name: String,
defPosition: Int,
usePosition: Int,
defCode: String,
useCode: Option[String] = None,
fieldName: Option[String] = None
): Boolean = {
(for {
defPos <- getByPosition(defCode, name, defPosition)
usePos <- getByPosition(useCode.getOrElse(defCode), fieldName.getOrElse(name), usePosition)
} yield {
val (defStart, defEnd) = defPos
val (useStart, useEnd) = usePos
c.allLocations.exists { case TokenLocation(useT, defT) =>
val defSpan = defT.unit._1
val useSpan = useT.unit._1
defSpan.startIndex == defStart && defSpan.endIndex == defEnd && useSpan.startIndex == useStart && useSpan.endIndex == useEnd
}
}).getOrElse(false)
}
def locationsToString(): List[String] =
c.allLocations.map { case TokenLocation(l, r) =>
val lSpan = l.unit._1
val rSpan = r.unit._1
s"($l($lSpan):$r($rSpan))"
}
def checkTokenLoc(
code: String,
checkName: String,
position: Int,
`type`: Type,
// if name is combined
fullName: Option[String] = None,
printFiltered: Boolean = false
): Boolean = {
getByPosition(code, checkName, position).exists { case (start, end) =>
val res = c.variables.exists { case VariableInfo(definition, _) =>
val span = definition.token.unit._1
definition.name == fullName.getOrElse(
checkName
) && span.startIndex == start && span.endIndex == end && definition.`type` == `type`
}
if (printFiltered)
println(
c.variables
.map(_.definition)
.filter(v => v.name == fullName.getOrElse(checkName) && v.`type` == `type`)
.map { case DefinitionInfo(name, token, t) =>
val span = token.unit._1
s"$name(${span.startIndex}:${span.endIndex}) $t"
}
)
res
}
}
}
private def aquaSource(src: Map[String, String], imports: Map[String, String]) = {
new AquaSources[Id, String, String] {
override def sources: Id[ValidatedNec[String, Chain[(String, String)]]] =
Validated.validNec(Chain.fromSeq(src.toSeq))
override def resolveImport(from: String, imp: String): Id[ValidatedNec[String, String]] =
Validated.validNec(imp)
override def load(file: String): Id[ValidatedNec[String, String]] =
Validated.fromEither(
(imports ++ src)
.get(file)
.toRight(NonEmptyChain.one(s"Cannot load imported file $file"))
)
}
}
def compile(
src: Map[String, String],
imports: Map[String, String] = Map.empty
): ValidatedNec[AquaError[String, String, S], Map[String, LspContext[S]]] = {
LSPCompiler
.compileToLsp[Id, String, String, Span.S](
aquaSource(src, imports),
id => txt => Parser.parse(Parser.parserSchema)(txt),
AquaCompilerConf(ConstantRaw.defaultConstants(None))
)
.leftMap { errors =>
println(errors)
errors
}
}
it should "return right tokens" in {
val main =
"""module Import
|import foo, strFunc, num from "export2.aqua"
|
|import "../gen/OneMore.aqua"
|
|func foo_wrapper() -> string:
| fooResult <- foo()
| if 1 == 1:
| someVar = "aaa"
| strFunc(someVar)
| else:
| someVar = 123
| num(someVar)
| OneMore fooResult
| OneMore.more_call()
|
|ability Ab:
| someField: u32
|
|data Str:
| someField: string
|
|func useAbAndStruct{Ab}():
| s = Str(someField = "asd")
| strFunc(s.someField)
| num(Ab.someField)
|
|""".stripMargin
val src = Map(
"index.aqua" -> main
)
val firstImport =
"""module Export declares strFunc, num, foo
|
|func absb() -> string:
| <- "ff"
|
|func strFunc(someVar: string) -> string:
| <- someVar
|
|func num(someVar: u32) -> u32:
| <- someVar
|
|func foo() -> string:
| <- "I am MyFooBar foo"
|
|""".stripMargin
val secondImport =
"""
|service OneMore:
| more_call()
| consume(s: string)
|""".stripMargin
val imports = Map(
"export2.aqua" ->
firstImport,
"../gen/OneMore.aqua" ->
secondImport
)
val res = compile(src, imports).toOption.get.values.head
val serviceType = ServiceType(
"OneMore",
NonEmptyMap.of(
("more_call", ArrowType(NilType, NilType)),
("consume", ArrowType(ProductType.labelled(("s", ScalarType.string) :: Nil), NilType))
)
)
// inside `foo_wrapper` func
res.checkTokenLoc(main, "fooResult", 0, ScalarType.string) shouldBe true
res.checkLocations("fooResult", 0, 1, main) shouldBe true
res.checkTokenLoc(main, "someVar", 0, LiteralType.string, None, true) shouldBe true
res.checkLocations("someVar", 0, 1, main) shouldBe true
res.checkTokenLoc(main, "someVar", 2, LiteralType.unsigned) shouldBe true
res.checkLocations("someVar", 2, 3, main) shouldBe true
// num usage
res.checkLocations("num", 1, 1, firstImport, Some(main)) shouldBe true
// strFunc usage
res.checkLocations("strFunc", 1, 1, firstImport, Some(main)) shouldBe true
res.checkLocations("strFunc", 1, 2, firstImport, Some(main)) shouldBe true
// Str.field
res.checkTokenLoc(main, "someField", 1, ScalarType.string, Some("Str.someField")) shouldBe true
res.checkLocations("someField", 1, 3, main, None) shouldBe true
// Ab.field
res.checkTokenLoc(
main,
"someField",
0,
ScalarType.u32,
Some("Ab.someField"),
true
) shouldBe true
// this is tokens from imports, if we will use `FileSpan.F` file names will be different
// OneMore service
res.checkTokenLoc(secondImport, "OneMore", 0, serviceType) shouldBe true
res.checkTokenLoc(
secondImport,
"more_call",
0,
ArrowType(NilType, NilType),
Some("OneMore.more_call"),
true
) shouldBe true
res.checkTokenLoc(
secondImport,
"consume",
0,
ArrowType(ProductType.labelled(("s", ScalarType.string) :: Nil), NilType),
Some("OneMore.consume")
) shouldBe true
// strFunc function and argument
res.checkTokenLoc(
firstImport,
"strFunc",
1,
ArrowType(
ProductType.labelled(("someVar", ScalarType.string) :: Nil),
ProductType(ScalarType.string :: Nil)
),
None,
true
) shouldBe true
res.checkTokenLoc(firstImport, "someVar", 0, ScalarType.string) shouldBe true
// num function and argument
res.checkTokenLoc(
firstImport,
"num",
1,
ArrowType(
ProductType.labelled(("someVar", ScalarType.u32) :: Nil),
ProductType(ScalarType.u32 :: Nil)
)
) shouldBe true
res.checkTokenLoc(firstImport, "someVar", 2, ScalarType.u32, None, true) shouldBe true
// foo function
res.checkTokenLoc(
firstImport,
"foo",
1,
ArrowType(NilType, ProductType(ScalarType.string :: Nil))
) shouldBe true
}
}

View File

@ -6,6 +6,11 @@ export interface TokenLocation {
endCol: number
}
export interface TokenInfo {
location: TokenLocation,
type: string
}
export interface TokenLink {
current: TokenLocation,
definition: TokenLocation
@ -36,7 +41,8 @@ export interface CompilationResult {
errors: ErrorInfo[],
warnings: WarningInfo[],
locations: TokenLink[],
importLocations: TokenImport[]
importLocations: TokenImport[],
tokens: TokenInfo[]
}
export class Compiler {

View File

@ -1,16 +1,14 @@
package aqua.tree
import aqua.helpers.tree.Tree
import cats.Show
import cats.data.Chain
import cats.free.Cofree
import cats.syntax.show.*
import cats.syntax.apply.*
import cats.syntax.show.*
import scala.annotation.tailrec
import aqua.helpers.Tree
trait TreeNodeCompanion[T <: TreeNode[T]] {
given showTreeLabel: Show[T]

View File

@ -1,10 +1,10 @@
package aqua.parser
import aqua.helpers.tree.Tree
import aqua.parser.expr.*
import aqua.parser.head.{HeadExpr, HeaderExpr}
import aqua.parser.lift.{LiftParser, Span}
import aqua.parser.lift.LiftParser.*
import aqua.helpers.Tree
import cats.data.{Chain, Validated, ValidatedNec}
import cats.syntax.flatMap.*

View File

@ -61,7 +61,7 @@ case class IntoCopy[F[_]: Comonad](
object PropertyOp {
private val parseField: P[PropertyOp[Span.S]] =
(`.` *> anyName).lift.map(IntoField(_))
`.` *> anyName.lift.map(IntoField(_))
val parseArrow: P[PropertyOp[Span.S]] =
(`.` *> CallArrowToken.callBraces).map { case CallBraces(name, abilities, args) =>

View File

@ -1,7 +1,5 @@
package aqua.parser.lexer
import aqua.parser.lift.Span.S
import cats.data.NonEmptyList
import cats.parse.{Accumulator0, Parser as P, Parser0 as P0}
import cats.syntax.functor.*

View File

@ -17,6 +17,15 @@ case class FileSpan(name: String, locationMap: Eval[LocationMap], span: Span) {
*/
def focus(ctx: Int): Option[FileSpan.Focus] =
span.focus(locationMap.value, ctx).map(FileSpan.Focus(name, locationMap, ctx, _))
override def hashCode(): Int = (name, span).hashCode()
override def equals(obj: Any): Boolean = {
obj match {
case FileSpan(n, _, s) => n == name && s == span
case _ => false
}
}
}
object FileSpan {

View File

@ -38,14 +38,6 @@ sealed abstract class Prog[Alg[_]: Monad, A] extends (Alg[A] => Alg[A]) {
(_: Unit, m: A) => N.endScope() as m
)
)
def locationsScope[S[_]]()(implicit L: LocationsAlgebra[S, Alg]): Prog[Alg, A] =
wrap(
RunAround(
L.beginScope(),
(_: Unit, m: A) => L.endScope() as m
)
)
}
case class RunAfter[Alg[_]: Monad, A](prog: Alg[A]) extends Prog[Alg, A] {

View File

@ -28,7 +28,7 @@ class AbilitySem[S[_]](val expr: AbilityExpr[S]) extends AnyVal {
): Prog[Alg, Raw] = {
Prog.after_(
for {
defs <- D.purgeDefs(expr.name)
defs <- D.purgeDefs()
fields = defs.view.mapValues(d => d.name -> d.`type`).toMap
abilityType <- T.defineAbilityType(expr.name, fields)
result = abilityType.map(st => TypeRaw(expr.name.value, st))

View File

@ -22,7 +22,7 @@ class DataStructSem[S[_]](val expr: DataStructExpr[S]) extends AnyVal {
): Prog[Alg, Raw] =
Prog.after((_: Raw) =>
for {
defs <- D.purgeDefs(expr.name)
defs <- D.purgeDefs()
fields = defs.view.mapValues(d => d.name -> d.`type`).toMap
structType <- T.defineStructType(expr.name, fields)
result = structType.map(st => TypeRaw(expr.name.value, st))

View File

@ -45,7 +45,7 @@ class ServiceSem[S[_]](val expr: ServiceExpr[S]) extends AnyVal {
)
)
serviceType <- EitherT.fromOptionF(
T.defineServiceType(expr.name, arrowsByName.toSortedMap.toMap),
T.defineServiceType(expr.name, arrowsByName.toSortedMap),
Raw.error("Failed to define service type")
)
arrowsDefs = arrows.map { case (name, _) => name.value -> name }.toNem

View File

@ -137,7 +137,6 @@ class ArrowSem[S[_]](val expr: ArrowExpr[S]) extends AnyVal {
T: TypesAlgebra[S, Alg],
N: NamesAlgebra[S, Alg],
A: AbilitiesAlgebra[S, Alg],
L: LocationsAlgebra[S, Alg],
M: ManglerAlgebra[Alg]
): Prog[Alg, Raw] =
Prog
@ -147,6 +146,4 @@ class ArrowSem[S[_]](val expr: ArrowExpr[S]) extends AnyVal {
)
.abilitiesScope(expr.arrowTypeExpr)
.namesScope(expr.arrowTypeExpr)
.locationsScope()
}

View File

@ -42,6 +42,4 @@ class CatchSem[S[_]](val expr: CatchExpr[S]) extends AnyVal {
)
.abilitiesScope[S](expr.token)
.namesScope(expr.token)
.locationsScope()
}

View File

@ -8,14 +8,9 @@ import aqua.raw.value.VarRaw
import aqua.semantics.Prog
import aqua.semantics.rules.names.NamesAlgebra
import aqua.semantics.rules.types.TypesAlgebra
import aqua.types.*
import cats.Monad
import cats.data.Chain
import cats.data.OptionT
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
class DeclareStreamSem[S[_]](val expr: DeclareStreamExpr[S]) {

View File

@ -39,5 +39,4 @@ class ElseOtherwiseSem[S[_]](val expr: ElseOtherwiseExpr[S]) extends AnyVal {
)
.abilitiesScope(expr.token)
.namesScope(expr.token)
.locationsScope()
}

View File

@ -56,5 +56,4 @@ class IfSem[S[_]](val expr: IfExpr[S]) extends AnyVal {
)
.abilitiesScope[S](expr.token)
.namesScope[S](expr.token)
.locationsScope()
}

View File

@ -37,5 +37,4 @@ class TrySem[S[_]](val expr: TryExpr[S]) extends AnyVal {
)
.abilitiesScope(expr.token)
.namesScope(expr.token)
.locationsScope()
}

View File

@ -1,9 +1,7 @@
package aqua.semantics.rules
import aqua.errors.Errors.internalError
import aqua.helpers.syntax.optiont.*
import aqua.parser.lexer.*
import aqua.parser.lexer.InfixToken.value
import aqua.parser.lexer.InfixToken.{BoolOp, CmpOp, EqOp, MathOp, Op as InfOp}
import aqua.parser.lexer.PrefixToken.Op as PrefOp
import aqua.raw.value.*

View File

@ -1,22 +1,20 @@
package aqua.semantics.rules.abilities
import aqua.parser.lexer.{Name, NamedTypeToken, Token, ValueToken}
import aqua.parser.lexer.{Name, NamedTypeToken, Token}
import aqua.raw.RawContext
import aqua.raw.value.ValueRaw
import aqua.raw.{RawContext, ServiceRaw}
import aqua.semantics.Levenshtein
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.mangler.ManglerAlgebra
import aqua.semantics.rules.report.ReportAlgebra
import aqua.semantics.rules.{StackInterpreter, abilities}
import aqua.types.{ArrowType, ServiceType}
import aqua.types.ArrowType
import cats.data.{NonEmptyMap, State}
import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.foldable.*
import cats.syntax.functor.*
import cats.syntax.option.*
import cats.syntax.traverse.*
import monocle.Lens
import monocle.macros.GenLens
@ -56,12 +54,6 @@ class AbilitiesInterpreter[S[_], X](using
case false =>
for {
_ <- modify(_.defineService(name, defaultId))
// TODO: Is it used?
_ <- locations.addTokenWithFields(
name.value,
name,
arrowDefs.toNel.toList
)
} yield true
}

View File

@ -9,7 +9,7 @@ import cats.data.{NonEmptyList, NonEmptyMap}
trait DefinitionsAlgebra[S[_], Alg[_]] {
def defineDef(name: Name[S], `type`: Type): Alg[Boolean]
def purgeDefs(token: NamedTypeToken[S]): Alg[Map[String, DefinitionsState.Def[S]]]
def purgeDefs(): Alg[Map[String, DefinitionsState.Def[S]]]
def defineArrow(arrow: Name[S], `type`: ArrowType): Alg[Boolean]

View File

@ -1,28 +1,17 @@
package aqua.semantics.rules.definitions
import aqua.parser.lexer.{Name, NamedTypeToken, Token}
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.report.ReportAlgebra
import aqua.semantics.rules.abilities.AbilitiesState
import aqua.semantics.rules.locations.{LocationsAlgebra, LocationsState}
import aqua.semantics.rules.types.TypesState
import aqua.types.{ArrowType, Type}
import cats.data.{NonEmptyList, NonEmptyMap, State}
import monocle.Lens
import monocle.macros.GenLens
import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
import cats.data.{NonEmptyList, State}
import cats.syntax.functor.*
import cats.syntax.option.*
import scala.collection.immutable.SortedMap
import monocle.Lens
class DefinitionsInterpreter[S[_], X](implicit
lens: Lens[X, DefinitionsState[S]],
report: ReportAlgebra[S, State[X, *]],
locations: LocationsAlgebra[S, State[X, *]]
report: ReportAlgebra[S, State[X, *]]
) extends DefinitionsAlgebra[S, State[X, *]] {
type SX[A] = State[X, A]
@ -55,16 +44,9 @@ class DefinitionsInterpreter[S[_], X](implicit
override def defineArrow(arrow: Name[S], `type`: ArrowType): SX[Boolean] =
define(arrow, `type`, "arrow")
override def purgeDefs(
token: NamedTypeToken[S]
): SX[Map[String, DefinitionsState.Def[S]]] =
override def purgeDefs(): SX[Map[String, DefinitionsState.Def[S]]] =
getState.map(_.definitions).flatMap { defs =>
val names = defs.view.mapValues(_.name)
for {
_ <- locations
.addTokenWithFields(token.value, token, names.toList)
.whenA(defs.nonEmpty)
_ <- modify(_.copy(definitions = Map.empty))
} yield defs
}

View File

@ -1,23 +1,19 @@
package aqua.semantics.rules.locations
import aqua.parser.lexer.Token
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.types.TypesState
import monocle.Lens
import monocle.macros.GenLens
import cats.data.{NonEmptyList, NonEmptyMap, State}
import cats.data.State
class DummyLocationsInterpreter[S[_], X] extends LocationsAlgebra[S, State[X, *]] {
def addToken(name: String, token: Token[S]): State[X, Unit] = State.pure(())
def addDefinition(definition: DefinitionInfo[S]): State[X, Unit] = State.pure(())
def addTokenWithFields(
name: String,
token: Token[S],
fields: List[(String, Token[S])]
def addDefinitionWithFields(
definition: DefinitionInfo[S],
fields: List[DefinitionInfo[S]]
): State[X, Unit] = State.pure(())
def pointFieldLocation(typeName: String, fieldName: String, token: Token[S]): State[X, Unit] = State.pure(())
def pointFieldLocation(typeName: String, fieldName: String, token: Token[S]): State[X, Unit] =
State.pure(())
def pointTokenWithFieldLocation(
typeName: String,

View File

@ -1,16 +1,23 @@
package aqua.semantics.rules.locations
import aqua.parser.lexer.Token
import aqua.types.Type
trait LocationsAlgebra[S[_], Alg[_]] {
def addToken(name: String, token: Token[S]): Alg[Unit]
def addTokenWithFields(name: String, token: Token[S], fields: List[(String, Token[S])]): Alg[Unit]
def addDefinition(definition: DefinitionInfo[S]): Alg[Unit]
def pointTokenWithFieldLocation(typeName: String, typeToken: Token[S], fieldName: String, token: Token[S]): Alg[Unit]
def addDefinitionWithFields(
definition: DefinitionInfo[S],
fields: List[DefinitionInfo[S]]
): Alg[Unit]
def pointTokenWithFieldLocation(
typeName: String,
typeToken: Token[S],
fieldName: String,
token: Token[S]
): Alg[Unit]
def pointFieldLocation(typeName: String, fieldName: String, token: Token[S]): Alg[Unit]
def pointLocation(name: String, token: Token[S]): Alg[Unit]
def pointLocations(locations: List[(String, Token[S])]): Alg[Unit]
def beginScope(): Alg[Unit]
def endScope(): Alg[Unit]
}

View File

@ -1,26 +1,55 @@
package aqua.semantics.rules.locations
import aqua.helpers.syntax.list.*
import aqua.parser.lexer.Token
import aqua.semantics.rules.types.TypesState
import cats.kernel.Monoid
import scribe.Logging
case class LocationsState[S[_]](
tokens: Map[String, Token[S]] = Map.empty[String, Token[S]],
locations: List[(Token[S], Token[S])] = Nil,
stack: List[LocationsState[S]] = Nil
) {
variables: List[VariableInfo[S]] = Nil
) extends Logging {
lazy val allLocations: List[(Token[S], Token[S])] = locations
def addDefinitions(newDefinitions: List[DefinitionInfo[S]]): LocationsState[S] =
copy(variables = newDefinitions.map(d => VariableInfo(d)) ++ variables)
def addDefinition(newDef: DefinitionInfo[S]): LocationsState[S] =
copy(variables = VariableInfo(newDef) +: variables)
private def addOccurrenceToFirst(
vars: List[VariableInfo[S]],
name: String,
token: Token[S]
): List[VariableInfo[S]] = {
if (!vars.exists(_.definition.name == name))
logger.error(s"Unexpected. Cannot add occurrence for $name")
vars.updateFirst(_.definition.name == name, v => v.copy(occurrences = token +: v.occurrences))
}
def addLocation(
name: String,
token: Token[S]
): LocationsState[S] =
copy(variables = addOccurrenceToFirst(variables, name, token))
def addLocations(
locations: List[(String, Token[S])]
): LocationsState[S] =
locations.foldLeft(this) { case (st, (name, token)) =>
st.addLocation(name, token)
}
}
object LocationsState {
implicit def locationsStateMonoid[S[_]]: Monoid[LocationsState[S]] = new Monoid[LocationsState[S]] {
implicit def locationsStateMonoid[S[_]]: Monoid[LocationsState[S]] =
new Monoid[LocationsState[S]] {
override def empty: LocationsState[S] = LocationsState()
override def combine(x: LocationsState[S], y: LocationsState[S]): LocationsState[S] =
LocationsState(
tokens = x.tokens ++ y.tokens
variables = x.variables ++ y.variables
)
}
}

View File

@ -0,0 +1,11 @@
package aqua.semantics.rules.locations
import aqua.parser.lexer.Token
import aqua.types.Type
case class DefinitionInfo[S[_]](name: String, token: Token[S], `type`: Type)
case class TokenLocation[S[_]](usage: Token[S], definition: Token[S])
case class VariableInfo[S[_]](definition: DefinitionInfo[S], occurrences: List[Token[S]] = Nil) {
def allLocations: List[TokenLocation[S]] = occurrences.map(o => TokenLocation(o, definition.token))
}

View File

@ -4,15 +4,12 @@ import aqua.errors.Errors.internalError
import aqua.parser.lexer.{Name, Token}
import aqua.semantics.Levenshtein
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.locations.{DefinitionInfo, LocationsAlgebra}
import aqua.semantics.rules.report.ReportAlgebra
import aqua.types.{ArrowType, StreamType, Type}
import cats.data.{OptionT, State}
import cats.syntax.all.*
import cats.syntax.applicative.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import monocle.Lens
import monocle.macros.GenLens
@ -117,13 +114,13 @@ class NamesInterpreter[S[_], X](using
case None =>
mapStackHeadM(report.error(name, "Cannot define a variable in the root scope").as(false))(
fr => (fr.addName(name, `type`) -> true).pure
) <* locations.addToken(name.value, name)
) <* locations.addDefinition(DefinitionInfo(name.value, name, `type`))
}
override def derive(name: Name[S], `type`: Type, derivedFrom: Set[String]): State[X, Boolean] =
define(name, `type`).flatTap(defined =>
mapStackHead_(_.derived(name, derivedFrom)).whenA(defined)
) <* locations.addToken(name.value, name)
)
override def getDerivedFrom(fromNames: List[Set[String]]): State[X, List[Set[String]]] =
mapStackHead(Nil)(frame =>
@ -142,7 +139,7 @@ class NamesInterpreter[S[_], X](using
constants = st.constants.updated(name.value, `type`)
)
).as(true)
}.flatTap(_ => locations.addToken(name.value, name))
}.flatTap(_ => locations.addDefinition(DefinitionInfo(name.value, name, `type`)))
override def defineArrow(name: Name[S], arrowType: ArrowType, isRoot: Boolean): SX[Boolean] =
readName(name.value).flatMap {
@ -166,8 +163,10 @@ class NamesInterpreter[S[_], X](using
report
.error(name, "Cannot define a variable in the root scope")
.as(false)
)(fr => (fr.addArrow(name, arrowType) -> true).pure)
}.flatTap(_ => locations.addToken(name.value, name))
)(fr => (fr.addArrow(name, arrowType) -> true).pure).flatTap(_ =>
locations.addDefinition(DefinitionInfo[S](name.value, name, arrowType))
)
}
override def streamsDefinedWithinScope(): SX[Map[String, StreamType]] =
mapStackHead(Map.empty) { frame =>

View File

@ -3,14 +3,14 @@ package aqua.semantics.rules.types
import aqua.parser.lexer.*
import aqua.raw.value.*
import aqua.semantics.rules.StackInterpreter
import aqua.semantics.rules.locations.LocationsAlgebra
import aqua.semantics.rules.locations.{DefinitionInfo, LocationsAlgebra}
import aqua.semantics.rules.report.ReportAlgebra
import aqua.semantics.rules.types.TypeResolution.TypeResolutionError
import aqua.types.*
import aqua.types.Type.*
import cats.data.*
import cats.data.Validated.{Invalid, Valid}
import cats.data.{Chain, NonEmptyList, NonEmptyMap, OptionT, State}
import cats.syntax.applicative.*
import cats.syntax.apply.*
import cats.syntax.flatMap.*
@ -97,7 +97,8 @@ class TypesInterpreter[S[_], X](using
nonEmptyFields =>
val `type` = AbilityType(name.value, nonEmptyFields)
modify(_.defineType(name, `type`)).as(`type`.some)
locateNamedType(name, `type`, fields) >> modify(_.defineType(name, `type`))
.as(`type`.some)
)
}
@ -131,10 +132,20 @@ class TypesInterpreter[S[_], X](using
).semiflatMap(nonEmptyArrows =>
val `type` = ServiceType(name.value, nonEmptyArrows)
modify(_.defineType(name, `type`)).as(`type`)
locateNamedType(name, `type`, fields) >> modify(_.defineType(name, `type`)).as(`type`)
).value
)
private def locateNamedType(
name: NamedTypeToken[S],
t: NamedType,
fields: Map[String, (Name[S], Type)]
) =
locations.addDefinitionWithFields(
DefinitionInfo[S](name.value, name, t),
fields.map { case (n, (t, ty)) => DefinitionInfo[S](n, t, ty) }.toList
)
override def defineStructType(
name: NamedTypeToken[S],
fields: Map[String, (Name[S], Type)]
@ -159,7 +170,8 @@ class TypesInterpreter[S[_], X](using
)(nonEmptyFields =>
val `type` = StructType(name.value, nonEmptyFields)
modify(_.defineType(name, `type`)).as(`type`.some)
locateNamedType(name, `type`, fields) >> modify(_.defineType(name, `type`))
.as(`type`.some)
)
)
)
@ -170,7 +182,7 @@ class TypesInterpreter[S[_], X](using
case Some(_) => report.error(name, s"Type `${name.value}` was already defined").as(false)
case None =>
modify(_.defineType(name, target))
.productL(locations.addToken(name.value, name))
.productL(locations.addDefinition(DefinitionInfo(name.value, name.asName, target)))
.as(true)
}

View File

@ -4,15 +4,15 @@ import aqua.errors.Errors.internalError
import aqua.types.*
import aqua.types.Type.*
import cats.data.NonEmptyList
import cats.data.NonEmptyMap
import cats.syntax.applicative.*
import cats.syntax.foldable.*
import cats.syntax.functor.*
import cats.syntax.option.*
import cats.syntax.partialOrder.*
import cats.syntax.show.*
import cats.syntax.traverse.*
import cats.{Eval, Foldable, Functor, PartialOrder, Traverse}
import cats.{Eval, Foldable, Functor, PartialOrder, Show, Traverse}
import scala.collection.immutable.SortedMap
sealed trait Type {
@ -282,7 +282,8 @@ object CollectionType {
.map[Type] {
case StreamType(el) => ArrayType(el)
case dt: DataType => dt
}.reduceLeftOption(_ `∩` _)
}
.reduceLeftOption(_ `∩` _)
.map {
// In case we mix values of uncomparable types, intersection returns bottom, meaning "uninhabited type".
// But we want to get to TopType instead: this would mean that intersection is empty, and you cannot
@ -516,4 +517,48 @@ object Type {
given PartialOrder[Type] =
CompareTypes.partialOrder
given Show[DataType] = {
case LiteralType.signed =>
"i32"
case LiteralType.unsigned =>
"u32"
case LiteralType.number =>
"u32"
case LiteralType.float =>
"f32"
case LiteralType.string =>
"string"
case LiteralType.bool =>
"bool"
case t =>
t.toString
}
// pretty print for Type
given Show[Type] = {
case ArrayType(el) =>
s"[]${el.show}"
case OptionType(el) =>
s"?${el.show}"
case StreamType(el) =>
s"*${el.show}"
case ArrowType(domain, codomain) =>
val domainStr = domain match {
case _: LabeledConsType =>
domain.toLabelledList().map { case (s, t) => s"$s: ${t.show}" }.mkString("(", ", ", ")")
case _ => domain.toList.mkString("(", ", ", ")")
}
val codomainStr = codomain.toList match {
case Nil => ""
case l => " -> " + l.mkString(", ")
}
domainStr + codomainStr
case nt: NamedType =>
s"${nt.fullName}(${nt.fields.map(_.show).toNel.toList.map(kv => kv._1 + ": " + kv._2).mkString(", ")})"
case t: DataType =>
t.show
case t =>
t.toString
}
}

View File

@ -0,0 +1,19 @@
package aqua.helpers.syntax
import scala.annotation.tailrec
object list {
extension[A] (l: List[A]) {
def updateFirst[B >: A](p: A => Boolean, f: A => B): List[B] = {
@tailrec
def update(left: List[B], right: List[A]): List[B] =
right match {
case a :: tail if p(a) => left.reverse ::: f(a) :: tail
case a :: tail => update(a :: left, tail)
case Nil => left.reverse
}
update(Nil, l)
}
}
}

View File

@ -1,13 +1,9 @@
package aqua.helpers
package aqua.helpers.tree
import cats.data.Chain
import cats.free.Cofree
import cats.Traverse
import cats.Show
import cats.Eval
import cats.syntax.show.*
import cats.syntax.traverse.*
import cats.syntax.foldable.*
import cats.syntax.show.*
import cats.{Eval, Show, Traverse}
object Tree {