From c9a4b19e230df01cf4a360f8e6d02ccda0c96f83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 06:54:50 +0000 Subject: [PATCH 1/4] Initial plan From 16bd44fff8925e85296b06609ea170c7e2e80a0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 07:13:41 +0000 Subject: [PATCH 2/4] Add compile-time branch elimination for null/default equality in conditional expressions - Extend TryReduceConditional to eliminate branches when test is null/default equality - Add IsNullDefault helper to check if a type's default value is null - Add TryReduceConditional call in the emit phase as fallback for non-primitive comparisons - Add TryReduceConditional call in the collect phase to skip dead branch closure collection - Add tests covering null==Default, Default==null, Default==Default, and nullable cases Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/babb0376-0bd1-4f18-aaef-62de01a6b8f9 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 43 +++++- ...ical_expressions_during_the_compilation.cs | 129 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 24caf5dd..c9277bde 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -1508,6 +1508,15 @@ public static Result TryCollectInfo(ref ClosureInfo closure, Expression expr, } case ExpressionType.Conditional: var condExpr = (ConditionalExpression)expr; + // Try structural branch elimination - skip collecting dead branch info + { + var reducedCond = Tools.TryReduceConditional(condExpr); + if (!ReferenceEquals(reducedCond, condExpr)) + { + expr = reducedCond; + continue; + } + } if ((r = TryCollectInfo(ref closure, condExpr.Test, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK || (r = TryCollectInfo(ref closure, condExpr.IfFalse, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK) return r; @@ -2247,6 +2256,15 @@ public static bool TryEmit(Expression expr, expr = testIsTrue ? condExpr.IfTrue : condExpr.IfFalse; continue; // no recursion, just continue with the left or right side of condition } + // Try structural branch elimination (e.g., null == Default(X) → always true/false) + { + var reducedCond = Tools.TryReduceConditional(condExpr); + if (!ReferenceEquals(reducedCond, condExpr)) + { + expr = reducedCond; + continue; + } + } return TryEmitConditional(testExpr, condExpr.IfTrue, condExpr.IfFalse, paramExprs, il, ref closure, setup, parent); case ExpressionType.PostIncrementAssign: @@ -8591,7 +8609,9 @@ public static Expression TryReduceConditional(ConditionalExpression condExpr) var testExpr = TryReduceConditionalTest(condExpr.Test); if (testExpr is BinaryExpression bi && (bi.NodeType == ExpressionType.Equal || bi.NodeType == ExpressionType.NotEqual)) { - if (bi.Left is ConstantExpression lc && bi.Right is ConstantExpression rc) + var left = bi.Left; + var right = bi.Right; + if (left is ConstantExpression lc && right is ConstantExpression rc) { #if INTERPRETATION_DIAGNOSTICS Console.WriteLine("//Reduced Conditional in Interpretation: " + condExpr); @@ -8601,12 +8621,33 @@ public static Expression TryReduceConditional(ConditionalExpression condExpr) ? (equals ? condExpr.IfTrue : condExpr.IfFalse) : (equals ? condExpr.IfFalse: condExpr.IfTrue); } + + // Handle compile-time branch elimination for null/default equality: + // e.g. Constant(null) == Default(typeof(X)) or Default(typeof(X)) == Constant(null) + // where X is a reference, interface, or nullable type - both represent null, so they are always equal + var leftIsNull = left is ConstantExpression lnc && lnc.Value == null || + left is DefaultExpression lde && IsNullDefault(lde.Type); + var rightIsNull = right is ConstantExpression rnc && rnc.Value == null || + right is DefaultExpression rde && IsNullDefault(rde.Type); + if (leftIsNull && rightIsNull) + { +#if INTERPRETATION_DIAGNOSTICS + Console.WriteLine("//Reduced Conditional (null/default equality) in Interpretation: " + condExpr); +#endif + // both sides represent null, so they are equal + return bi.NodeType == ExpressionType.Equal ? condExpr.IfTrue : condExpr.IfFalse; + } } return testExpr is ConstantExpression constExpr && constExpr.Value is bool testBool ? (testBool ? condExpr.IfTrue : condExpr.IfFalse) : condExpr; } + + // Returns true if the type's default value is null (reference types, interfaces, and Nullable) + [MethodImpl((MethodImplOptions)256)] + internal static bool IsNullDefault(Type type) => + type.IsClass || type.IsInterface || Nullable.GetUnderlyingType(type) != null; } [RequiresUnreferencedCode(Trimming.Message)] diff --git a/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs b/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs index 4eaf22a8..f7a5d03d 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs @@ -16,6 +16,12 @@ public void Run(TestRun t) { Logical_expression_started_with_not_Without_Interpreter_due_param_use(t); Logical_expression_started_with_not(t); + Condition_with_null_constant_equal_to_default_of_class_type_is_eliminated(t); + Condition_with_default_class_type_equal_to_null_constant_is_eliminated(t); + Condition_with_two_defaults_of_class_type_is_eliminated(t); + Condition_with_not_equal_null_and_default_of_class_type_is_eliminated(t); + Condition_with_nullable_default_equal_to_null_is_eliminated(t); + Condition_with_null_constant_equal_to_non_null_constant_is_not_eliminated(t); } public void Logical_expression_started_with_not(TestContext t) @@ -58,4 +64,127 @@ public void Logical_expression_started_with_not_Without_Interpreter_due_param_us t.IsFalse(ff(true)); t.IsTrue(ff(false)); } + + // Branch elimination: Constant(null) == Default(typeof(X)) where X is a class → always true + // Models the AutoMapper pattern: after inlining a null argument into a null-check lambda + public void Condition_with_null_constant_equal_to_default_of_class_type_is_eliminated(TestContext t) + { + // Condition(Equal(Constant(null), Default(typeof(string))), Constant("trueBranch"), Constant("falseBranch")) + // Since null == default(string) is always true, this should reduce to "trueBranch" + var expr = Lambda>( + Condition( + Equal(Constant(null, typeof(string)), Default(typeof(string))), + Constant("trueBranch"), + Constant("falseBranch"))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("trueBranch", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("trueBranch", ff()); + } + + // Branch elimination: Default(typeof(X)) == Constant(null) where X is a class → always true (symmetric) + public void Condition_with_default_class_type_equal_to_null_constant_is_eliminated(TestContext t) + { + var expr = Lambda>( + Condition( + Equal(Default(typeof(string)), Constant(null, typeof(string))), + Constant("trueBranch"), + Constant("falseBranch"))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("trueBranch", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("trueBranch", ff()); + } + + // Branch elimination: Default(typeof(X)) == Default(typeof(X)) where X is a class → always true + public void Condition_with_two_defaults_of_class_type_is_eliminated(TestContext t) + { + var expr = Lambda>( + Condition( + Equal(Default(typeof(string)), Default(typeof(string))), + Constant("trueBranch"), + Constant("falseBranch"))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("trueBranch", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("trueBranch", ff()); + } + + // Branch elimination: Constant(null) != Default(typeof(X)) where X is a class → always false + public void Condition_with_not_equal_null_and_default_of_class_type_is_eliminated(TestContext t) + { + var expr = Lambda>( + Condition( + NotEqual(Constant(null, typeof(string)), Default(typeof(string))), + Constant("trueBranch"), + Constant("falseBranch"))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("falseBranch", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("falseBranch", ff()); + } + + // Branch elimination: Constant(null) == Default(typeof(int?)) → always true (null == default(int?) is null == null) + public void Condition_with_nullable_default_equal_to_null_is_eliminated(TestContext t) + { + var expr = Lambda>( + Condition( + Equal(Constant(null, typeof(int?)), Default(typeof(int?))), + Constant(42, typeof(int?)), + Constant(0, typeof(int?)))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual(42, fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual(42, ff()); + } + + // Sanity check: Constant(null) == Constant("hello") should NOT be eliminated (false, not a null-null case) + public void Condition_with_null_constant_equal_to_non_null_constant_is_not_eliminated(TestContext t) + { + var expr = Lambda>( + Condition( + Equal(Constant(null, typeof(string)), Constant("hello", typeof(string))), + Constant("trueBranch"), + Constant("falseBranch"))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("falseBranch", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("falseBranch", ff()); + } } \ No newline at end of file From d4423e8735e4595df0293dea37902f92b5079fbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:01:07 +0000 Subject: [PATCH 3/4] Add switch branch elimination (#489): TryFindSwitchBranch in Interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Interpreter.TryFindSwitchBranch: detects constant/interpretable switch values and returns the matching case body, eliminating dead case branches at compile time - Supports integer/enum/char (constant + arithmetic via TryInterpretInt) and string - Wire into both collect phase (skips dead branch closure collection) and emit phase - Refactor: move ConvertValueObjectToLong from private EmittingVisitor to internal Interpreter - Add PValueToLong helper in Interpreter for PValue → long conversion - Add 5 tests: int, string, enum, no-match→default, interpreted arithmetic switch values Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/a41fbeeb-293f-421d-93ad-a1b8351344ee Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 148 +++++++++++++++--- ...ical_expressions_during_the_compilation.cs | 113 +++++++++++++ 2 files changed, 241 insertions(+), 20 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index c9277bde..8c017e77 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -1613,6 +1613,16 @@ public static Result TryCollectInfo(ref ClosureInfo closure, Expression expr, case ExpressionType.Switch: var switchExpr = ((SwitchExpression)expr); + // Compile-time switch branch elimination (#489): if switch value is interpretable, collect only the matching branch + if (Interpreter.TryFindSwitchBranch(switchExpr, flags, out var switchMatchedBody)) + { + if (switchMatchedBody != null) + { + expr = switchMatchedBody; + continue; + } + return r; // no matched body and no default → nothing to collect + } if ((r = TryCollectInfo(ref closure, switchExpr.SwitchValue, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK || switchExpr.DefaultBody != null && // todo: @check is the order of collection affects the result? (r = TryCollectInfo(ref closure, switchExpr.DefaultBody, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK) @@ -5397,25 +5407,8 @@ private struct TestValueAndMultiTestCaseIndex public int MultiTestValCaseBodyIdxPlusOne; // 0 means not multi-test case, otherwise index+1 } - private static long ConvertValueObjectToLong(object valObj) - { - Debug.Assert(valObj != null); - var type = valObj.GetType(); - type = type.IsEnum ? Enum.GetUnderlyingType(type) : type; - return Type.GetTypeCode(type) switch - { - TypeCode.Char => (long)(char)valObj, - TypeCode.SByte => (long)(sbyte)valObj, - TypeCode.Byte => (long)(byte)valObj, - TypeCode.Int16 => (long)(short)valObj, - TypeCode.UInt16 => (long)(ushort)valObj, - TypeCode.Int32 => (long)(int)valObj, - TypeCode.UInt32 => (long)(uint)valObj, - TypeCode.Int64 => (long)valObj, - TypeCode.UInt64 => (long)(ulong)valObj, - _ => 0 // unreachable - }; - } + private static long ConvertValueObjectToLong(object valObj) => + Interpreter.ConvertValueObjectToLong(valObj); #if LIGHT_EXPRESSION private static bool TryEmitSwitch(SwitchExpression expr, IParameterProvider paramExprs, ILGenerator il, ref ClosureInfo closure, @@ -5431,6 +5424,10 @@ private static bool TryEmitSwitch(SwitchExpression expr, IReadOnlyList param var caseCount = cases.Count; var defaultBody = expr.DefaultBody; + // Compile-time switch branch elimination (#489): if the switch value is interpretable, select the matching branch + if (Interpreter.TryFindSwitchBranch(expr, setup, out var matchedBody)) + return matchedBody == null || TryEmit(matchedBody, paramExprs, il, ref closure, setup, parent); + // Optimization for the single case if (caseCount == 1 & defaultBody != null) { @@ -7231,6 +7228,28 @@ internal static bool TryUnboxToPrimitiveValue(ref PValue value, object boxedValu _ => UnreachableCase(code, (object)null) }; + /// Converts an integer/enum/char boxed value to long for uniform comparison. + [MethodImpl((MethodImplOptions)256)] + internal static long ConvertValueObjectToLong(object valObj) + { + Debug.Assert(valObj != null); + var type = valObj.GetType(); + type = type.IsEnum ? Enum.GetUnderlyingType(type) : type; + return Type.GetTypeCode(type) switch + { + TypeCode.Char => (long)(char)valObj, + TypeCode.SByte => (long)(sbyte)valObj, + TypeCode.Byte => (long)(byte)valObj, + TypeCode.Int16 => (long)(short)valObj, + TypeCode.UInt16 => (long)(ushort)valObj, + TypeCode.Int32 => (long)(int)valObj, + TypeCode.UInt32 => (long)(uint)valObj, + TypeCode.Int64 => (long)valObj, + TypeCode.UInt64 => (long)(ulong)valObj, + _ => 0 // unreachable + }; + } + internal static bool ComparePrimitiveValues(ref PValue left, ref PValue right, TypeCode code, ExpressionType nodeType) { switch (nodeType) @@ -7563,7 +7582,7 @@ public static bool TryInterpretBool(out bool result, Expression expr, CompilerFl { var exprType = expr.Type; Debug.Assert(exprType.IsPrimitive, // todo: @feat nullables are not supported yet // || Nullable.GetUnderlyingType(exprType)?.IsPrimitive == true, - "Can only reduce the boolean for the expressions of primitive types but found " + expr.Type); + "Can only reduce the boolean for the expressions of primitive type but found " + expr.Type); result = false; if ((flags & CompilerFlags.DisableInterpreter) != 0) return false; @@ -7582,6 +7601,95 @@ public static bool TryInterpretBool(out bool result, Expression expr, CompilerFl } } + /// + /// Tries to determine at compile time which branch a switch expression will take. + /// Works for integer/enum and string switch values with no custom equality method. + /// Returns true when the switch value is deterministic; is set to + /// the branch body to emit (null means use default body which may itself be null). + /// + public static bool TryFindSwitchBranch(SwitchExpression switchExpr, CompilerFlags flags, out Expression matchedBody) + { + matchedBody = null; + if (switchExpr.Comparison != null) return false; // custom equality: can't interpret statically + if ((flags & CompilerFlags.DisableInterpreter) != 0) return false; + var switchValueExpr = switchExpr.SwitchValue; + var switchValueType = switchValueExpr.Type; + var cases = switchExpr.Cases; + try + { + // String switch: only constant switch values supported + if (switchValueType == typeof(string)) + { + if (switchValueExpr is not ConstantExpression ce) return false; + var switchStr = ce.Value; + for (var i = 0; i < cases.Count; i++) + { + var testValues = cases[i].TestValues; + for (var j = 0; j < testValues.Count; j++) + { + if (testValues[j] is not ConstantExpression testConst) return false; + if (Equals(switchStr, testConst.Value)) { matchedBody = cases[i].Body; return true; } + } + } + matchedBody = switchExpr.DefaultBody; + return true; + } + + // Integer / enum / char switch + var effectiveType = switchValueType.IsEnum ? Enum.GetUnderlyingType(switchValueType) : switchValueType; + var typeCode = Type.GetTypeCode(effectiveType); + if (typeCode < TypeCode.Char || typeCode > TypeCode.UInt64) return false; // non-integral (e.g. float, decimal) + + long switchValLong; + if (switchValueExpr is ConstantExpression switchConst && switchConst.Value != null) + switchValLong = ConvertValueObjectToLong(switchConst.Value); + else if (typeCode == TypeCode.Int32) + { + var intVal = 0; + if (!TryInterpretInt(ref intVal, switchValueExpr, switchValueExpr.NodeType)) return false; + switchValLong = intVal; + } + else + { + PValue pv = default; + if (!TryInterpretPrimitiveValue(ref pv, switchValueExpr, typeCode, switchValueExpr.NodeType)) return false; + switchValLong = PValueToLong(ref pv, typeCode); + } + + for (var i = 0; i < cases.Count; i++) + { + 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 (switchValLong == ConvertValueObjectToLong(testConst.Value)) { matchedBody = cases[i].Body; return true; } + } + } + matchedBody = switchExpr.DefaultBody; + return true; + } + catch + { + return false; + } + } + + /// Converts a union to a long for integer/char comparison. + [MethodImpl((MethodImplOptions)256)] + internal static long PValueToLong(ref PValue value, TypeCode code) => code switch + { + TypeCode.Char => (long)value.CharValue, + TypeCode.SByte => (long)value.SByteValue, + TypeCode.Byte => (long)value.ByteValue, + TypeCode.Int16 => (long)value.Int16Value, + TypeCode.UInt16 => (long)value.UInt16Value, + TypeCode.Int32 => (long)value.Int32Value, + TypeCode.UInt32 => (long)value.UInt32Value, + TypeCode.Int64 => value.Int64Value, + TypeCode.UInt64 => (long)value.UInt64Value, + _ => 0L, + }; + // todo: @perf try split to `TryInterpretBinary` overload to streamline the calls for TryEmitConditional and similar /// Tries to interpret the expression of the Primitive type of Constant, Convert, Logical, Comparison, Arithmetic. internal static bool TryInterpretBool(ref bool resultBool, Expression expr, ExpressionType nodeType) diff --git a/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs b/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs index f7a5d03d..e8a8dc54 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs @@ -22,6 +22,11 @@ public void Run(TestRun t) Condition_with_not_equal_null_and_default_of_class_type_is_eliminated(t); Condition_with_nullable_default_equal_to_null_is_eliminated(t); Condition_with_null_constant_equal_to_non_null_constant_is_not_eliminated(t); + Switch_with_constant_int_value_eliminates_dead_cases(t); + Switch_with_constant_string_value_eliminates_dead_cases(t); + Switch_with_constant_enum_value_eliminates_dead_cases(t); + Switch_with_constant_int_matching_no_case_falls_through_to_default(t); + Switch_with_interpreted_int_expression_eliminates_dead_cases(t); } public void Logical_expression_started_with_not(TestContext t) @@ -187,4 +192,112 @@ public void Condition_with_null_constant_equal_to_non_null_constant_is_not_elimi ff.PrintIL(); t.AreEqual("falseBranch", ff()); } + + // Switch branch elimination (#489): constant int switch value selects single case + public void Switch_with_constant_int_value_eliminates_dead_cases(TestContext t) + { + // Switch(Constant(2), default:"Z", case 1:"A", case 2:"B", case 5:"C") + // The switch value is a constant 2, so it should reduce to "B" + var expr = Lambda>( + Switch(Constant(2), + Constant("Z"), + SwitchCase(Constant("A"), Constant(1)), + SwitchCase(Constant("B"), Constant(2)), + SwitchCase(Constant("C"), Constant(5)))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("B", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("B", ff()); + } + + // Switch branch elimination (#489): constant string switch value selects single case + public void Switch_with_constant_string_value_eliminates_dead_cases(TestContext t) + { + var expr = Lambda>( + Switch(Constant("hello"), + Constant("unknown"), + SwitchCase(Constant("hit_hello"), Constant("hello")), + SwitchCase(Constant("hit_world"), Constant("world")))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("hit_hello", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("hit_hello", ff()); + } + + public enum Color { Red, Green, Blue } + + // Switch branch elimination (#489): constant enum switch value selects single case + public void Switch_with_constant_enum_value_eliminates_dead_cases(TestContext t) + { + var expr = Lambda>( + Switch(Constant(Color.Green), + Constant("unknown"), + SwitchCase(Constant("red"), Constant(Color.Red)), + SwitchCase(Constant("green"), Constant(Color.Green)), + SwitchCase(Constant("blue"), Constant(Color.Blue)))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("green", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("green", ff()); + } + + // Switch branch elimination (#489): constant int matches no case → default is emitted + public void Switch_with_constant_int_matching_no_case_falls_through_to_default(TestContext t) + { + var expr = Lambda>( + Switch(Constant(99), + Constant("default"), + SwitchCase(Constant("A"), Constant(1)), + SwitchCase(Constant("B"), Constant(2)))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("default", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("default", ff()); + } + + // Switch branch elimination (#489): computed int (arithmetic on constants) selects case + public void Switch_with_interpreted_int_expression_eliminates_dead_cases(TestContext t) + { + // Switch(1 + 4, ...) → Switch(5, ...) → "C" + var expr = Lambda>( + Switch(Add(Constant(1), Constant(4)), + Constant("Z"), + SwitchCase(Constant("A"), Constant(1)), + SwitchCase(Constant("B"), Constant(2)), + SwitchCase(Constant("C"), Constant(5)))); + + expr.PrintCSharp(); + + var fs = expr.CompileSys(); + fs.PrintIL(); + t.AreEqual("C", fs()); + + var ff = expr.CompileFast(false); + ff.PrintIL(); + t.AreEqual("C", ff()); + } } \ No newline at end of file From 05d5008e8a62a2e7fb58eb142cadd88b7cfa3b17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:20:26 +0000 Subject: [PATCH 4/4] Add switch branch-elimination benchmarks (Issue #489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Issue489_Switch_BranchElimination.cs with Compile and Invoke nested classes: - Baseline: Switch(Parameter(...), ...) — runtime value, no FEC branch elimination - Eliminated: Switch(Constant(2), ...) — constant value, FEC emits only matching branch - Both registered in Program.cs (commented-out as per existing convention) Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/03a3da65-f598-4c66-90ce-0545d43edc04 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../Issue489_Switch_BranchElimination.cs | 148 ++++++++++++++++++ .../Program.cs | 3 + 2 files changed, 151 insertions(+) create mode 100644 test/FastExpressionCompiler.Benchmarks/Issue489_Switch_BranchElimination.cs diff --git a/test/FastExpressionCompiler.Benchmarks/Issue489_Switch_BranchElimination.cs b/test/FastExpressionCompiler.Benchmarks/Issue489_Switch_BranchElimination.cs new file mode 100644 index 00000000..92f45dba --- /dev/null +++ b/test/FastExpressionCompiler.Benchmarks/Issue489_Switch_BranchElimination.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq.Expressions; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; + +namespace FastExpressionCompiler.Benchmarks; + +/// +/// Benchmarks for compile-time switch branch elimination introduced in #489. +/// +/// Two variants are compared: +/// - Baseline: Switch value is a runtime parameter — FEC cannot eliminate any branches. +/// - Eliminated: Switch value is a compile-time constant — FEC selects the single matching +/// branch and emits only that, skipping closure collection and IL for all others. +/// +/// Each variant has two nested classes: +/// - Compile: measures how fast `Compile()` / `CompileFast()` process the expression. +/// - Invoke: measures the execution speed of the resulting delegate. +/// +/// The Invoke benchmark is the most telling: the eliminated switch emits just +/// `ldstr "B" / ret` (2 IL instructions) vs the full switch table. +/// +public class Issue489_Switch_BranchElimination +{ + // ----------------------------------------------------------------- + // Shared expression factories + // ----------------------------------------------------------------- + + /// + /// Baseline: the switch value is a runtime parameter — no elimination possible. + /// switch (x) { case 1: "A"; case 2: "B"; case 5: "C"; default: "Z" } + /// + private static Expression> CreateExpr_Baseline() + { + var p = Expression.Parameter(typeof(int), "x"); + return Expression.Lambda>( + Expression.Switch(p, + Expression.Constant("Z"), + Expression.SwitchCase(Expression.Constant("A"), Expression.Constant(1)), + Expression.SwitchCase(Expression.Constant("B"), Expression.Constant(2)), + Expression.SwitchCase(Expression.Constant("C"), Expression.Constant(5))), + p); + } + + /// + /// Branch-eliminated: the switch value is the compile-time constant 2. + /// switch (2) { case 1: "A"; case 2: "B"; case 5: "C"; default: "Z" } + /// FEC reduces this to a single ldstr "B" / ret. + /// + private static Expression> CreateExpr_Eliminated() + { + return Expression.Lambda>( + Expression.Switch(Expression.Constant(2), + Expression.Constant("Z"), + Expression.SwitchCase(Expression.Constant("A"), Expression.Constant(1)), + Expression.SwitchCase(Expression.Constant("B"), Expression.Constant(2)), + Expression.SwitchCase(Expression.Constant("C"), Expression.Constant(5)))); + } + + // ----------------------------------------------------------------- + // Compilation benchmarks + // ----------------------------------------------------------------- + + /// + /// Measures how fast Compile / CompileFast process each expression variant. + /// Baseline: runtime parameter switch (no FEC branch elimination). + /// Eliminated: constant switch (FEC skips dead branch closure-collection and IL emission). + /// + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + public class Compile + { + /* + ## Results placeholder — run with: dotnet run -c Release --project test/FastExpressionCompiler.Benchmarks -- --filter *Issue489*Compile* + + | Method | Mean | Error | StdDev | Ratio | Rank | Allocated | + |----------------------------- |----------:|---------:|---------:|------:|-----:|----------:| + | Baseline_CompileFast | N/A | N/A | N/A | | | N/A | + | Baseline_Compile | N/A | N/A | N/A | | | N/A | + | Eliminated_CompileFast | N/A | N/A | N/A | | | N/A | + | Eliminated_Compile | N/A | N/A | N/A | | | N/A | + */ + + private static readonly Expression> _baseline = CreateExpr_Baseline(); + private static readonly Expression> _eliminated = CreateExpr_Eliminated(); + + [Benchmark(Baseline = true)] + public object Baseline_Compile() => _baseline.Compile(); + + [Benchmark] + public object Baseline_CompileFast() => _baseline.CompileFast(); + + [Benchmark] + public object Eliminated_Compile() => _eliminated.Compile(); + + [Benchmark] + public object Eliminated_CompileFast() => _eliminated.CompileFast(); + } + + // ----------------------------------------------------------------- + // Invocation benchmarks + // ----------------------------------------------------------------- + + /// + /// Measures invocation speed of the compiled delegates. + /// The eliminated FEC delegate emits only 2 IL instructions (ldstr + ret), + /// while the system-compiled one runs a full switch dispatch at runtime. + /// + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + public class Invoke + { + /* + ## Results placeholder — run with: dotnet run -c Release --project test/FastExpressionCompiler.Benchmarks -- --filter *Issue489*Invoke* + + | Method | Mean | Error | StdDev | Ratio | Rank | Allocated | + |-------------------------------- |-----:|------:|-------:|------:|-----:|----------:| + | Baseline_Compiled | N/A | N/A | N/A | | | N/A | + | Baseline_CompiledFast | N/A | N/A | N/A | | | N/A | + | Eliminated_Compiled | N/A | N/A | N/A | | | N/A | + | Eliminated_CompiledFast | N/A | N/A | N/A | | | N/A | + */ + + private Func _baselineCompiled; + private Func _baselineCompiledFast; + private Func _eliminatedCompiled; + private Func _eliminatedCompiledFast; + + [GlobalSetup] + public void Setup() + { + _baselineCompiled = CreateExpr_Baseline().Compile(); + _baselineCompiledFast = CreateExpr_Baseline().CompileFast(); + _eliminatedCompiled = CreateExpr_Eliminated().Compile(); + _eliminatedCompiledFast = CreateExpr_Eliminated().CompileFast(); + } + + [Benchmark(Baseline = true)] + public string Baseline_Compiled() => _baselineCompiled(2); + + [Benchmark] + public string Baseline_CompiledFast() => _baselineCompiledFast(2); + + [Benchmark] + public string Eliminated_Compiled() => _eliminatedCompiled(); + + [Benchmark] + public string Eliminated_CompiledFast() => _eliminatedCompiledFast(); + } +} diff --git a/test/FastExpressionCompiler.Benchmarks/Program.cs b/test/FastExpressionCompiler.Benchmarks/Program.cs index b00d5c60..0e7d8fd3 100644 --- a/test/FastExpressionCompiler.Benchmarks/Program.cs +++ b/test/FastExpressionCompiler.Benchmarks/Program.cs @@ -50,6 +50,9 @@ public static void Main() //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run();