From 74ae2506e14231e2cc258d53f40c5d7025c227f7 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 10 Nov 2026 09:11:28 +0100 Subject: [PATCH] fix: box nullable values directly for reference conversions Converting an empty nullable value to an interface can throw InvalidOperationException instead of returning null. Box the nullable itself for reference conversions, retaining runtime casts for interfaces not implemented by the underlying type. --- .../FastExpressionCompiler.cs | 14 ++-- .../Program.cs | 2 + .../Program.cs | 2 + .../NullableBoxingRegressionTests.cs | 68 +++++++++++++++++++ 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/NullableBoxingRegressionTests.cs 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 }; + } +}