diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 7a05bfcb..7db05d66 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -3878,17 +3878,13 @@ private static bool TryEmitConvert(UnaryExpression expr, ILGenerator il, ref Com return il.EmitPopIfIgnoreResult(parent); } - if (targetType == typeof(object) && sourceType.IsValueType) + if (sourceType.IsValueType && (targetType == typeof(object) || + underlyingNullableSourceType != null && !targetType.IsValueType && + (targetType.IsInterface || targetType.IsAssignableFrom(underlyingNullableSourceType)))) { il.Demit(OpCodes.Box, sourceType); - return il.EmitPopIfIgnoreResult(parent); - } - - if (underlyingNullableSourceType != null && !targetType.IsValueType && - targetType.IsAssignableFrom(underlyingNullableSourceType)) - { - il.Demit(OpCodes.Box, sourceType); - il.Demit(OpCodes.Castclass, targetType); + if (targetType.IsInterface && !targetType.IsAssignableFrom(underlyingNullableSourceType ?? sourceType)) + il.Demit(OpCodes.Castclass, targetType); return il.EmitPopIfIgnoreResult(parent); } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index bd8124c2..253d48f4 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -35,6 +35,7 @@ static void RunLightExpressionTests(object state) var t = (LightExpression.TestRun)state; t.Run(new LightExpression.UnitTests.InvokeRefArgumentRegressionTests()); + t.Run(new LightExpression.UnitTests.NullableBoxingRegressionTests()); t.Run(new LightExpression.UnitTests.RuntimeSwitchCaseRegressionTests()); t.Run(new LightExpression.UnitTests.StaticFieldAddressRegressionTests()); t.Run(new LightExpression.UnitTests.MultidimensionalGetStorageRegressionTests()); @@ -84,6 +85,7 @@ static void RunLightExpressionTests(object state) var fecTests = new TestRun(); fecTests.Run(new InvokeRefArgumentRegressionTests()); + fecTests.Run(new NullableBoxingRegressionTests()); fecTests.Run(new LiftedRefConversionRegressionTests()); fecTests.Run(new RuntimeSwitchCaseRegressionTests()); fecTests.Run(new StaticFieldAddressRegressionTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 5dd58047..a5497178 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -64,6 +64,7 @@ public static void Main(string[] args) { var ltStopwatch = Stopwatch.StartNew(); lt.Run(new LightExpression.UnitTests.InvokeRefArgumentRegressionTests()); + lt.Run(new LightExpression.UnitTests.NullableBoxingRegressionTests()); lt.Run(new LightExpression.UnitTests.RuntimeSwitchCaseRegressionTests()); lt.Run(new LightExpression.UnitTests.StaticFieldAddressRegressionTests()); lt.Run(new LightExpression.UnitTests.MultidimensionalGetStorageRegressionTests()); @@ -110,6 +111,7 @@ public static void Main(string[] args) var stStopwatch = Stopwatch.StartNew(); st.Run(new InvokeRefArgumentRegressionTests()); + st.Run(new NullableBoxingRegressionTests()); st.Run(new LiftedRefConversionRegressionTests()); st.Run(new RuntimeSwitchCaseRegressionTests()); st.Run(new StaticFieldAddressRegressionTests()); diff --git a/test/FastExpressionCompiler.UnitTests/NullableBoxingRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/NullableBoxingRegressionTests.cs new file mode 100644 index 00000000..143d2c04 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/NullableBoxingRegressionTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Reflection; +using SysExpr = System.Linq.Expressions.Expression; + +#if LIGHT_EXPRESSION +using Expr = FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +using Expr = System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct NullableBoxingRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Nullable_parameters_box_to_reference_types(t); + Nullable_constants_box_to_reference_types(t); + Nullable_locals_box_to_reference_types(t); + + } + + public void Nullable_parameters_box_to_reference_types(TestContext t) => Check(t, 0); + public void Nullable_constants_box_to_reference_types(TestContext t) => Check(t, 1); + public void Nullable_locals_box_to_reference_types(TestContext t) => Check(t, 2); + + private static void Check(TestContext t, int shape) + { + Type[] types = { typeof(int), typeof(DayOfWeek), typeof(DateTime), typeof(Tagged) }; + object[] values = { 42, DayOfWeek.Friday, DateTime.MinValue, new Tagged { Text = "value" } }; + Type[] targets = { typeof(object), typeof(ValueType), typeof(Enum), typeof(IComparable), typeof(ITagged) }; + for (var i = 0; i < types.Length; ++i) + foreach (var target in targets) + foreach (var value in new[] { null, values[i] }) + foreach (var isChecked in new[] { false, true }) + { + var nullable = typeof(Nullable<>).MakeGenericType(types[i]); + var parameter = SysExpr.Parameter(nullable); + System.Linq.Expressions.UnaryExpression conversion; + try { conversion = isChecked ? SysExpr.ConvertChecked(parameter, target) : SysExpr.Convert(parameter, target); } + catch (InvalidOperationException) { continue; } + var constant = SysExpr.Constant(value, nullable); + var body = shape == 1 ? conversion.Update(constant) + : shape == 2 ? (SysExpr)SysExpr.Block(new[] { parameter }, SysExpr.Assign(parameter, constant), conversion) + : conversion; + var expression = shape == 0 ? SysExpr.Lambda(body, parameter) : SysExpr.Lambda(body); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var args = shape == 0 ? new[] { value } : Array.Empty(); + t.AreEqual(Invoke(expression.Compile(), args), Invoke(fast, args)); + } + } + + private static object Invoke(Delegate function, object[] arguments) + { + try { return function.DynamicInvoke(arguments); } + catch (TargetInvocationException e) { return e.InnerException.GetType(); } + } + + public interface ITagged { } + public struct Tagged : ITagged { public string Text; } + public class Box { public int Value; } + public struct Convertible + { + public int Value; + public static implicit operator Box(Convertible value) => new Box { Value = value.Value }; + } +}