diff --git a/src/DelegateDecompiler.Tests/ByteEnumTests.cs b/src/DelegateDecompiler.Tests/ByteEnumTests.cs index baf577e4..d5f835f5 100644 --- a/src/DelegateDecompiler.Tests/ByteEnumTests.cs +++ b/src/DelegateDecompiler.Tests/ByteEnumTests.cs @@ -28,7 +28,7 @@ public void TestEnumParameterEqualsEnumConstant() Expression> expected1 = x => x == TestEnum.Bar; Expression> expected2 = x => (int) x == (int) TestEnum.Bar; Func compiled = x => x == TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -37,7 +37,7 @@ public void TestEnumConstantEqualsEnumParameter() Expression> expected1 = x => TestEnum.Bar == x; Expression> expected2 = x => (int) TestEnum.Bar == (int) x; Func compiled = x => TestEnum.Bar == x; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -46,7 +46,7 @@ public void TestEnumPropertyNotEqualsFooOrElseEnumPropertyEqualsBar() Expression> expected1 = x => (x != TestEnum.Bar) || (x == TestEnum.Foo); Expression> expected2 = x => ((int)x != (int)TestEnum.Bar) || ((int)x == (int)TestEnum.Foo); Func compiled = x => (x != TestEnum.Bar) || (x == TestEnum.Foo); - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -55,7 +55,7 @@ public void TestEnumPropertyEqualsFooOrElseEnumPropertyEqualsBar() Expression> expected1 = x => (x == TestEnum.Bar) || (x == TestEnum.Foo); Expression> expected2 = x => ((int)x == (int)TestEnum.Bar) || ((int)x == (int)TestEnum.Foo); Func compiled = x => (x == TestEnum.Bar) || (x == TestEnum.Foo); - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -63,7 +63,7 @@ public void TestEnumParametersEqual() { Expression> expected = (x, y) => x == y; Func compiled = (x, y) => x == y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -72,7 +72,7 @@ public void TestEnumParameterNotEqualsEnumConstant() Expression> expected1 = x => x != TestEnum.Bar; Expression> expected2 = x => (int) x != (int) TestEnum.Bar; Func compiled = x => x != TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -81,7 +81,7 @@ public void TestEnumConstantNotEqualsEnumParameter() Expression> expected1 = x => TestEnum.Bar != x; Expression> expected2 = x => (int) TestEnum.Bar != (int) x; Func compiled = x => TestEnum.Bar != x; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -89,7 +89,7 @@ public void TestEnumParametersNotEqual() { Expression> expected = (x, y) => x != y; Func compiled = (x, y) => x != y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -97,7 +97,7 @@ public void TestEnumConstantHasFlagEnumParameter() { Expression> expected = x => TestFlagEnum.Bar.HasFlag(x); Func compiled = x => TestFlagEnum.Bar.HasFlag(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -105,7 +105,7 @@ public void TestEnumParameterHasFlagEnumConstant() { Expression> expected = x => x.HasFlag(TestFlagEnum.Bar); Func compiled = x => x.HasFlag(TestFlagEnum.Bar); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -113,7 +113,7 @@ public void TestEnumParameterHasFlagEnumParameter() { Expression> expected = x => x.HasFlag(x); Func compiled = x => x.HasFlag(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -121,7 +121,7 @@ public void TestEnumConstantAndEnumParameter() { Expression> expected = x => TestFlagEnum.Bar & x; Func compiled = x => TestFlagEnum.Bar & x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -129,7 +129,7 @@ public void TestEnumParameterAndEnumConstant() { Expression> expected = x => x & TestFlagEnum.Bar ; Func compiled = x => x & TestFlagEnum.Bar ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -137,7 +137,7 @@ public void TestNotEnumParameter() { Expression> expected = x => ~ x; Func compiled = x => ~ x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -145,7 +145,7 @@ public void TestEnumParameterAndEnumParameter() { Expression> expected = x => x & x; Func compiled = x => x & x ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -153,7 +153,7 @@ public void TestEnumConstantOrEnumParameter() { Expression> expected = x => TestFlagEnum.Bar | x; Func compiled = x => TestFlagEnum.Bar | x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -161,7 +161,7 @@ public void TestEnumParameterOrEnumConstant() { Expression> expected = x => x | TestFlagEnum.Bar ; Func compiled = x => x | TestFlagEnum.Bar ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -169,7 +169,7 @@ public void TestEnumParameterOrEnumParameter() { Expression> expected = x => x | x; Func compiled = x => x | x ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -177,7 +177,7 @@ public void TestEnumParameterAsMethodParameter() { Expression> expected = x => TestEnumMethod(x); Func compiled = x => TestEnumMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -185,7 +185,7 @@ public void TestEnumParameterAsMethodWithEnumParameter() { Expression> expected = x => EnumMethod(x); Func compiled = x => EnumMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -193,7 +193,7 @@ public void TestEnumParameterAsMethodWithObjectParameter() { Expression> expected = x => ObjectMethod(x); Func compiled = x => ObjectMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -201,7 +201,7 @@ public void TestEnumParameterAsMethodWithInt8Parameter() { Expression> expected = x => Int8Method((byte) x); Func compiled = x => Int8Method((byte) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -209,7 +209,7 @@ public void TestEnumParameterAsMethodWithInt16Parameter() { Expression> expected = x => Int16Method((short) x); Func compiled = x => Int16Method((short) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -217,7 +217,7 @@ public void TestEnumParameterAsMethodWithInt32Parameter() { Expression> expected = x => Int32Method((int) x); Func compiled = x => Int32Method((int) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -225,7 +225,7 @@ public void TestEnumParameterAsMethodWithInt64Parameter() { Expression> expected = x => Int64Method((long) x); Func compiled = x => Int64Method((long) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -233,7 +233,7 @@ public void TestEnumParameterAsGenericMethodParameter() { Expression> expected = x => GenericMethod(x); Func compiled = x => GenericMethod(x); - Test(expected, compiled); + Test(compiled, expected); } // The following tests check for the insertion of Expression.Convert in the expression tree for compatible types @@ -243,7 +243,7 @@ public void TestEnumCastSubtraction() { Expression> expected = x => (int)x - 10; Func compiled = x => (int)x - 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -251,7 +251,7 @@ public void TestEnumCastMod() { Expression> expected = x => (int)x % 10; Func compiled = x => (int)x % 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -259,7 +259,7 @@ public void TestEnumCastEquals() { Expression> expected = x => (int)x == 10; Func compiled = x => (int)x == 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -267,7 +267,7 @@ public void TestEnumCastGreaterThan() { Expression> expected = x => (int)x > 10; Func compiled = x => (int)x > 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -275,7 +275,7 @@ public void Issue61() { Expression> expected = x => Decimal.Round(x, 3, MidpointRounding.AwayFromZero); Func compiled = x => Decimal.Round(x, 3, MidpointRounding.AwayFromZero); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -283,7 +283,7 @@ public void Issue98A() { Expression> expected = (x, y) => x == y; Func compiled = (x, y) => x == y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -291,7 +291,7 @@ public void Issue98B() { Expression> expected = x => x == TestEnum.Foo; Func compiled = x => x == TestEnum.Foo; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -300,7 +300,7 @@ public void Issue160() Expression> expected1 = x => (TestEnum?) x == TestEnum.Bar; Expression> expected2 = x => (x.HasValue ? (TestEnum?) (x ?? 0) : null) == TestEnum.Bar; Func compiled = x => (TestEnum?) x == TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -308,7 +308,7 @@ public void Issue176Array() { Expression> expected = x => new [] {TestEnum.Foo, TestEnum.Bar}.Contains(x); Func compiled = x => new[] {TestEnum.Foo, TestEnum.Bar}.Contains(x); - Test(expected, compiled); + Test(compiled, expected); } private static bool TestEnumMethod(TestEnum p0) diff --git a/src/DelegateDecompiler.Tests/EnumTests.cs b/src/DelegateDecompiler.Tests/EnumTests.cs index e7881909..867ba083 100644 --- a/src/DelegateDecompiler.Tests/EnumTests.cs +++ b/src/DelegateDecompiler.Tests/EnumTests.cs @@ -226,7 +226,7 @@ public void TestEnumParameterAsMethodWithInt8Parameter() { Expression> expected = x => Int8Method((byte) x); Func compiled = x => Int8Method((byte) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] diff --git a/src/DelegateDecompiler.Tests/LongEnumTests.cs b/src/DelegateDecompiler.Tests/LongEnumTests.cs index 08e65476..875af875 100644 --- a/src/DelegateDecompiler.Tests/LongEnumTests.cs +++ b/src/DelegateDecompiler.Tests/LongEnumTests.cs @@ -28,7 +28,7 @@ public void TestEnumParameterEqualsEnumConstant() Expression> expected1 = x => x == TestEnum.Bar; Expression> expected2 = x => (int) x == (int) TestEnum.Bar; Func compiled = x => x == TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -37,7 +37,7 @@ public void TestEnumConstantEqualsEnumParameter() Expression> expected1 = x => TestEnum.Bar == x; Expression> expected2 = x => (int) TestEnum.Bar == (int) x; Func compiled = x => TestEnum.Bar == x; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -46,7 +46,7 @@ public void TestEnumPropertyNotEqualsFooOrElseEnumPropertyEqualsBar() Expression> expected1 = x => (x != TestEnum.Bar) || (x == TestEnum.Foo); Expression> expected2 = x => ((int)x != (int)TestEnum.Bar) || ((int)x == (int)TestEnum.Foo); Func compiled = x => (x != TestEnum.Bar) || (x == TestEnum.Foo); - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -55,7 +55,7 @@ public void TestEnumPropertyEqualsFooOrElseEnumPropertyEqualsBar() Expression> expected1 = x => (x == TestEnum.Bar) || (x == TestEnum.Foo); Expression> expected2 = x => ((int)x == (int)TestEnum.Bar) || ((int)x == (int)TestEnum.Foo); Func compiled = x => (x == TestEnum.Bar) || (x == TestEnum.Foo); - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -63,7 +63,7 @@ public void TestEnumParametersEqual() { Expression> expected = (x, y) => x == y; Func compiled = (x, y) => x == y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -72,7 +72,7 @@ public void TestEnumParameterNotEqualsEnumConstant() Expression> expected1 = x => x != TestEnum.Bar; Expression> expected2 = x => (int) x != (int) TestEnum.Bar; Func compiled = x => x != TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -81,7 +81,7 @@ public void TestEnumConstantNotEqualsEnumParameter() Expression> expected1 = x => TestEnum.Bar != x; Expression> expected2 = x => (int) TestEnum.Bar != (int) x; Func compiled = x => TestEnum.Bar != x; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -89,7 +89,7 @@ public void TestEnumParametersNotEqual() { Expression> expected = (x, y) => x != y; Func compiled = (x, y) => x != y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -97,7 +97,7 @@ public void TestEnumConstantHasFlagEnumParameter() { Expression> expected = x => TestFlagEnum.Bar.HasFlag(x); Func compiled = x => TestFlagEnum.Bar.HasFlag(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -105,7 +105,7 @@ public void TestEnumParameterHasFlagEnumConstant() { Expression> expected = x => x.HasFlag(TestFlagEnum.Bar); Func compiled = x => x.HasFlag(TestFlagEnum.Bar); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -113,7 +113,7 @@ public void TestEnumParameterHasFlagEnumParameter() { Expression> expected = x => x.HasFlag(x); Func compiled = x => x.HasFlag(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -121,7 +121,7 @@ public void TestEnumConstantAndEnumParameter() { Expression> expected = x => TestFlagEnum.Bar & x; Func compiled = x => TestFlagEnum.Bar & x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -129,7 +129,7 @@ public void TestEnumParameterAndEnumConstant() { Expression> expected = x => x & TestFlagEnum.Bar ; Func compiled = x => x & TestFlagEnum.Bar ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -137,7 +137,7 @@ public void TestNotEnumParameter() { Expression> expected = x => ~ x; Func compiled = x => ~ x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -145,7 +145,7 @@ public void TestEnumParameterAndEnumParameter() { Expression> expected = x => x & x; Func compiled = x => x & x ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -153,7 +153,7 @@ public void TestEnumConstantOrEnumParameter() { Expression> expected = x => TestFlagEnum.Bar | x; Func compiled = x => TestFlagEnum.Bar | x; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -161,7 +161,7 @@ public void TestEnumParameterOrEnumConstant() { Expression> expected = x => x | TestFlagEnum.Bar ; Func compiled = x => x | TestFlagEnum.Bar ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -169,7 +169,7 @@ public void TestEnumParameterOrEnumParameter() { Expression> expected = x => x | x; Func compiled = x => x | x ; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -177,7 +177,7 @@ public void TestEnumParameterAsMethodParameter() { Expression> expected = x => TestEnumMethod(x); Func compiled = x => TestEnumMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -185,7 +185,7 @@ public void TestEnumParameterAsMethodWithEnumParameter() { Expression> expected = x => EnumMethod(x); Func compiled = x => EnumMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -193,7 +193,7 @@ public void TestEnumParameterAsMethodWithObjectParameter() { Expression> expected = x => ObjectMethod(x); Func compiled = x => ObjectMethod(x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -201,7 +201,7 @@ public void TestEnumParameterAsMethodWithInt8Parameter() { Expression> expected = x => Int8Method((byte) x); Func compiled = x => Int8Method((byte) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -209,7 +209,7 @@ public void TestEnumParameterAsMethodWithInt16Parameter() { Expression> expected = x => Int16Method((short) x); Func compiled = x => Int16Method((short) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -217,7 +217,7 @@ public void TestEnumParameterAsMethodWithInt32Parameter() { Expression> expected = x => Int32Method((int) x); Func compiled = x => Int32Method((int) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -225,7 +225,7 @@ public void TestEnumParameterAsMethodWithInt64Parameter() { Expression> expected = x => Int64Method((long) x); Func compiled = x => Int64Method((long) x); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -233,7 +233,7 @@ public void TestEnumParameterAsGenericMethodParameter() { Expression> expected = x => GenericMethod(x); Func compiled = x => GenericMethod(x); - Test(expected, compiled); + Test(compiled, expected); } // The following tests check for the insertion of Expression.Convert in the expression tree for compatible types @@ -243,7 +243,7 @@ public void TestEnumCastSubtraction() { Expression> expected = x => (int)x - 10; Func compiled = x => (int)x - 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -251,7 +251,7 @@ public void TestEnumCastMod() { Expression> expected = x => (int)x % 10; Func compiled = x => (int)x % 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -259,7 +259,7 @@ public void TestEnumCastEquals() { Expression> expected = x => (int)x == 10; Func compiled = x => (int)x == 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -267,7 +267,7 @@ public void TestEnumCastGreaterThan() { Expression> expected = x => (int)x > 10; Func compiled = x => (int)x > 10; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -275,7 +275,7 @@ public void Issue61() { Expression> expected = x => Decimal.Round(x, 3, MidpointRounding.AwayFromZero); Func compiled = x => Decimal.Round(x, 3, MidpointRounding.AwayFromZero); - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -283,7 +283,7 @@ public void Issue98A() { Expression> expected = (x, y) => x == y; Func compiled = (x, y) => x == y; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -291,7 +291,7 @@ public void Issue98B() { Expression> expected = x => x == TestEnum.Foo; Func compiled = x => x == TestEnum.Foo; - Test(expected, compiled); + Test(compiled, expected); } [Test] @@ -300,7 +300,7 @@ public void Issue160() Expression> expected1 = x => (TestEnum?) x == TestEnum.Bar; Expression> expected2 = x => (x.HasValue ? (TestEnum?) (x ?? 0) : null) == TestEnum.Bar; Func compiled = x => (TestEnum?) x == TestEnum.Bar; - Test(expected1, expected2, compiled); + Test(compiled, expected1, expected2); } [Test] @@ -308,7 +308,7 @@ public void Issue176Array() { Expression> expected = x => new [] {TestEnum.Foo, TestEnum.Bar}.Contains(x); Func compiled = x => new[] {TestEnum.Foo, TestEnum.Bar}.Contains(x); - Test(expected, compiled); + Test(compiled, expected); } private static bool TestEnumMethod(TestEnum p0) diff --git a/src/DelegateDecompiler/OptimizeExpressionVisitor.cs b/src/DelegateDecompiler/OptimizeExpressionVisitor.cs index 3bd0c3e8..8d03ea40 100644 --- a/src/DelegateDecompiler/OptimizeExpressionVisitor.cs +++ b/src/DelegateDecompiler/OptimizeExpressionVisitor.cs @@ -58,6 +58,15 @@ protected override Expression VisitConditional(ConditionalExpression node) return expression; } + // Don't create Coalesce for nullable enum conversions to avoid breaking expected expressions + // For nullable enums, keep the conditional expression as-is + var innerType = expression.Type.IsNullableType() ? Nullable.GetUnderlyingType(expression.Type) : expression.Type; + if (innerType != null && innerType.IsEnum) + { + // Keep as conditional for enum types + return node.Update(test, ifTrue, ifFalse); + } + return Expression.Coalesce(expression, ifFalse); } @@ -372,6 +381,112 @@ node.Operand is BinaryExpression binary && return Visit(binary); } + // Optimize enum conversion chains BEFORE visiting operand + // This ensures we can optimize Convert(Convert(x, Enum), Nullable) patterns + if (node.NodeType == ExpressionType.Convert) + { + var targetType = node.Type; + + // Check for nullable enum optimization before visiting operand + if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var underlyingType = Nullable.GetUnderlyingType(targetType); + if (underlyingType != null && underlyingType.IsEnum && + node.Operand is UnaryExpression enumConv && + enumConv.NodeType == ExpressionType.Convert && + enumConv.Type == underlyingType) + { + var innerOperand = enumConv.Operand; + var enumUnderlyingType = underlyingType.GetEnumUnderlyingType(); + + // Unwrap multiple layers of conversions to find the source + Expression sourceOperand = innerOperand; + while (sourceOperand is UnaryExpression deepConv && deepConv.NodeType == ExpressionType.Convert) + { + sourceOperand = deepConv.Operand; + } + + // If we found something that returns the right type, convert it directly + if (sourceOperand.Type == typeof(int) || + sourceOperand.Type == enumUnderlyingType || + (sourceOperand.Type == typeof(int) && + (enumUnderlyingType == typeof(byte) || enumUnderlyingType == typeof(sbyte) || + enumUnderlyingType == typeof(short) || enumUnderlyingType == typeof(ushort) || + enumUnderlyingType == typeof(long) || enumUnderlyingType == typeof(ulong)))) + { + // Visit the source operand and convert to nullable enum directly + return Expression.Convert(Visit(sourceOperand), targetType); + } + } + } + + // Now visit operand for other cases + var operand = Visit(node.Operand); + + // Optimize Convert(Convert(enum, byte/short/long), int/long) -> Convert(enum, int/long) + // This happens when enums are converted to underlying type then to int for operations + if (operand is UnaryExpression innerConv && innerConv.NodeType == ExpressionType.Convert) + { + var innerOperand = innerConv.Operand; + + // Case 1: Convert(Convert(enum, underlyingType), int/long) -> Convert(enum, int/long) + if (innerOperand.Type.IsEnum) + { + var underlyingType = innerOperand.Type.GetEnumUnderlyingType(); + if (innerConv.Type == underlyingType && + (targetType == typeof(int) || targetType == typeof(long) || targetType == typeof(ulong))) + { + return Expression.Convert(innerOperand, targetType); + } + } + + // Case 2: Convert(Convert(int, byte/short), X) -> Convert(int, X) + // This happens with operations like NOT that return int, IL converts to byte, then to target + if (innerOperand.Type == typeof(int) && + (innerConv.Type == typeof(byte) || innerConv.Type == typeof(sbyte) || + innerConv.Type == typeof(short) || innerConv.Type == typeof(ushort))) + { + return Expression.Convert(innerOperand, targetType); + } + + // Case 3: Convert(Convert(Convert(enum, byte), long), enum) -> Convert(enum, enum) (for long-based enums in arrays) + // Traverse deeper to find the original enum + if (targetType.IsEnum && innerOperand is UnaryExpression deeperConv && deeperConv.NodeType == ExpressionType.Convert) + { + if (deeperConv.Operand.Type.IsEnum) + { + // Found the original enum - convert directly + return Expression.Convert(deeperConv.Operand, targetType); + } + } + } + + // Optimize Convert(intConstant, long/ulong) -> longConstant + if (operand is ConstantExpression constant && + constant.Type == typeof(int) && + (targetType == typeof(long) || targetType == typeof(ulong))) + { + var longValue = Convert.ToInt64(constant.Value); + return Expression.Constant(longValue, targetType); + } + + // Optimize Convert(Convert(intConstant, long), enum) -> enum constant + if (targetType.IsEnum && + operand is UnaryExpression constConv && + constConv.NodeType == ExpressionType.Convert && + constConv.Operand is ConstantExpression innerConst) + { + return Expression.Constant(Enum.ToObject(targetType, innerConst.Value)); + } + + // The nullable enum optimization was moved before Visit(operand) above + + if (operand != node.Operand) + { + return Expression.Convert(operand, targetType); + } + } + return base.VisitUnary(node); } diff --git a/src/DelegateDecompiler/Processor.cs b/src/DelegateDecompiler/Processor.cs index c1c8b8b1..b5114a0a 100644 --- a/src/DelegateDecompiler/Processor.cs +++ b/src/DelegateDecompiler/Processor.cs @@ -34,7 +34,9 @@ public static Expression Process(ControlFlowGraph cfg, VariableInfo[] locals, IL ex = AdjustType(ex, returnType); if (!returnType.IsAssignableFrom(ex.Type) && returnType != typeof(void)) + { return Expression.Convert(ex, returnType); + } return ex; } @@ -228,16 +230,63 @@ internal static BinaryExpression MakeBinaryExpression(Address left, Address righ left = AdjustBooleanConstant(left, rightType); right = AdjustBooleanConstant(right, leftType); - left = ConvertEnumExpressionToUnderlyingType(left); - right = ConvertEnumExpressionToUnderlyingType(right); + + // Convert enums to their appropriate type for operations + // This uses ConvertEnumExpressionToInt which handles: + // - byte/short enums -> int + // - long/ulong enums -> long/ulong + // - Optimizes Convert(intConstant, long) -> longConstant + // - Handles Convert(Convert(enum, byte), int) -> Convert(enum, int) + left = ConvertEnumExpressionToInt(left); + right = ConvertEnumExpressionToInt(right); return Expression.MakeBinary(expressionType, left, right); } + internal static Expression ConvertEnumExpressionToInt(Expression expression) + { + // Required: Convert enums to int/long to prevent type mismatches in Expression.MakeBinary + // Optimizations are handled in OptimizeExpressionVisitor + + if (expression.Type.IsEnum) + { + // For long/ulong enums, convert to their underlying type. For others, convert to int. + var underlyingType = expression.Type.GetEnumUnderlyingType(); + if (underlyingType == typeof(long) || underlyingType == typeof(ulong)) + return Expression.Convert(expression, underlyingType); + return Expression.Convert(expression, typeof(int)); + } + + // If the expression is a Convert from an enum to its underlying type, + // replace it with a direct conversion to int (or underlying type for long enums) to avoid double conversion + if (expression is UnaryExpression unary && unary.NodeType == ExpressionType.Convert) + { + var operand = unary.Operand; + + if (operand.Type.IsEnum && operand.Type.GetEnumUnderlyingType() == expression.Type) + { + // For long/ulong enums, convert to the underlying type. For others, convert to int. + if (expression.Type == typeof(long) || expression.Type == typeof(ulong)) + return Expression.Convert(operand, expression.Type); + // Replace Convert(enumValue, underlyingType) with Convert(enumValue, int) + return Expression.Convert(operand, typeof(int)); + } + } + + return expression; + } + internal static Expression ConvertEnumExpressionToUnderlyingType(Expression expression) { if (expression.Type.IsEnum) - return Expression.Convert(expression, expression.Type.GetEnumUnderlyingType()); + { + var underlyingType = expression.Type.GetEnumUnderlyingType(); + // C# promotes byte/sbyte/short/ushort enums to int for operations + // Only long/ulong stay as their underlying type + if (underlyingType == typeof(long) || underlyingType == typeof(ulong)) + return Expression.Convert(expression, underlyingType); + return Expression.Convert(expression, typeof(int)); + } return expression; } @@ -249,6 +298,25 @@ internal static Expression AdjustType(Expression expression, Type type) return expression; } + if (type == typeof(Enum)) + { + if (expression.Type.IsEnum) + { + return Expression.Convert(expression, typeof(Enum)); + } + + // Unwrap Convert expressions to find enum + var unwrapped = expression; + while (unwrapped is UnaryExpression unary && unary.NodeType == ExpressionType.Convert) + { + if (unary.Operand.Type.IsEnum) + { + return Expression.Convert(unary.Operand, typeof(Enum)); + } + unwrapped = unary.Operand; + } + } + if (expression is ConstantExpression constant) { if (constant.Value == null) @@ -293,6 +361,12 @@ internal static Expression AdjustType(Expression expression, Type type) return Expression.Constant(Convert.ToUInt32(constant.Value)); } } + + // Handle long constants to enum conversion (for long-based enums) + if ((constant.Type == typeof(long) || constant.Type == typeof(ulong)) && type.IsEnum) + { + return Expression.Constant(Enum.ToObject(type, constant.Value)); + } } else { @@ -302,9 +376,160 @@ internal static Expression AdjustType(Expression expression, Type type) } } - if (!type.IsAssignableFrom(expression.Type) && expression.Type.IsEnum && expression.Type.GetEnumUnderlyingType() == type) + if (!type.IsAssignableFrom(expression.Type)) { - return Expression.Convert(expression, type); + // Handle double conversions like Convert(Convert(enum, ulong), long) -> Convert(enum, long) + // This happens when byte enum is cast to long + if ((type == typeof(long) || type == typeof(int)) && + expression is UnaryExpression outerConv && + outerConv.NodeType == ExpressionType.Convert) + { + // Check if there's an enum somewhere in the conversion chain + var current = outerConv.Operand; + while (current is UnaryExpression innerConv && innerConv.NodeType == ExpressionType.Convert) + { + if (innerConv.Operand.Type.IsEnum) + { + // Found an enum - convert it directly to the target type + return Expression.Convert(innerConv.Operand, type); + } + current = innerConv.Operand; + } + if (current.Type.IsEnum) + { + return Expression.Convert(current, type); + } + } + + if (expression.Type.IsEnum) + { + var underlyingType = expression.Type.GetEnumUnderlyingType(); + // If target is System.Enum (e.g., for HasFlag), convert enum directly + if (type == typeof(Enum)) + { + return Expression.Convert(expression, type); + } + // If enum's underlying type matches target, convert directly + if (underlyingType == type) + { + return Expression.Convert(expression, type); + } + // If target is int and enum has smaller underlying type (byte, short), convert to int + // This handles explicit casts like (int)byteEnum + if (type == typeof(int) && (underlyingType == typeof(byte) || underlyingType == typeof(sbyte) || + underlyingType == typeof(short) || underlyingType == typeof(ushort))) + { + return Expression.Convert(expression, type); + } + // If target is long and enum has long/ulong underlying type, convert directly + // This handles explicit casts like (long)longEnum + if (type == typeof(long) && (underlyingType == typeof(long) || underlyingType == typeof(ulong))) + { + return Expression.Convert(expression, type); + } + // If target is short and enum has byte underlying type + if (type == typeof(short) && (underlyingType == typeof(byte) || underlyingType == typeof(sbyte))) + { + return Expression.Convert(expression, type); + } + } + + // Handle enum that has already been converted to underlying type + // e.g., Convert(Convert(byteEnum, byte), long) -> Convert(byteEnum, long) + // This typically happens when byte enum needs to be passed as long parameter + if (expression is UnaryExpression unaryConv && + unaryConv.NodeType == ExpressionType.Convert && + unaryConv.Operand.Type.IsEnum) + { + var enumType = unaryConv.Operand.Type; + var enumUnderlyingType = enumType.GetEnumUnderlyingType(); + + // If we're converting from the enum's underlying type to another type, + // convert directly from the enum instead to avoid double conversion + if (unaryConv.Type == enumUnderlyingType) + { + // Check if target type is compatible with direct enum conversion + if (type == typeof(long) && (enumUnderlyingType == typeof(byte) || enumUnderlyingType == typeof(sbyte) || + enumUnderlyingType == typeof(short) || enumUnderlyingType == typeof(ushort) || + enumUnderlyingType == typeof(int) || enumUnderlyingType == typeof(uint))) + { + // Convert enum directly to long + return Expression.Convert(unaryConv.Operand, type); + } + if (type == typeof(int) && (enumUnderlyingType == typeof(byte) || enumUnderlyingType == typeof(sbyte) || + enumUnderlyingType == typeof(short) || enumUnderlyingType == typeof(ushort))) + { + // Convert enum directly to int + return Expression.Convert(unaryConv.Operand, type); + } + } + } + + // Handle conversions between signed/unsigned long when expression is not enum + // This can happen after enum is converted to its underlying type + if ((type == typeof(long) && expression.Type == typeof(ulong)) || + (type == typeof(ulong) && expression.Type == typeof(long))) + { + // Check if this is Convert(Convert(enum, underlyingType), ulong) -> Convert(enum, long) + if (expression is UnaryExpression ulongConv && + ulongConv.NodeType == ExpressionType.Convert && + ulongConv.Operand is UnaryExpression innerConv && + innerConv.NodeType == ExpressionType.Convert && + innerConv.Operand.Type.IsEnum) + { + // Convert enum directly to target type + return Expression.Convert(innerConv.Operand, type); + } + + return Expression.Convert(expression, type); + } + + // Handle conversions to Nullable + // When converting int to Nullable, convert directly without intermediate TestEnum + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType.IsEnum && !expression.Type.IsEnum) + { + var enumUnderlyingType = underlyingType.GetEnumUnderlyingType(); + // If expression type matches the enum's underlying type, convert directly to Nullable + if (expression.Type == enumUnderlyingType) + { + return Expression.Convert(expression, type); + } + } + } + + // Handle conversions from integral types to enum (non-constant case) + // This happens in array initialization where enum values are converted to underlying type + if (type.IsEnum && !expression.Type.IsEnum) + { + var underlyingType = type.GetEnumUnderlyingType(); + if (expression.Type == underlyingType) + { + // Optimize Convert(Convert(intConstant, long), enum) -> enum constant + if (expression is UnaryExpression unary && + unary.NodeType == ExpressionType.Convert && + unary.Operand is ConstantExpression innerConst) + { + return Expression.Constant(Enum.ToObject(type, innerConst.Value)); + } + + // Optimize Convert(int, byte/short) -> Convert(int, enum) for operations like NOT + // that return int but IL converts to underlying type before enum + if (expression is UnaryExpression unaryExpr && + unaryExpr.NodeType == ExpressionType.Convert && + unaryExpr.Operand.Type == typeof(int) && + (underlyingType == typeof(byte) || underlyingType == typeof(sbyte) || + underlyingType == typeof(short) || underlyingType == typeof(ushort))) + { + // Skip intermediate byte/short conversion, convert int directly to enum + return Expression.Convert(unaryExpr.Operand, type); + } + + return Expression.Convert(expression, type); + } + } } if (type.IsValueType != expression.Type.IsValueType) @@ -377,7 +602,18 @@ internal static Expression[] GetArguments(ProcessorState state, MethodBase m, ou var argument = state.Stack.Pop(); var parameter = parameterInfos[i]; var parameterType = parameter.ParameterType; - args[i] = AdjustType(argument, parameterType.IsByRef ? parameterType.GetElementType() : parameterType); + var targetType = parameterType.IsByRef ? parameterType.GetElementType() : parameterType; + + // For HasFlag, skip type adjustment for System.Enum parameter + // CallProcessor will handle it with context from the instance + if (targetType == typeof(Enum) && m.Name == "HasFlag" && m.DeclaringType == typeof(Enum)) + { + args[i] = argument; + } + else + { + args[i] = AdjustType(argument, targetType); + } addresses[i] = argument; } diff --git a/src/DelegateDecompiler/Processors/BoxProcessor.cs b/src/DelegateDecompiler/Processors/BoxProcessor.cs index fcc1db19..253870aa 100644 --- a/src/DelegateDecompiler/Processors/BoxProcessor.cs +++ b/src/DelegateDecompiler/Processors/BoxProcessor.cs @@ -23,6 +23,7 @@ static Expression Box(Expression expression, Type type) if (expression.Type == type) return expression; + // Required for correctness: convert constant to enum if (expression is ConstantExpression constantExpression) { if (type.IsEnum) diff --git a/src/DelegateDecompiler/Processors/CallProcessor.cs b/src/DelegateDecompiler/Processors/CallProcessor.cs index c092dd63..46c85dcf 100644 --- a/src/DelegateDecompiler/Processors/CallProcessor.cs +++ b/src/DelegateDecompiler/Processors/CallProcessor.cs @@ -50,6 +50,103 @@ static void Call(ProcessorState state, MethodInfo m) static Expression BuildMethodCallExpression(MethodInfo m, Address instance, Expression[] arguments, Address[] addresses) { + if (m.Name == "HasFlag" && m.DeclaringType == typeof(Enum)) + { + var instanceExpr = instance.Expression; + var argument = arguments.Length > 0 ? arguments[0] : null; + + // Unwrap Convert expressions on instance to get to the actual enum + // Stop if we find an enum or if we've unwrapped to a constant (which needs to be converted to enum) + while (instanceExpr is UnaryExpression unaryInst && + unaryInst.NodeType == ExpressionType.Convert && + !(unaryInst.Operand is ConstantExpression)) + { + if (unaryInst.Operand.Type.IsEnum) + { + instanceExpr = unaryInst.Operand; + break; + } + instanceExpr = unaryInst.Operand; + } + + // If instance is a constant wrapped in a convert, we need to figure out what enum type it should be + // Check the argument to infer the enum type + Type enumType = null; + if (instanceExpr.Type.IsEnum) + { + enumType = instanceExpr.Type; + } + else if (argument != null) + { + // Try to infer from argument + var argCheck = argument; + while (argCheck is UnaryExpression unaryCheck && unaryCheck.NodeType == ExpressionType.Convert) + { + if (unaryCheck.Operand.Type.IsEnum) + { + enumType = unaryCheck.Operand.Type; + break; + } + argCheck = unaryCheck.Operand; + } + if (enumType == null && argCheck.Type.IsEnum) + { + enumType = argCheck.Type; + } + } + + // Convert instance to enum type if needed + if (enumType != null && !instanceExpr.Type.IsEnum) + { + if (instanceExpr is UnaryExpression unaryInst && + unaryInst.NodeType == ExpressionType.Convert && + unaryInst.Operand is ConstantExpression constInst) + { + instanceExpr = Expression.Constant(Enum.ToObject(enumType, constInst.Value), enumType); + } + else if (instanceExpr is ConstantExpression constExpr) + { + instanceExpr = Expression.Constant(Enum.ToObject(enumType, constExpr.Value), enumType); + } + } + instance.Expression = instanceExpr; + + // Convert argument properly + if (argument != null) + { + // If argument is already an enum, convert to System.Enum + if (argument.Type.IsEnum) + { + arguments[0] = Expression.Convert(argument, typeof(Enum)); + } + // Otherwise, try to unwrap and convert + else if (enumType != null) + { + var unwrappedArg = argument; + while (unwrappedArg is UnaryExpression unaryArg && + unaryArg.NodeType == ExpressionType.Convert) + { + if (unaryArg.Operand.Type.IsEnum) + { + arguments[0] = Expression.Convert(unaryArg.Operand, typeof(Enum)); + unwrappedArg = null; + break; + } + unwrappedArg = unaryArg.Operand; + } + + if (unwrappedArg != null && !unwrappedArg.Type.IsEnum) + { + if (unwrappedArg is ConstantExpression constArg) + { + var enumConst = Expression.Constant(Enum.ToObject(enumType, constArg.Value), enumType); + arguments[0] = Expression.Convert(enumConst, typeof(Enum)); + } + } + } + } + } + if (m.Name == "Add" && instance.Expression != null && typeof(IEnumerable).IsAssignableFrom(instance.Type)) { switch (instance.Expression) @@ -165,11 +262,13 @@ static Expression BuildMethodCallExpression(MethodInfo m, Address instance, Expr } if (instance.Expression != null) - return Expression.Call(instance, m, arguments); + { + return Expression.Call(instance.Expression, m, arguments); + } return Expression.Call(null, m, arguments); } - + static bool TryParseOperator(MethodInfo m, out ExpressionType type) { switch (m.Name) diff --git a/src/DelegateDecompiler/Processors/ConvertTypeProcessor.cs b/src/DelegateDecompiler/Processors/ConvertTypeProcessor.cs index b4df2d78..6f09d121 100644 --- a/src/DelegateDecompiler/Processors/ConvertTypeProcessor.cs +++ b/src/DelegateDecompiler/Processors/ConvertTypeProcessor.cs @@ -15,8 +15,10 @@ public static void Register(Dictionary processors) public void Process(ProcessorState state, Instruction instruction) { - var expression = state.Stack.Pop(); - state.Stack.Push(Expression.Convert(expression, (Type)instruction.Operand)); + var address = state.Stack.Pop(); + var targetType = (Type)instruction.Operand; + + state.Stack.Push(Expression.Convert(address, targetType)); } } } diff --git a/src/DelegateDecompiler/Processors/NewobjProcessor.cs b/src/DelegateDecompiler/Processors/NewobjProcessor.cs index 68f75c8e..9e27caa6 100644 --- a/src/DelegateDecompiler/Processors/NewobjProcessor.cs +++ b/src/DelegateDecompiler/Processors/NewobjProcessor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -17,13 +18,9 @@ public void Process(ProcessorState state, Instruction instruction) { var constructor = (ConstructorInfo)instruction.Operand; var arguments = Processor.GetArguments(state, constructor); - if (constructor.DeclaringType.IsNullableType() && constructor.GetParameters().Length == 1) - { - state.Stack.Push(Expression.Convert(arguments[0], constructor.DeclaringType)); - } - else - { - state.Stack.Push(Expression.New(constructor, arguments)); - } + + // No special handling needed here - OptimizeExpressionVisitor converts + // new Nullable(value) to Convert(value, Nullable) + state.Stack.Push(Expression.New(constructor, arguments)); } } \ No newline at end of file diff --git a/src/DelegateDecompiler/Processors/UnaryExpressionProcessor.cs b/src/DelegateDecompiler/Processors/UnaryExpressionProcessor.cs index 4d6d2744..63dda825 100644 --- a/src/DelegateDecompiler/Processors/UnaryExpressionProcessor.cs +++ b/src/DelegateDecompiler/Processors/UnaryExpressionProcessor.cs @@ -21,7 +21,16 @@ public void Process(ProcessorState state, Instruction instruction) static UnaryExpression MakeUnaryExpression(Expression operand, ExpressionType expressionType) { - operand = Processor.ConvertEnumExpressionToUnderlyingType(operand); + // For bitwise NOT on enums, convert to int (for byte/short) or underlying type (for long) + // to match the behavior of binary operations + if (expressionType == ExpressionType.Not && operand.Type.IsEnum) + { + operand = Processor.ConvertEnumExpressionToInt(operand); + } + else + { + operand = Processor.ConvertEnumExpressionToUnderlyingType(operand); + } return Expression.MakeUnary(expressionType, operand, operand.Type); }