From c62a22c429e3c6e3534a8d60cc413e2dc85bb0c4 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Mon, 9 Nov 2026 09:31:06 +0100 Subject: [PATCH] fix: preserve runtime numeric switch-case evaluation Fall back to ordered comparisons for numeric case tests that are evaluated at runtime. Stop constant-switch folding at a runtime-dependent test so its effects and first-match semantics are preserved. --- .../FastExpressionCompiler.cs | 7 ++-- .../Program.cs | 2 ++ .../Program.cs | 2 ++ .../RuntimeSwitchCaseRegressionTests.cs | 32 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/RuntimeSwitchCaseRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 0f12ead3..1025669b 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -6045,8 +6045,7 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com var testValExpr = testValues[v]; if (testValExpr is not ConstantExpression && testValExpr is not DefaultExpression) { - Debug.Assert(false, $"Not supported non-constant,non-default switch case value expression: `{testValExpr}`"); - return false; + goto OrderedDispatch; } var testValueLong = testValExpr is ConstantExpression constExpr ? ConvertValueObjectToLong(constExpr.Value) : 0L; @@ -6296,6 +6295,7 @@ private static bool TryEmitSwitch(SwitchExpression expr, ILGenerator il, ref Com } } + OrderedDispatch: var switchValueIsNullable = switchValueType.IsNullable(); Type switchNullableUnderlyingValueType = null; MethodInfo switchNullableHasValueMethod = null; @@ -8645,7 +8645,8 @@ public static bool TryFindSwitchBranch(SwitchExpression switchExpr, CompilerFlag var testValues = cases[i].TestValues; for (var j = 0; j < testValues.Count; j++) { - if (testValues[j] is not ConstantExpression testConst || testConst.Value == null) continue; + if (testValues[j] is not ConstantExpression testConst) return false; + if (testConst.Value == null) continue; if (switchValLong == ConvertValueObjectToLong(testConst.Value)) { matchedBody = cases[i].Body; return true; } } } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 684f4f78..f18a8a4e 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -34,6 +34,7 @@ static void RunLightExpressionTests(object state) var justLightTestsStopwatch = Stopwatch.StartNew(); var t = (LightExpression.TestRun)state; + t.Run(new LightExpression.UnitTests.RuntimeSwitchCaseRegressionTests()); t.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests()); t.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests()); t.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests()); @@ -73,6 +74,7 @@ static void RunLightExpressionTests(object state) var fecTestsStopwatch = Stopwatch.StartNew(); var fecTests = new TestRun(); + fecTests.Run(new RuntimeSwitchCaseRegressionTests()); fecTests.Run(new ReadonlyRefFieldRegressionTests()); fecTests.Run(new LiftedUserDefinedLogicalRegressionTests()); fecTests.Run(new UserDefinedLogicalRegressionTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 579ff0d0..5e3149f0 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -63,6 +63,7 @@ public static void Main(string[] args) var ltTask = Task.Run(() => { var ltStopwatch = Stopwatch.StartNew(); + lt.Run(new LightExpression.UnitTests.RuntimeSwitchCaseRegressionTests()); lt.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests()); lt.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests()); lt.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests()); @@ -99,6 +100,7 @@ public static void Main(string[] args) }); var stStopwatch = Stopwatch.StartNew(); + st.Run(new RuntimeSwitchCaseRegressionTests()); st.Run(new ReadonlyRefFieldRegressionTests()); st.Run(new LiftedUserDefinedLogicalRegressionTests()); st.Run(new UserDefinedLogicalRegressionTests()); diff --git a/test/FastExpressionCompiler.UnitTests/RuntimeSwitchCaseRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/RuntimeSwitchCaseRegressionTests.cs new file mode 100644 index 00000000..a8b3523a --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/RuntimeSwitchCaseRegressionTests.cs @@ -0,0 +1,32 @@ +using System; +using S = System.Linq.Expressions.Expression; +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct RuntimeSwitchCaseRegressionTests : ITestX +{ + public void Run(TestRun t) { Numeric_runtime_case_values_are_supported(t); Constant_input_does_not_skip_runtime_tests(t); } + public void Numeric_runtime_case_values_are_supported(TestContext t) + { + var value = S.Parameter(typeof(int)); var test = S.Parameter(typeof(int)); + var tree = S.Lambda>(S.Switch(value, S.Constant(-1), S.SwitchCase(S.Constant(7), S.Add(test, S.Constant(1)))), value, test); + var system = tree.Compile(); var fast = tree.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + foreach (var x in new[] { int.MinValue, -1, 0, 42, int.MaxValue }) + foreach (var y in new[] { -1, 0, 41, int.MaxValue }) t.AreEqual(system(x, y), fast(x, y)); + } + public static int Calls; + public static int Test() { ++Calls; return 42; } + public void Constant_input_does_not_skip_runtime_tests(TestContext t) + { + var tree = S.Lambda>(S.Switch(S.Constant(42), S.Constant(-1), + S.SwitchCase(S.Constant(7), S.Call(typeof(RuntimeSwitchCaseRegressionTests).GetMethod(nameof(Test)))), + S.SwitchCase(S.Constant(9), S.Constant(42)))); + var fast = tree.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + Calls = 0; t.AreEqual(7, fast()); t.AreEqual(1, Calls); + } +}