Skip to content

Commit 89026ce

Browse files
Merge pull request #344 from marius-bughiu/chore/code-review/topk-alias-guard
fix(PartialSort): reject a TopK destination that overlaps its source
2 parents bd51f74 + 14ade71 commit 89026ce

4 files changed

Lines changed: 36 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ All notable changes to Celerity are documented here. This project follows [Keep
2727

2828
### Fixed
2929

30+
- `PartialSort.TopK` now throws `ArgumentException` when its `destination` overlaps its `source`, instead of silently returning a wrong answer and writing to the source it documents as untouched. Disjoint slices of one array are still accepted, matching `RadixSort` and `CountingSort`.
3031
- Eight documentation links pointed at anchors that do not exist: seven `CeleritySet` / `SwissSet` references in `docs/api/collections.md` and one in `CHANGELOG.md`. GitHub deletes `<`, `>` and `,` from a heading without substituting a separator, so `CeleritySet&lt;T, THasher&gt;` anchors as `#celeritysett-thasher`, not the `#celerityset-t-thasher` everyone writes. Closes [#339](https://github.com/marius-bughiu/Celerity/issues/339).
3132

3233
## [2.5.0] - 2026-08-02

docs/api/sorting.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ struct hashers follow. Nulls sort first under the natural order, matching `Compa
182182
| `TopK<T, TComparer>(ReadOnlySpan<T> source, Span<T> destination, TComparer comparer)` | The same under a custom order — pass a reversing comparer to take the *smallest*. |
183183

184184
**Exceptions.** `ArgumentOutOfRangeException` when `count` is negative or greater than `keys.Length`.
185+
`ArgumentException` when a `TopK` `destination` shares storage with its `source`. As everywhere else
186+
in the package, only genuine overlap is rejected — two disjoint slices of one array are fine.
185187

186188
**Properties worth relying on.**
187189

@@ -195,7 +197,8 @@ struct hashers follow. Nulls sort first under the natural order, matching `Compa
195197
an `IComparer<T>`-typed helper and so boxes a `struct` comparer on every call. If you want
196198
introsort's constant factor over a whole span, call the BCL directly.
197199
- **Not stable**, in any form.
198-
- **`TopK` never writes to `source`** and allocates nothing — the destination *is* the heap.
200+
- **`TopK` never writes to `source`** and allocates nothing — the destination *is* the heap, which
201+
is why a `destination` overlapping `source` is rejected rather than silently answered wrongly.
199202

200203
```csharp
201204
using Celerity.Sorting;

src/Celerity.Sorting/PartialSort.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public static void Sort<T, TComparer>(Span<T> keys, int count, TComparer compare
130130
/// <param name="source">The elements to scan. Not modified.</param>
131131
/// <param name="destination">Receives the top <c>destination.Length</c> elements; its length is <c>k</c>.</param>
132132
/// <returns>The number of elements written — <c>destination.Length</c>, or <c>source.Length</c> when the source is shorter.</returns>
133+
/// <exception cref="ArgumentException"><paramref name="destination"/> shares storage with <paramref name="source"/>.</exception>
133134
public static int TopK<T>(ReadOnlySpan<T> source, Span<T> destination)
134135
where T : IComparable<T> =>
135136
TopK<T, ComparableComparer<T>>(source, destination, default);
@@ -148,9 +149,15 @@ public static int TopK<T>(ReadOnlySpan<T> source, Span<T> destination)
148149
/// <param name="destination">Receives the top <c>destination.Length</c> elements; its length is <c>k</c>.</param>
149150
/// <param name="comparer">The comparer defining the order.</param>
150151
/// <returns>The number of elements written — <c>destination.Length</c>, or <c>source.Length</c> when the source is shorter.</returns>
152+
/// <exception cref="ArgumentException"><paramref name="destination"/> shares storage with <paramref name="source"/>.</exception>
151153
public static int TopK<T, TComparer>(ReadOnlySpan<T> source, Span<T> destination, TComparer comparer)
152154
where TComparer : struct, IComparer<T>
153155
{
156+
// The destination *is* the heap, so an aliasing destination would rewrite the source the
157+
// method promises not to touch and read back its own partial output — a wrong answer rather
158+
// than a failure, which is the same reason the two other sorters reject overlapping buffers.
159+
SortingGuard.RequireDistinctStorage(source, destination, nameof(destination));
160+
154161
int k = destination.Length;
155162
if (k == 0)
156163
{

src/Celerity.Tests/Sorting/SortingArgumentValidationTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,30 @@ public void Sort_ShouldNotThrow_WhenThePayloadIsADisjointSliceOfTheSameArray()
145145
Assert.Equal([1, 2, 3], buffer.Take(3));
146146
}
147147

148+
[Fact]
149+
public void TopK_ShouldThrow_WhenTheDestinationSharesStorageWithTheSource()
150+
{
151+
int[] buffer = [5, 1, 4, 2, 3];
152+
153+
var whole = Assert.Throws<ArgumentException>(
154+
() => PartialSort.TopK<int>(buffer, buffer.AsSpan()));
155+
Assert.Equal("destination", whole.ParamName);
156+
157+
var partial = Assert.Throws<ArgumentException>(
158+
() => PartialSort.TopK<int>(buffer.AsSpan(0, 4), buffer.AsSpan(3, 2)));
159+
Assert.Equal("destination", partial.ParamName);
160+
}
161+
162+
[Fact]
163+
public void TopK_ShouldSucceed_WhenTheDestinationOnlyNeighboursTheSource()
164+
{
165+
// Co-residence in one buffer is fine; only genuine overlap is rejected.
166+
int[] buffer = [5, 1, 4, 2, 0, 0];
167+
168+
Assert.Equal(2, PartialSort.TopK<int>(buffer.AsSpan(0, 4), buffer.AsSpan(4, 2)));
169+
Assert.Equal([5, 4], buffer.AsSpan(4, 2).ToArray());
170+
}
171+
148172
[Fact]
149173
public void ArgSort_ShouldThrow_WhenTheIndexBufferSharesStorageWithTheKeys()
150174
{

0 commit comments

Comments
 (0)