Skip to content

Commit 16bd44f

Browse files
Copilotdadhi
andauthored
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>
1 parent c9a4b19 commit 16bd44f

2 files changed

Lines changed: 171 additions & 1 deletion

File tree

src/FastExpressionCompiler/FastExpressionCompiler.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,15 @@ public static Result TryCollectInfo(ref ClosureInfo closure, Expression expr,
15081508
}
15091509
case ExpressionType.Conditional:
15101510
var condExpr = (ConditionalExpression)expr;
1511+
// Try structural branch elimination - skip collecting dead branch info
1512+
{
1513+
var reducedCond = Tools.TryReduceConditional(condExpr);
1514+
if (!ReferenceEquals(reducedCond, condExpr))
1515+
{
1516+
expr = reducedCond;
1517+
continue;
1518+
}
1519+
}
15111520
if ((r = TryCollectInfo(ref closure, condExpr.Test, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK ||
15121521
(r = TryCollectInfo(ref closure, condExpr.IfFalse, paramExprs, nestedLambda, ref rootNestedLambdas, flags)) != Result.OK)
15131522
return r;
@@ -2247,6 +2256,15 @@ public static bool TryEmit(Expression expr,
22472256
expr = testIsTrue ? condExpr.IfTrue : condExpr.IfFalse;
22482257
continue; // no recursion, just continue with the left or right side of condition
22492258
}
2259+
// Try structural branch elimination (e.g., null == Default(X) → always true/false)
2260+
{
2261+
var reducedCond = Tools.TryReduceConditional(condExpr);
2262+
if (!ReferenceEquals(reducedCond, condExpr))
2263+
{
2264+
expr = reducedCond;
2265+
continue;
2266+
}
2267+
}
22502268
return TryEmitConditional(testExpr, condExpr.IfTrue, condExpr.IfFalse, paramExprs, il, ref closure, setup, parent);
22512269

22522270
case ExpressionType.PostIncrementAssign:
@@ -8591,7 +8609,9 @@ public static Expression TryReduceConditional(ConditionalExpression condExpr)
85918609
var testExpr = TryReduceConditionalTest(condExpr.Test);
85928610
if (testExpr is BinaryExpression bi && (bi.NodeType == ExpressionType.Equal || bi.NodeType == ExpressionType.NotEqual))
85938611
{
8594-
if (bi.Left is ConstantExpression lc && bi.Right is ConstantExpression rc)
8612+
var left = bi.Left;
8613+
var right = bi.Right;
8614+
if (left is ConstantExpression lc && right is ConstantExpression rc)
85958615
{
85968616
#if INTERPRETATION_DIAGNOSTICS
85978617
Console.WriteLine("//Reduced Conditional in Interpretation: " + condExpr);
@@ -8601,12 +8621,33 @@ public static Expression TryReduceConditional(ConditionalExpression condExpr)
86018621
? (equals ? condExpr.IfTrue : condExpr.IfFalse)
86028622
: (equals ? condExpr.IfFalse: condExpr.IfTrue);
86038623
}
8624+
8625+
// Handle compile-time branch elimination for null/default equality:
8626+
// e.g. Constant(null) == Default(typeof(X)) or Default(typeof(X)) == Constant(null)
8627+
// where X is a reference, interface, or nullable type - both represent null, so they are always equal
8628+
var leftIsNull = left is ConstantExpression lnc && lnc.Value == null ||
8629+
left is DefaultExpression lde && IsNullDefault(lde.Type);
8630+
var rightIsNull = right is ConstantExpression rnc && rnc.Value == null ||
8631+
right is DefaultExpression rde && IsNullDefault(rde.Type);
8632+
if (leftIsNull && rightIsNull)
8633+
{
8634+
#if INTERPRETATION_DIAGNOSTICS
8635+
Console.WriteLine("//Reduced Conditional (null/default equality) in Interpretation: " + condExpr);
8636+
#endif
8637+
// both sides represent null, so they are equal
8638+
return bi.NodeType == ExpressionType.Equal ? condExpr.IfTrue : condExpr.IfFalse;
8639+
}
86048640
}
86058641

86068642
return testExpr is ConstantExpression constExpr && constExpr.Value is bool testBool
86078643
? (testBool ? condExpr.IfTrue : condExpr.IfFalse)
86088644
: condExpr;
86098645
}
8646+
8647+
// Returns true if the type's default value is null (reference types, interfaces, and Nullable<T>)
8648+
[MethodImpl((MethodImplOptions)256)]
8649+
internal static bool IsNullDefault(Type type) =>
8650+
type.IsClass || type.IsInterface || Nullable.GetUnderlyingType(type) != null;
86108651
}
86118652

86128653
[RequiresUnreferencedCode(Trimming.Message)]

test/FastExpressionCompiler.IssueTests/Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public void Run(TestRun t)
1616
{
1717
Logical_expression_started_with_not_Without_Interpreter_due_param_use(t);
1818
Logical_expression_started_with_not(t);
19+
Condition_with_null_constant_equal_to_default_of_class_type_is_eliminated(t);
20+
Condition_with_default_class_type_equal_to_null_constant_is_eliminated(t);
21+
Condition_with_two_defaults_of_class_type_is_eliminated(t);
22+
Condition_with_not_equal_null_and_default_of_class_type_is_eliminated(t);
23+
Condition_with_nullable_default_equal_to_null_is_eliminated(t);
24+
Condition_with_null_constant_equal_to_non_null_constant_is_not_eliminated(t);
1925
}
2026

2127
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
5864
t.IsFalse(ff(true));
5965
t.IsTrue(ff(false));
6066
}
67+
68+
// Branch elimination: Constant(null) == Default(typeof(X)) where X is a class → always true
69+
// Models the AutoMapper pattern: after inlining a null argument into a null-check lambda
70+
public void Condition_with_null_constant_equal_to_default_of_class_type_is_eliminated(TestContext t)
71+
{
72+
// Condition(Equal(Constant(null), Default(typeof(string))), Constant("trueBranch"), Constant("falseBranch"))
73+
// Since null == default(string) is always true, this should reduce to "trueBranch"
74+
var expr = Lambda<Func<string>>(
75+
Condition(
76+
Equal(Constant(null, typeof(string)), Default(typeof(string))),
77+
Constant("trueBranch"),
78+
Constant("falseBranch")));
79+
80+
expr.PrintCSharp();
81+
82+
var fs = expr.CompileSys();
83+
fs.PrintIL();
84+
t.AreEqual("trueBranch", fs());
85+
86+
var ff = expr.CompileFast(false);
87+
ff.PrintIL();
88+
t.AreEqual("trueBranch", ff());
89+
}
90+
91+
// Branch elimination: Default(typeof(X)) == Constant(null) where X is a class → always true (symmetric)
92+
public void Condition_with_default_class_type_equal_to_null_constant_is_eliminated(TestContext t)
93+
{
94+
var expr = Lambda<Func<string>>(
95+
Condition(
96+
Equal(Default(typeof(string)), Constant(null, typeof(string))),
97+
Constant("trueBranch"),
98+
Constant("falseBranch")));
99+
100+
expr.PrintCSharp();
101+
102+
var fs = expr.CompileSys();
103+
fs.PrintIL();
104+
t.AreEqual("trueBranch", fs());
105+
106+
var ff = expr.CompileFast(false);
107+
ff.PrintIL();
108+
t.AreEqual("trueBranch", ff());
109+
}
110+
111+
// Branch elimination: Default(typeof(X)) == Default(typeof(X)) where X is a class → always true
112+
public void Condition_with_two_defaults_of_class_type_is_eliminated(TestContext t)
113+
{
114+
var expr = Lambda<Func<string>>(
115+
Condition(
116+
Equal(Default(typeof(string)), Default(typeof(string))),
117+
Constant("trueBranch"),
118+
Constant("falseBranch")));
119+
120+
expr.PrintCSharp();
121+
122+
var fs = expr.CompileSys();
123+
fs.PrintIL();
124+
t.AreEqual("trueBranch", fs());
125+
126+
var ff = expr.CompileFast(false);
127+
ff.PrintIL();
128+
t.AreEqual("trueBranch", ff());
129+
}
130+
131+
// Branch elimination: Constant(null) != Default(typeof(X)) where X is a class → always false
132+
public void Condition_with_not_equal_null_and_default_of_class_type_is_eliminated(TestContext t)
133+
{
134+
var expr = Lambda<Func<string>>(
135+
Condition(
136+
NotEqual(Constant(null, typeof(string)), Default(typeof(string))),
137+
Constant("trueBranch"),
138+
Constant("falseBranch")));
139+
140+
expr.PrintCSharp();
141+
142+
var fs = expr.CompileSys();
143+
fs.PrintIL();
144+
t.AreEqual("falseBranch", fs());
145+
146+
var ff = expr.CompileFast(false);
147+
ff.PrintIL();
148+
t.AreEqual("falseBranch", ff());
149+
}
150+
151+
// Branch elimination: Constant(null) == Default(typeof(int?)) → always true (null == default(int?) is null == null)
152+
public void Condition_with_nullable_default_equal_to_null_is_eliminated(TestContext t)
153+
{
154+
var expr = Lambda<Func<int?>>(
155+
Condition(
156+
Equal(Constant(null, typeof(int?)), Default(typeof(int?))),
157+
Constant(42, typeof(int?)),
158+
Constant(0, typeof(int?))));
159+
160+
expr.PrintCSharp();
161+
162+
var fs = expr.CompileSys();
163+
fs.PrintIL();
164+
t.AreEqual(42, fs());
165+
166+
var ff = expr.CompileFast(false);
167+
ff.PrintIL();
168+
t.AreEqual(42, ff());
169+
}
170+
171+
// Sanity check: Constant(null) == Constant("hello") should NOT be eliminated (false, not a null-null case)
172+
public void Condition_with_null_constant_equal_to_non_null_constant_is_not_eliminated(TestContext t)
173+
{
174+
var expr = Lambda<Func<string>>(
175+
Condition(
176+
Equal(Constant(null, typeof(string)), Constant("hello", typeof(string))),
177+
Constant("trueBranch"),
178+
Constant("falseBranch")));
179+
180+
expr.PrintCSharp();
181+
182+
var fs = expr.CompileSys();
183+
fs.PrintIL();
184+
t.AreEqual("falseBranch", fs());
185+
186+
var ff = expr.CompileFast(false);
187+
ff.PrintIL();
188+
t.AreEqual("falseBranch", ff());
189+
}
61190
}

0 commit comments

Comments
 (0)