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;