Skip to content

Commit c98eb62

Browse files
committed
Fixed bugs in InternalEnumExtensions.
1 parent 2078e67 commit c98eb62

4 files changed

Lines changed: 103 additions & 91 deletions

File tree

DomainModeling.Generator/EnumExtensions.cs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,30 +90,27 @@ public static T Unless<T>(this T enumValue, bool condition)
9090
public static bool HasFlags<T>(this T subject, T flag)
9191
where T : unmanaged, Enum
9292
{
93-
var numericSubject = GetNumericValue(subject);
94-
var numericFlag = GetNumericValue(flag);
93+
var numericSubject = GetBinaryValue(subject);
94+
var numericFlag = GetBinaryValue(flag);
9595

9696
return (numericSubject & numericFlag) == numericFlag;
9797
}
9898

9999
/// <summary>
100100
/// <para>
101-
/// Returns the numeric value of the given <paramref name="enumValue"/>.
102-
/// </para>
103-
/// <para>
104-
/// The resulting <see cref="UInt64"/> can be cast to the intended integral type, even if it is a signed type.
101+
/// Returns the binary value of the given <paramref name="enumValue"/>, contained in a <see cref="UInt64"/>.
105102
/// </para>
106103
/// </summary>
107104
[MethodImpl(MethodImplOptions.AggressiveInlining)]
108-
private static ulong GetNumericValue<T>(T enumValue)
105+
private static ulong GetBinaryValue<T>(T enumValue)
109106
where T : unmanaged, Enum
110107
{
111108
var result = 0UL;
112109

113-
// Since the actual value may be smaller than ulong's 8 bytes, we must align to the least significant byte
114-
// This way, casting the ulong back to the original type gets back the exact original bytes
115-
// On little-endian, that means aligning to the left of the bytes
116-
// On big-endian, that means aligning to the right of the bytes
110+
// TEnum could be shorter than 8 bytes
111+
// Little-endian will automatically write into the least significant bytes, since those are on the left for little-endian
112+
// For big-endian, they are on the right, so we need to look at the rightmost portion of the ulong, depending on size of TEnum
113+
// For example, a ushort TEnum will look at the rightmost 2 bytes of the ulong
117114
if (BitConverter.IsLittleEndian)
118115
Unsafe.WriteUnaligned(ref Unsafe.As<ulong, byte>(ref result), enumValue);
119116
else

DomainModeling.Tests/Enums/InternalEnumExtensionsTests.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Runtime.CompilerServices;
21
using Architect.DomainModeling.Enums;
32
using Xunit;
43

@@ -30,6 +29,7 @@ public void GetNumericValue_WithByte_ShouldReturnExpectedResult()
3029
public void GetNumericValue_WithLong_ShouldReturnExpectedResult()
3130
{
3231
Assert.Equal((Int128)(-1), LongEnum.MinusOne.GetNumericValue());
32+
Assert.Equal((Int128)(Int64.MinValue), LongEnum.Min.GetNumericValue());
3333
}
3434

3535
[Fact]
@@ -39,33 +39,35 @@ public void GetNumericValue_WithUlong_ShouldReturnExpectedResult()
3939
}
4040

4141
[Fact]
42-
public void GetBinaryValue_WithByte_ShouldCastAndUnsafeConvertToUnderlyingTypeCorrectly()
42+
public void GetBinaryValue_WithByte_ShouldCastAndGetEnumValueToUnderlyingTypeCorrectly()
4343
{
4444
var result = ByteEnum.One.GetBinaryValue();
4545
Assert.Equal(1, (byte)result);
46-
Assert.Equal(1, Unsafe.As<ulong, byte>(ref result));
47-
Assert.Equal(ByteEnum.One, Unsafe.As<ulong, ByteEnum>(ref result));
46+
Assert.Equal(ByteEnum.One, (ByteEnum)result);
47+
Assert.Equal(ByteEnum.One, InternalEnumExtensions.GetEnumValue<ByteEnum>(result));
4848
}
4949

5050
[Fact]
51-
public void GetBinaryValue_WithLong_ShouldCastAndUnsafeConvertToUnderlyingTypeCorrectly()
51+
public void GetBinaryValue_WithLong_ShouldCastAndGetEnumValueToUnderlyingTypeCorrectly()
5252
{
5353
var result1 = LongEnum.MinusOne.GetBinaryValue();
5454
var result2 = LongEnum.Min.GetBinaryValue();
5555

5656
Assert.Equal(-1, (long)result1);
57-
Assert.Equal(-1, Unsafe.As<ulong, long>(ref result1));
58-
Assert.Equal(LongEnum.MinusOne, Unsafe.As<ulong, LongEnum>(ref result1));
57+
Assert.Equal(LongEnum.MinusOne, (LongEnum)result1);
58+
Assert.Equal(LongEnum.MinusOne, InternalEnumExtensions.GetEnumValue<LongEnum>(result1));
5959

6060
Assert.Equal(Int64.MinValue, (long)result2);
61-
Assert.Equal(Int64.MinValue, Unsafe.As<ulong, long>(ref result2));
62-
Assert.Equal(LongEnum.Min, Unsafe.As<ulong, LongEnum>(ref result2));
61+
Assert.Equal(LongEnum.Min, (LongEnum)result2);
62+
Assert.Equal(LongEnum.Min, InternalEnumExtensions.GetEnumValue<LongEnum>(result2));
6363
}
6464

6565
[Fact]
66-
public void GetBinaryValue_WithUlong_ShouldCastAndUnsafeConvertToUnderlyingTypeCorrectly()
66+
public void GetBinaryValue_WithUlong_ShouldCastAndGetEnumValueToUnderlyingTypeCorrectly()
6767
{
6868
var result = UlongEnum.Max.GetBinaryValue();
69-
Assert.Equal(UInt64.MaxValue, result); // Already the type we would cast to
69+
Assert.Equal(UInt64.MaxValue, result); // Already the type we would cast to
70+
Assert.Equal(UlongEnum.Max, (UlongEnum)result);
71+
Assert.Equal(UlongEnum.Max, InternalEnumExtensions.GetEnumValue<UlongEnum>(result));
7072
}
7173
}

DomainModeling/Enums/DefinedEnum.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Diagnostics.CodeAnalysis;
2-
using System.Runtime.CompilerServices;
3-
using Architect.DomainModeling.Enums;
4-
2+
using Architect.DomainModeling.Enums;
3+
54
#pragma warning disable IDE0130 // Namespace does not match folder structure
65
namespace Architect.DomainModeling;
76

@@ -68,9 +67,9 @@ public static class UndefinedValues<TEnum>
6867
private static readonly bool IsFlags = typeof(TEnum).IsDefined(typeof(FlagsAttribute), inherit: false);
6968
internal static readonly ulong AllFlags = Enum.GetValues<TEnum>().Aggregate(0UL, (current, next) => current | next.GetBinaryValue());
7069

71-
public static TEnum UndefinedValue { get; } = ~AllFlags is var unusedBits && Unsafe.As<ulong, TEnum>(ref unusedBits) is var value && value.GetBinaryValue() != 0UL // Any bits unused?
70+
public static TEnum UndefinedValue { get; } = ~AllFlags is var unusedBits && InternalEnumExtensions.GetEnumValue<TEnum>(unusedBits) is var value && value.GetBinaryValue() is not 0UL // Any bits unused?
7271
? value
73-
: !IsFlags && InternalEnumExtensions.TryGetUndefinedValue(out value) // With all bits used, for non-flags we can still look for an individual unused value, since values are not combined
72+
: !IsFlags && InternalEnumExtensions.TryGetUndefinedValue<TEnum>(out value) // With all bits used, for non-flags we can still look for an individual unused value, since values are not combined
7473
? value
7574
: throw new NotSupportedException($"Type {typeof(TEnum).Name} does not leave any possible values undefined (or flag bits unused).");
7675
}
Lines changed: 78 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,42 @@
1+
using System.Diagnostics;
12
using System.Runtime.CompilerServices;
23

34
namespace Architect.DomainModeling.Enums;
45

56
internal static class InternalEnumExtensions
67
{
7-
private static readonly byte DefaultUndefinedValue = 191; // Greatest prime under 3/4 of Byte.MaxValue
8-
private static readonly ushort FallbackUndefinedValue = 49139; // Greatest prime under 3/4 of UInt16.MaxValue
9-
8+
private static readonly ulong DefaultUndefinedValue = 191; // Greatest prime under 3/4 of Byte.MaxValue
9+
private static readonly ulong FallbackUndefinedValue = 49139; // Greatest prime under 3/4 of UInt16.MaxValue
10+
1011
/// <summary>
1112
/// <para>
1213
/// Attempts to return one of a small set of predefined values if one is undefined for <typeparamref name="TEnum"/>.
1314
/// </para>
1415
/// <para>
15-
/// Does not accounts for the <see cref="FlagsAttribute"/>.
16+
/// Does not account for the <see cref="FlagsAttribute"/>.
1617
/// </para>
1718
/// </summary>
1819
public static bool TryGetUndefinedValueFast<TEnum>(out TEnum value)
1920
where TEnum : unmanaged, Enum
20-
{
21-
var defaultUndefined = Unsafe.As<byte, TEnum>(ref Unsafe.AsRef(in DefaultUndefinedValue));
22-
if (!Enum.IsDefined(defaultUndefined))
23-
{
24-
value = defaultUndefined;
21+
{
22+
value = GetEnumValue<TEnum>(DefaultUndefinedValue);
23+
if (!Enum.IsDefined(value))
2524
return true;
26-
}
27-
28-
var fallbackUndefined = Unsafe.As<ushort, TEnum>(ref Unsafe.AsRef(in FallbackUndefinedValue));
29-
if (Unsafe.SizeOf<TEnum>() >= 2 && !Enum.IsDefined(fallbackUndefined))
30-
{
31-
value = fallbackUndefined;
25+
26+
value = GetEnumValue<TEnum>(FallbackUndefinedValue);
27+
if (Unsafe.SizeOf<TEnum>() >= 2 && !Enum.IsDefined(value))
3228
return true;
33-
}
3429

3530
value = default;
3631
return false;
37-
}
38-
32+
}
33+
3934
/// <summary>
4035
/// <para>
4136
/// Attempts to find an undefined value for <typeparamref name="TEnum"/>.
4237
/// </para>
4338
/// <para>
44-
/// Does not accounts for the <see cref="FlagsAttribute"/>.
39+
/// Does not account for the <see cref="FlagsAttribute"/>.
4540
/// </para>
4641
/// </summary>
4742
public static bool TryGetUndefinedValue<TEnum>(out TEnum value)
@@ -51,17 +46,17 @@ public static bool TryGetUndefinedValue<TEnum>(out TEnum value)
5146
return true;
5247

5348
var values = Enum.GetValues<TEnum>();
54-
System.Diagnostics.Debug.Assert(values.Select(GetBinaryValue).Order().SequenceEqual(values.Select(GetBinaryValue)), "Enum.GetValues() was expected to return elements in binary order.");
55-
56-
// If we do not end with the binary maximum, then use that
57-
var enumBinaryMax = ~0UL >> (64 - 8 * Unsafe.SizeOf<TEnum>()); // E.g. 64-0 bits for ulong/long, 64-32 for uint/int, and so on
49+
Debug.Assert(values.Select(GetBinaryValue).Order().SequenceEqual(values.Select(GetBinaryValue)), "Enum.GetValues() was expected to return elements in binary order.");
50+
51+
// If we do not end with the binary maximum, then use that
52+
var enumBinaryMax = ~0UL >> (64 - 8 * Unsafe.SizeOf<TEnum>()); // E.g. 64-0 bits for ulong/long, 64-32 for uint/int, and so on
5853
if (values.Length == 0 || values[^1].GetBinaryValue() < enumBinaryMax)
5954
{
60-
value = Unsafe.As<ulong, TEnum>(ref enumBinaryMax);
55+
value = GetEnumValue<TEnum>(enumBinaryMax);
6156
return true;
62-
}
63-
64-
// If we do not start with the default, then use that
57+
}
58+
59+
// If we do not start with the default, then use that
6560
ulong previousValue;
6661
if ((previousValue = values[0].GetBinaryValue()) != 0UL)
6762
{
@@ -70,67 +65,86 @@ public static bool TryGetUndefinedValue<TEnum>(out TEnum value)
7065
}
7166

7267
foreach (var definedValue in values.Skip(1))
73-
{
74-
// If there is a gap between the current and previous item
68+
{
69+
// If there is a gap between the current and previous item
7570
var currentValue = definedValue.GetBinaryValue();
7671
if (currentValue > previousValue + 1)
7772
{
78-
previousValue++;
79-
value = Unsafe.As<ulong, TEnum>(ref previousValue);
73+
value = GetEnumValue<TEnum>(previousValue + 1);
8074
return true;
8175
}
8276
previousValue = currentValue;
8377
}
8478

8579
value = default;
8680
return false;
87-
}
88-
81+
}
82+
8983
/// <summary>
90-
/// Returns the numeric value of the given <paramref name="enumValue"/>.
84+
/// Returns the numeric value of the given <paramref name="enumValue"/>.
85+
/// Unlike <see cref="GetBinaryValue"/>, this retains negative values for signed enum types.
9186
/// </summary>
9287
[MethodImpl(MethodImplOptions.AggressiveInlining)]
93-
public static Int128 GetNumericValue<T>(this T enumValue)
94-
where T : unmanaged, Enum
95-
{
96-
// Optimized by JIT, as Type.GetTypeCode(T) is treated as a constant
97-
return Type.GetTypeCode(typeof(T)) switch
98-
{
99-
TypeCode.Byte => (Int128)Unsafe.As<T, byte>(ref enumValue),
100-
TypeCode.SByte => (Int128)Unsafe.As<T, sbyte>(ref enumValue),
101-
TypeCode.Int16 => (Int128)Unsafe.As<T, short>(ref enumValue),
102-
TypeCode.UInt16 => (Int128)Unsafe.As<T, ushort>(ref enumValue),
103-
TypeCode.Int32 => (Int128)Unsafe.As<T, int>(ref enumValue),
104-
TypeCode.UInt32 => (Int128)Unsafe.As<T, uint>(ref enumValue),
105-
TypeCode.Int64 => (Int128)Unsafe.As<T, long>(ref enumValue),
106-
TypeCode.UInt64 => (Int128)Unsafe.As<T, ulong>(ref enumValue),
107-
_ => default,
108-
};
109-
}
110-
88+
public static Int128 GetNumericValue<TEnum>(this TEnum enumValue)
89+
where TEnum : unmanaged, Enum
90+
{
91+
// Branches optimized away by JIT, as Type.GetEnumUnderlyingType() is [Intrinsic] and treated as a constant
92+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(byte)) return (Int128)Unsafe.As<TEnum, byte>(ref enumValue);
93+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(sbyte)) return (Int128)Unsafe.As<TEnum, sbyte>(ref enumValue);
94+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(ushort)) return (Int128)Unsafe.As<TEnum, ushort>(ref enumValue);
95+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(short)) return (Int128)Unsafe.As<TEnum, short>(ref enumValue);
96+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(uint)) return (Int128)Unsafe.As<TEnum, uint>(ref enumValue);
97+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(int)) return (Int128)Unsafe.As<TEnum, int>(ref enumValue);
98+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(ulong)) return (Int128)Unsafe.As<TEnum, ulong>(ref enumValue);
99+
if (typeof(TEnum).GetEnumUnderlyingType() == typeof(long)) return (Int128)Unsafe.As<TEnum, long>(ref enumValue);
100+
throw new UnreachableException();
101+
}
102+
111103
/// <summary>
112104
/// <para>
113105
/// Returns the binary value of the given <paramref name="enumValue"/>, contained in a <see cref="UInt64"/>.
114106
/// </para>
115-
/// <para>
116-
/// The original value's bytes can be retrieved by doing a cast or <see cref="Unsafe.As{TFrom, TTo}"/> to the original enum or underlying type.
107+
/// <para>
108+
/// This method is the inverse of <see cref="GetEnumValue"/>.
117109
/// </para>
118110
/// </summary>
119111
[MethodImpl(MethodImplOptions.AggressiveInlining)]
120-
public static ulong GetBinaryValue<T>(this T enumValue)
121-
where T : unmanaged, Enum
112+
public static ulong GetBinaryValue<TEnum>(this TEnum enumValue)
113+
where TEnum : unmanaged, Enum
122114
{
123-
var result = 0UL;
124-
125-
// Since the actual value may be smaller than ulong's 8 bytes, we must align to the least significant byte
126-
// This way, casting the ulong back to the original type gets back the exact original bytes
127-
// On little-endian, that means aligning to the left of the bytes
128-
// On big-endian, that means aligning to the right of the bytes
115+
var result = 0UL;
116+
117+
// TEnum could be shorter than 8 bytes
118+
// Little-endian will automatically write into the least significant bytes, since those are on the left for little-endian
119+
// For big-endian, they are on the right, so we need to look at the rightmost portion of the ulong, depending on size of TEnum
120+
// For example, a ushort TEnum will look at the rightmost 2 bytes of the ulong
129121
if (BitConverter.IsLittleEndian)
130122
Unsafe.WriteUnaligned(ref Unsafe.As<ulong, byte>(ref result), enumValue);
131123
else
132-
Unsafe.WriteUnaligned(ref Unsafe.Add(ref Unsafe.As<ulong, byte>(ref result), sizeof(ulong) - Unsafe.SizeOf<T>()), enumValue);
124+
Unsafe.WriteUnaligned(ref Unsafe.Add(ref Unsafe.As<ulong, byte>(ref result), sizeof(ulong) - Unsafe.SizeOf<TEnum>()), enumValue);
133125

134126
return result;
127+
}
128+
129+
/// <summary>
130+
/// <para>
131+
/// Returns the <typeparamref name="TEnum"/> enum value of the given <paramref name="binaryValue"/>.
132+
/// </para>
133+
/// <para>
134+
/// This method is the inverse of <see cref="GetBinaryValue"/>.
135+
/// </para>
136+
/// </summary>
137+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
138+
public static TEnum GetEnumValue<TEnum>(ulong binaryValue)
139+
where TEnum : unmanaged, Enum
140+
{
141+
// TEnum could be shorter than 8 bytes
142+
// Little-endian will automatically write into the least significant bytes, since those are on the left for little-endian
143+
// For big-endian, they are on the right, so we need to look at the rightmost portion of the ulong, depending on size of TEnum
144+
// For example, a ushort TEnum will look at the rightmost 2 bytes of the ulong
145+
if (BitConverter.IsLittleEndian)
146+
return Unsafe.ReadUnaligned<TEnum>(ref Unsafe.As<ulong, byte>(ref binaryValue));
147+
else
148+
return Unsafe.ReadUnaligned<TEnum>(ref Unsafe.Add(ref Unsafe.As<ulong, byte>(ref binaryValue), sizeof(ulong) - Unsafe.SizeOf<TEnum>()));
135149
}
136-
}
150+
}

0 commit comments

Comments
 (0)