diff --git a/build.sbt b/build.sbt index f0f6d0f99dd..6c11991bf58 100644 --- a/build.sbt +++ b/build.sbt @@ -570,6 +570,17 @@ lazy val `core-definition` = (project in file("lib/core-definition")) .dependsOn(graph) .dependsOn(syntax.jvm) +lazy val searcher = project + .in(file("lib/searcher")) + .configs(Test) + .settings( + libraryDependencies ++= Seq( + "com.typesafe.slick" %% "slick" % "3.3.2", + "org.xerial" % "sqlite-jdbc" % "3.31.1", + "org.scalatest" %% "scalatest" % scalatestVersion % Test, + ) + ) + // ============================================================================ // === Sub-Projects =========================================================== // ============================================================================ @@ -746,6 +757,7 @@ lazy val runtime = (project in file("engine/runtime")) .dependsOn(graph) .dependsOn(`polyglot-api`) .dependsOn(`text-buffer`) + .dependsOn(`searcher`) /* Note [Unmanaged Classpath] * ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala b/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala new file mode 100644 index 00000000000..dbed355a9d8 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala @@ -0,0 +1,330 @@ +package org.enso.compiler.context + +import org.enso.compiler.core.IR +import org.enso.compiler.pass.resolve.{DocumentationComments, TypeSignatures} +import org.enso.searcher.Suggestion +import org.enso.syntax.text.Location + +import scala.collection.immutable.VectorBuilder +import scala.collection.mutable + +/** Module that extracts [[Suggestion]] entries from the [[IR]]. */ +final class SuggestionBuilder { + + import SuggestionBuilder._ + + /** Build suggestions from the given `ir`. + * + * @param ir the input `IR` + * @return the list of suggestion entries extracted from the given `IR` + */ + def build(ir: IR.Module): Vector[Suggestion] = { + @scala.annotation.tailrec + def go( + scope: Scope, + scopes: mutable.Queue[Scope], + acc: mutable.Builder[Suggestion, Vector[Suggestion]] + ): Vector[Suggestion] = + if (scope.queue.isEmpty) { + if (scopes.isEmpty) { + acc.result() + } else { + val scope = scopes.dequeue() + go(scope, scopes, acc) + } + } else { + val ir = scope.queue.dequeue() + val doc = ir.getMetadata(DocumentationComments).map(_.documentation) + ir match { + case IR.Module.Scope.Definition.Method + .Explicit( + IR.Name.MethodReference(typePtr, methodName, _, _, _), + IR.Function.Lambda(args, body, _, _, _, _), + _, + _, + _ + ) => + val typeSignature = ir.getMetadata(TypeSignatures) + acc += buildMethod(methodName, typePtr, args, doc, typeSignature) + scopes += Scope(body.children, body.location.map(_.location)) + go(scope, scopes, acc) + case IR.Expression.Binding( + name, + IR.Function.Lambda(args, body, _, _, _, _), + _, + _, + _ + ) if name.location.isDefined => + val typeSignature = ir.getMetadata(TypeSignatures) + acc += buildFunction(name, args, scope.location.get, typeSignature) + scopes += Scope(body.children, body.location.map(_.location)) + go(scope, scopes, acc) + case IR.Expression.Binding(name, expr, _, _, _) + if name.location.isDefined => + val typeSignature = ir.getMetadata(TypeSignatures) + acc += buildLocal(name.name, scope.location.get, typeSignature) + scopes += Scope(expr.children, expr.location.map(_.location)) + go(scope, scopes, acc) + case IR.Module.Scope.Definition.Atom(name, arguments, _, _, _) => + acc += buildAtom(name.name, arguments, doc) + go(scope, scopes, acc) + case _ => + go(scope, scopes, acc) + } + } + + go( + Scope(ir.children, ir.location.map(_.location)), + mutable.Queue(), + new VectorBuilder() + ) + } + + private def buildMethod( + name: IR.Name, + typeRef: Seq[IR.Name], + args: Seq[IR.DefinitionArgument], + doc: Option[String], + typeSignature: Option[TypeSignatures.Metadata] + ): Suggestion.Method = { + typeSignature match { + case Some(TypeSignatures.Signature(typeExpr)) => + val selfType = buildSelfType(typeRef) + val typeSig = buildTypeSignature(typeExpr) + val (methodArgs, returnTypeDef) = + buildMethodArguments(args, typeSig, selfType) + Suggestion.Method( + name = name.name, + arguments = methodArgs, + selfType = selfType, + returnType = buildReturnType(returnTypeDef), + documentation = doc + ) + case _ => + Suggestion.Method( + name = name.name, + arguments = args.map(buildArgument), + selfType = buildSelfType(typeRef), + returnType = Any, + documentation = doc + ) + } + } + + private def buildFunction( + name: IR.Name, + args: Seq[IR.DefinitionArgument], + location: Location, + typeSignature: Option[TypeSignatures.Metadata] + ): Suggestion.Function = { + typeSignature match { + case Some(TypeSignatures.Signature(typeExpr)) => + val typeSig = buildTypeSignature(typeExpr) + val (methodArgs, returnTypeDef) = + buildFunctionArguments(args, typeSig) + Suggestion.Function( + name = name.name, + arguments = methodArgs, + returnType = buildReturnType(returnTypeDef), + scope = buildScope(location) + ) + case _ => + Suggestion.Function( + name = name.name, + arguments = args.map(buildArgument), + returnType = Any, + scope = buildScope(location) + ) + } + } + + private def buildLocal( + name: String, + location: Location, + typeSignature: Option[TypeSignatures.Metadata] + ): Suggestion.Local = + typeSignature match { + case Some(TypeSignatures.Signature(tname: IR.Name)) => + Suggestion.Local(name, tname.name, buildScope(location)) + case _ => + Suggestion.Local(name, Any, buildScope(location)) + } + + private def buildAtom( + name: String, + arguments: Seq[IR.DefinitionArgument], + doc: Option[String] + ): Suggestion.Atom = + Suggestion.Atom( + name = name, + arguments = arguments.map(buildArgument), + returnType = name, + documentation = doc + ) + + private def buildTypeSignature(typeExpr: IR.Expression): Vector[TypeArg] = { + @scala.annotation.tailrec + def go(typeExpr: IR.Expression, args: Vector[TypeArg]): Vector[TypeArg] = + typeExpr match { + case IR.Function.Lambda(List(targ), body, _, _, _, _) => + val tdef = TypeArg(targ.name.name, targ.suspended) + go(body, args :+ tdef) + case tname: IR.Name => + args :+ TypeArg(tname.name, isSuspended = false) + case _ => + args + } + + go(typeExpr, Vector()) + } + + private def buildMethodArguments( + vargs: Seq[IR.DefinitionArgument], + targs: Seq[TypeArg], + selfType: String + ): (Seq[Suggestion.Argument], Option[TypeArg]) = { + @scala.annotation.tailrec + def go( + vargs: Seq[IR.DefinitionArgument], + targs: Seq[TypeArg], + acc: Vector[Suggestion.Argument] + ): (Vector[Suggestion.Argument], Option[TypeArg]) = + if (vargs.isEmpty) { + (acc, targs.lastOption) + } else { + vargs match { + case IR.DefinitionArgument.Specified( + name: IR.Name.This, + defaultValue, + suspended, + _, + _, + _ + ) +: vtail => + val thisArg = Suggestion.Argument( + name = name.name, + reprType = selfType, + isSuspended = suspended, + hasDefault = defaultValue.isDefined, + defaultValue = defaultValue.flatMap(buildDefaultValue) + ) + go(vtail, targs, acc :+ thisArg) + case varg +: vtail => + targs match { + case targ +: ttail => + go(vtail, ttail, acc :+ buildTypedArgument(varg, targ)) + case _ => + go(vtail, targs, acc :+ buildArgument(varg)) + } + } + } + + go(vargs, targs, Vector()) + } + + private def buildFunctionArguments( + vargs: Seq[IR.DefinitionArgument], + targs: Seq[TypeArg] + ): (Seq[Suggestion.Argument], Option[TypeArg]) = { + @scala.annotation.tailrec + def go( + vargs: Seq[IR.DefinitionArgument], + targs: Seq[TypeArg], + acc: Vector[Suggestion.Argument] + ): (Seq[Suggestion.Argument], Option[TypeArg]) = + if (vargs.isEmpty) { + (acc, targs.lastOption) + } else { + vargs match { + case varg +: vtail => + targs match { + case targ +: ttail => + go(vtail, ttail, acc :+ buildTypedArgument(varg, targ)) + case _ => + go(vtail, targs, acc :+ buildArgument(varg)) + } + } + } + + go(vargs, targs, Vector()) + } + + private def buildTypedArgument( + varg: IR.DefinitionArgument, + targ: TypeArg + ): Suggestion.Argument = + Suggestion.Argument( + name = varg.name.name, + reprType = targ.name, + isSuspended = targ.isSuspended, + hasDefault = varg.defaultValue.isDefined, + defaultValue = varg.defaultValue.flatMap(buildDefaultValue) + ) + + private def buildArgument(arg: IR.DefinitionArgument): Suggestion.Argument = + Suggestion.Argument( + name = arg.name.name, + reprType = Any, + isSuspended = arg.suspended, + hasDefault = arg.defaultValue.isDefined, + defaultValue = arg.defaultValue.flatMap(buildDefaultValue) + ) + + def buildArgument( + varg: IR.DefinitionArgument, + targ: Option[TypeArg] + ): Suggestion.Argument = + Suggestion.Argument( + name = varg.name.name, + reprType = targ.fold(Any)(_.name), + isSuspended = targ.fold(varg.suspended)(_.isSuspended), + hasDefault = varg.defaultValue.isDefined, + defaultValue = varg.defaultValue.flatMap(buildDefaultValue) + ) + + private def buildReturnType(typeDef: Option[TypeArg]): String = + typeDef match { + case Some(TypeArg(name, _)) => name + case None => Any + } + + private def buildSelfType(ref: Seq[IR.Name]): String = + ref.map(_.name).mkString(".") + + private def buildDefaultValue(expr: IR): Option[String] = + expr match { + case IR.Literal.Number(value, _, _, _) => Some(value) + case IR.Literal.Text(text, _, _, _) => Some(text) + case _ => None + } + + private def buildScope(location: Location): Suggestion.Scope = + Suggestion.Scope(location.start, location.end) +} + +object SuggestionBuilder { + + /** A single level of an `IR`. + * + * @param queue the nodes in the scope + * @param location the scope location + */ + private case class Scope(queue: mutable.Queue[IR], location: Option[Location]) + + private object Scope { + + /** Create new scope from the list of items. */ + def apply(items: Seq[IR], location: Option[Location]): Scope = + new Scope(mutable.Queue(items: _*), location) + } + + /** Type of the argument. + * + * @param name the name of the type + * @param isSuspended is the argument lazy + */ + private case class TypeArg(name: String, isSuspended: Boolean) + + private val Any: String = "Any" + +} diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/context/SuggestionBuilderTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/context/SuggestionBuilderTest.scala new file mode 100644 index 00000000000..b6945190150 --- /dev/null +++ b/engine/runtime/src/test/scala/org/enso/compiler/test/context/SuggestionBuilderTest.scala @@ -0,0 +1,399 @@ +package org.enso.compiler.test.context + +import org.enso.compiler.Passes +import org.enso.compiler.context.{ + FreshNameSupply, + ModuleContext, + SuggestionBuilder +} +import org.enso.compiler.core.IR +import org.enso.compiler.pass.PassManager +import org.enso.compiler.test.CompilerTest +import org.enso.searcher.Suggestion + +class SuggestionBuilderTest extends CompilerTest { + + implicit val passManager: PassManager = new Passes().passManager + + "SuggestionBuilder" should { + + "build method without explicit arguments" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = """foo = 42""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "foo", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = None + ) + ) + } + + "build method with documentation" in { + pending // fix documentation + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """## The foo + |foo = 42""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "foo", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = Some(" The foo") + ) + ) + } + + "build method with arguments" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """foo a b = + | x : Number + | x = a + 1 + | y = b - 2 + | x * y""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "foo", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("a", "Any", false, false, None), + Suggestion.Argument("b", "Any", false, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = None + ), + Suggestion.Local("x", "Number", Suggestion.Scope(9, 62)), + Suggestion.Local("y", "Any", Suggestion.Scope(9, 62)) + ) + } + + "build method with default arguments" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """foo (a = 0) = a + 1""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "foo", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("a", "Any", false, true, Some("0")) + ), + selfType = "here", + returnType = "Any", + documentation = None + ) + ) + } + + "build method with associated type signature" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """ + |MyAtom.bar : Number -> Number -> Number + |MyAtom.bar a b = a + b + |""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "bar", + arguments = Seq( + Suggestion.Argument("this", "MyAtom", false, false, None), + Suggestion.Argument("a", "Number", false, false, None), + Suggestion.Argument("b", "Number", false, false, None) + ), + selfType = "MyAtom", + returnType = "Number", + documentation = None + ) + ) + } + + "build method with lazy arguments" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """foo ~a = a + 1""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "foo", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("a", "Any", true, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = None + ) + ) + } + + "build function" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """main = + | foo a = a + 1 + | foo 42""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "main", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = None + ), + Suggestion.Function( + name = "foo", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None) + ), + returnType = "Any", + scope = Suggestion.Scope(6, 35) + ) + ) + } + + "build function with associated type signature" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """main = + | foo : Number -> Number + | foo a = a + 1 + | foo 42""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Method( + name = "main", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None) + ), + selfType = "here", + returnType = "Any", + documentation = None + ), + Suggestion.Function( + name = "foo", + arguments = Seq( + Suggestion.Argument("a", "Number", false, false, None) + ), + returnType = "Number", + scope = Suggestion.Scope(6, 62) + ) + ) + } + + "build atom simple" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = """type MyType a b""" + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "MyType", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None), + Suggestion.Argument("b", "Any", false, false, None) + ), + returnType = "MyType", + documentation = None + ) + ) + } + + "build atom with documentation" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """## My sweet type + |type MyType a b""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "MyType", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None), + Suggestion.Argument("b", "Any", false, false, None) + ), + returnType = "MyType", + documentation = Some(" My sweet type") + ) + ) + } + + "build type simple" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """type Maybe + | type Nothing + | type Just a""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "Nothing", + arguments = Seq(), + returnType = "Nothing", + documentation = None + ), + Suggestion.Atom( + name = "Just", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None) + ), + returnType = "Just", + documentation = None + ) + ) + } + + "build type with documentation" in { + implicit val moduleContext: ModuleContext = freshModuleContext + + val code = + """## When in doubt + |type Maybe + | ## Nothing here + | type Nothing + | ## Something there + | type Just a""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "Nothing", + arguments = Seq(), + returnType = "Nothing", + documentation = Some(" Nothing here") + ), + Suggestion.Atom( + name = "Just", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None) + ), + returnType = "Just", + documentation = Some(" Something there") + ) + ) + } + + "build type with methods" in { + implicit val moduleContext: ModuleContext = freshModuleContext + val code = + """type Maybe + | type Nothing + | type Just a + | + | map f = case this of + | Just a -> Just (f a) + | Nothing -> Nothing""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "Nothing", + arguments = Seq(), + returnType = "Nothing", + documentation = None + ), + Suggestion.Atom( + name = "Just", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None) + ), + returnType = "Just", + documentation = None + ), + Suggestion.Method( + name = "map", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("f", "Any", false, false, None) + ), + selfType = "Just", + returnType = "Any", + documentation = None + ), + Suggestion.Method( + name = "map", + arguments = Seq( + Suggestion.Argument("this", "Any", false, false, None), + Suggestion.Argument("f", "Any", false, false, None) + ), + selfType = "Nothing", + returnType = "Any", + documentation = None + ) + ) + } + + "build type with methods with type signature" in { + implicit val moduleContext: ModuleContext = freshModuleContext + val code = + """type MyType + | type MyAtom + | + | is_atom : this -> Boolean + | is_atom = true""".stripMargin + val module = code.preprocessModule + + build(module) should contain theSameElementsAs Seq( + Suggestion.Atom( + name = "MyAtom", + arguments = Seq(), + returnType = "MyAtom", + documentation = None + ), + Suggestion.Method( + name = "is_atom", + arguments = Seq( + Suggestion.Argument("this", "MyAtom", false, false, None) + ), + selfType = "MyAtom", + returnType = "Boolean", + documentation = None + ) + ) + } + } + + private def build(ir: IR.Module): Vector[Suggestion] = + new SuggestionBuilder().build(ir) + + private def freshModuleContext: ModuleContext = + ModuleContext(freshNameSupply = Some(new FreshNameSupply)) +} diff --git a/lib/searcher/src/main/resources/application.conf b/lib/searcher/src/main/resources/application.conf new file mode 100644 index 00000000000..25f86080d49 --- /dev/null +++ b/lib/searcher/src/main/resources/application.conf @@ -0,0 +1,8 @@ +searcher { + db { + url = "jdbc:sqlite:searcher.db" + driver = org.sqlite.JDBC + connectionPool = disabled + keepAliveConnection = true + } +} diff --git a/lib/searcher/src/main/scala/org/enso/searcher/Suggestion.scala b/lib/searcher/src/main/scala/org/enso/searcher/Suggestion.scala new file mode 100644 index 00000000000..8970d28c792 --- /dev/null +++ b/lib/searcher/src/main/scala/org/enso/searcher/Suggestion.scala @@ -0,0 +1,81 @@ +package org.enso.searcher + +/** A search suggestion. */ +sealed trait Suggestion +object Suggestion { + + /** An argument of an atom or a function. + * + * @param name the argument name + * @param reprType the type of the argument + * @param isSuspended is the argument lazy + * @param hasDefault does the argument have a default + * @param defaultValue optional default value + */ + case class Argument( + name: String, + reprType: String, + isSuspended: Boolean, + hasDefault: Boolean, + defaultValue: Option[String] + ) + + /** The definition scope. + * @param start the start of the definition scope + * @param end the end of the definition scope + */ + case class Scope(start: Int, end: Int) + + /** A value constructor. + * + * @param name the atom name + * @param arguments the list of arguments + * @param returnType the type of an atom + * @param documentation the documentation string + */ + case class Atom( + name: String, + arguments: Seq[Argument], + returnType: String, + documentation: Option[String] + ) extends Suggestion + + /** A function defined on a type or a module. + * + * @param name the method name + * @param arguments the function arguments + * @param selfType the self type of a method + * @param returnType the return type of a method + * @param documentation the documentation string + */ + case class Method( + name: String, + arguments: Seq[Argument], + selfType: String, + returnType: String, + documentation: Option[String] + ) extends Suggestion + + /** A local function definition. + * + * @param name the function name + * @param arguments the function arguments + * @param returnType the return type of a function + * @param scope the scope where the function is defined + */ + case class Function( + name: String, + arguments: Seq[Argument], + returnType: String, + scope: Scope + ) extends Suggestion + + /** A local value. + * + * @param name the name of a value + * @param returnType the type of a local value + * @param scope the scope where the value is defined + */ + case class Local(name: String, returnType: String, scope: Scope) + extends Suggestion +} diff --git a/lib/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala b/lib/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala new file mode 100644 index 00000000000..c7e049597da --- /dev/null +++ b/lib/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala @@ -0,0 +1,28 @@ +package org.enso.searcher.sql + +import org.enso.searcher.Suggestion + +/** The object for accessing the suggestions database. */ +trait SuggestionsRepo[F[_]] { + + /** Find suggestions by the return type. + * + * @param returnType the return type of a suggestion + * @return the list of suggestions + */ + def findBy(returnType: String): F[Seq[Suggestion]] + + /** Select the suggestion by id. + * + * @param id the id of a suggestion + * @return return the suggestion + */ + def select(id: Long): F[Option[Suggestion]] + + /** Insert the suggestion + * + * @param suggestion the suggestion to insert + * @return the id of an inserted suggestion + */ + def insert(suggestion: Suggestion): F[Long] +} diff --git a/lib/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala b/lib/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala new file mode 100644 index 00000000000..1efdbcfed8d --- /dev/null +++ b/lib/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala @@ -0,0 +1,177 @@ +package org.enso.searcher.sql + +import org.enso.searcher.Suggestion +import slick.jdbc.SQLiteProfile.api._ + +import scala.concurrent.ExecutionContext + +/** The object for accessing the suggestions database. */ +final class SqlSuggestionsRepo(implicit ec: ExecutionContext) + extends SuggestionsRepo[DBIO] { + + /** The query returning the arguments joined with the corresponding + * suggestions. */ + private val joined: Query[ + (Rep[Option[ArgumentsTable]], SuggestionsTable), + (Option[ArgumentRow], SuggestionRow), + Seq + ] = + arguments + .joinRight(suggestions) + .on(_.suggestionId === _.id) + + /** @inheritdoc **/ + override def findBy(returnType: String): DBIO[Seq[Suggestion]] = { + val query = for { + (argument, suggestion) <- joined + if suggestion.returnType === returnType + } yield (argument, suggestion) + query.result.map(joinedToSuggestion) + } + + /** @inheritdoc **/ + override def select(id: Long): DBIO[Option[Suggestion]] = { + val query = for { + (argument, suggestion) <- joined + if suggestion.id === id + } yield (argument, suggestion) + query.result.map(coll => joinedToSuggestion(coll).headOption) + } + + /** @inheritdoc **/ + override def insert(suggestion: Suggestion): DBIO[Long] = { + val (suggestionRow, args) = toSuggestionRow(suggestion) + for { + id <- suggestions.returning(suggestions.map(_.id)) += suggestionRow + _ <- arguments ++= args.map(toArgumentRow(id, _)) + } yield id + } + + private def joinedToSuggestion( + coll: Seq[(Option[ArgumentRow], SuggestionRow)] + ): Seq[Suggestion] = { + coll + .groupBy(_._2) + .view + .mapValues(_.flatMap(_._1)) + .map(Function.tupled(toSuggestion)) + .toSeq + } + + private def toSuggestionRow( + suggestion: Suggestion + ): (SuggestionRow, Seq[Suggestion.Argument]) = + suggestion match { + case Suggestion.Atom(name, args, returnType, doc) => + val row = SuggestionRow( + id = None, + kind = SuggestionKind.ATOM, + name = name, + selfType = None, + returnType = returnType, + documentation = doc, + scopeStart = None, + scopeEnd = None + ) + row -> args + case Suggestion.Method(name, args, selfType, returnType, doc) => + val row = SuggestionRow( + id = None, + kind = SuggestionKind.METHOD, + name = name, + selfType = Some(selfType), + returnType = returnType, + documentation = doc, + scopeStart = None, + scopeEnd = None + ) + row -> args + case Suggestion.Function(name, args, returnType, scope) => + val row = SuggestionRow( + id = None, + kind = SuggestionKind.FUNCTION, + name = name, + selfType = None, + returnType = returnType, + documentation = None, + scopeStart = Some(scope.start), + scopeEnd = Some(scope.end) + ) + row -> args + case Suggestion.Local(name, returnType, scope) => + val row = SuggestionRow( + id = None, + kind = SuggestionKind.LOCAL, + name = name, + selfType = None, + returnType = returnType, + documentation = None, + scopeStart = Some(scope.start), + scopeEnd = Some(scope.end) + ) + row -> Seq() + } + + private def toArgumentRow( + suggestionId: Long, + argument: Suggestion.Argument + ): ArgumentRow = + ArgumentRow( + id = None, + suggestionId = suggestionId, + name = argument.name, + tpe = argument.reprType, + isSuspended = argument.isSuspended, + hasDefault = argument.hasDefault, + defaultValue = argument.defaultValue + ) + + private def toSuggestion( + suggestion: SuggestionRow, + arguments: Seq[ArgumentRow] + ): Suggestion = + suggestion.kind match { + case SuggestionKind.ATOM => + Suggestion.Atom( + name = suggestion.name, + arguments = arguments.map(toArgument), + returnType = suggestion.returnType, + documentation = suggestion.documentation + ) + case SuggestionKind.METHOD => + Suggestion.Method( + name = suggestion.name, + arguments = arguments.map(toArgument), + selfType = suggestion.selfType.get, + returnType = suggestion.returnType, + documentation = suggestion.documentation + ) + case SuggestionKind.FUNCTION => + Suggestion.Function( + name = suggestion.name, + arguments = arguments.map(toArgument), + returnType = suggestion.returnType, + scope = + Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get) + ) + case SuggestionKind.LOCAL => + Suggestion.Local( + name = suggestion.name, + returnType = suggestion.returnType, + scope = + Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get) + ) + + case k => + throw new NoSuchElementException(s"Unknown suggestion kind: $k") + } + + private def toArgument(row: ArgumentRow): Suggestion.Argument = + Suggestion.Argument( + name = row.name, + reprType = row.tpe, + isSuspended = row.isSuspended, + hasDefault = row.hasDefault, + defaultValue = row.defaultValue + ) +} diff --git a/lib/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala b/lib/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala new file mode 100644 index 00000000000..e2abb2abc74 --- /dev/null +++ b/lib/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala @@ -0,0 +1,110 @@ +package org.enso.searcher.sql + +import slick.jdbc.SQLiteProfile.api._ + +/** A row in the arguments table. + * + * @param id the id of an argument + * @param suggestionId the id of the suggestion + * @param name the argument name + * @param tpe the argument type + * @param isSuspended is the argument lazy + * @param hasDefault does the argument have the default value + * @param defaultValue optional default value + */ +case class ArgumentRow( + id: Option[Long], + suggestionId: Long, + name: String, + tpe: String, + isSuspended: Boolean, + hasDefault: Boolean, + defaultValue: Option[String] +) + +/** A row in the suggestions table. + * + * @param id the id of a suggestion + * @param kind the type of a suggestion + * @param name the suggestion name + * @param selfType the self type of a suggestion + * @param returnType the return type of a suggestion + * @param documentation the documentation string + * @param scopeStart the start of the scope + * @param scopeEnd the end of the scope + */ +case class SuggestionRow( + id: Option[Long], + kind: Byte, + name: String, + selfType: Option[String], + returnType: String, + documentation: Option[String], + scopeStart: Option[Int], + scopeEnd: Option[Int] +) + +/** The type of a suggestion. */ +object SuggestionKind { + + val ATOM: Byte = 0 + val METHOD: Byte = 1 + val FUNCTION: Byte = 2 + val LOCAL: Byte = 3 +} + +/** The schema of the arguments table. */ +final class ArgumentsTable(tag: Tag) + extends Table[ArgumentRow](tag, "arguments") { + + def id = column[Long]("id", O.PrimaryKey, O.AutoInc) + def suggestionId = column[Long]("suggestion_id") + def name = column[String]("name") + def tpe = column[String]("type") + def isSuspended = column[Boolean]("is_suspended", O.Default(false)) + def hasDefault = column[Boolean]("has_default", O.Default(false)) + def defaultValue = column[Option[String]]("default_value") + def * = + (id.?, suggestionId, name, tpe, isSuspended, hasDefault, defaultValue) <> + (ArgumentRow.tupled, ArgumentRow.unapply) + + def suggestion = + foreignKey("suggestion_fk", suggestionId, suggestions)( + _.id, + onUpdate = ForeignKeyAction.Restrict, + onDelete = ForeignKeyAction.Cascade + ) +} + +/** The schema of the suggestions table. */ +final class SuggestionsTable(tag: Tag) + extends Table[SuggestionRow](tag, "suggestions") { + + def id = column[Long]("id", O.PrimaryKey, O.AutoInc) + def kind = column[Byte]("kind") + def name = column[String]("name") + def selfType = column[Option[String]]("self_type") + def returnType = column[String]("return_type") + def documentation = column[Option[String]]("documentation") + def scopeStart = column[Option[Int]]("scope_start") + def scopeEnd = column[Option[Int]]("scope_end") + def * = + ( + id.?, + kind, + name, + selfType, + returnType, + documentation, + scopeStart, + scopeEnd + ) <> + (SuggestionRow.tupled, SuggestionRow.unapply) + + def selfTypeIdx = index("self_type_idx", selfType) + def returnTypeIdx = index("return_type_idx", name) +} + +object arguments extends TableQuery(new ArgumentsTable(_)) + +object suggestions extends TableQuery(new SuggestionsTable(_)) diff --git a/lib/searcher/src/test/resources/application.conf b/lib/searcher/src/test/resources/application.conf new file mode 100644 index 00000000000..e9bec0bf3f7 --- /dev/null +++ b/lib/searcher/src/test/resources/application.conf @@ -0,0 +1,3 @@ +searcher.db { + url = "jdbc:sqlite:file::memory:?cache=shared" +} diff --git a/lib/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala b/lib/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala new file mode 100644 index 00000000000..d82a96c2f72 --- /dev/null +++ b/lib/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala @@ -0,0 +1,101 @@ +package org.enso.searcher.sql + +import org.enso.searcher.Suggestion +import org.scalatest.BeforeAndAfterAll +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import slick.jdbc.SQLiteProfile.api._ + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + +class SuggestionsRepoTest + extends AnyWordSpec + with Matchers + with BeforeAndAfterAll { + + val Timeout: FiniteDuration = 3.seconds + + val db = Database.forConfig("searcher.db") + val repo = new SqlSuggestionsRepo() + + override def beforeAll(): Unit = { + Await.ready( + db.run((suggestions.schema ++ arguments.schema).createIfNotExists), + Timeout + ) + } + + override def afterAll(): Unit = { + db.close() + } + + "SuggestionsDBIO" should { + + "select suggestion by id" in { + val action = + for { + id <- db.run(repo.insert(suggestion.atom)) + res <- db.run(repo.select(id)) + } yield res + + Await.result(action, Timeout) shouldEqual Some(suggestion.atom) + } + + "find suggestion by returnType" in { + val action = + for { + _ <- db.run(repo.insert(suggestion.local)) + _ <- db.run(repo.insert(suggestion.method)) + _ <- db.run(repo.insert(suggestion.function)) + res <- db.run(repo.findBy("MyType")) + } yield res + + Await.result(action, Timeout) should contain theSameElementsAs Seq( + suggestion.local, + suggestion.function + ) + } + } + + object suggestion { + + val atom: Suggestion.Atom = + Suggestion.Atom( + name = "Pair", + arguments = Seq( + Suggestion.Argument("a", "Any", false, false, None), + Suggestion.Argument("b", "Any", false, false, None) + ), + returnType = "Pair", + documentation = Some("Awesome") + ) + + val method: Suggestion.Method = + Suggestion.Method( + name = "main", + arguments = Seq(), + selfType = "Main", + returnType = "IO", + documentation = None + ) + + val function: Suggestion.Function = + Suggestion.Function( + name = "bar", + arguments = Seq( + Suggestion.Argument("x", "Number", false, true, Some("0")) + ), + returnType = "MyType", + scope = Suggestion.Scope(5, 9) + ) + + val local: Suggestion.Local = + Suggestion.Local( + name = "bazz", + returnType = "MyType", + scope = Suggestion.Scope(37, 84) + ) + } +}