diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala index c2f6fca7b4c6..3b64a3bf6447 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/search/SuggestionsHandler.scala @@ -1,7 +1,7 @@ package org.enso.languageserver.search import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} -import akka.pattern.pipe +import akka.pattern.{ask, pipe} import org.enso.languageserver.capability.CapabilityProtocol.{ AcquireCapability, CapabilityAcquired, @@ -26,6 +26,7 @@ import org.enso.languageserver.session.SessionRouter.DeliverToJsonController import org.enso.languageserver.util.UnhandledLogging import org.enso.pkg.PackageManager import org.enso.polyglot.Suggestion +import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.runtime.Runtime.Api import org.enso.searcher.data.QueryResult import org.enso.searcher.{FileVersionsRepo, SuggestionsRepo} @@ -98,6 +99,10 @@ final class SuggestionsHandler( context.system.eventStream .subscribe(self, InitializedEvent.SuggestionsRepoInitialized.getClass) + runtimeConnector + .ask(Api.Request(Api.GetTypeGraphRequest()))(timeout, self) + .pipeTo(self) + config.contentRoots.foreach { case (_, contentRoot) => PackageManager.Default .fromDirectory(contentRoot) @@ -122,23 +127,29 @@ final class SuggestionsHandler( Some(InitializedEvent.SuggestionsRepoInitialized) ) ) + case Api.GetTypeGraphResponse(g) => + tryInitialize(init.copy(typeGraph = Some(g))) case _ => stash() } - def initialized(projectName: String, clients: Set[ClientId]): Receive = { + def initialized( + projectName: String, + graph: TypeGraph, + clients: Set[ClientId] + ): Receive = { case AcquireCapability( client, CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates()) ) => sender() ! CapabilityAcquired - context.become(initialized(projectName, clients + client.clientId)) + context.become(initialized(projectName, graph, clients + client.clientId)) case ReleaseCapability( client, CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates()) ) => sender() ! CapabilityReleased - context.become(initialized(projectName, clients - client.clientId)) + context.become(initialized(projectName, graph, clients - client.clientId)) case msg: Api.SuggestionsDatabaseModuleUpdateNotification => val isVersionChanged = @@ -216,6 +227,8 @@ final class SuggestionsHandler( .pipeTo(sender()) case Completion(path, pos, selfType, returnType, tags) => + val selfTypes = + selfType.toList.flatMap(ty => (graph.getParents(ty) + ty).toSeq) getModuleName(projectName, path) .fold( Future.successful, @@ -223,7 +236,7 @@ final class SuggestionsHandler( suggestionsRepo .search( Some(module), - selfType, + selfTypes, returnType, tags.map(_.map(SuggestionKind.toSuggestion)), Some(toPosition(pos)) @@ -295,7 +308,7 @@ final class SuggestionsHandler( .pipeTo(self) case ProjectNameUpdated(name) => - context.become(initialized(name, clients)) + context.become(initialized(name, graph, clients)) } /** Transition the initialization process. @@ -303,10 +316,11 @@ final class SuggestionsHandler( * @param state current initialization state */ private def tryInitialize(state: SuggestionsHandler.Initialization): Unit = { - state.initialized.fold(context.become(initializing(state))) { name => - log.debug("Initialized") - context.become(initialized(name, Set())) - unstashAll() + state.initialized.fold(context.become(initializing(state))) { + case (name, graph) => + log.debug("Initialized") + context.become(initialized(name, graph, Set())) + unstashAll() } } @@ -443,21 +457,25 @@ object SuggestionsHandler { * * @param project the project name * @param suggestions the initialization event of the suggestions repo + * @param typeGraph the Enso type hierarchy */ private case class Initialization( - project: Option[String] = None, - suggestions: Option[InitializedEvent.SuggestionsRepoInitialized.type] = None + project: Option[String] = None, + suggestions: Option[InitializedEvent.SuggestionsRepoInitialized.type] = + None, + typeGraph: Option[TypeGraph] = None ) { /** Check if all the components are initialized. * * @return the project name */ - def initialized: Option[String] = + def initialized: Option[(String, TypeGraph)] = for { - _ <- suggestions - name <- project - } yield name + _ <- suggestions + name <- project + graph <- typeGraph + } yield (name, graph) } /** Creates a configuration object used to create a [[SuggestionsHandler]]. @@ -484,5 +502,4 @@ object SuggestionsHandler { runtimeConnector ) ) - } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/search/Suggestions.scala b/engine/language-server/src/test/scala/org/enso/languageserver/search/Suggestions.scala index 11629404f2b5..f10c27efe84e 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/search/Suggestions.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/search/Suggestions.scala @@ -7,57 +7,95 @@ import org.enso.polyglot.Suggestion /** Suggestion instances used in tests. */ object Suggestions { - val atom: Suggestion.Atom = - Suggestion.Atom( - externalId = None, - module = "Test.Main", - name = "MyType", - arguments = Vector(Suggestion.Argument("a", "Any", false, false, None)), - returnType = "MyAtom", - documentation = None - ) - - val method: Suggestion.Method = - Suggestion.Method( - externalId = - Some(UUID.fromString("ea9d7734-26a7-4f65-9dd9-c648eaf57d63")), - module = "Test.Main", - name = "foo", - arguments = Vector( - Suggestion.Argument("this", "MyType", false, false, None), - Suggestion.Argument("foo", "Number", false, true, Some("42")) - ), - selfType = "MyType", - returnType = "Number", - documentation = Some("Lovely") - ) - - val function: Suggestion.Function = - Suggestion.Function( - externalId = - Some(UUID.fromString("78d452ce-ed48-48f1-b4f2-b7f45f8dff89")), - module = "Test.Main", - name = "print", - arguments = Vector( - Suggestion.Argument("a", "Any", false, false, None), - Suggestion.Argument("b", "Any", true, false, None), - Suggestion.Argument("c", "Any", false, true, Some("C")) - ), - returnType = "IO", - scope = - Suggestion.Scope(Suggestion.Position(1, 9), Suggestion.Position(1, 22)) - ) - - val local: Suggestion.Local = - Suggestion.Local( - externalId = - Some(UUID.fromString("dc077227-d9b6-4620-9b51-792c2a69419d")), - module = "Test.Main", - name = "x", - returnType = "Number", - scope = - Suggestion.Scope(Suggestion.Position(21, 0), Suggestion.Position(89, 0)) - ) - - val all = Seq(atom, method, function, local) + val atom: Suggestion.Atom = Suggestion.Atom( + externalId = None, + module = "Test.Main", + name = "MyType", + arguments = Vector(Suggestion.Argument("a", "Any", false, false, None)), + returnType = "MyAtom", + documentation = None + ) + + val method: Suggestion.Method = Suggestion.Method( + externalId = Some(UUID.fromString("ea9d7734-26a7-4f65-9dd9-c648eaf57d63")), + module = "Test.Main", + name = "foo", + arguments = Vector( + Suggestion.Argument("this", "MyType", false, false, None), + Suggestion.Argument("foo", "Number", false, true, Some("42")) + ), + selfType = "MyType", + returnType = "Number", + documentation = Some("Lovely") + ) + + val function: Suggestion.Function = Suggestion.Function( + externalId = Some(UUID.fromString("78d452ce-ed48-48f1-b4f2-b7f45f8dff89")), + module = "Test.Main", + name = "print", + arguments = Vector( + Suggestion.Argument("a", "Any", false, false, None), + Suggestion.Argument("b", "Any", true, false, None), + Suggestion.Argument("c", "Any", false, true, Some("C")) + ), + returnType = "IO", + scope = + Suggestion.Scope(Suggestion.Position(1, 9), Suggestion.Position(1, 22)) + ) + + val local: Suggestion.Local = Suggestion.Local( + externalId = Some(UUID.fromString("dc077227-d9b6-4620-9b51-792c2a69419d")), + module = "Test.Main", + name = "x", + returnType = "Number", + scope = + Suggestion.Scope(Suggestion.Position(21, 0), Suggestion.Position(89, 0)) + ) + + val methodOnAny: Suggestion.Method = Suggestion.Method( + externalId = Some(UUID.fromString("6cfe1538-5df7-42e4-bf55-64f8ac2ededa")), + module = "Standard.Base.Data.Any.Extensions", + name = "<<", + arguments = Vector( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("that", "Any", false, false, None) + ), + selfType = "Any", + returnType = "Any", + documentation = Some("Lovely") + ) + + val methodOnNumber: Suggestion.Method = Suggestion.Method( + externalId = Some(UUID.fromString("33b426aa-2f74-42c0-9032-1159b5386eac")), + module = "Standard.Base.Data.Number.Extensions", + name = "asin", + arguments = Vector( + Suggestion.Argument("this", "Number", false, false, None) + ), + selfType = "Number", + returnType = "Number", + documentation = None + ) + + val methodOnInteger: Suggestion.Method = Suggestion.Method( + externalId = Some(UUID.fromString("2849c0f0-3c27-44df-abcb-5c163dd7ac91")), + module = "Builtins.Main", + name = "+", + arguments = Vector( + Suggestion.Argument("that", "Number", false, false, None) + ), + selfType = "Integer", + returnType = "Number", + documentation = Some("Blah, blah") + ) + + val all = Seq( + atom, + method, + function, + local, + methodOnAny, + methodOnNumber, + methodOnInteger + ) } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/search/SuggestionsHandlerSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/search/SuggestionsHandlerSpec.scala index 9cbd0a9304cf..a0a5cc92ef7c 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/search/SuggestionsHandlerSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/search/SuggestionsHandlerSpec.scala @@ -3,7 +3,6 @@ package org.enso.languageserver.search import java.io.File import java.nio.file.Files import java.util.UUID - import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{ImplicitSender, TestKit, TestProbe} import org.apache.commons.io.FileUtils @@ -17,7 +16,7 @@ import org.enso.languageserver.filemanager.Path import org.enso.languageserver.search.SearchProtocol.SuggestionDatabaseEntry import org.enso.languageserver.session.JsonSession import org.enso.languageserver.session.SessionRouter.DeliverToJsonController -import org.enso.polyglot.data.Tree +import org.enso.polyglot.data.{Tree, TypeGraph} import org.enso.polyglot.runtime.Runtime.Api import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo} import org.enso.searcher.{FileVersionsRepo, SuggestionsRepo} @@ -95,7 +94,10 @@ class SuggestionsHandlerSpec router.expectMsg( DeliverToJsonController( clientId, - SearchProtocol.SuggestionsDatabaseUpdateNotification(4L, updates) + SearchProtocol.SuggestionsDatabaseUpdateNotification( + Suggestions.all.size.toLong, + updates + ) ) ) @@ -154,7 +156,7 @@ class SuggestionsHandlerSpec DeliverToJsonController( clientId, SearchProtocol.SuggestionsDatabaseUpdateNotification( - 8L, + Suggestions.all.size * 2L, updatesAdd ++ updatesRemove ) ) @@ -343,6 +345,13 @@ class SuggestionsHandlerSpec ) expectMsg(CapabilityAcquired) + val suggestions = Seq( + Suggestions.atom, + Suggestions.method, + Suggestions.function, + Suggestions.local + ) + val tree1 = Tree.Root( Vector( Tree.Node( @@ -410,7 +419,7 @@ class SuggestionsHandlerSpec Tree.Root(Vector()) ) - val updates2 = Suggestions.all.zipWithIndex.map { case (_, ix) => + val updates2 = suggestions.zipWithIndex.map { case (_, ix) => SearchProtocol.SuggestionsDatabaseUpdate.Remove(ix + 1L) } router.expectMsg( @@ -468,6 +477,15 @@ class SuggestionsHandlerSpec Await.ready(repo.insert(Suggestions.atom), Timeout) handler ! SearchProtocol.InvalidateSuggestionsDatabase + connector.expectMsgClass(classOf[Api.Request]) match { + case Api.Request(_, Api.GetTypeGraphRequest()) => + case Api.Request(_, msg) => + fail(s"Runtime connector receive unexpected message: $msg") + } + connector.reply( + Api.Response(Api.GetTypeGraphResponse(buildTestTypeGraph)) + ) + connector.expectMsgClass(classOf[Api.Request]) match { case Api.Request(_, Api.InvalidateModulesIndexRequest()) => case Api.Request(_, msg) => @@ -492,15 +510,21 @@ class SuggestionsHandlerSpec expectMsg( SearchProtocol.CompletionResult( - 4L, - Seq(inserted(0).get, inserted(1).get) + 7L, + Seq( + inserted(0).get, + inserted(6).get, + inserted(4).get, + inserted(5).get, + inserted(1).get + ) ) ) } "search entries by self type" taggedAs Retry in withDb { (config, repo, _, _, handler) => - val (_, Seq(_, methodId, _, _)) = + val (_, Seq(_, methodId, _, _, methodOnAnyId, _, _)) = Await.result(repo.insertAll(Suggestions.all), Timeout) handler ! SearchProtocol.Completion( file = mkModulePath(config, "Main.enso"), @@ -510,12 +534,54 @@ class SuggestionsHandlerSpec tags = None ) - expectMsg(SearchProtocol.CompletionResult(4L, Seq(methodId).flatten)) + expectMsg( + SearchProtocol.CompletionResult( + 7L, + Seq(methodOnAnyId, methodId).flatten + ) + ) + } + + "search entries based on supertypes of self" taggedAs Retry in withDb { + (config, repo, _, _, handler) => + val (_, Seq(_, _, _, _, anyMethodId, numberMethodId, integerMethodId)) = + Await.result(repo.insertAll(Suggestions.all), Timeout) + + handler ! SearchProtocol.Completion( + file = mkModulePath(config, "Main.enso"), + position = Position(0, 0), + selfType = Some("Integer"), + returnType = None, + tags = None + ) + + expectMsg( + SearchProtocol.CompletionResult( + 7L, + Seq(anyMethodId, integerMethodId, numberMethodId).flatten + ) + ) + } + + "search entries for any" taggedAs Retry in withDb { + (config, repo, _, _, handler) => + val (_, Seq(_, _, _, _, anyMethodId, _, _)) = + Await.result(repo.insertAll(Suggestions.all), Timeout) + + handler ! SearchProtocol.Completion( + file = mkModulePath(config, "Main.enso"), + position = Position(0, 0), + selfType = Some("Any"), + returnType = None, + tags = None + ) + + expectMsg(SearchProtocol.CompletionResult(7L, Seq(anyMethodId).flatten)) } "search entries by return type" taggedAs Retry in withDb { (config, repo, _, _, handler) => - val (_, Seq(_, _, functionId, _)) = + val (_, Seq(_, _, functionId, _, _, _, _)) = Await.result(repo.insertAll(Suggestions.all), Timeout) handler ! SearchProtocol.Completion( file = mkModulePath(config, "Main.enso"), @@ -525,12 +591,12 @@ class SuggestionsHandlerSpec tags = None ) - expectMsg(SearchProtocol.CompletionResult(4L, Seq(functionId).flatten)) + expectMsg(SearchProtocol.CompletionResult(7L, Seq(functionId).flatten)) } "search entries by tags" taggedAs Retry in withDb { (config, repo, _, _, handler) => - val (_, Seq(_, _, _, localId)) = + val (_, Seq(_, _, _, localId, _, _, _)) = Await.result(repo.insertAll(Suggestions.all), Timeout) handler ! SearchProtocol.Completion( file = mkModulePath(config, "Main.enso"), @@ -540,7 +606,7 @@ class SuggestionsHandlerSpec tags = Some(Seq(SearchProtocol.SuggestionKind.Local)) ) - expectMsg(SearchProtocol.CompletionResult(4L, Seq(localId).flatten)) + expectMsg(SearchProtocol.CompletionResult(7L, Seq(localId).flatten)) } } @@ -562,9 +628,18 @@ class SuggestionsHandlerSpec ) ) handler ! SuggestionsHandler.ProjectNameUpdated("Test") + handler ! Api.GetTypeGraphResponse(buildTestTypeGraph) handler } + def buildTestTypeGraph: TypeGraph = { + val graph = TypeGraph("Any") + graph.insert("Number", "Any") + graph.insert("Integer", "Number") + + graph + } + def newConfig(root: File): Config = { Config( Map(UUID.randomUUID() -> root), diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index 437ca73da7e0..e05800233b39 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -30,6 +30,8 @@ import org.enso.languageserver.runtime.ContextRegistry import org.enso.languageserver.search.SuggestionsHandler import org.enso.languageserver.session.SessionRouter import org.enso.languageserver.text.BufferRegistry +import org.enso.polyglot.data.TypeGraph +import org.enso.polyglot.runtime.Runtime.Api import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo} import org.enso.text.Sha3_224VersionCalculator @@ -48,6 +50,13 @@ class BaseServerTest extends JsonRpcServerTestKit { val runtimeConnectorProbe = TestProbe() val versionCalculator = Sha3_224VersionCalculator + val typeGraph: TypeGraph = { + val graph = TypeGraph("Any") + graph.insert("Number", "Any") + graph.insert("Integer", "Number") + graph + } + sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile)) def mkConfig: Config = @@ -146,6 +155,8 @@ class BaseServerTest extends JsonRpcServerTestKit { ) // initialize + runtimeConnectorProbe.receiveN(1) + suggestionsHandler ! Api.GetTypeGraphResponse(typeGraph) Await.ready(initializationComponent.init(), timeout) new JsonConnectionControllerFactory( diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/data/TypeGraph.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/data/TypeGraph.scala new file mode 100644 index 000000000000..e80cb9bdb076 --- /dev/null +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/data/TypeGraph.scala @@ -0,0 +1,86 @@ +package org.enso.polyglot.data + +import com.fasterxml.jackson.annotation.JsonIgnore + +import scala.collection.mutable + +/** A collection that represents subsumption relationships between types. + * + * These relationships are useful for dispatch and for collating suggestions + * for users. All names in this collection should be represented fully + * qualified. + * + * This structure is a graph and may contain loops. The query functions will + * ensure that an infinite loop doesn't occur in such circumstances. + * + * This collection does not implement the Any : Any axiom as that is currently + * not encoded anywhere in the language. + * + * @param defaultRootType the name of the root type of the type subsumption + * hierarchy + * @param parentLinks the parent links for the types + */ +case class TypeGraph( + defaultRootType: String, + parentLinks: mutable.HashMap[String, Set[String]] = mutable.HashMap() +) { + insertWithoutParent(defaultRootType) + + /** Inserts a type without a parent into the graph. + * + * @param name the fully-qualified typename + */ + @JsonIgnore + def insertWithoutParent(name: String): Unit = { + parentLinks.update(name, Set()) + } + + /** Insert a type-parent relationship into the graph. + * + * @param typeName the fully-qualified name of the type to set the parent for + * @param parentName the fully-qualified name of the parent of `typeName` + */ + @JsonIgnore + def insert(typeName: String, parentName: String): Unit = { + parentLinks.updateWith(typeName) { + case Some(parents) => Some(parents + parentName) + case None => Some(Set(parentName)) + } + } + + /** Get the direct parents of the provided typename. + * + * The direct parents of a type are those that it is a child of + * non-transitively. + * + * @param typeName the fully-qualified name of the type to get the direct + * parents for + * @return the set of direct parents for `typeName` + */ + @JsonIgnore + def getDirectParents(typeName: String): Set[String] = { + parentLinks.getOrElse(typeName, Set(defaultRootType)) + } + + /** Get all of the parents (transitively) of the provided typename. + * + * @param typeName the fully-qualified type name for which to get the parents + * @return all parents of `typeName` + */ + @JsonIgnore + def getParents(typeName: String): Set[String] = { + var seenNodes: Set[String] = Set() + + val parents = getDirectParents(typeName) + parents ++ parents.flatMap(parent => { + if (!seenNodes.contains(parent)) { + seenNodes += parent + getParents(parent) + } else { Set() } + }) + } +} +object TypeGraph { + def fromJava(rootTypeName: String): TypeGraph = + new TypeGraph(rootTypeName) +} diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index a2c2edee2d75..aee247848379 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -11,7 +11,7 @@ import com.fasterxml.jackson.module.scala.{ ScalaObjectMapper } import org.enso.polyglot.Suggestion -import org.enso.polyglot.data.Tree +import org.enso.polyglot.data.{Tree, TypeGraph} import org.enso.text.ContentVersion import org.enso.text.editing.model import org.enso.text.editing.model.{Range, TextEdit} @@ -188,6 +188,14 @@ object Runtime { new JsonSubTypes.Type( value = classOf[Api.ImportSuggestionResponse], name = "importSuggestionResponse" + ), + new JsonSubTypes.Type( + value = classOf[Api.GetTypeGraphRequest], + name = "getTypeGraphRequest" + ), + new JsonSubTypes.Type( + value = classOf[Api.GetTypeGraphResponse], + name = "getTypeGraphResponse" ) ) ) @@ -1052,6 +1060,15 @@ object Runtime { exports: Seq[Export] ) extends ApiResponse + /** A request for the type hierarchy graph. */ + case class GetTypeGraphRequest() extends ApiRequest + + /** The result of the type graph request. + * + * @param graph the graph. + */ + case class GetTypeGraphResponse(graph: TypeGraph) extends ApiResponse + private lazy val mapper = { val factory = new CBORFactory() val mapper = new ObjectMapper(factory) with ScalaObjectMapper diff --git a/engine/polyglot-api/src/test/scala/org/enso/polyglot/data/TypeGraphTest.scala b/engine/polyglot-api/src/test/scala/org/enso/polyglot/data/TypeGraphTest.scala new file mode 100644 index 000000000000..cd2ec5e6f303 --- /dev/null +++ b/engine/polyglot-api/src/test/scala/org/enso/polyglot/data/TypeGraphTest.scala @@ -0,0 +1,66 @@ +package org.enso.polyglot.data + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class TypeGraphTest extends AnyWordSpec with Matchers { + + "The type graph" should { + "be able to insert links" in { + val graph = new TypeGraph("Builtins.Main.Any") + graph.insert("Builtins.Main.Number", "Builtins.Main.Any") + graph.insert("Builtins.Main.Decimal", "Builtins.Main.Number") + graph.insert("Builtins.Main.Integer", "Builtins.Main.Number") + } + + "be able to query direct parents" in { + val graph = new TypeGraph("Builtins.Main.Any") + graph.insert("Builtins.Main.Number", "Builtins.Main.Any") + graph.insert("Builtins.Main.Decimal", "Builtins.Main.Number") + graph.insert("Builtins.Main.Integer", "Builtins.Main.Number") + + graph.getDirectParents("Builtins.Main.Decimal") shouldEqual Set( + "Builtins.Main.Number" + ) + graph.getDirectParents("Builtins.Main.Integer") shouldEqual Set( + "Builtins.Main.Number" + ) + graph.getDirectParents("Builtins.Main.Number") shouldEqual Set( + "Builtins.Main.Any" + ) + graph.getDirectParents("Builtins.Main.Any") shouldBe empty + } + + "be able to query all parents" in { + val graph = new TypeGraph("Builtins.Main.Any") + graph.insert("Builtins.Main.Number", "Builtins.Main.Any") + graph.insert("Builtins.Main.Decimal", "Builtins.Main.Number") + graph.insert("Builtins.Main.Integer", "Builtins.Main.Number") + + graph.getParents("Builtins.Main.Any") shouldEqual Set() + graph.getParents("Builtins.Main.Number") shouldEqual Set( + "Builtins.Main.Any" + ) + graph.getParents("Builtins.Main.Integer") shouldEqual Set( + "Builtins.Main.Number", + "Builtins.Main.Any" + ) + graph.getParents("Builtins.Main.Decimal") shouldEqual Set( + "Builtins.Main.Number", + "Builtins.Main.Any" + ) + } + + "have a fallback parent for any typename" in { + val graph = new TypeGraph("Builtins.Main.Any") + graph.insert("Builtins.Main.Number", "Builtins.Main.Any") + graph.insert("Builtins.Main.Decimal", "Builtins.Main.Number") + graph.insert("Builtins.Main.Integer", "Builtins.Main.Number") + + graph.getParents("My_User_Type") shouldEqual Set("Builtins.Main.Any") + graph.getParents("Standard.Base.Vector") shouldEqual Set( + "Builtins.Main.Any" + ) + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java index ef7cf6a5b512..f125ea8a810e 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java @@ -17,6 +17,8 @@ import org.enso.interpreter.runtime.error.PanicSentinel; import org.enso.interpreter.runtime.number.EnsoBigInteger; import org.enso.interpreter.runtime.scope.ModuleScope; +import org.enso.polyglot.data.TypeGraph; +import org.yaml.snakeyaml.scanner.Constant; /** * This class defines the interpreter-level type system for Enso. @@ -49,6 +51,8 @@ }) public class Types { + private static TypeGraph typeHierarchy = buildTypeHierarchy(); + /** * A simple pair type * @@ -195,4 +199,30 @@ public static Pair extractArguments(Object[] arguments, Class cl } return new Pair<>((A) arguments[0], (B) arguments[1]); } + + /** @return the language type hierarchy */ + public static TypeGraph getTypeHierarchy() { + return typeHierarchy; + } + + private static TypeGraph buildTypeHierarchy() { + TypeGraph graph = TypeGraph.fromJava(Constants.ANY); + + graph.insert(Constants.ARRAY, Constants.ANY); + graph.insert(Constants.BOOLEAN, Constants.ANY); + graph.insert(Constants.DECIMAL, Constants.NUMBER); + graph.insert(Constants.ERROR, Constants.ANY); + graph.insert(Constants.FUNCTION, Constants.ANY); + graph.insert(Constants.INTEGER, Constants.NUMBER); + graph.insert(Constants.MANAGED_RESOURCE, Constants.ANY); + graph.insert(Constants.NOTHING, Constants.ANY); + graph.insert(Constants.PANIC, Constants.ANY); + graph.insert(Constants.REF, Constants.ANY); + graph.insert(Constants.TEXT, Constants.ANY); + graph.insertWithoutParent(Constants.PANIC); + graph.insertWithoutParent(Constants.THUNK); + graph.insertWithoutParent(Constants.UNRESOLVED_SYMBOL); + + return graph; + } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala index d0d85066958c..f8f025e1f36a 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/CommandFactory.scala @@ -54,6 +54,9 @@ object CommandFactory { throw new IllegalArgumentException( "ShutDownRuntimeServer request is not convertible to command object" ) + + case _: Api.GetTypeGraphRequest => + new GetTypeGraphCommand(request.requestId) } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/GetTypeGraphCommand.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/GetTypeGraphCommand.scala new file mode 100644 index 000000000000..3b192f9e524d --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/command/GetTypeGraphCommand.scala @@ -0,0 +1,21 @@ +package org.enso.interpreter.instrument.command + +import org.enso.interpreter.instrument.execution.RuntimeContext +import org.enso.interpreter.runtime.`type`.Types +import org.enso.polyglot.runtime.Runtime.Api +import org.enso.polyglot.runtime.Runtime.Api.RequestId + +import scala.concurrent.{ExecutionContext, Future} + +class GetTypeGraphCommand(maybeRequestId: Option[RequestId]) + extends Command(maybeRequestId) { + + /** @inheritdoc */ + override def execute(implicit + ctx: RuntimeContext, + ec: ExecutionContext + ): Future[Unit] = Future { + val typeGraph = Types.getTypeHierarchy + reply(Api.GetTypeGraphResponse(typeGraph)) + } +} diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index 6238db1a3150..e76a2ff55f0f 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -1,11 +1,12 @@ package org.enso.interpreter.test.instrument import org.enso.interpreter.instrument.execution.Timer -import org.enso.interpreter.runtime.`type`.Constants +import org.enso.interpreter.runtime.`type`.{Constants, Types} import org.enso.interpreter.runtime.{Context => EnsoContext} import org.enso.interpreter.test.Metadata import org.enso.pkg.{Package, PackageManager} import org.enso.polyglot._ +import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.runtime.Runtime.Api import org.enso.text.editing.model import org.enso.text.editing.model.TextEdit @@ -3706,4 +3707,13 @@ class RuntimeServerTest ) } + it should "send the type graph" in { + val requestId = UUID.randomUUID() + val expectedGraph: TypeGraph = Types.getTypeHierarchy + + context.send(Api.Request(requestId, Api.GetTypeGraphRequest())) + context.receive shouldEqual Some( + Api.Response(requestId, Api.GetTypeGraphResponse(expectedGraph)) + ) + } } diff --git a/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java b/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java index 9f2dc6020707..0689f9af1c5b 100644 --- a/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java +++ b/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java @@ -1,5 +1,6 @@ package org.enso.searcher.sql; +import java.util.ArrayList; import org.enso.polyglot.Suggestion; import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.runner.Runner; @@ -18,6 +19,8 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Stream; +import scala.jdk.CollectionConverters; + @BenchmarkMode(Mode.AverageTime) @Fork(1) @Warmup(iterations = 5) @@ -69,35 +72,64 @@ static scala.Option none() { @Benchmark public Object searchBaseline() throws TimeoutException, InterruptedException { - return Await.result(repo.search(none(), none(), none(), none(), none()), TIMEOUT); + return Await.result( + repo.search( + none(), + CollectionConverters.ListHasAsScala(new ArrayList()).asScala().toSeq(), + none(), + none(), + none()), + TIMEOUT); } @Benchmark public Object searchByReturnType() throws TimeoutException, InterruptedException { return Await.result( - repo.search(none(), none(), scala.Some.apply("MyType"), none(), none()), TIMEOUT); + repo.search( + none(), + CollectionConverters.ListHasAsScala(new ArrayList()).asScala().toSeq(), + scala.Some.apply("MyType"), + none(), + none()), + TIMEOUT); } @Benchmark public Object searchBySelfType() throws TimeoutException, InterruptedException { + var selfTypes = new ArrayList(); + selfTypes.add("MyType"); return Await.result( - repo.search(none(), scala.Some.apply("MyType"), none(), none(), none()), TIMEOUT); + repo.search( + none(), + CollectionConverters.ListHasAsScala(selfTypes).asScala().toSeq(), + none(), + none(), + none()), + TIMEOUT); } @Benchmark public Object searchBySelfReturnTypes() throws TimeoutException, InterruptedException { + var selfTypes = new ArrayList(); + selfTypes.add("SelfType"); return Await.result( repo.search( - none(), scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), none(), none()), + none(), + CollectionConverters.ListHasAsScala(selfTypes).asScala().toSeq(), + scala.Some.apply("ReturnType"), + none(), + none()), TIMEOUT); } @Benchmark public Object searchByAll() throws TimeoutException, InterruptedException { + var selfTypes = new ArrayList(); + selfTypes.add("SelfType"); return Await.result( repo.search( none(), - scala.Some.apply("SelfType"), + CollectionConverters.ListHasAsScala(selfTypes).asScala().toSeq(), scala.Some.apply("ReturnType"), scala.Some.apply(kinds), none()), diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala index 92e50f2c650f..68b73de2137a 100644 --- a/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala +++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala @@ -34,7 +34,7 @@ trait SuggestionsRepo[F[_]] { /** Search suggestion by various parameters. * * @param module the module name search parameter - * @param selfType the selfType search parameter + * @param selfType the self types to search for * @param returnType the returnType search parameter * @param kinds the list suggestion kinds to search * @param position the absolute position in the text @@ -42,7 +42,7 @@ trait SuggestionsRepo[F[_]] { */ def search( module: Option[String], - selfType: Option[String], + selfType: Seq[String], returnType: Option[String], kinds: Option[Seq[Suggestion.Kind]], position: Option[Suggestion.Position] diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala index 70b554245f30..762231d6f6ee 100644 --- a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala +++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala @@ -56,7 +56,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext) /** @inheritdoc */ override def search( module: Option[String], - selfType: Option[String], + selfType: Seq[String], returnType: Option[String], kinds: Option[Seq[Suggestion.Kind]], position: Option[Suggestion.Position] @@ -219,7 +219,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext) */ private def searchQuery( module: Option[String], - selfType: Option[String], + selfType: Seq[String], returnType: Option[String], kinds: Option[Seq[Suggestion.Kind]], position: Option[Suggestion.Position] @@ -656,7 +656,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext) * global symbols (atoms and method). * * @param module the module name search parameter - * @param selfType the selfType search parameter + * @param selfTypes the selfType search parameter * @param returnType the returnType search parameter * @param kinds the list suggestion kinds to search * @param position the absolute position in the text @@ -664,7 +664,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext) */ private def searchQueryBuilder( module: Option[String], - selfType: Option[String], + selfTypes: Seq[String], returnType: Option[String], kinds: Option[Seq[Suggestion.Kind]], position: Option[Suggestion.Position] @@ -673,9 +673,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext) .filterOpt(module) { case (row, value) => row.scopeStartLine === ScopeColumn.EMPTY || row.module === value } - .filterOpt(selfType) { case (row, value) => - row.selfType === value - } + .filterIf(selfTypes.nonEmpty) {row => row.selfType.inSet(selfTypes) } .filterOpt(returnType) { case (row, value) => row.returnType === value } diff --git a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala index 538baa4014b6..464394cd9ce8 100644 --- a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala +++ b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala @@ -718,7 +718,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(None, None, None, None, None) + res <- repo.search(None, Seq(), None, None, None) } yield res._2 val res = Await.result(action, Timeout) @@ -731,7 +731,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id2 <- repo.insert(suggestion.method) id3 <- repo.insert(suggestion.function) id4 <- repo.insert(suggestion.local) - res <- repo.search(Some("Test.Main"), None, None, None, None) + res <- repo.search(Some("Test.Main"), Seq(), None, None, None) } yield (id1, id2, id3, id4, res._2) val (id1, id2, id3, id4, res) = Await.result(action, Timeout) @@ -744,7 +744,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id2 <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(Some(""), None, None, None, None) + res <- repo.search(Some(""), Seq(), None, None, None) } yield (res._2, Seq(id1, id2)) val (res, globals) = Await.result(action, Timeout) @@ -757,7 +757,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id2 <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(None, Some("Main"), None, None, None) + res <- repo.search(None, Seq("Main"), None, None, None) } yield (id2, res._2) val (id, res) = Await.result(action, Timeout) @@ -770,7 +770,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) id3 <- repo.insert(suggestion.function) id4 <- repo.insert(suggestion.local) - res <- repo.search(None, None, Some("MyType"), None, None) + res <- repo.search(None, Seq(), Some("MyType"), None, None) } yield (id3, id4, res._2) val (id1, id2, res) = Await.result(action, Timeout) @@ -784,7 +784,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) id4 <- repo.insert(suggestion.local) - res <- repo.search(None, None, None, Some(kinds), None) + res <- repo.search(None, Seq(), None, Some(kinds), None) } yield (id1, id4, res._2) val (id1, id2, res) = Await.result(action, Timeout) @@ -797,7 +797,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(None, None, None, Some(Seq()), None) + res <- repo.search(None, Seq(), None, Some(Seq()), None) } yield res._2 val res = Await.result(action, Timeout) @@ -811,7 +811,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) res <- - repo.search(None, None, None, None, Some(Suggestion.Position(99, 42))) + repo.search(None, Seq(), None, None, Some(Suggestion.Position(99, 42))) } yield (id1, id2, res._2) val (id1, id2, res) = Await.result(action, Timeout) @@ -825,7 +825,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id3 <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) res <- - repo.search(None, None, None, None, Some(Suggestion.Position(1, 5))) + repo.search(None, Seq(), None, None, Some(Suggestion.Position(1, 5))) } yield (id1, id2, id3, res._2) val (id1, id2, id3, res) = Await.result(action, Timeout) @@ -839,7 +839,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id2 <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(Some("Test.Main"), Some("Main"), None, None, None) + res <- repo.search(Some("Test.Main"), Seq("Main"), None, None, None) } yield (id2, res._2) val (id, res) = Await.result(action, Timeout) @@ -854,7 +854,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) id4 <- repo.insert(suggestion.local) - res <- repo.search(None, None, Some("MyType"), Some(kinds), None) + res <- repo.search(None, Seq(), Some("MyType"), Some(kinds), None) } yield (id4, res._2) val (id, res) = Await.result(action, Timeout) @@ -870,7 +870,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id4 <- repo.insert(suggestion.local) res <- repo.search( None, - None, + Seq(), Some("MyType"), None, Some(Suggestion.Position(42, 0)) @@ -890,7 +890,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.local) res <- repo.search( None, - None, + Seq(), None, Some(kinds), Some(Suggestion.Position(99, 1)) @@ -908,7 +908,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.method) _ <- repo.insert(suggestion.function) _ <- repo.insert(suggestion.local) - res <- repo.search(None, Some("Main"), Some("MyType"), None, None) + res <- repo.search(None, Seq("Main"), Some("MyType"), None, None) } yield res._2 val res = Await.result(action, Timeout) @@ -925,7 +925,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id4 <- repo.insert(suggestion.local) res <- repo.search( Some("Test.Main"), - None, + Seq(), Some("MyType"), Some(kinds), None @@ -946,7 +946,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { id4 <- repo.insert(suggestion.local) res <- repo.search( None, - None, + Seq(), Some("MyType"), Some(kinds), Some(Suggestion.Position(42, 0)) @@ -970,7 +970,7 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec { _ <- repo.insert(suggestion.local) res <- repo.search( Some("Test.Main"), - Some("Main"), + Seq("Main"), Some("MyType"), Some(kinds), Some(Suggestion.Position(42, 0))