From 7df4072d1d87f8c300b40b06aa1de53f0817518a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 6 Sep 2026 18:46:55 +0200 Subject: [PATCH] TransformExpressionTrees: Fix built-in comparisons with value types A hand-built Expression.Equal whose operands are a type parameter has no lambda to decompile to: `v == other` is CS0019 for a type parameter, and boxing both operands compiles but compares box identity where the tree compares values once the parameter is a value type. The conversion now declines, leaving the Expression calls that built the tree. Found in EF Core's BoolToTwoValuesConverter.ToBool, which decompiled to code that does not compile. Assisted-by: Claude:claude-opus-5:Claude Code --- .../TestCases/Correctness/ExpressionTrees.cs | 59 +++++++++++++++++++ .../IL/Transforms/TransformExpressionTrees.cs | 6 ++ 2 files changed, 65 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ExpressionTrees.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ExpressionTrees.cs index b622384eb..f7a6d3597 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ExpressionTrees.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ExpressionTrees.cs @@ -28,6 +28,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness Test(); var twice = GetExpression(Expression.Constant(2)).Compile(); Console.WriteLine(twice(21)); + TypeParameterComparisons(); + ValueTypeComparisons(); } static void Test() @@ -43,5 +45,62 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "x"); return Expression.Lambda>(Expression.Multiply(parameterExpression, factor), parameterExpression); } + + // A comparison of two type parameters, which no lambda can spell: `v == other` is CS0019 + // for a type parameter, and boxing both operands compiles but compares box identity, + // where Equal on two T compares values once T is a value type. The int instantiation + // below is what tells those two apart. + static Expression> EqualsValue(T other) + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "v"); + return Expression.Lambda>( + Expression.Equal(parameterExpression, Expression.Constant(other, typeof(T))), parameterExpression); + } + + static Expression> NotEqualsValue(T other) + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(T), "v"); + return Expression.Lambda>( + Expression.NotEqual(parameterExpression, Expression.Constant(other, typeof(T))), parameterExpression); + } + + static void TypeParameterComparisons() + { + object a = new object(); + object b = new object(); + Console.WriteLine(EqualsValue(a).Compile()(a)); + Console.WriteLine(EqualsValue(a).Compile()(b)); + Console.WriteLine(NotEqualsValue(a).Compile()(a)); + Console.WriteLine(NotEqualsValue(a).Compile()(b)); + Console.WriteLine(EqualsValue(42).Compile()(42)); + Console.WriteLine(EqualsValue(42).Compile()(43)); + } + + // The same shape on value types that are not type parameters: decimal and a nullable + // both compare as themselves, and boxing either would turn the comparison into a + // reference comparison of two boxes. + static Expression> GreaterThanDecimal(decimal limit) + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(decimal), "v"); + return Expression.Lambda>( + Expression.GreaterThan(parameterExpression, Expression.Constant(limit, typeof(decimal))), parameterExpression); + } + + static Expression> EqualsNullable(int? other) + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(int?), "v"); + return Expression.Lambda>( + Expression.Equal(parameterExpression, Expression.Constant(other, typeof(int?))), parameterExpression); + } + + static void ValueTypeComparisons() + { + Console.WriteLine(GreaterThanDecimal(1m).Compile()(2m)); + Console.WriteLine(GreaterThanDecimal(1m).Compile()(0m)); + Console.WriteLine(EqualsNullable(1).Compile()(1)); + Console.WriteLine(EqualsNullable(1).Compile()(2)); + Console.WriteLine(EqualsNullable(1).Compile()(null)); + Console.WriteLine(EqualsNullable(null).Compile()(null)); + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs index 1f9916e07..e37d552ff 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs @@ -1089,6 +1089,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms } return new Call(operatorMethod) { Arguments = { leftInst, rightInst } }; } + // A comparison of type parameters has no C# spelling: `v == other` is CS0019 for + // one, and boxing both operands would change the comparison the tree asks for - + // Equal on two T is value equality once T is a value type, box identity is not. + // Leave the tree as the Expression calls that built it. + if (leftType.Kind == TypeKind.TypeParameter || rightType.Kind == TypeKind.TypeParameter) + return null; var lifting = NullableType.IsNullable(leftType) ? ComparisonLiftingKind.CSharp : ComparisonLiftingKind.None; var utype = NullableType.GetUnderlyingType(leftType); return new Comp(kind, lifting, utype.GetStackType(), utype.GetSign(), leftInst, rightInst);