diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index e3fd7f88e..c039caaa4 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -126,6 +126,7 @@
+
@@ -199,6 +200,8 @@
+
+
diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
index c1c6070b8..2026ad906 100644
--- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
@@ -359,6 +359,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
+ [Test]
+ public async Task EnumArithmeticOutOfRange()
+ {
+ await Run();
+ }
+
[Test]
public async Task GuessAccessors()
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.cs
new file mode 100644
index 000000000..71082be55
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.cs
@@ -0,0 +1,22 @@
+// The constants are out of range for the enums' underlying types, so they must not be
+// recovered as the members whose bit patterns they happen to truncate to (Val1 in both
+// cases): the enum arithmetic would compute a different result than the IL does.
+public enum ByteEnum : byte
+{
+ Val1 = 112
+}
+public static class EnumArithmeticOutOfRange
+{
+ public static int SubtractFromUShortEnum(UShortEnum value)
+ {
+ return (int)value - -501;
+ }
+ public static int SubtractFromByteEnum(ByteEnum value)
+ {
+ return (int)value - 70000;
+ }
+}
+public enum UShortEnum : ushort
+{
+ Val1 = 65035
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.il
new file mode 100644
index 000000000..ae5f93812
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.il
@@ -0,0 +1,50 @@
+#define CORE_ASSEMBLY "System.Runtime"
+
+.assembly extern CORE_ASSEMBLY
+{
+ .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
+ .ver 4:0:0:0
+}
+
+// The constant operands below are out of range for the enums' underlying types, but their
+// low bits do match a member: -501 truncates to UShortEnum.Val1 (0xfe0b) and 70000 to
+// ByteEnum.Val1 (0x70). Reinterpreting them as those members would change the result of the
+// subtraction, so the decompiler has to keep the integer arithmetic.
+
+.class public auto ansi sealed UShortEnum
+ extends [CORE_ASSEMBLY]System.Enum
+{
+ .field public specialname rtspecialname uint16 value__
+ .field public static literal valuetype UShortEnum Val1 = uint16(0xfe0b)
+}
+
+.class public auto ansi sealed ByteEnum
+ extends [CORE_ASSEMBLY]System.Enum
+{
+ .field public specialname rtspecialname uint8 value__
+ .field public static literal valuetype ByteEnum Val1 = uint8(0x70)
+}
+
+.class public auto ansi abstract sealed beforefieldinit EnumArithmeticOutOfRange
+ extends [CORE_ASSEMBLY]System.Object
+{
+ .method public hidebysig static int32 SubtractFromUShortEnum (valuetype UShortEnum 'value') cil managed
+ {
+ .maxstack 8
+
+ ldarg.0
+ ldc.i4 -501
+ sub
+ ret
+ }
+
+ .method public hidebysig static int32 SubtractFromByteEnum (valuetype ByteEnum 'value') cil managed
+ {
+ .maxstack 8
+
+ ldarg.0
+ ldc.i4 70000
+ sub
+ ret
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs
index a1359d0c4..5db791bbc 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs
@@ -245,6 +245,39 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return byteArray[(ushort)i];
}
+ public void EnumSubtractionWithHighBitValues(UIntEnum value)
+ {
+ if (value - UIntEnum.Val1 <= 4)
+ {
+ Console.WriteLine(value - UIntEnum.Val1);
+ }
+ }
+
+ public void EnumSubtractionWithHighBitValues16(UShortEnum value)
+ {
+ // A 16-bit enum member always fits its underlying type as a positive constant,
+ // so the compiler emits it as such and enum-minus-underlying resolves right away.
+ if ((int)(value - 65035) <= 4)
+ {
+ Console.WriteLine((int)(value - 65035));
+ }
+ }
+
+ public void EnumSubtractionWithHighBitValues64(ULongEnum value)
+ {
+ if (value - ULongEnum.Val1 <= 4)
+ {
+ Console.WriteLine(value - ULongEnum.Val1);
+ }
+ }
+
+ public void EnumSubtractionWithZeroExtendedValue(ULongEnum value)
+ {
+ // Val3 is emitted as a zero-extended uint constant (ldc.i4.m1 + conv.u8), so it
+ // must not be reinterpreted as the sign-extended long -1.
+ Console.WriteLine((ulong)(value - uint.MaxValue));
+ }
+
public StringComparison EnumDiffNumber(StringComparison data)
{
return data - 1;
@@ -345,4 +378,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return (int)a == 0;
}
}
+
+ public enum UIntEnum : uint
+ {
+ Val1 = 4294966795u,
+ Val2 = 4294966799u
+ }
+
+ public enum ULongEnum : ulong
+ {
+ Val1 = 18446744073709551115uL,
+ Val2 = 18446744073709551119uL,
+ Val3 = 4294967295uL
+ }
+
+ public enum UShortEnum : ushort
+ {
+ Val1 = 65035,
+ Val2 = 65039
+ }
}
diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
index 0d884f053..cdbe7920a 100644
--- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
@@ -1711,6 +1711,26 @@ namespace ICSharpCode.Decompiler.CSharp
}
var rr = resolverWithOverflowCheck.ResolveBinaryOperator(op, left.ResolveResult, right.ResolveResult);
+ if ((rr.IsError || NullableType.GetUnderlyingType(rr.Type).GetStackType() != inst.UnderlyingResultType
+ || !IsCompatibleWithSign(rr.Type, inst.Sign))
+ && op is BinaryOperatorType.Add or BinaryOperatorType.Subtract
+ && (left.Type.Kind == TypeKind.Enum || right.Type.Kind == TypeKind.Enum))
+ {
+ // enum +/- constant did not resolve (e.g. an int constant whose numeric value
+ // does not fit the unsigned underlying type, issue #1142); the IL constant is
+ // the enum's bit pattern, so retry with it reinterpreted in the enum type
+ // before falling back to integer arithmetic with casts.
+ var adjustedLeft = AdjustConstantExpressionToType(left, right.Type);
+ var adjustedRight = AdjustConstantExpressionToType(right, left.Type);
+ var adjustedRR = resolverWithOverflowCheck.ResolveBinaryOperator(op, adjustedLeft.ResolveResult, adjustedRight.ResolveResult);
+ if (!(adjustedRR.IsError || NullableType.GetUnderlyingType(adjustedRR.Type).GetStackType() != inst.UnderlyingResultType
+ || !IsCompatibleWithSign(adjustedRR.Type, inst.Sign)))
+ {
+ left = adjustedLeft;
+ right = adjustedRight;
+ rr = adjustedRR;
+ }
+ }
if (rr.IsError || NullableType.GetUnderlyingType(rr.Type).GetStackType() != inst.UnderlyingResultType
|| !IsCompatibleWithSign(rr.Type, inst.Sign))
{
@@ -4006,7 +4026,13 @@ namespace ICSharpCode.Decompiler.CSharp
}
else if (typeHint.Kind == TypeKind.Enum || typeHint.IsKnownType(KnownTypeCode.Char) || typeHint.IsCSharpSmallIntegerType())
{
- var castRR = resolver.WithCheckForOverflow(true).ResolveCast(typeHint, rr);
+ // An IL constant is a bit pattern: converting it to an integer type at least as
+ // wide only reinterprets it, which is lossless even where the numeric value
+ // changes sign (e.g. int -501 for a uint-based enum member 0xfffffe0b, or for a
+ // ulong-based one where the IL sign-extends it with conv.i8). Only narrowing can
+ // lose information, so limit the overflow check to that case.
+ bool mayBeLossy = !rr.Type.GetStackType().IsIntegerType() || rr.Type.GetSize() > typeHint.GetSize();
+ var castRR = resolver.WithCheckForOverflow(mayBeLossy).ResolveCast(typeHint, rr);
if (castRR.IsCompileTimeConstant && !castRR.IsError)
{
rr = castRR;