diff --git a/src/thirtytwo/Support/SpanExtensions.cs b/src/thirtytwo/Support/SpanExtensions.cs index e6133e5..e00593e 100644 --- a/src/thirtytwo/Support/SpanExtensions.cs +++ b/src/thirtytwo/Support/SpanExtensions.cs @@ -32,7 +32,7 @@ public static IEnumerable Split(this ReadOnlySpan span, char delim { List strings = []; SpanReader reader = new(span); - while (reader.TryReadTo(out var next, delimiter)) + while (reader.TryReadTo(delimiter, out var next)) { if (includeEmptyStrings || !next.IsEmpty) { diff --git a/src/thirtytwo/Support/SpanReader.cs b/src/thirtytwo/Support/SpanReader.cs index a5bd0d5..7dd6063 100644 --- a/src/thirtytwo/Support/SpanReader.cs +++ b/src/thirtytwo/Support/SpanReader.cs @@ -2,18 +2,32 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Windows.Support; /// -/// Simple span reader. Follows . +/// Fast stack based reader. /// -public ref struct SpanReader(ReadOnlySpan span) where T : unmanaged, IEquatable +/// +/// +/// Inspired by patterns. +/// +/// +public unsafe ref struct SpanReader(ReadOnlySpan span) where T : unmanaged, IEquatable { + private ReadOnlySpan _unread = span; public ReadOnlySpan Span { get; } = span; - public int Index { get; private set; } + public readonly ReadOnlySpan UnreadSpan => _unread; - public readonly ReadOnlySpan Remaining => Span[Index..]; + /// + /// Try to read everything up to the given . Advances the reader past the + /// if found. + /// + /// + public bool TryReadTo(T delimiter, out ReadOnlySpan span) => + TryReadTo(delimiter, advancePastDelimiter: true, out span); /// /// Try to read everything up to the given . @@ -22,23 +36,180 @@ public ref struct SpanReader(ReadOnlySpan span) where T : unmanaged, IEqua /// The delimiter to look for. /// to move past the if found. /// if the was found. - public bool TryReadTo(out ReadOnlySpan span, T delimiter, bool advancePastDelimiter = true) + public bool TryReadTo(T delimiter, bool advancePastDelimiter, out ReadOnlySpan span) { bool found = false; - ReadOnlySpan remaining = Remaining; - int index = remaining.IndexOf(delimiter); + int index = _unread.IndexOf(delimiter); + span = default; if (index != -1) { - span = index == 0 ? default : remaining[..index]; - Index += index + (advancePastDelimiter ? 1 : 0); found = true; + if (index > 0) + { + span = _unread; + UncheckedSliceTo(ref span, index); + if (advancePastDelimiter) + { + index++; + } + + UncheckedSlice(ref _unread, index, _unread.Length - index); + } + } + + return found; + } + + /// + /// Try to read the next value. + /// + public bool TryRead(out T value) + { + bool success; + + if (_unread.IsEmpty) + { + value = default; + success = false; } else + { + success = true; + value = _unread[0]; + UnsafeAdvance(1); + } + + return success; + } + + /// + /// Try to read a span of the given . + /// + public bool TryRead(int count, out ReadOnlySpan span) + { + bool success; + + if (count > _unread.Length) { span = default; + success = false; + } + else + { + success = true; + span = _unread[..count]; + UnsafeAdvance(count); } - return found; + return success; + } + + /// + /// Try to read a value of the given type. The size of the value must be evenly divisible by the size of + /// . + /// + /// + /// + /// This is just a straight copy of bits. If has methods that depend on + /// specific field value constraints this could be unsafe. + /// + /// + /// The compiler will often optimize away the struct copy if you only read from the value. + /// + /// + public bool TryRead(out TValue value) where TValue : unmanaged + { + if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0) + { + throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}."); + } + + bool success; + + if (sizeof(TValue) > _unread.Length * sizeof(T)) + { + value = default; + success = false; + } + else + { + success = true; + value = Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(_unread))); + UnsafeAdvance(sizeof(TValue) / sizeof(T)); + } + + return success; + } + + /// + /// Try to read a span of values of the given type. The size of the value must be evenly divisible by the size of + /// . + /// + /// + /// + /// This effectively does a and the same + /// caveats apply about safety. + /// + /// + public bool TryRead(int count, out ReadOnlySpan value) where TValue : unmanaged + { + if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0) + { + throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}."); + } + + bool success; + + if (sizeof(TValue) * count > _unread.Length * sizeof(T)) + { + value = default; + success = false; + } + else + { + success = true; + value = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref MemoryMarshal.GetReference(_unread)), count); + UnsafeAdvance((sizeof(TValue) / sizeof(T)) * count); + } + + return success; + } + + /// + /// Advance the reader by the given . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) => _unread = _unread[count..]; + + /// + /// Rewind the reader by the given . + /// + public void Rewind(int count) => _unread = Span[(Span.Length - _unread.Length - count)..]; + + /// + /// Reset the reader to the beginning of the span. + /// + public void Reset() => _unread = Span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UnsafeAdvance(int count) + { + Debug.Assert((uint)count <= (uint)_unread.Length); + UncheckedSlice(ref _unread, count, _unread.Length - count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncheckedSliceTo(ref ReadOnlySpan span, int length) + { + Debug.Assert((uint)length <= (uint)span.Length); + span = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(span), length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncheckedSlice(ref ReadOnlySpan span, int start, int length) + { + Debug.Assert((uint)start <= (uint)span.Length && (uint)length <= (uint)(span.Length - start)); + span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)start), length); } } \ No newline at end of file diff --git a/src/thirtytwo_tests/Support/SpanReaderTests.cs b/src/thirtytwo_tests/Support/SpanReaderTests.cs new file mode 100644 index 0000000..5b20a0c --- /dev/null +++ b/src/thirtytwo_tests/Support/SpanReaderTests.cs @@ -0,0 +1,158 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Drawing; + +namespace Windows.Support; + +public class SpanReaderTests +{ + [Fact] + public void SpanReader_TryReadTo_SkipDelimiter() + { + ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.TryReadTo(3, out var read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([1, 2]); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([4, 5]); + + reader.TryReadTo(5, out read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([4]); + reader.UnreadSpan.ToArray().Should().BeEmpty(); + } + + [Fact] + public void SpanReader_TryReadTo_DontSkipDelimiter() + { + ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.TryReadTo(3, advancePastDelimiter: false, out var read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([1, 2]); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + + reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([3, 4]); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + + reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue(); + read.ToArray().Should().BeEmpty(); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + } + + [Fact] + public void SpanReader_Advance() + { + ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.Advance(2); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + + reader.Advance(2); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + + reader.Advance(1); + reader.UnreadSpan.ToArray().Should().BeEmpty(); + + try + { + reader.Advance(1); + Assert.Fail($"Expected {nameof(ArgumentOutOfRangeException)}"); + } + catch (ArgumentOutOfRangeException) + { + // Expected + } + } + + [Fact] + public void SpanReader_Rewind() + { + ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.Advance(2); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + + reader.Rewind(1); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([2, 3, 4, 5]); + + try + { + reader.Rewind(2); + Assert.Fail($"Expected {nameof(ArgumentOutOfRangeException)}"); + } + catch (ArgumentOutOfRangeException) + { + // Expected + } + + reader.Rewind(1); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 5]); + } + + [Fact] + public void SpanReader_TryRead_ReadPoints() + { + ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.TryRead(out Point value).Should().BeTrue(); + value.Should().Be(new Point(1, 2)); + + reader.TryRead(out value).Should().BeTrue(); + value.Should().Be(new Point(3, 4)); + + reader.TryRead(out value).Should().BeFalse(); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + } + + [Fact] + public void SpanReader_TryRead_ReadPointCounts() + { + ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.TryRead(2, out ReadOnlySpan value).Should().BeTrue(); + value.ToArray().Should().BeEquivalentTo([new Point(1, 2), new Point(3, 4)]); + + // This fails to compile as the span is read only, as expected. + // value[0].X = 0; + + reader.TryRead(2, out value).Should().BeFalse(); + value.ToArray().Should().BeEmpty(); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(1, 1)] + [InlineData(2, 2)] + [InlineData(3, 2)] + public void SpanReader_TryRead_ReadPointFCounts_NotEnoughBuffer(int bufferSize, int readCount) + { + ReadOnlySpan span = new float[bufferSize]; + SpanReader reader = new(span); + + reader.TryRead(readCount, out _).Should().BeFalse(); + } + + [Fact] + public void SpanReader_TryRead_Count() + { + ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + SpanReader reader = new(span); + + reader.TryRead(2, out var read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([1, 2]); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + + reader.TryRead(2, out read).Should().BeTrue(); + read.ToArray().Should().BeEquivalentTo([3, 4]); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + + reader.TryRead(2, out read).Should().BeFalse(); + reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + } +}