From ec179de75d7d07592b772489089f06245973162e Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 3 Nov 2026 14:02:07 +0100 Subject: [PATCH 1/2] fix: preserve non-runtime Type constant identity Keep TypeDelegator and unfinished TypeBuilder objects in the existing constants closure rather than emitting ldtoken for them. Keep runtime Type token emission and the ordinary constant classification order. Add shared Standard/Light identity and runtime-token regressions. --- .../FastExpressionCompiler.cs | 3 +- .../Program.cs | 2 ++ .../Program.cs | 2 ++ .../RuntimeTypeConstantRegressionTests.cs | 34 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 test/FastExpressionCompiler.UnitTests/RuntimeTypeConstantRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 724109851..353ca1479 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -1107,7 +1107,8 @@ public static Action /// Helps to identify constants as the one to be put into the Closure public static bool IsClosureBoundConstant(object value, Type type) => value is Delegate || type.IsArray || - !type.IsPrimitive && !type.IsEnum && value is string == false && value is Type == false && value is decimal == false; + !type.IsPrimitive && !type.IsEnum && value is string == false && + (value is Type == false || type != typeof(object).GetType()) && value is decimal == false; public enum Result { diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 123fb7054..eaeefdf4f 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.RuntimeTypeConstantRegressionTests()); 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 RuntimeTypeConstantRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 2a4134368..7a867a16d 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.RuntimeTypeConstantRegressionTests()); 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 RuntimeTypeConstantRegressionTests()); 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/RuntimeTypeConstantRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/RuntimeTypeConstantRegressionTests.cs new file mode 100644 index 000000000..59482f1a0 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/RuntimeTypeConstantRegressionTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; +#if LIGHT_EXPRESSION +using E = FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +using E = System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.UnitTests; +#endif +public struct RuntimeTypeConstantRegressionTests : ITestX +{ + public void Run(TestRun t) { Nonruntime_type_constants_preserve_identity(t); Runtime_type_constants_keep_token_semantics(t); } + public void Nonruntime_type_constants_preserve_identity(TestContext t) + { + var module = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("FecTypeConstant"), AssemblyBuilderAccess.Run).DefineDynamicModule("M"); + foreach (Type value in new Type[] { new TypeDelegator(typeof(int)), module.DefineType("Unfinished") }) + { + var expression = E.Lambda>(E.Constant(value, typeof(Type))); + var fast = expression.CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(true, ReferenceEquals(value, fast())); + var repeated = E.Lambda>(E.ReferenceEqual(E.Constant(value, typeof(Type)), E.Constant(value, typeof(Type)))).CompileFast(true); + if (t.IsNotNull(repeated)) t.AreEqual(true, repeated()); + } + } + public void Runtime_type_constants_keep_token_semantics(TestContext t) + { + foreach (var value in new Type[] { null, typeof(int), typeof(Func<>), typeof(Func<>).GetGenericArguments()[0], typeof(int).MakeByRefType(), typeof(int[,]) }) + { + var fast = E.Lambda>(E.Constant(value, typeof(Type))).CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(true, ReferenceEquals(value, fast())); + } + } +} From 55dca610ca01e4c68977a6bc416ccfd94cec67bf Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 3 Nov 2026 23:37:23 +0100 Subject: [PATCH 2/2] refactor: clarify runtime type constant classification Cache the concrete runtime Type implementation once and explain why TypeDelegator and TypeBuilder constants must stay in the closure to preserve identity, as requested in the review of #580. Verified Standard and Light identity regressions on net8.0, net10.0 and net472; full Release build passes. Legacy suites and Framework runner pass. Existing master nullable MultiplyChecked parity failures remain unchanged. Measured control allocations and decoded IL are unchanged. --- src/FastExpressionCompiler/FastExpressionCompiler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 353ca1479..8ee51c680 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -1104,11 +1104,15 @@ public static Action (t1, t2, t3, t4, t5, t6, t7, t8, t9, t10) => f(c, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10); } + // Only the real RuntimeType can be emitted via ldtoken; TypeDelegator/TypeBuilder + // must stay in the closure to preserve identity. + private static readonly Type RuntimeType = typeof(object).GetType(); + /// Helps to identify constants as the one to be put into the Closure public static bool IsClosureBoundConstant(object value, Type type) => value is Delegate || type.IsArray || !type.IsPrimitive && !type.IsEnum && value is string == false && - (value is Type == false || type != typeof(object).GetType()) && value is decimal == false; + (value is Type == false || type != RuntimeType) && value is decimal == false; public enum Result {