Skip to content
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
20 changes: 20 additions & 0 deletions Jint.Tests/Runtime/ArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,26 @@ public void EmptyArrayCallbackMethodsValidateTheCallback(string expression)
Invoking(() => engine.Evaluate(expression)).Should().Throw<JavaScriptException>();
}

[Fact]
public void SortIsStableForEqualElements()
{
// ES2019 requires Array.prototype.sort to be stable. The element count matters: List<T>.Sort
// falls back to insertion sort at 16 elements or fewer and happens to be stable there, so a
// smaller input passes even on an unstable implementation.
const string Script = """
var items = [];
for (var i = 0; i < 32; i++) {
items.push({ order: i });
}
items.sort(function () { return 0; });
items.map(function (x) { return x.order; }).join(',');
""";

var engine = new Engine();

engine.Evaluate(Script).AsString().Should().Be(string.Join(",", Enumerable.Range(0, 32)));
}

[Fact]
public void PopWrappedGenericList()
{
Expand Down
62 changes: 62 additions & 0 deletions Jint/Extensions/Polyfills.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,72 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Jint;

internal static class Polyfills
{
#if !NET8_0_OR_GREATER
// Enumerable.Order arrived in .NET 7 and is not in netstandard2.1, so net462, netstandard2.0 and
// netstandard2.1 all need it.
//
// It deliberately does NOT delegate to OrderBy. .NET Framework's LINQ sorts with a plain quicksort
// that has neither a recursion-depth limit nor a fallback, so an inconsistent comparer makes it spin
// forever rather than terminate. A JavaScript comparison function is free to be inconsistent — the
// spec leaves the resulting order implementation-defined but still requires the sort to finish — so
// that is a reachable hang, not a theoretical one. .NET Core's introsort escapes to heapsort and is
// why the modern targets are fine. A bottom-up merge sort is stable, always O(n log n), and
// terminates for any comparer whatsoever, which is the behaviour being backfilled.
internal static IEnumerable<T> Order<T>(this IEnumerable<T> source, IComparer<T>? comparer)
{
// Copy rather than sort a caller-visible array in place; Enumerable.Order never mutates its source.
var items = source.ToArray();
if (items.Length > 1)
{
MergeSort(items, new T[items.Length], 0, items.Length, comparer ?? Comparer<T>.Default);
}

return items;
}

private static void MergeSort<T>(T[] items, T[] buffer, int start, int end, IComparer<T> comparer)
{
if (end - start <= 1)
{
return;
}

var middle = start + ((end - start) >> 1);
MergeSort(items, buffer, start, middle, comparer);
MergeSort(items, buffer, middle, end, comparer);

if (comparer.Compare(items[middle - 1], items[middle]) <= 0)
{
// Already in order, so the merge would only copy the range back onto itself.
return;
}

int left = start, right = middle, index = start;
while (left < middle && right < end)
{
// Taking the left element on a tie is what makes the sort stable.
buffer[index++] = comparer.Compare(items[left], items[right]) <= 0 ? items[left++] : items[right++];
}

while (left < middle)
{
buffer[index++] = items[left++];
}

while (right < end)
{
buffer[index++] = items[right++];
}

System.Array.Copy(buffer, start, items, start, end - start);
}
#endif

#if NETFRAMEWORK || NETSTANDARD2_0
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
internal static bool Contains(this string source, char c) => source.IndexOf(c) != -1;
Expand Down
10 changes: 1 addition & 9 deletions Jint/Native/Array/ArrayPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,16 +1494,8 @@ private JsValue Sort(JsValue thisObject, JsValue arg0)
else
{
var comparer = ArrayComparer.WithFunction(_engine, compareFn);
#if NETCOREAPP
// OrderBy is stable; List<T>.Sort is not. Stability is required by the spec since ES2019.
#if NET8_0_OR_GREATER
// Order is stable; List<T>.Sort is not. Stability is required by the spec since ES2019.
items = items.Order(comparer).ToList();
#else
items = items.OrderBy(x => x, comparer).ToList();
#endif
#else
items.Sort(comparer);
#endif
}

for (uint j = 0; j < itemCount; j++)
Expand Down