From eb91373a94f4fcd5b6c563da043de437a0bb922b Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 4 Nov 2026 00:05:00 +0100 Subject: [PATCH] fix: read by-ref conversion operands as values Mark conversion operands as value reads and dereference nullable ref parameters in arithmetic/value contexts while preserving nullable member-access addresses. This prevents boxing managed addresses in array initializers. Standard and Light regressions cover primitive, reference-containing struct and nullable ref parameters, including null nullable values. Focused tests pass on net8.0, net10.0 and net472. The unfixed regression terminates the net10.0 process with AccessViolationException. Full solution builds, legacy suites and Framework runners pass. Existing master MultiplyChecked parity failures reproduce identically on net8.0/net10.0. --- .../FastExpressionCompiler.cs | 5 +- .../Program.cs | 2 + .../Program.cs | 2 + .../ByRefConversionRegressionTests.cs | 54 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/ByRefConversionRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 724109851..980cc961f 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -3237,7 +3237,8 @@ public static bool TryEmitParameter(ParameterExpression paramExpr, ILGenerator i var paramExprCount = paramExprs.GetParamCount(); var paramType = paramExpr.Type; var isValueType = paramType.IsValueType; - var isParamOrVarByRef = paramExpr.IsByRef && !paramType.IsNullable(); // for the nullable part check the #461 cases with nullable + var isParamOrVarByRef = paramExpr.IsByRef && + (!paramType.IsNullable() || (parent & (ParentFlags.Arithmetic | ParentFlags.AssignmentRightValue)) != 0); // #461 member access uses the nullable address; value operations dereference it var isPassedRef = byRefIndex != -1; // Parameter may represent a variable, so first look if this is the case, @@ -3712,7 +3713,7 @@ private static bool TryEmitConvert(UnaryExpression expr, ILGenerator il, ref Com return TryEmit(opExpr, il, ref context, parent & ~ParentFlags.InstanceAccess); // Emit the operand before going to the fast checks below - if (!TryEmit(opExpr, il, ref context, parent & ~ParentFlags.IgnoreResult & ~ParentFlags.InstanceAccess)) + if (!TryEmit(opExpr, il, ref context, (parent & ~ParentFlags.IgnoreResult & ~ParentFlags.InstanceAccess) | ParentFlags.AssignmentRightValue)) return false; if (sourceType == targetType) diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 123fb7054..5fefad4c7 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -43,6 +43,7 @@ static void RunLightExpressionTests(object state) t.Run(new LightExpression.IssueTests.Issue476_System_ExecutionEngineException_with_nullables_on_repeated_calls_to_ConcurrentDictionary()); t.Run(new LightExpression.IssueTests.Issue498_InvalidProgramException_when_using_loop()); t.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); + t.Run(new LightExpression.UnitTests.ByRefConversionRegressionTests()); t.Run(new LightExpression.UnitTests.ParityTests()); Console.WriteLine($"Just LightExpression tests are passing in {justLightTestsStopwatch.ElapsedMilliseconds} ms."); @@ -64,6 +65,7 @@ static void RunLightExpressionTests(object state) fecTests.Run(new Issue498_InvalidProgramException_when_using_loop()); fecTests.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); fecTests.Run(new ReductionProgressRegressionTests()); + fecTests.Run(new ByRefConversionRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 2a4134368..b3a5933ca 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -75,6 +75,7 @@ public static void Main(string[] args) lt.Run(new LightExpression.IssueTests.Issue476_System_ExecutionEngineException_with_nullables_on_repeated_calls_to_ConcurrentDictionary()); lt.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); lt.Run(new LightExpression.IssueTests.Issue561_NonBooleanEqualityOperator()); + lt.Run(new LightExpression.UnitTests.ByRefConversionRegressionTests()); lt.Run(new LightExpression.UnitTests.ParityTests()); ltTime = ltStopwatch.ElapsedMilliseconds; }); @@ -83,6 +84,7 @@ public static void Main(string[] args) st.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); st.Run(new Issue561_NonBooleanEqualityOperator()); st.Run(new ReductionProgressRegressionTests()); + st.Run(new ByRefConversionRegressionTests()); st.Run(new ParityTests()); st.Run(new Issue498_InvalidProgramException_when_using_loop()); st.Run(new Issue495_Incomplete_pattern_detection_for_NotSupported_1007_Return_goto_from_TryCatch_with_Assign_generates_invalid_IL()); diff --git a/test/FastExpressionCompiler.UnitTests/ByRefConversionRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/ByRefConversionRegressionTests.cs new file mode 100644 index 000000000..2e6684cf8 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/ByRefConversionRegressionTests.cs @@ -0,0 +1,54 @@ +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 ByRefConversionRegressionTests : ITestX +{ + public void Run(TestRun t) + { + By_ref_values_box_inside_array_initializers(t); + By_ref_nullable_values_box_inside_array_initializers(t); + } + + public void By_ref_values_box_inside_array_initializers(TestContext t) => Check(t, false); + public void By_ref_nullable_values_box_inside_array_initializers(TestContext t) => Check(t, true); + + private static void Check(TestContext t, bool nullable) + { + Type[] types = { typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal), typeof(Tagged) }; + object[] values = { (short)42, 42, 42L, 42f, 42d, 42m, new Tagged { Text = "value" } }; + for (var i = 0; i < types.Length; ++i) + { + var type = nullable ? typeof(Nullable<>).MakeGenericType(types[i]) : types[i]; + var delegateType = typeof(Read<>).MakeGenericType(type); + var parameter = SysExpr.Parameter(type.MakeByRefType()); + var system = SysExpr.Lambda(delegateType, SysExpr.NewArrayInit(typeof(object), + SysExpr.Convert(parameter, typeof(object)), SysExpr.Convert(parameter, typeof(object))), parameter).Compile(); + var fastParameter = Expr.Parameter(type.MakeByRefType()); + var fast = Expr.Lambda(delegateType, Expr.NewArrayInit(typeof(object), + Expr.Convert(fastParameter, typeof(object)), Expr.Convert(fastParameter, typeof(object))), fastParameter).CompileFast(true); + if (!t.IsNotNull(fast)) return; + Compare(t, system, fast, values[i]); + if (nullable) Compare(t, system, fast, null); + } + } + + private static void Compare(TestContext t, Delegate system, Delegate fast, object value) + { + var expected = (object[])system.DynamicInvoke(new[] { value }); + var actual = (object[])fast.DynamicInvoke(new[] { value }); + t.AreEqual(expected[0], actual[0]); + t.AreEqual(expected[1], actual[1]); + } + + public delegate object[] Read(ref T value); + public struct Tagged { public string Text; } +}