Runtime Cache Integration Part 2 (#800)

This commit is contained in:
Dmitry Bushev 2020-06-04 20:25:23 +03:00 committed by GitHub
parent 7f1f484ada
commit 2a3ec07c87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 345 additions and 122 deletions

View File

@ -17,8 +17,7 @@ import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.polyglot.LanguageInfo; import org.enso.polyglot.LanguageInfo;
import org.enso.polyglot.MethodNames; import org.enso.polyglot.MethodNames;
import org.enso.text.buffer.Rope; import org.enso.text.buffer.Rope;
import org.enso.text.editing.JavaEditorAdapter; import org.enso.text.editing.*;
import org.enso.text.editing.model;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
@ -220,7 +219,7 @@ public class ExecutionService {
* @param edits the edits to apply. * @param edits the edits to apply.
* @return an object for computing the changed IR nodes. * @return an object for computing the changed IR nodes.
*/ */
public Optional<Changeset> modifyModuleSources(File path, List<model.TextEdit> edits) { public Optional<Changeset<Rope>> modifyModuleSources(File path, List<model.TextEdit> edits) {
Optional<Module> moduleMay = context.getModuleForFile(path); Optional<Module> moduleMay = context.getModuleForFile(path);
if (!moduleMay.isPresent()) { if (!moduleMay.isPresent()) {
return Optional.empty(); return Optional.empty();
@ -229,8 +228,12 @@ public class ExecutionService {
if (module.getLiteralSource() == null) { if (module.getLiteralSource() == null) {
return Optional.empty(); return Optional.empty();
} }
Changeset changeset = Changeset<Rope> changeset =
new Changeset(module.getLiteralSource().toString(), module.parseIr(context)); new Changeset<>(
module.getLiteralSource(),
module.parseIr(context),
TextEditor.ropeTextEditor(),
IndexedSource.RopeIndexedSource());
Optional<Rope> editedSource = JavaEditorAdapter.applyEdits(module.getLiteralSource(), edits); Optional<Rope> editedSource = JavaEditorAdapter.applyEdits(module.getLiteralSource(), edits);
editedSource.ifPresent(module::setLiteralSource); editedSource.ifPresent(module::setLiteralSource);
return Optional.of(changeset); return Optional.of(changeset);

View File

@ -1,96 +1,246 @@
package org.enso.compiler.context package org.enso.compiler.context
import java.util.UUID
import org.enso.compiler.core.IR import org.enso.compiler.core.IR
import org.enso.compiler.exception.CompilerError import org.enso.compiler.exception.CompilerError
import org.enso.compiler.pass.analyse.DataflowAnalysis import org.enso.compiler.pass.analyse.DataflowAnalysis
import org.enso.syntax.text.Location import org.enso.syntax.text.Location
import org.enso.text.editing.model.{Position, TextEdit} import org.enso.text.editing.model.TextEdit
import org.enso.text.editing.{IndexedSource, TextEditor}
import scala.collection.mutable import scala.collection.mutable
/** /** Compute invalidated expressions.
* Compute invalidated expressions.
* *
* @param source the text source. * @param source the text source
* @param ir the IR node. * @param ir the IR node
* @tparam A a source type
*/ */
final class Changeset(val source: CharSequence, ir: IR) { final class Changeset[A: TextEditor: IndexedSource](val source: A, ir: IR) {
/** /** Traverses the IR and returns a list of all IR nodes affected by the edit
* Traverses the IR and returns a list of all IR nodes affected by the edit
* using the [[DataflowAnalysis]] information. * using the [[DataflowAnalysis]] information.
* *
* @param edit the text edit. * @param edits the text edits
* @throws CompilerError if the IR is missing DataflowAnalysis metadata * @throws CompilerError if the IR is missing DataflowAnalysis metadata
* @return the list of all IR nodes affected by the edit. * @return the set of all IR nodes affected by the edit
*/ */
@throws[CompilerError] @throws[CompilerError]
def compute(edit: TextEdit): Seq[IR.ExternalId] = { def compute(edits: Seq[TextEdit]): Set[IR.ExternalId] = {
val metadata = ir val metadata = ir
.unsafeGetMetadata( .unsafeGetMetadata(
DataflowAnalysis, DataflowAnalysis,
"Empty dataflow analysis metadata during changeset calculation." "Empty dataflow analysis metadata during changeset calculation."
) )
invalidated(edit) invalidated(edits)
.map(toDataflowDependencyType) .map(Changeset.toDataflowDependencyType)
.flatMap(metadata.getExternal) .flatMap(metadata.getExternal)
.flatten .flatten
} }
/** /** Traverses the IR and returns a list of the most specific (the innermost)
* Traverses the IR and returns a list of the most specific (the innermost)
* IR nodes directly affected by the edit by comparing the source locations. * IR nodes directly affected by the edit by comparing the source locations.
* *
* @param edit the text edit. * @param edits the text edits
* @return the list of IR nodes directly affected by the edit. * @return the set of IR nodes directly affected by the edit
*/ */
def invalidated(edit: TextEdit): Seq[Changeset.Node] = { def invalidated(edits: Seq[TextEdit]): Set[Changeset.NodeId] = {
@scala.annotation.tailrec @scala.annotation.tailrec
def go( def go(
edit: Location, tree: Changeset.Tree,
queue: mutable.Queue[IR], source: A,
acc: mutable.Builder[Changeset.Node, Vector[Changeset.Node]] edits: mutable.Queue[TextEdit],
): Seq[Changeset.Node] = ids: mutable.Set[Changeset.NodeId]
if (queue.isEmpty) { ): Set[Changeset.NodeId] = {
acc.result() if (edits.isEmpty) ids.toSet
} else { else {
val ir = queue.dequeue() val edit = edits.dequeue()
val invalidatedChildren = ir.children.filter(intersect(edit, _)) val locationEdit = Changeset.toLocationEdit(edit, source)
if (invalidatedChildren.isEmpty) { val invalidatedSet = Changeset.invalidated(tree, locationEdit.location)
if (intersect(edit, ir)) { val newTree = Changeset.updateLocations(tree, locationEdit)
go(edit, queue, acc += Changeset.Node(ir)) val newSource = TextEditor[A].edit(source, edit)
} else { go(newTree, newSource, edits, ids ++= invalidatedSet.map(_.id))
go(edit, queue ++= ir.children, acc)
} }
} else {
go(edit, queue ++= invalidatedChildren, acc)
} }
val tree = Changeset.buildTree(ir)
go(tree, source, mutable.Queue.from(edits), mutable.HashSet())
} }
go(
toLocation(edit, source),
mutable.Queue(ir),
Vector.newBuilder[Changeset.Node]
)
} }
/** object Changeset {
* Checks if the IR is affected by the edit.
/** An identifier of IR node.
* *
* @param edit location of the edit. * @param internalId internal IR id
* @param ir the IR node. * @param externalId external IR id
* @return true if the node is affected by the edit.
*/ */
def intersect(edit: Location, ir: IR): Boolean = { case class NodeId(
ir.location.map(_.location).exists(intersect(edit, _)) internalId: IR.Identifier,
externalId: Option[IR.ExternalId]
)
object NodeId {
/** Create a [[NodeId]] identifier from [[IR]].
*
* @param ir the source IR
* @return the identifier
*/
def apply(ir: IR): NodeId =
new NodeId(ir.getId, ir.getExternalId)
implicit val ordering: Ordering[NodeId] = (x: NodeId, y: NodeId) => {
val cmpInternal = Ordering[UUID].compare(x.internalId, y.internalId)
if (cmpInternal == 0) {
Ordering[Option[UUID]].compare(x.externalId, y.externalId)
} else {
cmpInternal
}
}
} }
/** // === Changeset Internals ==================================================
* Checks if the node location intersects the edit location.
/** Internal representation of an [[IR]]. */
private type Tree = mutable.TreeSet[Node]
/** The location that has been edited.
* *
* @param edit location of the edit. * @param location the location of the edit
* @param node location of the node. * @param length the length of the inserted text
* @return true if the node and edit locations are intersecting. */
private case class LocationEdit(location: Location, length: Int) {
/** The difference in length between the edited text and the inserted text.
* Determines how much the rest of the text will be shifted after applying
* the edit.
*/
val locationDifference: Int = {
val editRange = location.end - location.start
length - editRange
}
}
/** Internal representation of an `IR` node in the changeset.
*
* @param id the node id
* @param location the node location
*/
private case class Node(id: NodeId, location: Location) {
/** Shift the node location.
*
* @param offset the offset relative to the previous node location
* @return the node with a new location
*/
def shift(offset: Int): Node = {
val newLocation = location.copy(
start = location.start + offset,
end = location.end + offset
)
copy(location = newLocation)
}
}
private object Node {
/** Create a node from [[IR]].
*
* @param ir the source IR
* @return the node if `ir` contains a location
*/
def fromIr(ir: IR): Option[Node] =
ir.location.map(loc => Node(NodeId(ir), loc.location))
/** Create an artificial node with fixed [[NodeId]]. It is used to select
* nodes by location in the tree.
*
* @param location the location of the node
* @return a select node
*/
def select(location: Location): Node =
new Node(NodeId(UUID.nameUUIDFromBytes(Array()), None), location)
implicit val ordering: Ordering[Node] = (x: Node, y: Node) => {
val compareStart =
Ordering[Int].compare(x.location.start, y.location.start)
if (compareStart == 0) {
val compareEnd = Ordering[Int].compare(y.location.end, x.location.end)
if (compareEnd == 0) Ordering[NodeId].compare(x.id, y.id)
else compareEnd
} else compareStart
}
}
/** Build an internal representation of the [[IR]].
*
* @param ir the source IR
* @return the tree representation of the IR
*/
private def buildTree(ir: IR): Tree = {
@scala.annotation.tailrec
def go(input: mutable.Queue[IR], acc: Tree): Tree =
if (input.isEmpty) acc
else {
val ir = input.dequeue()
if (ir.children.isEmpty) {
Node.fromIr(ir).foreach(acc.add)
}
go(input ++= ir.children, acc)
}
go(mutable.Queue(ir), mutable.TreeSet())
}
/** Update the tree locations after applying the edit.
*
* @param tree the source tree
* @param edit the edit to apply
* @return the tree with updated locations
*/
private def updateLocations(tree: Tree, edit: LocationEdit): Tree = {
val range = tree.rangeFrom(Node.select(edit.location)).toSeq
range.foreach { updated =>
tree -= updated
tree += updated.shift(edit.locationDifference)
}
tree
}
/** Calculate the invalidated subset of the tree affected by the edit by
* comparing the source locations.
*
* @param tree the source tree
* @param edit the location of the edit
* @return the invalidated nodes of the tree
*/
private def invalidated(tree: Tree, edit: Location): Tree = {
val invalidated = mutable.TreeSet[Changeset.Node]()
tree.iterator.foreach { node =>
if (intersect(edit, node)) {
invalidated += node
tree -= node
}
}
invalidated
}
/** Check if the node location intersects the edit location.
*
* @param edit location of the edit
* @param node the node
* @return true if the node and edit locations are intersecting
*/
private def intersect(edit: Location, node: Changeset.Node): Boolean = {
intersect(edit, node.location)
}
/** Check if the node location intersects the edit location.
*
* @param edit location of the edit
* @param node location of the node
* @return true if the node and edit locations are intersecting
*/ */
private def intersect(edit: Location, node: Location): Boolean = { private def intersect(edit: Location, node: Location): Boolean = {
inside(node.start, edit) || inside(node.start, edit) ||
@ -99,69 +249,53 @@ final class Changeset(val source: CharSequence, ir: IR) {
inside(edit.end, node) inside(edit.end, node)
} }
/** /** Check if the character position index is inside the location.
* Checks if the character position index is inside the location.
* *
* @param index the character position. * @param index the character position
* @param location the location. * @param location the location
* @return true if the index is inside the location. * @return true if the index is inside the location
*/ */
private def inside(index: Int, location: Location): Boolean = private def inside(index: Int, location: Location): Boolean =
index >= location.start && index <= location.end index >= location.start && index <= location.end
/** /** Convert [[TextEdit]] to [[Changeset.LocationEdit]] edit in the provided
* Converts [[TextEdit]] location to [[Location]] in the provided source.
*
* @param edit the text edit.
* @param source the source text.
* @return location of the text edit in the source text.
*/
private def toLocation(edit: TextEdit, source: CharSequence): Location = {
val start = edit.range.start
val end = edit.range.end
Location(toIndex(start, source), toIndex(end, source))
}
/**
* Converts position relative to a line to an absolute position in the
* source. * source.
* *
* @param pos character position. * @param edit the text edit
* @param source the source text. * @param source the source text
* @return absolute position in the source. * @return the edit location in the source text
*/ */
private def toIndex(pos: Position, source: CharSequence): Int = { private def toLocationEdit[A: IndexedSource](
val prefix = source.toString.linesIterator.take(pos.line) edit: TextEdit,
prefix.mkString("\n").length + pos.character source: A
): LocationEdit = {
LocationEdit(toLocation(edit, source), edit.text.length)
} }
/** /** Convert [[TextEdit]] location to [[Location]] in the provided source.
* Converts invalidated node to the dataflow dependency type.
* *
* @param node the invalidated node. * @param edit the text edit
* @return the dataflow dependency type. * @param source the source text
* @return location of the text edit in the source text
*/
private def toLocation[A: IndexedSource](
edit: TextEdit,
source: A
): Location = {
Location(
IndexedSource[A].toIndex(edit.range.start, source),
IndexedSource[A].toIndex(edit.range.end, source)
)
}
/** Convert invalidated node to the dataflow dependency type.
*
* @param node the invalidated node
* @return the dataflow dependency type
*/ */
private def toDataflowDependencyType( private def toDataflowDependencyType(
node: Changeset.Node node: NodeId
): DataflowAnalysis.DependencyInfo.Type.Static = ): DataflowAnalysis.DependencyInfo.Type.Static =
DataflowAnalysis.DependencyInfo.Type DataflowAnalysis.DependencyInfo.Type
.Static(node.internalId, node.externalId) .Static(node.internalId, node.externalId)
} }
object Changeset {
/**
* An invalidated IR node.
*
* @param internalId internal IR id.
* @param externalId external IR id.
*/
case class Node(internalId: IR.Identifier, externalId: Option[IR.ExternalId])
object Node {
def apply(ir: IR): Node =
new Node(ir.getId, ir.getExternalId)
}
}

View File

@ -28,7 +28,7 @@ class EditFileCmd(request: Api.EditFileNotification)
.toScala .toScala
val invalidateExpressionsCommand = changesetOpt.map { changeset => val invalidateExpressionsCommand = changesetOpt.map { changeset =>
CacheInvalidation.Command.InvalidateKeys( CacheInvalidation.Command.InvalidateKeys(
request.edits.flatMap(changeset.compute) changeset.compute(request.edits)
) )
} }
val invalidateStaleCommand = changesetOpt.map { changeset => val invalidateStaleCommand = changesetOpt.map { changeset =>

View File

@ -3,6 +3,7 @@ package org.enso.compiler.test.context
import org.enso.compiler.context.Changeset import org.enso.compiler.context.Changeset
import org.enso.compiler.core.IR import org.enso.compiler.core.IR
import org.enso.compiler.test.CompilerTest import org.enso.compiler.test.CompilerTest
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.{Position, Range, TextEdit} import org.enso.text.editing.model.{Position, Range, TextEdit}
class ChangesetTest extends CompilerTest { class ChangesetTest extends CompilerTest {
@ -17,7 +18,7 @@ class ChangesetTest extends CompilerTest {
val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary] val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary]
val two = rhs.right.value val two = rhs.right.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
two.getId two.getId
) )
} }
@ -30,7 +31,7 @@ class ChangesetTest extends CompilerTest {
val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary] val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary]
val two = rhs.right.value val two = rhs.right.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
two.getId two.getId
) )
} }
@ -43,7 +44,7 @@ class ChangesetTest extends CompilerTest {
val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary] val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary]
val two = rhs.right.value val two = rhs.right.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
two.getId two.getId
) )
} }
@ -55,7 +56,7 @@ class ChangesetTest extends CompilerTest {
val ir = code.toIrExpression.get.asInstanceOf[IR.Expression.Binding] val ir = code.toIrExpression.get.asInstanceOf[IR.Expression.Binding]
val x = ir.name val x = ir.name
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
x.getId x.getId
) )
} }
@ -67,7 +68,7 @@ class ChangesetTest extends CompilerTest {
val ir = code.toIrExpression.get.asInstanceOf[IR.Expression.Binding] val ir = code.toIrExpression.get.asInstanceOf[IR.Expression.Binding]
val x = ir.name val x = ir.name
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
x.getId x.getId
) )
} }
@ -81,7 +82,7 @@ class ChangesetTest extends CompilerTest {
val plus = rhs.operator val plus = rhs.operator
val two = rhs.right.value val two = rhs.right.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
plus.getId, plus.getId,
two.getId two.getId
) )
@ -96,7 +97,7 @@ class ChangesetTest extends CompilerTest {
val x = ir.name val x = ir.name
val one = rhs.left.value val one = rhs.left.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
x.getId, x.getId,
one.getId one.getId
) )
@ -111,7 +112,7 @@ class ChangesetTest extends CompilerTest {
val one = val one =
ir.expression.asInstanceOf[IR.Application.Operator.Binary].left.value ir.expression.asInstanceOf[IR.Application.Operator.Binary].left.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
x.getId, x.getId,
one.getId one.getId
) )
@ -131,7 +132,7 @@ class ChangesetTest extends CompilerTest {
val plus = secondLine.operator val plus = secondLine.operator
val x = secondLine.right.value val x = secondLine.right.value
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
y.getId, y.getId,
plus.getId, plus.getId,
x.getId x.getId
@ -154,7 +155,56 @@ class ChangesetTest extends CompilerTest {
val y = thirdLine.left.value val y = thirdLine.left.value
val plus = thirdLine.operator val plus = thirdLine.operator
invalidated(edit, code, ir) should contain theSameElementsAs Seq( invalidated(ir, code, edit) should contain theSameElementsAs Seq(
z.getId,
y.getId,
plus.getId
)
}
"multiple single expression" in {
val code = """x = 1 + 2"""
val edits = Seq(
TextEdit(Range(Position(0, 0), Position(0, 0)), "inde"),
TextEdit(Range(Position(0, 8), Position(0, 9)), "40"),
TextEdit(Range(Position(0, 11), Position(0, 12)), "-"),
TextEdit(Range(Position(0, 8), Position(0, 10)), "44")
)
val ir = code.toIrExpression.get.asInstanceOf[IR.Expression.Binding]
val x = ir.name
val rhs = ir.expression.asInstanceOf[IR.Application.Operator.Binary]
val one = rhs.left.value
val plus = rhs.operator
invalidated(ir, code, edits: _*) should contain theSameElementsAs Seq(
x.getId,
one.getId,
plus.getId
)
}
"multiple multiline" in {
val code =
"""foo x =
| z = 1
| y = z
| y + x""".stripMargin.linesIterator.mkString("\n")
val edits = Seq(
TextEdit(Range(Position(0, 0), Position(0, 0)), "bar = 123\n\n"),
TextEdit(Range(Position(4, 8), Position(5, 7)), "42\n y -")
)
val ir = code.toIrExpression.get.asInstanceOf[IR.Function.Binding]
val secondLine = ir.body.children(1).asInstanceOf[IR.Expression.Binding]
val z = secondLine.expression
val thirdLine =
ir.body.children(2).asInstanceOf[IR.Application.Operator.Binary]
val y = thirdLine.left.value
val plus = thirdLine.operator
invalidated(ir, code, edits: _*) should contain theSameElementsAs Seq(
ir.name.getId,
z.getId, z.getId,
y.getId, y.getId,
plus.getId plus.getId
@ -162,6 +212,6 @@ class ChangesetTest extends CompilerTest {
} }
} }
def invalidated(edit: TextEdit, code: String, ir: IR): Seq[IR.Identifier] = def invalidated(ir: IR, code: String, edits: TextEdit*): Set[IR.Identifier] =
new Changeset(code, ir).invalidated(edit).map(_.internalId) new Changeset(Rope(code), ir).invalidated(edits).map(_.internalId)
} }

View File

@ -12,7 +12,6 @@ import org.enso.interpreter.instrument.{
} }
import org.enso.interpreter.test.Metadata import org.enso.interpreter.test.Metadata
import org.enso.pkg.{Package, PackageManager} import org.enso.pkg.{Package, PackageManager}
import org.enso.polyglot.runtime.Runtime.Api.VisualisationUpdate
import org.enso.polyglot.runtime.Runtime.{Api, ApiRequest} import org.enso.polyglot.runtime.Runtime.{Api, ApiRequest}
import org.enso.polyglot.{ import org.enso.polyglot.{
LanguageInfo, LanguageInfo,

View File

@ -0,0 +1,37 @@
package org.enso.text.editing
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.Position
/** A source which character positions can be accessed by index.
*
* @tparam A a source type
*/
trait IndexedSource[A] {
/** Converts position relative to a line to an absolute position in the
* source.
*
* @param pos character position.
* @param source the source text.
* @return absolute position in the source.
*/
def toIndex(pos: Position, source: A): Int
}
object IndexedSource {
def apply[A](implicit is: IndexedSource[A]): IndexedSource[A] = is
implicit val CharSequenceIndexedSource: IndexedSource[CharSequence] =
(pos: Position, source: CharSequence) => {
val prefix = source.toString.linesIterator.take(pos.line)
prefix.mkString("\n").length + pos.character
}
implicit val RopeIndexedSource: IndexedSource[Rope] =
(pos: Position, source: Rope) => {
val prefix = source.lines.take(pos.line)
prefix.characters.length + pos.character
}
}