Skip to content
626 changes: 626 additions & 0 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public void Serialize_the_nullable_decimal_array()
var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression();
restoredExpr.PrintCSharp();
Asserts.AreEqual(expr.ToCSharpString(), restoredExpr.ToCSharpString());
Asserts.IsTrue(expr.EqualsTo(restoredExpr));
#endif

var fs = expr.CompileSys();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,7 @@ public void Test_case_2_Full_ExecutionEngineException()
#if LIGHT_EXPRESSION
var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression();
// todo: @feature #431 compare the restored target and source expressions directly instead of strings
Asserts.AreEqual(expr.ToCSharpString(), restoredExpr.ToCSharpString());
Asserts.IsTrue(expr.EqualsTo(restoredExpr));
#endif

var fs = expr.CompileSys();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void Original_case()
var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression();
restoredExpr.PrintCSharp();
Asserts.AreEqual(expr.ToCSharpString(), restoredExpr.ToCSharpString());
Asserts.IsTrue(expr.EqualsTo(restoredExpr));
#endif

var fs = expr.CompileSys();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

#if LIGHT_EXPRESSION
using static FastExpressionCompiler.LightExpression.Expression;
using FastExpressionCompiler.LightExpression;
using FastExpressionCompiler.LightExpression.ImTools;
namespace FastExpressionCompiler.LightExpression.IssueTests;
#else
using static System.Linq.Expressions.Expression;
using FastExpressionCompiler.ImTools;
namespace FastExpressionCompiler.IssueTests;
#endif

public struct Issue431_Add_structural_equality_comparison_to_LightExpression : ITestX
{
public static readonly ConstructorInfo CtorOfA = typeof(A).GetTypeInfo().DeclaredConstructors.First();
public static readonly ConstructorInfo CtorOfB = typeof(B).GetTypeInfo().DeclaredConstructors.First();
public static readonly PropertyInfo PropAProp = typeof(A).GetTypeInfo().DeclaredProperties.First(p => p.Name == "Prop");

public void Run(TestRun t)
{
Eq_simple_lambda(t);
Eq_lambda_with_parameters(t);
Eq_constants(t);
Eq_member_access(t);
Eq_method_call(t);
Eq_new_expression(t);
Eq_member_init(t);
Eq_new_array(t);
Eq_conditional(t);
Eq_block_with_variables(t);
Eq_try_catch(t);
Eq_loop_with_labels(t);
Eq_switch(t);
#if LIGHT_EXPRESSION
Eq_complex_lambda_round_trip(t);
#endif
Hash_equal_expressions_have_equal_hashes(t);
Hash_used_as_dictionary_key(t);
Hash_used_as_smallmap_key(t);
NotEq_different_constants(t);
NotEq_different_types(t);
NotEq_different_parameters(t);
}

public void Eq_simple_lambda(TestContext t)
{
var e1 = Lambda<Func<int>>(Constant(42));
var e2 = Lambda<Func<int>>(Constant(42));
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_lambda_with_parameters(TestContext t)
{
var p1a = Parameter(typeof(int), "x");
var p1b = Parameter(typeof(int), "y");
var e1 = Lambda<Func<int, int, int>>(Add(p1a, p1b), p1a, p1b);

var p2a = Parameter(typeof(int), "x");
var p2b = Parameter(typeof(int), "y");
var e2 = Lambda<Func<int, int, int>>(Add(p2a, p2b), p2a, p2b);

t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_constants(TestContext t)
{
t.IsTrue(Constant(42).EqualsTo(Constant(42)));
t.IsTrue(Constant("hello").EqualsTo(Constant("hello")));
t.IsTrue(Constant(null, typeof(string)).EqualsTo(Constant(null, typeof(string))));
}

public void Eq_member_access(TestContext t)
{
var prop = typeof(string).GetProperty(nameof(string.Length));
var p1 = Parameter(typeof(string), "s");
var p2 = Parameter(typeof(string), "s");
var e1 = Lambda<Func<string, int>>(Property(p1, prop), p1);
var e2 = Lambda<Func<string, int>>(Property(p2, prop), p2);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_method_call(TestContext t)
{
var method = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) });
var p1 = Parameter(typeof(string), "a");
var p2 = Parameter(typeof(string), "b");
var pa = Parameter(typeof(string), "a");
var pb = Parameter(typeof(string), "b");
var e1 = Lambda<Func<string, string, string>>(Call(method, p1, p2), p1, p2);
var e2 = Lambda<Func<string, string, string>>(Call(method, pa, pb), pa, pb);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_new_expression(TestContext t)
{
var ctor = typeof(B).GetConstructor(Type.EmptyTypes);
var e1 = New(ctor);
var e2 = New(ctor);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_member_init(TestContext t)
{
var e1 = MemberInit(New(CtorOfA, New(CtorOfB)), Bind(PropAProp, New(CtorOfB)));
var e2 = MemberInit(New(CtorOfA, New(CtorOfB)), Bind(PropAProp, New(CtorOfB)));
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_new_array(TestContext t)
{
var e1 = NewArrayInit(typeof(int), Constant(1), Constant(2), Constant(3));
var e2 = NewArrayInit(typeof(int), Constant(1), Constant(2), Constant(3));
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_conditional(TestContext t)
{
var p1 = Parameter(typeof(int), "x");
var p2 = Parameter(typeof(int), "x");
var e1 = Lambda<Func<int, int>>(Condition(Equal(p1, Constant(0)), Constant(1), p1), p1);
var e2 = Lambda<Func<int, int>>(Condition(Equal(p2, Constant(0)), Constant(1), p2), p2);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_block_with_variables(TestContext t)
{
var v1 = Variable(typeof(int), "i");
var v2 = Variable(typeof(int), "i");
var e1 = Block(new[] { v1 }, Assign(v1, Constant(5)), v1);
var e2 = Block(new[] { v2 }, Assign(v2, Constant(5)), v2);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_try_catch(TestContext t)
{
var ex1 = Parameter(typeof(Exception), "ex");
var ex2 = Parameter(typeof(Exception), "ex");
var e1 = TryCatch(Constant(1), Catch(ex1, Constant(2)));
var e2 = TryCatch(Constant(1), Catch(ex2, Constant(2)));
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_loop_with_labels(TestContext t)
{
var brk1 = Label(typeof(void), "break");
var cnt1 = Label(typeof(void), "continue");
var brk2 = Label(typeof(void), "break");
var cnt2 = Label(typeof(void), "continue");
var e1 = Loop(Block(Break(brk1), Continue(cnt1)), brk1, cnt1);
var e2 = Loop(Block(Break(brk2), Continue(cnt2)), brk2, cnt2);
t.IsTrue(e1.EqualsTo(e2));
}

public void Eq_switch(TestContext t)
{
var p1 = Parameter(typeof(int), "x");
var p2 = Parameter(typeof(int), "x");
var e1 = Lambda<Func<int, int>>(
Switch(p1, Constant(-1), SwitchCase(Constant(10), Constant(1)), SwitchCase(Constant(20), Constant(2))),
p1);
var e2 = Lambda<Func<int, int>>(
Switch(p2, Constant(-1), SwitchCase(Constant(10), Constant(1)), SwitchCase(Constant(20), Constant(2))),
p2);
t.IsTrue(e1.EqualsTo(e2));
}

#if LIGHT_EXPRESSION
public void Eq_complex_lambda_round_trip(TestContext t)
{
var expr = Lambda<Func<object[], object>>(
MemberInit(
New(CtorOfA, New(CtorOfB)),
Bind(PropAProp, New(CtorOfB))),
Parameter(typeof(object[]), "p"));

var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression<Func<object[], object>>();

t.IsTrue(expr.EqualsTo(restoredExpr));
}
#endif

public void Hash_equal_expressions_have_equal_hashes(TestContext t)
{
// Structural equality implies equal hashes (mandatory contract for use as dictionary key).
var p1 = Parameter(typeof(int), "x");
var p2 = Parameter(typeof(int), "y"); // different name — structurally same
var e1 = Lambda<Func<int, int>>(Add(p1, Constant(1)), p1);
var e2 = Lambda<Func<int, int>>(Add(p2, Constant(1)), p2);
t.IsTrue(e1.EqualsTo(e2));
t.AreEqual(ExpressionEqualityComparer.GetHashCode(e1), ExpressionEqualityComparer.GetHashCode(e2));

// Constants
t.AreEqual(ExpressionEqualityComparer.GetHashCode(Constant(42)), ExpressionEqualityComparer.GetHashCode(Constant(42)));

// Different constants must have different hashes (not guaranteed in general, but these are obviously distinct)
t.AreNotEqual(ExpressionEqualityComparer.GetHashCode(Constant(1)), ExpressionEqualityComparer.GetHashCode(Constant(2)));
}

public void Hash_used_as_dictionary_key(TestContext t)
{
// Verify that structurally-equal expressions resolve to the same Dictionary bucket.
var cmp = default(ExpressionEqualityComparer);
var dict = new Dictionary<
#if LIGHT_EXPRESSION
FastExpressionCompiler.LightExpression.Expression,
#else
System.Linq.Expressions.Expression,
#endif
string>(cmp);

var p1 = Parameter(typeof(int), "x");
var e1 = Lambda<Func<int, int>>(Add(p1, Constant(1)), p1);
dict[e1] = "found";

var p2 = Parameter(typeof(int), "y"); // different identity/name
var e2 = Lambda<Func<int, int>>(Add(p2, Constant(1)), p2);
t.IsTrue(dict.TryGetValue(e2, out var v));
t.AreEqual("found", v);
}

public void Hash_used_as_smallmap_key(TestContext t)
{
// Verify lookup via SmallMap8 which uses GetHashCode + Equals internally.
var p1 = Parameter(typeof(int), "x");
var e1 = Lambda<Func<int, int>>(Add(p1, Constant(1)), p1);
var h1 = ExpressionEqualityComparer.GetHashCode(e1);

var p2 = Parameter(typeof(int), "y");
var e2 = Lambda<Func<int, int>>(Add(p2, Constant(1)), p2);
var h2 = ExpressionEqualityComparer.GetHashCode(e2);

// Structurally equal ⟹ same hash
t.AreEqual(h1, h2);

// Structurally different ⟹ different hash (for obviously distinct constants)
var e3 = Lambda<Func<int, int>>(Add(p1, Constant(99)), p1);
t.AreNotEqual(h1, ExpressionEqualityComparer.GetHashCode(e3));
}

public void NotEq_different_constants(TestContext t)
{
t.IsFalse(Constant(42).EqualsTo(Constant(43)));
t.IsFalse(Constant("a").EqualsTo(Constant("b")));
}

public void NotEq_different_types(TestContext t)
{
t.IsFalse(Constant(42).EqualsTo(Constant(42L)));
t.IsFalse(Default(typeof(int)).EqualsTo(Default(typeof(long))));
}

public void NotEq_different_parameters(TestContext t)
{
var p1 = Parameter(typeof(int), "x");
var p2 = Parameter(typeof(int), "y");
var e1 = Lambda<Func<int, int>>(p1, p1);
var e2 = Lambda<Func<int, int>>(p2, p2);
// When mapped by position in a lambda, different-named params ARE equal structurally (same position)
t.IsTrue(e1.EqualsTo(e2));
// But accessing a param outside its lambda context uses name comparison
t.IsFalse(p1.EqualsTo(p2));
}

public class A
{
public B Prop { get; set; }
public A(B b) { Prop = b; }
}

public class B { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void Can_Create_Func(int paramCount)
var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression();
restoredExpr.PrintCSharp();
Asserts.AreEqual(expr.ToCSharpString(), restoredExpr.ToCSharpString());
Asserts.IsTrue(expr.EqualsTo(restoredExpr));
#endif

// (a, b, c, ...) => new[] { a, b, c, ... }
Expand Down
1 change: 1 addition & 0 deletions test/FastExpressionCompiler.TestsRunner.Net472/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ static void RunLightExpressionTests(object state)
var t = (LightExpression.TestRun)state;
t.Run(new LightExpression.IssueTests.Issue183_NullableDecimal());
t.Run(new LightExpression.IssueTests.Issue398_Optimize_Switch_with_OpCodes_Switch());
t.Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression());
t.Run(new LightExpression.IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET());
t.Run(new LightExpression.IssueTests.Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation());
t.Run(new LightExpression.IssueTests.Issue473_InvalidProgramException_when_using_Expression_Condition_with_converted_decimal_expression());
Expand Down
2 changes: 2 additions & 0 deletions test/FastExpressionCompiler.TestsRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static void Main()
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());
st.Run(new Issue480_CLR_detected_an_invalid_program_exception());
st.Run(new Issue431_Add_structural_equality_comparison_to_LightExpression());
#if NET8_0_OR_GREATER
st.Run(new Issue487_Fix_ToCSharpString_output_for_boolean_equality_expressions());
st.Run(new Issue475_Reuse_DynamicMethod_if_possible());
Expand All @@ -67,6 +68,7 @@ public static void Main()
#endif

lt.Run(new LightExpression.IssueTests.Issue398_Optimize_Switch_with_OpCodes_Switch());
lt.Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression());
lt.Run(new LightExpression.IssueTests.Issue468_Optimize_the_delegate_access_to_the_Closure_object_for_the_modern_NET());
lt.Run(new LightExpression.IssueTests.Issue472_TryInterpret_and_Reduce_primitive_arithmetic_and_logical_expressions_during_the_compilation());
lt.Run(new LightExpression.IssueTests.Issue473_InvalidProgramException_when_using_Expression_Condition_with_converted_decimal_expression());
Expand Down
3 changes: 1 addition & 2 deletions test/FastExpressionCompiler.UnitTests/AssignTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,7 @@ public void Array_multi_dimensional_index_assign_value_type_block()
var sysExpr = expr.ToLambdaExpression();
var restoredExpr = sysExpr.ToLightExpression();
restoredExpr.PrintCSharp();
// todo: @wip #431 generates different names for the unnamed variables which is not comparable
Asserts.AreEqual(expr.ToCSharpString(), restoredExpr.ToCSharpString());
Asserts.IsTrue(expr.EqualsTo(restoredExpr));
#endif
Asserts.IsNotNull(fs);
Asserts.AreEqual(5, fs());
Expand Down
Loading