Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update interval tree code for clarity #73874

Merged
merged 6 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ private static ImmutableArray<TextSpan> GetNonOverlappingSpans(
SyntaxNode root, ImmutableArray<TextSpan> spans, CancellationToken cancellationToken)
{
// Create interval tree for spans
var intervalTree = BinaryIntervalTree.Create(new TextSpanIntervalIntrospector(), spans);
var intervalTree = SimpleMutableIntervalTree.Create(new TextSpanIntervalIntrospector(), spans);

// Find tokens that are outside of spans
var tokenSpans = new List<TextSpan>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public TextSpan GetSpan(TextChange value)
private readonly IDocumentTextDifferencingService _differenceService;

private readonly SimpleMutableIntervalTree<TextChange, IntervalIntrospector> _totalChangesIntervalTree =
BinaryIntervalTree.Create(new IntervalIntrospector(), Array.Empty<TextChange>());
SimpleMutableIntervalTree.Create(new IntervalIntrospector(), Array.Empty<TextChange>());

public TextChangeMerger(Document document)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Workspaces/CoreTest/UtilityTest/IntervalTreeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ public sealed class BinaryIntervalTreeTests : IntervalTreeTests
{
private protected override IEnumerable<IIntervalTree<Tuple<int, int, string>>> CreateTrees(IEnumerable<Tuple<int, int, string>> values)
{
yield return BinaryIntervalTree.Create(new TupleIntrospector<string>(), values);
yield return SimpleMutableIntervalTree.Create(new TupleIntrospector<string>(), values);
}

private protected override bool HasIntervalThatIntersectsWith(IIntervalTree<Tuple<int, int, string>> tree, int position)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,9 @@ public static ImmutableIntervalTree<T> CreateFromUnsorted<TIntrospector>(in TInt
/// pointer chasing operations, which is both slow, and which impacts the GC which has to track all those writes.
/// </summary>
/// <remarks>
/// <list type="bullet">The values must be sorted such that given any two elements 'a' and 'b' in the list, if 'a'
/// comes before 'b' in the list, then it's "start position" (as determined by the introspector) must be less than
/// or equal to 'b's start position. This is a requirement for the algorithm to work correctly.
/// </list>
/// <list type="bullet">The <paramref name="values"/> list will be mutated as part of this operation.
/// </list>
/// The values must be sorted such that given any two elements 'a' and 'b' in the list, if 'a' comes before 'b' in
/// the list, then it's "start position" (as determined by the introspector) must be less than or equal to 'b's
/// start position. This is a requirement for the algorithm to work correctly.
/// </remarks>
public static ImmutableIntervalTree<T> CreateFromSorted<TIntrospector>(in TIntrospector introspector, SegmentedList<T> values)
where TIntrospector : struct, IIntervalIntrospector<T>
Expand Down Expand Up @@ -134,8 +131,25 @@ static int GetRootSourceIndex(int subtreeNodeCount)
//
// The perfect case is trivial. We simply take the middle element of the array and make it the root, and
// then recurse into the left and right halves of the array.
//
// The interesting cases case be demonstrated with the following examples:

// The height of the perfect portion of the tree (the rows that are completely full from left to right).
// This is '3' in both of the examples below. It will be the height of the whole tree if the whole tree is
// perfect itself.
var perfectPortionHeight = SegmentedArraySortUtils.Log2((uint)subtreeNodeCount + 1);

// Then number of nodes in the perfect portion. For the example trees below this is 7.
var perfectPortionNodeCount = PerfectTreeNodeCount(perfectPortionHeight);

// If the entire subtree we're looking at is perfect or not. It's perfect if every layer is full.
// In the above example, both trees are not perfect.
var wholeSubtreeIsPerfect = perfectPortionNodeCount == subtreeNodeCount;

// If we do have a perfect tree, the root item trivially the s the middle element of that tree.
var perfectPortionMidwayPoint = perfectPortionNodeCount / 2;
if (wholeSubtreeIsPerfect)
return perfectPortionMidwayPoint;

// The interesting, imperfect, cases case be demonstrated with the following examples:
//
// 10 elements:
// g, d, i, b, f, h, j, a, c, e
Expand All @@ -146,7 +160,7 @@ static int GetRootSourceIndex(int subtreeNodeCount)
// __/ \__ / \
// b f h j
// / \ /
// a c e
// a c e
//
// 13 elements:
// h, d, l, b, f, j, m, a, c, e, g, i, k
Expand All @@ -161,51 +175,38 @@ static int GetRootSourceIndex(int subtreeNodeCount)
//
// The difference in these cases is the 'd' subtree. We either have:
//
// 1. enough elements to fill it and start filling its right sibling ('l'). This is the case in the 13
// element tree.
//
// 2. not enough elements to start filling the right sibling ('i'). This is the case in the 10 element
// 1. not enough elements to start filling the right sibling ('i'). This is the case in the 10 element
// tree.
//
// In both cases, one of the two children of the root will be a perfect tree (the right child in the 10
// element case, and the left element in the 13 element case). From the initial discussion, we know that
// perfect trees are trivial to handle when we recurse into them. For the other side, we will get a tree
// that we can then recursively apply the more complex logic to. And the process repeats downwards.

// The height of the perfect portion of the tree (the rows that are completely full from left to right).
// This is '3' in both of the examples above.
var perfectPortionHeight = SegmentedArraySortUtils.Log2((uint)subtreeNodeCount + 1);

// Then number of nodes in the perfect section. For both trees above this is 7.
var perfectSectionNodeCount = (1 << perfectPortionHeight) - 1;

// If the entire subtree we're looking at is perfect or not. It's perfect if every layer is full.
// In the above example, both trees are not perfect.
var isPerfect = perfectSectionNodeCount == subtreeNodeCount;

// The total tree height. If we're perfect, it's the height of the perfect portion. Otherwise
// it's one higher (to fit the remaining incomplete row).
var treeHeight = isPerfect ? perfectPortionHeight : perfectPortionHeight + 1;

// How many nodes would be in the tree if it was perfect.
var nodeCountIfTreeWerePerfect = (1 << treeHeight) - 1;

// Here we can figure out which case we have, and where the pivot is. First, we start with
// 2. enough elements to fill it and start filling its right sibling ('l'). This is the case in the 13
// element tree.
//
// 1. `a = subtreeNodeCount - perfectSectionNodeCount`. The number of elements in the 'incomplete' last row.
// 2. `b = nodeCountIfTreeWerePerfect - perfectSectionNodeCount`. The number of elements in the last row if
// it were entirely complete.
// 3. `c = b / 2`. Half the number of elements in the last row if it were entirely complete.
// 4. `d = Min(a, c)`. The min point in the last row. If we have it filled less than half full, it's the
// number of elements. If it is more than half full, it's the midway point.
// 5. `e = perfectSectionNodeCount / 2`. Halfway through the perfect top section.
// 6. `f = e + d`. The pivot point in the array. While filling up the first half of the final row, we're
// continually incrementing the pivot point (so we include more elements in the left tree). Once we hit
// the halfway point in the last row, then we want to stop incrementing the pivot point (so that we
// include more elements in the right tree).
return (perfectSectionNodeCount / 2) + Math.Min(subtreeNodeCount - perfectSectionNodeCount, (nodeCountIfTreeWerePerfect - perfectSectionNodeCount) / 2);
// In both cases, one of the two children of the root will be a perfect tree (the right child in the 10
// element case, and the left element in the 13 element case). So, when we recurse into either, we either
// recurse into a perfect tree (which we know is trivial to handle). Or we will recurse into another tree
// that we can handle using the balancing logic below.

// The total tree height. Since we know we're not perfect, we computed based on one greater than than the
// perfect-portion height.
var nodeCountIfTreeWerePerfect = PerfectTreeNodeCount(height: perfectPortionHeight + 1);

var elementsInLastIncompleteRow = subtreeNodeCount - perfectPortionNodeCount;
var elementsInLastRowIfTreeWerePerfect = nodeCountIfTreeWerePerfect - perfectPortionNodeCount;

// The min point in the last row. If we have it filled less than half full, it's the number of elements.
// If it is more than half full, it's the midway point.
var elementsInLastRowCappedAtMidwayPoint = Math.Min(elementsInLastIncompleteRow, elementsInLastRowIfTreeWerePerfect / 2);

// The pivot point in the array. While filling up the first half of the final row, we're continually
// incrementing the pivot point (so we include more elements in the left tree). Once we hit the halfway
// point in the last row, then we want to stop incrementing the pivot point (so that we include more
// elements in the right tree).
return perfectPortionMidwayPoint + elementsInLastRowCappedAtMidwayPoint;
}

static int PerfectTreeNodeCount(int height)
=> (1 << height) - 1;

// Returns the max end *position* of tree rooted at currentNodeIndex. If there is no tree here (it refers to a
// null child), then this will return -1;
static int ComputeMaxEndNodes(SegmentedArray<Node> array, int currentNodeIndex, in TIntrospector introspector)
Expand Down Expand Up @@ -261,27 +262,27 @@ private static int GetRightChildIndex(int nodeIndex)
=> (2 * nodeIndex) + 2;

bool IIntervalTree<T>.Any<TIntrospector>(int start, int length, TestInterval<T, TIntrospector> testInterval, in TIntrospector introspector)
=> IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeHelper>.Any(this, start, length, testInterval, in introspector);
=> IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeWitness>.Any(this, start, length, testInterval, in introspector);

int IIntervalTree<T>.FillWithIntervalsThatMatch<TIntrospector>(
int start, int length, TestInterval<T, TIntrospector> testInterval,
ref TemporaryArray<T> builder, in TIntrospector introspector,
bool stopAfterFirst)
{
return IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeHelper>.FillWithIntervalsThatMatch(
return IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeWitness>.FillWithIntervalsThatMatch(
this, start, length, testInterval, ref builder, in introspector, stopAfterFirst);
}

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();

public IEnumerator<T> GetEnumerator()
=> IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeHelper>.GetEnumerator(this);
=> IntervalTreeHelpers<T, ImmutableIntervalTree<T>, /*TNode*/ int, FlatArrayIntervalTreeWitness>.GetEnumerator(this);

/// <summary>
/// Wrapper type to allow the IntervalTreeHelpers type to work with this type.
/// </summary>
private readonly struct FlatArrayIntervalTreeHelper : IIntervalTreeWitness<T, ImmutableIntervalTree<T>, int>
private readonly struct FlatArrayIntervalTreeWitness : IIntervalTreeWitness<T, ImmutableIntervalTree<T>, int>
{
public T GetValue(ImmutableIntervalTree<T> tree, int node)
=> tree._array[node].Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public static MutableIntervalTree<T> Create<TIntrospector>(in TIntrospector intr
public IntervalTreeAlgorithms<T, MutableIntervalTree<T>> Algorithms => new(this);

bool IIntervalTree<T>.Any<TIntrospector>(int start, int length, TestInterval<T, TIntrospector> testInterval, in TIntrospector introspector)
=> IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeHelper>.Any(this, start, length, testInterval, in introspector);
=> IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeWitness>.Any(this, start, length, testInterval, in introspector);

int IIntervalTree<T>.FillWithIntervalsThatMatch<TIntrospector>(
int start, int length, TestInterval<T, TIntrospector> testInterval,
ref TemporaryArray<T> builder, in TIntrospector introspector,
bool stopAfterFirst)
{
return IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeHelper>.FillWithIntervalsThatMatch(
return IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeWitness>.FillWithIntervalsThatMatch(
this, start, length, testInterval, ref builder, in introspector, stopAfterFirst);
}

Expand Down Expand Up @@ -124,7 +124,7 @@ static int BalanceFactor(Node? node)
}

public IEnumerator<T> GetEnumerator()
=> IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeHelper>.GetEnumerator(this);
=> IntervalTreeHelpers<T, MutableIntervalTree<T>, Node, BinaryIntervalTreeWitness>.GetEnumerator(this);

IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
Expand All @@ -143,7 +143,7 @@ private static int Height(Node? node)
/// <summary>
/// Wrapper type to allow the IntervalTreeHelpers type to work with this type.
/// </summary>
private readonly struct BinaryIntervalTreeHelper : IIntervalTreeWitness<T, MutableIntervalTree<T>, Node>
private readonly struct BinaryIntervalTreeWitness : IIntervalTreeWitness<T, MutableIntervalTree<T>, Node>
{
public T GetValue(MutableIntervalTree<T> tree, Node node)
=> node.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Microsoft.CodeAnalysis.Shared.Collections;

internal class BinaryIntervalTree
internal static class SimpleMutableIntervalTree
{
public static SimpleMutableIntervalTree<T, TIntrospector> Create<T, TIntrospector>(in TIntrospector introspector, params T[] values)
where TIntrospector : struct, IIntervalIntrospector<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@
<Compile Include="$(MSBuildThisFileDirectory)CodeStyle\ParenthesesPreference.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CodeStyle\VisualBasic\VisualBasicCodeStyleOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CodeStyle\VisualBasic\VisualBasicIdeCodeStyleOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\FlatArrayIntervalTree`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\ImmutableIntervalTree`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IIntervalTree`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IntervalTreeAlgorithms`2.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IntervalTreeHelpers.cs" />
Expand Down Expand Up @@ -289,12 +289,12 @@
<Compile Include="$(MSBuildThisFileDirectory)CodeStyle\UnusedParametersPreference.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CodeStyle\UnusedValuePreference.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IIntervalIntrospector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IntervalTree`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\IntervalTree`1.Node.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\MutableIntervalTree`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\MutableIntervalTree`1.Node.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\NormalizedTextSpanCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\SimpleIntervalTree.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\SimpleIntervalTree`2.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\TextSpanIntervalIntrospector.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\SimpleMutableIntervalTree.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\SimpleMutableIntervalTree`2.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Collections\TextSpanMutableIntervalTree.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Editing\GenerationOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AnalyzerConfigOptionsExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ChildSyntaxListExtensions.cs" />
Expand All @@ -320,7 +320,7 @@
<Compile Include="$(MSBuildThisFileDirectory)FlowAnalysis\SymbolUsageAnalysis\SymbolUsageResult.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\AbstractSyntaxFormatting.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\BottomUpBaseIndentationFinder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\ContextIntervalTree.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\ContextMutableIntervalTree.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\Context\FormattingContext.AnchorData.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\Context\FormattingContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatting\Context\FormattingContext.IndentationData.cs" />
Expand Down
Loading