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
22 changes: 15 additions & 7 deletions eng/StackExchange.Redis.Build/AsciiHash.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ static partial class bin
{
public const int Length = 3;
public const long HashCS = ...
public const long HashCI = ...
public const long HashUC = ...
public static ReadOnlySpan<byte> U8 => @"bin"u8;
public static string Text => @"bin";
public static bool IsCI(long hash, in RawResult value) => ...
public static bool IsCS(long hash, in ReadOnlySpan<byte> value) => ...
public static bool IsCS(in ReadOnlySpan<byte> value, long cs) => ...
public static bool IsCI(in RawResult value, long uc) => ...

}
```
The `CS` and `CI` are case-sensitive and case-insensitive tools, respectively.
The `CS` and `UC` are case-sensitive and case-insensitive (using upper-case) tools, respectively.

(this API is strictly an internal implementation detail, and can change at any time)

Expand All @@ -46,18 +47,18 @@ var key = ...
var hash = key.HashCS();
switch (key.Length)
{
case bin.Length when bin.Is(hash, key):
case bin.Length when bin.Is(key, hash):
// handle bin
break;
case f32.Length when f32.Is(hash, key):
case f32.Length when f32.Is(key, hash):
// handle f32
break;
}
```

The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler)
as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches
must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality.
must also perform a sequence equality check - the `Is(value, hash)` convenience method validates both hash and equality.

Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties
that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but
Expand All @@ -81,6 +82,13 @@ Now, `bin.Hash` can be supplied to a caller that takes an `AsciiHash` instance (
which then has *instance* methods for case-sensitive and case-insensitive matching; the instance already knows
the target hash and payload values.

The `AsciiHash` returned implements `IEquatable<AsciiHash>` implementing case-sensitive equality; there are
also independent case-sensitive and case-insensitive comparers available via the static
`CaseSensitiveEqualityComparer` and `CaseInsensitiveEqualityComparer` properties respectively.

Comparison values can be constructed on the fly on top of transient buffers using the constructors **that take
arrays**. Note that the other constructors may allocate on a per-usage basis.

## Enum parsing (part 1)

When identifying multiple values, an `enum` may be more convenient. Consider:
Expand Down
64 changes: 16 additions & 48 deletions eng/StackExchange.Redis.Build/AsciiHashGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ private void BuildEnumParsers(
}
else
{
NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name).Append(", out var hashCS, out var hashCI);");
NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name).Append(", out var hashCS, out var hashUC);");
}

if (string.IsNullOrEmpty(method.CaseSensitive.Name))
Expand Down Expand Up @@ -544,68 +544,36 @@ void Write(bool caseSensitive)
.ThenBy(x => x.ParseText))
{
var len = member.ParseText.Length;
AsciiHash.Hash(member.ParseText, out var hashCS, out var hashCI);
AsciiHash.Hash(member.ParseText, out var hashCS, out var hashUC);

bool valueCaseSensitive = caseSensitive || !HasCaseSensitiveCharacters(member.ParseText);

line = NewLine().Append(len);
if (valueCaseSensitive)
{
line.Append(" when hashCS is ").Append(hashCS);
if (len > AsciiHash.MaxBytesHashIsEqualityCS)
{
line.Append(" && ");
WriteValueTest(member.ParseText, true);
}
}
else
{
// optimize for "all_lower" or "ALL_UPPER" matches; "Mixed_Match" comes last
var ucText = member.ParseText.ToUpperInvariant();
var lcText = member.ParseText.ToLowerInvariant();
long hashUC = AsciiHash.HashCS(ucText), hashLC = AsciiHash.HashCS(lcText);

if (len <= AsciiHash.MaxBytesHashIsEqualityCS)
{
// note we know the lc and uc hash must be different
line.Append(" when (hashCS is ").Append(hashUC).Append(" or ").Append(hashLC)
.Append(") || (hashCI is ").Append(hashCI).Append(" && ");
WriteValueTest(member.ParseText, false);
line.Append(")");
}
else if (hashLC == hashCS && hashUC == hashCS)
{
// there are alphas, but not in the hashed portion
line.Append(" when hashCS is ").Append(hashLC).Append(" && ");
WriteValueTest(member.ParseText, false);
}
else
{
line.Append(" when (hashCS is ").Append(hashLC).Append(" && ");
WriteValueTest(lcText, true);
line.Append(") || (hashCS is ").Append(hashUC).Append(" && ");
WriteValueTest(ucText, true);
line.Append(") || (hashCI is ").Append(hashCI).Append(" && ");
WriteValueTest(member.ParseText, false);
line.Append(")");
}
line.Append(" when hashUC is ").Append(hashUC);
}
line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(",");

void WriteValueTest(string value, bool testCS)
if (len > AsciiHash.MaxBytesHashed)
{
line.Append(" && ");
var csValue = SyntaxFactory
.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(value))
SyntaxFactory.Literal(member.ParseText))
.ToFullString();

line.Append("global::RESPite.AsciiHash.")
.Append(testCS ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI))
.Append(valueCaseSensitive ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI))
.Append("(").Append(method.From.Name).Append(", ").Append(csValue);
if (method.From.IsBytes) line.Append("u8");
line.Append(")");
}

line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(",");
}

NewLine().Append("_ => (").Append(method.To.Type).Append(")").Append(method.DefaultValue)
Expand Down Expand Up @@ -717,29 +685,29 @@ private static void BuildTypeImplementations(
.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value))
.ToFullString();

AsciiHash.Hash(literal.Value, out var hashCS, out var hashCI);
AsciiHash.Hash(literal.Value, out var hashCS, out var hashUC);
NewLine().Append("static partial class ").Append(literal.Name);
NewLine().Append("{");
indent++;
NewLine().Append("public const int Length = ").Append(literal.Value.Length).Append(';');
NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';');
NewLine().Append("public const long HashCI = ").Append(hashCI).Append(';');
NewLine().Append("public const long HashUC = ").Append(hashUC).Append(';');
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
if (literal.Value.Length <= AsciiHash.MaxBytesHashIsEqualityCS)
if (literal.Value.Length <= AsciiHash.MaxBytesHashed)
{
// the case-sensitive hash enforces all the values
NewLine().Append(
"public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS & value.Length == Length;");
"public static bool IsCS(ReadOnlySpan<byte> value, long cs) => cs == HashCS & value.Length == Length;");
NewLine().Append(
"public static bool IsCI(long hash, ReadOnlySpan<byte> value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8));");
"public static bool IsCI(ReadOnlySpan<byte> value, long uc) => uc == HashUC & value.Length == Length;");
}
else
{
NewLine().Append(
"public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS && value.SequenceEqual(U8);");
"public static bool IsCS(ReadOnlySpan<byte> value, long cs) => cs == HashCS && value.SequenceEqual(U8);");
NewLine().Append(
"public static bool IsCI(long hash, ReadOnlySpan<byte> value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8);");
"public static bool IsCI(ReadOnlySpan<byte> value, long uc) => uc == HashUC && global::RESPite.AsciiHash.SequenceEqualsCI(value, U8);");
}

indent--;
Expand Down
24 changes: 16 additions & 8 deletions src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
#nullable enable
[SER004]const RESPite.Buffers.CycleBuffer.GetAnything = 0 -> int
[SER004]const RESPite.Buffers.CycleBuffer.GetFullPagesOnly = -1 -> int
[SER004]override RESPite.AsciiHash.Equals(object? other) -> bool
[SER004]override RESPite.AsciiHash.GetHashCode() -> int
[SER004]override RESPite.AsciiHash.ToString() -> string!
[SER004]RESPite.AsciiHash
[SER004]RESPite.AsciiHash.AsciiHash() -> void
[SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlyMemory<byte> value) -> void
[SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr) -> void
[SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr, int index, int length) -> void
[SER004]RESPite.AsciiHash.AsciiHash(string! value) -> void
[SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlySpan<byte> value) -> void
[SER004]RESPite.AsciiHash.BufferLength.get -> int
[SER004]RESPite.AsciiHash.IsCI(long hash, System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.AsciiHash.Equals(in RESPite.AsciiHash other) -> bool
[SER004]RESPite.AsciiHash.IsCI(System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.AsciiHash.IsCS(long hash, System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.AsciiHash.IsCS(System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.AsciiHash.Length.get -> int
[SER004]RESPite.AsciiHash.Span.get -> System.ReadOnlySpan<byte>
[SER004]RESPite.AsciiHashAttribute
[SER004]RESPite.AsciiHashAttribute.AsciiHashAttribute(string! token = "") -> void
[SER004]RESPite.AsciiHashAttribute.CaseSensitive.get -> bool
Expand Down Expand Up @@ -46,21 +51,24 @@
[SER004]RESPite.Messages.RespReader.ReadArray<TState, TResult>(ref TState state, RESPite.Messages.RespReader.Projection<TState, TResult>! projection, bool scalar = false) -> TResult[]?
[SER004]RESPite.Messages.RespReader.ReadPastArray<TState, TResult>(ref TState state, RESPite.Messages.RespReader.Projection<TState, TResult>! projection, bool scalar = false) -> TResult[]?
[SER004]RESPite.Messages.RespReader.ScalarParser<TSource, TValue>
[SER004]static RESPite.AsciiHash.CaseInsensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer<RESPite.AsciiHash>!
[SER004]static RESPite.AsciiHash.CaseSensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer<RESPite.AsciiHash>!
[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan<char> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan<char> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan<byte> value, out long cs, out long ci) -> void
[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan<char> value, out long cs, out long ci) -> void
[SER004]static RESPite.AsciiHash.HashCI(scoped System.ReadOnlySpan<byte> value) -> long
[SER004]static RESPite.AsciiHash.HashCI(scoped System.ReadOnlySpan<char> value) -> long
[SER004]static RESPite.AsciiHash.HashCS(in System.Buffers.ReadOnlySequence<byte> value) -> long
[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan<byte> value, out long cs, out long uc) -> void
[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan<char> value, out long cs, out long uc) -> void
[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan<byte> value) -> long
[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan<char> value) -> long
[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan<byte> value) -> long
[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan<char> value) -> long
[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan<char> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan<char> first, System.ReadOnlySpan<char> second) -> bool
[SER004]static RESPite.AsciiHash.ToLower(System.Span<byte> span) -> void
[SER004]static RESPite.AsciiHash.ToUpper(System.Span<byte> span) -> void
[SER004]static RESPite.Buffers.CycleBuffer.Create(System.Buffers.MemoryPool<byte>? pool = null, int pageSize = 8192) -> RESPite.Buffers.CycleBuffer
[SER004]const RESPite.Messages.RespScanState.MinBytes = 3 -> int
[SER004]override RESPite.Messages.RespScanState.Equals(object? obj) -> bool
Expand Down
37 changes: 37 additions & 0 deletions src/RESPite/Shared/AsciiHash.Comparers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace RESPite;

public readonly partial struct AsciiHash
{
public static IEqualityComparer<AsciiHash> CaseSensitiveEqualityComparer => CaseSensitiveComparer.Instance;
public static IEqualityComparer<AsciiHash> CaseInsensitiveEqualityComparer => CaseInsensitiveComparer.Instance;

private sealed class CaseSensitiveComparer : IEqualityComparer<AsciiHash>
{
private CaseSensitiveComparer() { }
public static readonly CaseSensitiveComparer Instance = new();

public bool Equals(AsciiHash x, AsciiHash y)
{
var len = x.Length;
return (len == y.Length & x._hashCS == y._hashCS)
&& (len <= MaxBytesHashed || x.Span.SequenceEqual(y.Span));
}

public int GetHashCode(AsciiHash obj) => obj._hashCS.GetHashCode();
}

private sealed class CaseInsensitiveComparer : IEqualityComparer<AsciiHash>
{
private CaseInsensitiveComparer() { }
public static readonly CaseInsensitiveComparer Instance = new();

public bool Equals(AsciiHash x, AsciiHash y)
{
var len = x.Length;
return (len == y.Length & x._hashUC == y._hashUC)
&& (len <= MaxBytesHashed || SequenceEqualsCI(x.Span, y.Span));
}

public int GetHashCode(AsciiHash obj) => obj._hashUC.GetHashCode();
}
}
72 changes: 72 additions & 0 deletions src/RESPite/Shared/AsciiHash.Instance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace RESPite;

public readonly partial struct AsciiHash : IEquatable<AsciiHash>
{
// ReSharper disable InconsistentNaming
private readonly long _hashCS, _hashUC;
// ReSharper restore InconsistentNaming
private readonly int _index, _length;
private readonly byte[] _arr;

public int Length => _length;

/// <summary>
/// The optimal buffer length (with padding) to use for this value.
/// </summary>
public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size

public ReadOnlySpan<byte> Span => new(_arr ?? [], _index, _length);

public AsciiHash(ReadOnlySpan<byte> value) : this(value.ToArray(), 0, value.Length) { }
public AsciiHash(string value) : this(Encoding.ASCII.GetBytes(value)) { }

/// <inheritdoc/>
public override int GetHashCode() => _hashCS.GetHashCode();

/// <inheritdoc/>
public override string ToString() => _length == 0 ? "" : Encoding.ASCII.GetString(_arr, _index, _length);

/// <inheritdoc/>
public override bool Equals(object? other) => other is AsciiHash hash && Equals(hash);

/// <inheritdoc cref="Equals(object)" />
public bool Equals(in AsciiHash other)
{
return (_length == other.Length & _hashCS == other._hashCS)
&& (_length <= MaxBytesHashed || Span.SequenceEqual(other.Span));
}

bool IEquatable<AsciiHash>.Equals(AsciiHash other) => Equals(other);

public AsciiHash(byte[] arr) : this(arr, 0, -1) { }

public AsciiHash(byte[] arr, int index, int length)
{
_arr = arr ?? [];
_index = index;
_length = length < 0 ? (_arr.Length - index) : length;

var span = new ReadOnlySpan<byte>(_arr, _index, _length);
Hash(span, out _hashCS, out _hashUC);
}

public bool IsCS(ReadOnlySpan<byte> value)
{
var cs = HashCS(value);
var len = _length;
if (cs != _hashCS | value.Length != len) return false;
return len <= MaxBytesHashed || Span.SequenceEqual(value);
}

public bool IsCI(ReadOnlySpan<byte> value)
{
var uc = HashUC(value);
var len = _length;
if (uc != _hashUC | value.Length != len) return false;
return len <= MaxBytesHashed || SequenceEqualsCI(Span, value);
}
}
Loading
Loading