From dfe78dbf07bc5f54839ef6505a10084fcbc32ac3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 07:42:24 +0000 Subject: [PATCH 1/7] Initial plan From a3b49354aae70b3d1db26054920fc4dd6a6f5749 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 07:55:40 +0000 Subject: [PATCH 2/7] Add ExpressionEqualityComparer with EqualsTo extension method for LightExpression Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/961c247d-26e0-4577-9e1c-7d227466ebaa Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 402 ++++++++++++++++++ .../Issue261_Loop_wih_conditions_fails.cs | 2 +- ...Issue274_Failing_Expressions_in_Linq2DB.cs | 3 +- ...yCatch_Bad_label_content_in_ILGenerator.cs | 2 +- ..._equality_comparison_to_LightExpression.cs | 217 ++++++++++ .../Issue363_ActionFunc16Generics.cs | 2 +- .../Program.cs | 2 + .../Program.cs | 2 + .../AssignTests.cs | 3 +- 9 files changed, 628 insertions(+), 7 deletions(-) create mode 100644 test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 24caf5dd..3f930b7a 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10000,6 +10000,408 @@ internal static StringBuilder CreateExpressionString(this Expression e, StringBu } } +#if LIGHT_EXPRESSION + /// Provides structural equality comparison for the LightExpression. + public static class ExpressionEqualityComparer + { + /// Structurally compares two expressions. + /// Parameters are matched by their position within their enclosing lambda, and label targets by identity pairing. + /// No heap allocations for expressions with up to 4 lambda parameters or label targets. + public static bool EqualsTo(this Expression x, Expression y) + { + SmallList xps = default, yps = default; + SmallList xls = default, yls = default; + return Eq(x, y, ref xps, ref yps, ref xls, ref yls); + } + + private static bool Eq(Expression x, Expression y, + ref SmallList xps, ref SmallList yps, + ref SmallList xls, ref SmallList yls) + { + if (ReferenceEquals(x, y)) return true; + if (x == null | y == null) return false; + if (x.NodeType != y.NodeType | x.Type != y.Type) return false; + switch (x.NodeType) + { + case ExpressionType.Parameter: + { + var px = (ParameterExpression)x; + var py = (ParameterExpression)y; + for (var i = 0; i < xps.Count; i++) + if (ReferenceEquals(xps.Items[i], px)) + return ReferenceEquals(yps.Items[i], py); + // unmapped — compare structurally (Type already checked) + return px.IsByRef == py.IsByRef && px.Name == py.Name; + } + + case ExpressionType.Constant: + { + var cx = (ConstantExpression)x; + var cy = (ConstantExpression)y; + return Equals(cx.Value, cy.Value); + } + + case ExpressionType.Lambda: + { + var lx = (LambdaExpression)x; + var ly = (LambdaExpression)y; + var pc = lx.ParameterCount; + if (pc != ly.ParameterCount) return false; + var sc = xps.Count; + for (var i = 0; i < pc; i++) + { + xps.AddDefaultAndGetRef() = lx.GetParameter(i); + yps.AddDefaultAndGetRef() = ly.GetParameter(i); + } + var eq = Eq(lx.Body, ly.Body, ref xps, ref yps, ref xls, ref yls); + xps.Count = sc; + yps.Count = sc; + return eq; + } + + case ExpressionType.Negate: case ExpressionType.NegateChecked: + case ExpressionType.UnaryPlus: case ExpressionType.Not: + case ExpressionType.ArrayLength: case ExpressionType.TypeAs: + case ExpressionType.Convert: case ExpressionType.ConvertChecked: + case ExpressionType.Quote: case ExpressionType.Throw: + case ExpressionType.OnesComplement: case ExpressionType.IsTrue: case ExpressionType.IsFalse: + case ExpressionType.Increment: case ExpressionType.Decrement: + case ExpressionType.PreIncrementAssign: case ExpressionType.PostIncrementAssign: + case ExpressionType.PreDecrementAssign: case ExpressionType.PostDecrementAssign: + case ExpressionType.Unbox: + { + var ux = (UnaryExpression)x; + var uy = (UnaryExpression)y; + return ux.Method == uy.Method && + Eq(ux.Operand, uy.Operand, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Add: case ExpressionType.AddChecked: + case ExpressionType.Subtract: case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: case ExpressionType.Modulo: + case ExpressionType.Power: case ExpressionType.And: + case ExpressionType.Or: case ExpressionType.ExclusiveOr: + case ExpressionType.LeftShift: case ExpressionType.RightShift: + case ExpressionType.AndAlso: case ExpressionType.OrElse: + case ExpressionType.Equal: case ExpressionType.NotEqual: + case ExpressionType.LessThan: case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Coalesce: case ExpressionType.ArrayIndex: + case ExpressionType.Assign: + case ExpressionType.AddAssign: case ExpressionType.AddAssignChecked: + case ExpressionType.SubtractAssign: case ExpressionType.SubtractAssignChecked: + case ExpressionType.MultiplyAssign: case ExpressionType.MultiplyAssignChecked: + case ExpressionType.DivideAssign: case ExpressionType.ModuloAssign: + case ExpressionType.PowerAssign: case ExpressionType.AndAssign: + case ExpressionType.OrAssign: case ExpressionType.ExclusiveOrAssign: + case ExpressionType.LeftShiftAssign: case ExpressionType.RightShiftAssign: + { + var bx = (BinaryExpression)x; + var by = (BinaryExpression)y; + return bx.Method == by.Method && + Eq(bx.Conversion, by.Conversion, ref xps, ref yps, ref xls, ref yls) && + Eq(bx.Left, by.Left, ref xps, ref yps, ref xls, ref yls) && + Eq(bx.Right, by.Right, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Call: + { + var mx = (MethodCallExpression)x; + var my = (MethodCallExpression)y; + return mx.Method == my.Method && + Eq(mx.Object, my.Object, ref xps, ref yps, ref xls, ref yls) && + EqArgs(mx, my, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.MemberAccess: + { + var fx = (MemberExpression)x; + var fy = (MemberExpression)y; + return fx.Member == fy.Member && + Eq(fx.Expression, fy.Expression, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.New: + { + var nx = (NewExpression)x; + var ny = (NewExpression)y; + return nx.Constructor == ny.Constructor && + EqArgs(nx, ny, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + { + var nx = (NewArrayExpression)x; + var ny = (NewArrayExpression)y; + return EqArgs(nx, ny, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Conditional: + { + var cx = (ConditionalExpression)x; + var cy = (ConditionalExpression)y; + return Eq(cx.Test, cy.Test, ref xps, ref yps, ref xls, ref yls) && + Eq(cx.IfTrue, cy.IfTrue, ref xps, ref yps, ref xls, ref yls) && + Eq(cx.IfFalse, cy.IfFalse, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Block: + { + var bx = (BlockExpression)x; + var by = (BlockExpression)y; + var vc = bx.Variables.Count; + if (vc != by.Variables.Count) return false; + var ec = bx.Expressions.Count; + if (ec != by.Expressions.Count) return false; + var sc = xps.Count; + for (var i = 0; i < vc; i++) + { + xps.AddDefaultAndGetRef() = bx.Variables[i]; + yps.AddDefaultAndGetRef() = by.Variables[i]; + } + var eq = true; + for (var i = 0; i < ec && eq; i++) + eq = Eq(bx.Expressions.GetSurePresentRef(i), by.Expressions.GetSurePresentRef(i), + ref xps, ref yps, ref xls, ref yls); + xps.Count = sc; + yps.Count = sc; + return eq; + } + + case ExpressionType.MemberInit: + { + var mx = (MemberInitExpression)x; + var my = (MemberInitExpression)y; + var bc = mx.Bindings.Count; + if (bc != my.Bindings.Count) return false; + if (!Eq(mx.Expression, my.Expression, ref xps, ref yps, ref xls, ref yls)) return false; + for (var i = 0; i < bc; i++) + if (!EqBinding(mx.Bindings[i], my.Bindings[i], ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + + case ExpressionType.ListInit: + { + var lx = (ListInitExpression)x; + var ly = (ListInitExpression)y; + var ic = lx.Initializers.Count; + if (ic != ly.Initializers.Count) return false; + if (!Eq(lx.NewExpression, ly.NewExpression, ref xps, ref yps, ref xls, ref yls)) return false; + for (var i = 0; i < ic; i++) + if (!EqElementInit(lx.Initializers[i], ly.Initializers[i], ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + + case ExpressionType.TypeIs: + case ExpressionType.TypeEqual: + { + var tx = (TypeBinaryExpression)x; + var ty = (TypeBinaryExpression)y; + return tx.TypeOperand == ty.TypeOperand && + Eq(tx.Expression, ty.Expression, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Invoke: + { + var ix = (InvocationExpression)x; + var iy = (InvocationExpression)y; + return Eq(ix.Expression, iy.Expression, ref xps, ref yps, ref xls, ref yls) && + EqArgs(ix, iy, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Index: + { + var ix = (IndexExpression)x; + var iy = (IndexExpression)y; + return ix.Indexer == iy.Indexer && + Eq(ix.Object, iy.Object, ref xps, ref yps, ref xls, ref yls) && + EqArgs(ix, iy, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Default: + return true; // Type already matched above + + case ExpressionType.Label: + { + var lx = (LabelExpression)x; + var ly = (LabelExpression)y; + return EqLabel(lx.Target, ly.Target, ref xls, ref yls) && + Eq(lx.DefaultValue, ly.DefaultValue, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Goto: + { + var gx = (GotoExpression)x; + var gy = (GotoExpression)y; + return gx.Kind == gy.Kind && + EqLabel(gx.Target, gy.Target, ref xls, ref yls) && + Eq(gx.Value, gy.Value, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Loop: + { + var lx = (LoopExpression)x; + var ly = (LoopExpression)y; + return EqLabel(lx.BreakLabel, ly.BreakLabel, ref xls, ref yls) && + EqLabel(lx.ContinueLabel, ly.ContinueLabel, ref xls, ref yls) && + Eq(lx.Body, ly.Body, ref xps, ref yps, ref xls, ref yls); + } + + case ExpressionType.Try: + { + var tx = (TryExpression)x; + var ty = (TryExpression)y; + if (!Eq(tx.Body, ty.Body, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(tx.Finally, ty.Finally, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(tx.Fault, ty.Fault, ref xps, ref yps, ref xls, ref yls)) return false; + var hc = tx.Handlers.Count; + if (hc != ty.Handlers.Count) return false; + for (var i = 0; i < hc; i++) + { + var hx = tx.Handlers[i]; + var hy = ty.Handlers[i]; + if (hx.Test != hy.Test) return false; + var sc = xps.Count; + if (hx.Variable != null | hy.Variable != null) + { + if (hx.Variable == null | hy.Variable == null) return false; + if (hx.Variable.Type != hy.Variable.Type) return false; + xps.AddDefaultAndGetRef() = hx.Variable; + yps.AddDefaultAndGetRef() = hy.Variable; + } + var ceq = Eq(hx.Body, hy.Body, ref xps, ref yps, ref xls, ref yls) && + Eq(hx.Filter, hy.Filter, ref xps, ref yps, ref xls, ref yls); + xps.Count = sc; + yps.Count = sc; + if (!ceq) return false; + } + return true; + } + + case ExpressionType.Switch: + { + var sx = (SwitchExpression)x; + var sy = (SwitchExpression)y; + if (sx.Comparison != sy.Comparison) return false; + if (!Eq(sx.SwitchValue, sy.SwitchValue, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(sx.DefaultBody, sy.DefaultBody, ref xps, ref yps, ref xls, ref yls)) return false; + var cc = sx.Cases.Count; + if (cc != sy.Cases.Count) return false; + for (var i = 0; i < cc; i++) + { + var cx = sx.Cases[i]; + var cy = sy.Cases[i]; + if (!Eq(cx.Body, cy.Body, ref xps, ref yps, ref xls, ref yls)) return false; + var tc = cx.TestValues.Count; + if (tc != cy.TestValues.Count) return false; + for (var j = 0; j < tc; j++) + if (!Eq(cx.TestValues[j], cy.TestValues[j], ref xps, ref yps, ref xls, ref yls)) return false; + } + return true; + } + + case ExpressionType.RuntimeVariables: + { + var rx = (RuntimeVariablesExpression)x; + var ry = (RuntimeVariablesExpression)y; + var vc = rx.Variables.Count; + if (vc != ry.Variables.Count) return false; + for (var i = 0; i < vc; i++) + if (!Eq(rx.Variables[i], ry.Variables[i], ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + + case ExpressionType.DebugInfo: + { + var dx = (DebugInfoExpression)x; + var dy = (DebugInfoExpression)y; + return dx.IsClear == dy.IsClear && + dx.StartLine == dy.StartLine && dx.StartColumn == dy.StartColumn && + dx.EndLine == dy.EndLine && dx.EndColumn == dy.EndColumn && + dx.Document?.FileName == dy.Document?.FileName; + } + + default: + return false; + } + } + + private static bool EqLabel(LabelTarget x, LabelTarget y, + ref SmallList xls, ref SmallList yls) + { + if (ReferenceEquals(x, y)) return true; + if (x == null | y == null) return false; + if (x.Type != y.Type) return false; + for (var i = 0; i < xls.Count; i++) + if (ReferenceEquals(xls.Items[i], x)) + return ReferenceEquals(yls.Items[i], y); + // Register the pair and compare by name + xls.AddDefaultAndGetRef() = x; + yls.AddDefaultAndGetRef() = y; + return x.Name == y.Name; + } + + private static bool EqArgs(IArgumentProvider x, IArgumentProvider y, + ref SmallList xps, ref SmallList yps, + ref SmallList xls, ref SmallList yls) + { + var c = x.ArgumentCount; + if (c != y.ArgumentCount) return false; + for (var i = 0; i < c; i++) + if (!Eq(x.GetArgument(i), y.GetArgument(i), ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + + private static bool EqElementInit(ElementInit x, ElementInit y, + ref SmallList xps, ref SmallList yps, + ref SmallList xls, ref SmallList yls) + { + if (x.AddMethod != y.AddMethod) return false; + var ac = x.ArgumentCount; + if (ac != y.ArgumentCount) return false; + for (var i = 0; i < ac; i++) + if (!Eq(x.GetArgument(i), y.GetArgument(i), ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + + private static bool EqBinding(MemberBinding x, MemberBinding y, + ref SmallList xps, ref SmallList yps, + ref SmallList xls, ref SmallList yls) + { + if (x.BindingType != y.BindingType | x.Member != y.Member) return false; + switch (x.BindingType) + { + case MemberBindingType.Assignment: + return Eq(((MemberAssignment)x).Expression, ((MemberAssignment)y).Expression, + ref xps, ref yps, ref xls, ref yls); + case MemberBindingType.MemberBinding: + { + var mb = (MemberMemberBinding)x; + var mbOther = (MemberMemberBinding)y; + var bc = mb.Bindings.Count; + if (bc != mbOther.Bindings.Count) return false; + for (var i = 0; i < bc; i++) + if (!EqBinding(mb.Bindings[i], mbOther.Bindings[i], ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + case MemberBindingType.ListBinding: + { + var lb = (MemberListBinding)x; + var lbOther = (MemberListBinding)y; + var ic = lb.Initializers.Count; + if (ic != lbOther.Initializers.Count) return false; + for (var i = 0; i < ic; i++) + if (!EqElementInit(lb.Initializers[i], lbOther.Initializers[i], ref xps, ref yps, ref xls, ref yls)) return false; + return true; + } + default: return false; + } + } + } +#endif + /// Converts the expression into the valid C# code representation [RequiresUnreferencedCode(Trimming.Message)] public static class ToCSharpPrinter diff --git a/test/FastExpressionCompiler.IssueTests/Issue261_Loop_wih_conditions_fails.cs b/test/FastExpressionCompiler.IssueTests/Issue261_Loop_wih_conditions_fails.cs index c6c9cb59..b47df6ef 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue261_Loop_wih_conditions_fails.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue261_Loop_wih_conditions_fails.cs @@ -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(); diff --git a/test/FastExpressionCompiler.IssueTests/Issue274_Failing_Expressions_in_Linq2DB.cs b/test/FastExpressionCompiler.IssueTests/Issue274_Failing_Expressions_in_Linq2DB.cs index bfc126b0..73be1e77 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue274_Failing_Expressions_in_Linq2DB.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue274_Failing_Expressions_in_Linq2DB.cs @@ -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(); diff --git a/test/FastExpressionCompiler.IssueTests/Issue430_TryCatch_Bad_label_content_in_ILGenerator.cs b/test/FastExpressionCompiler.IssueTests/Issue430_TryCatch_Bad_label_content_in_ILGenerator.cs index 34620737..78a92bb5 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue430_TryCatch_Bad_label_content_in_ILGenerator.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue430_TryCatch_Bad_label_content_in_ILGenerator.cs @@ -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(); diff --git a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs new file mode 100644 index 00000000..e7d1c30d --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs @@ -0,0 +1,217 @@ +using System; +using System.Linq; +using System.Reflection; + + +#if LIGHT_EXPRESSION +using static FastExpressionCompiler.LightExpression.Expression; +using FastExpressionCompiler.LightExpression; +namespace FastExpressionCompiler.LightExpression.IssueTests; +#else +using static System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.IssueTests; +#endif + + +#if LIGHT_EXPRESSION +public class Issue431_Add_structural_equality_comparison_to_LightExpression : ITest +{ + public int Run() + { + Eq_simple_lambda(); + Eq_lambda_with_parameters(); + Eq_constants(); + Eq_member_access(); + Eq_method_call(); + Eq_new_expression(); + Eq_member_init(); + Eq_new_array(); + Eq_conditional(); + Eq_block_with_variables(); + Eq_try_catch(); + Eq_loop_with_labels(); + Eq_switch(); + Eq_complex_lambda_round_trip(); + NotEq_different_constants(); + NotEq_different_types(); + NotEq_different_parameters(); + return 17; + } + + public void Eq_simple_lambda() + { + var e1 = Lambda>(Constant(42)); + var e2 = Lambda>(Constant(42)); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_lambda_with_parameters() + { + var p1a = Parameter(typeof(int), "x"); + var p1b = Parameter(typeof(int), "y"); + var e1 = Lambda>(Add(p1a, p1b), p1a, p1b); + + var p2a = Parameter(typeof(int), "x"); + var p2b = Parameter(typeof(int), "y"); + var e2 = Lambda>(Add(p2a, p2b), p2a, p2b); + + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_constants() + { + Asserts.IsTrue(Constant(42).EqualsTo(Constant(42))); + Asserts.IsTrue(Constant("hello").EqualsTo(Constant("hello"))); + Asserts.IsTrue(Constant(null, typeof(string)).EqualsTo(Constant(null, typeof(string)))); + } + + public void Eq_member_access() + { + var prop = typeof(string).GetProperty(nameof(string.Length)); + var p1 = Parameter(typeof(string), "s"); + var p2 = Parameter(typeof(string), "s"); + var e1 = Lambda>(Property(p1, prop), p1); + var e2 = Lambda>(Property(p2, prop), p2); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_method_call() + { + 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>(Call(method, p1, p2), p1, p2); + var e2 = Lambda>(Call(method, pa, pb), pa, pb); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_new_expression() + { + var ctor = typeof(B).GetConstructor(Type.EmptyTypes); + var e1 = New(ctor); + var e2 = New(ctor); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public static ConstructorInfo CtorOfA = typeof(A).GetTypeInfo().DeclaredConstructors.First(); + public static ConstructorInfo CtorOfB = typeof(B).GetTypeInfo().DeclaredConstructors.First(); + public static PropertyInfo PropAProp = typeof(A).GetTypeInfo().DeclaredProperties.First(p => p.Name == "Prop"); + + public void Eq_member_init() + { + var e1 = MemberInit(New(CtorOfA, New(CtorOfB)), Bind(PropAProp, New(CtorOfB))); + var e2 = MemberInit(New(CtorOfA, New(CtorOfB)), Bind(PropAProp, New(CtorOfB))); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_new_array() + { + var e1 = NewArrayInit(typeof(int), Constant(1), Constant(2), Constant(3)); + var e2 = NewArrayInit(typeof(int), Constant(1), Constant(2), Constant(3)); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_conditional() + { + var p1 = Parameter(typeof(int), "x"); + var p2 = Parameter(typeof(int), "x"); + var e1 = Lambda>(Condition(Equal(p1, Constant(0)), Constant(1), p1), p1); + var e2 = Lambda>(Condition(Equal(p2, Constant(0)), Constant(1), p2), p2); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_block_with_variables() + { + 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); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_try_catch() + { + 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))); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_loop_with_labels() + { + 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); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_switch() + { + var p1 = Parameter(typeof(int), "x"); + var p2 = Parameter(typeof(int), "x"); + var e1 = Lambda>( + Switch(p1, Constant(-1), SwitchCase(Constant(10), Constant(1)), SwitchCase(Constant(20), Constant(2))), + p1); + var e2 = Lambda>( + Switch(p2, Constant(-1), SwitchCase(Constant(10), Constant(1)), SwitchCase(Constant(20), Constant(2))), + p2); + Asserts.IsTrue(e1.EqualsTo(e2)); + } + + public void Eq_complex_lambda_round_trip() + { + var expr = Lambda>( + MemberInit( + New(CtorOfA, New(CtorOfB)), + Bind(PropAProp, New(CtorOfB))), + ParameterOf("p")); + + var sysExpr = expr.ToLambdaExpression(); + var restoredExpr = sysExpr.ToLightExpression>(); + + Asserts.IsTrue(expr.EqualsTo(restoredExpr)); + } + + public void NotEq_different_constants() + { + Asserts.IsFalse(Constant(42).EqualsTo(Constant(43))); + Asserts.IsFalse(Constant("a").EqualsTo(Constant("b"))); + } + + public void NotEq_different_types() + { + Asserts.IsFalse(Constant(42).EqualsTo(Constant(42L))); + Asserts.IsFalse(Default(typeof(int)).EqualsTo(Default(typeof(long)))); + } + + public void NotEq_different_parameters() + { + // Parameters with different names should not be equal when unmapped + var p1 = Parameter(typeof(int), "x"); + var p2 = Parameter(typeof(int), "y"); + var e1 = Lambda>(p1, p1); + var e2 = Lambda>(p2, p2); + // When mapped by position in a lambda, different-named params ARE equal structurally (same position) + Asserts.IsTrue(e1.EqualsTo(e2)); + + // But accessing a param outside its lambda context uses name comparison + Asserts.IsFalse(p1.EqualsTo(p2)); + } + + public class A + { + public B Prop { get; set; } + public A(B b) { Prop = b; } + } + + public class B { } +} +#endif diff --git a/test/FastExpressionCompiler.LightExpression.IssueTests/Issue363_ActionFunc16Generics.cs b/test/FastExpressionCompiler.LightExpression.IssueTests/Issue363_ActionFunc16Generics.cs index 131a3840..88fc2b17 100644 --- a/test/FastExpressionCompiler.LightExpression.IssueTests/Issue363_ActionFunc16Generics.cs +++ b/test/FastExpressionCompiler.LightExpression.IssueTests/Issue363_ActionFunc16Generics.cs @@ -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, ... } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index b82abb6e..010bef38 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -382,6 +382,8 @@ void Run(Func run, string name = null) Run(new Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); Run(new LightExpression.IssueTests.Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); + Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression().Run); + Run(new Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); Run(new LightExpression.IssueTests.Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index b2b11cfa..a2abaa52 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -391,6 +391,8 @@ void Run(Func run, string name = null) Run(new Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); Run(new LightExpression.IssueTests.Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); + Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression().Run); + Run(new Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); Run(new LightExpression.IssueTests.Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); diff --git a/test/FastExpressionCompiler.UnitTests/AssignTests.cs b/test/FastExpressionCompiler.UnitTests/AssignTests.cs index f4a8d47c..9a7f79d9 100644 --- a/test/FastExpressionCompiler.UnitTests/AssignTests.cs +++ b/test/FastExpressionCompiler.UnitTests/AssignTests.cs @@ -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()); From aff076d43f68c1f106f7507a98de78663fe96506 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:12:34 +0000 Subject: [PATCH 3/7] Convert ExpressionEqualityComparer to IEqualityComparer and Issue431 tests to ITestX struct Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/9850fc00-8dc7-4468-a75a-3af2c6eeac31 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 20 ++- ..._equality_comparison_to_LightExpression.cs | 139 +++++++++--------- .../Program.cs | 3 +- .../Program.cs | 3 +- 4 files changed, 87 insertions(+), 78 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 3f930b7a..856d59a7 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10002,18 +10002,26 @@ internal static StringBuilder CreateExpressionString(this Expression e, StringBu #if LIGHT_EXPRESSION /// Provides structural equality comparison for the LightExpression. - public static class ExpressionEqualityComparer + public sealed class ExpressionEqualityComparer : IEqualityComparer { + /// The default singleton instance. + public static readonly ExpressionEqualityComparer Default = new ExpressionEqualityComparer(); + /// Structurally compares two expressions. /// Parameters are matched by their position within their enclosing lambda, and label targets by identity pairing. /// No heap allocations for expressions with up to 4 lambda parameters or label targets. - public static bool EqualsTo(this Expression x, Expression y) + public bool Equals(Expression x, Expression y) { SmallList xps = default, yps = default; SmallList xls = default, yls = default; return Eq(x, y, ref xps, ref yps, ref xls, ref yls); } + /// Returns a hash based on NodeType and Type of the expression. + /// Consistent with structural equality: expressions that compare equal will have the same hash. + public int GetHashCode(Expression obj) => + obj == null ? 0 : HashCombiner.Combine((int)obj.NodeType, obj.Type?.GetHashCode() ?? 0); + private static bool Eq(Expression x, Expression y, ref SmallList xps, ref SmallList yps, ref SmallList xls, ref SmallList yls) @@ -10400,6 +10408,14 @@ private static bool EqBinding(MemberBinding x, MemberBinding y, } } } + + /// Extension method convenience wrapper for structural equality via . + public static class ExpressionEqualityComparerExtensions + { + /// Structurally compares two expressions using . + public static bool EqualsTo(this Expression x, Expression y) => + ExpressionEqualityComparer.Default.Equals(x, y); + } #endif /// Converts the expression into the valid C# code representation diff --git a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs index e7d1c30d..c02e4084 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs @@ -14,38 +14,41 @@ namespace FastExpressionCompiler.IssueTests; #if LIGHT_EXPRESSION -public class Issue431_Add_structural_equality_comparison_to_LightExpression : ITest +public struct Issue431_Add_structural_equality_comparison_to_LightExpression : ITestX { - public int Run() - { - Eq_simple_lambda(); - Eq_lambda_with_parameters(); - Eq_constants(); - Eq_member_access(); - Eq_method_call(); - Eq_new_expression(); - Eq_member_init(); - Eq_new_array(); - Eq_conditional(); - Eq_block_with_variables(); - Eq_try_catch(); - Eq_loop_with_labels(); - Eq_switch(); - Eq_complex_lambda_round_trip(); - NotEq_different_constants(); - NotEq_different_types(); - NotEq_different_parameters(); - return 17; - } - - public void Eq_simple_lambda() + 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); + Eq_complex_lambda_round_trip(t); + NotEq_different_constants(t); + NotEq_different_types(t); + NotEq_different_parameters(t); + } + + public void Eq_simple_lambda(TestContext t) { var e1 = Lambda>(Constant(42)); var e2 = Lambda>(Constant(42)); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_lambda_with_parameters() + public void Eq_lambda_with_parameters(TestContext t) { var p1a = Parameter(typeof(int), "x"); var p1b = Parameter(typeof(int), "y"); @@ -55,27 +58,27 @@ public void Eq_lambda_with_parameters() var p2b = Parameter(typeof(int), "y"); var e2 = Lambda>(Add(p2a, p2b), p2a, p2b); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_constants() + public void Eq_constants(TestContext t) { - Asserts.IsTrue(Constant(42).EqualsTo(Constant(42))); - Asserts.IsTrue(Constant("hello").EqualsTo(Constant("hello"))); - Asserts.IsTrue(Constant(null, typeof(string)).EqualsTo(Constant(null, typeof(string)))); + 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() + 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>(Property(p1, prop), p1); var e2 = Lambda>(Property(p2, prop), p2); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_method_call() + 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"); @@ -84,65 +87,59 @@ public void Eq_method_call() var pb = Parameter(typeof(string), "b"); var e1 = Lambda>(Call(method, p1, p2), p1, p2); var e2 = Lambda>(Call(method, pa, pb), pa, pb); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_new_expression() + public void Eq_new_expression(TestContext t) { var ctor = typeof(B).GetConstructor(Type.EmptyTypes); var e1 = New(ctor); var e2 = New(ctor); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public static ConstructorInfo CtorOfA = typeof(A).GetTypeInfo().DeclaredConstructors.First(); - public static ConstructorInfo CtorOfB = typeof(B).GetTypeInfo().DeclaredConstructors.First(); - public static PropertyInfo PropAProp = typeof(A).GetTypeInfo().DeclaredProperties.First(p => p.Name == "Prop"); - - public void Eq_member_init() + 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))); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_new_array() + 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)); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_conditional() + public void Eq_conditional(TestContext t) { var p1 = Parameter(typeof(int), "x"); var p2 = Parameter(typeof(int), "x"); var e1 = Lambda>(Condition(Equal(p1, Constant(0)), Constant(1), p1), p1); var e2 = Lambda>(Condition(Equal(p2, Constant(0)), Constant(1), p2), p2); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_block_with_variables() + 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); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_try_catch() + 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))); - Asserts.IsTrue(e1.EqualsTo(e2)); + 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() + public void Eq_loop_with_labels(TestContext t) { var brk1 = Label(typeof(void), "break"); var cnt1 = Label(typeof(void), "continue"); @@ -150,10 +147,10 @@ public void Eq_loop_with_labels() 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); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_switch() + public void Eq_switch(TestContext t) { var p1 = Parameter(typeof(int), "x"); var p2 = Parameter(typeof(int), "x"); @@ -163,10 +160,10 @@ public void Eq_switch() var e2 = Lambda>( Switch(p2, Constant(-1), SwitchCase(Constant(10), Constant(1)), SwitchCase(Constant(20), Constant(2))), p2); - Asserts.IsTrue(e1.EqualsTo(e2)); + t.IsTrue(e1.EqualsTo(e2)); } - public void Eq_complex_lambda_round_trip() + public void Eq_complex_lambda_round_trip(TestContext t) { var expr = Lambda>( MemberInit( @@ -177,33 +174,31 @@ public void Eq_complex_lambda_round_trip() var sysExpr = expr.ToLambdaExpression(); var restoredExpr = sysExpr.ToLightExpression>(); - Asserts.IsTrue(expr.EqualsTo(restoredExpr)); + t.IsTrue(expr.EqualsTo(restoredExpr)); } - public void NotEq_different_constants() + public void NotEq_different_constants(TestContext t) { - Asserts.IsFalse(Constant(42).EqualsTo(Constant(43))); - Asserts.IsFalse(Constant("a").EqualsTo(Constant("b"))); + t.IsFalse(Constant(42).EqualsTo(Constant(43))); + t.IsFalse(Constant("a").EqualsTo(Constant("b"))); } - public void NotEq_different_types() + public void NotEq_different_types(TestContext t) { - Asserts.IsFalse(Constant(42).EqualsTo(Constant(42L))); - Asserts.IsFalse(Default(typeof(int)).EqualsTo(Default(typeof(long)))); + t.IsFalse(Constant(42).EqualsTo(Constant(42L))); + t.IsFalse(Default(typeof(int)).EqualsTo(Default(typeof(long)))); } - public void NotEq_different_parameters() + public void NotEq_different_parameters(TestContext t) { - // Parameters with different names should not be equal when unmapped var p1 = Parameter(typeof(int), "x"); var p2 = Parameter(typeof(int), "y"); var e1 = Lambda>(p1, p1); var e2 = Lambda>(p2, p2); // When mapped by position in a lambda, different-named params ARE equal structurally (same position) - Asserts.IsTrue(e1.EqualsTo(e2)); - + t.IsTrue(e1.EqualsTo(e2)); // But accessing a param outside its lambda context uses name comparison - Asserts.IsFalse(p1.EqualsTo(p2)); + t.IsFalse(p1.EqualsTo(p2)); } public class A diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 010bef38..7b0a050f 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -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()); @@ -382,8 +383,6 @@ void Run(Func run, string name = null) Run(new Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); Run(new LightExpression.IssueTests.Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); - Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression().Run); - Run(new Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); Run(new LightExpression.IssueTests.Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index a2abaa52..c7124d5b 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -67,6 +67,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()); @@ -391,8 +392,6 @@ void Run(Func run, string name = null) Run(new Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); Run(new LightExpression.IssueTests.Issue430_TryCatch_Bad_label_content_in_ILGenerator().Run); - Run(new LightExpression.IssueTests.Issue431_Add_structural_equality_comparison_to_LightExpression().Run); - Run(new Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); Run(new LightExpression.IssueTests.Issue437_Shared_variables_with_nested_lambdas_returning_incorrect_values().Run); From d2de24468413d80016c342a6222850452675df12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:17:01 +0000 Subject: [PATCH 4/7] Convert ExpressionEqualityComparer to struct, share between LE and SLE Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/c32e3754-4004-47e7-972e-602275e21140 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 232 +++++++++--------- ..._equality_comparison_to_LightExpression.cs | 10 +- .../Program.cs | 1 + 3 files changed, 126 insertions(+), 117 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 856d59a7..af8a8623 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10000,31 +10000,37 @@ internal static StringBuilder CreateExpressionString(this Expression e, StringBu } } -#if LIGHT_EXPRESSION - /// Provides structural equality comparison for the LightExpression. - public sealed class ExpressionEqualityComparer : IEqualityComparer + /// Provides structural equality comparison for expression trees (both LightExpression and System.Linq.Expressions). + /// Use the static method as the primary entry point — it creates a temporary struct on the stack with no heap allocation. + /// Parameters are matched by their position within their enclosing lambda or block, and label targets by identity pairing. + /// No heap allocations for expressions with up to 4 lambda parameters or label targets. + public struct ExpressionEqualityComparer : IEqualityComparer { - /// The default singleton instance. - public static readonly ExpressionEqualityComparer Default = new ExpressionEqualityComparer(); + private SmallList _xps, _yps; + private SmallList _xls, _yls; - /// Structurally compares two expressions. - /// Parameters are matched by their position within their enclosing lambda, and label targets by identity pairing. - /// No heap allocations for expressions with up to 4 lambda parameters or label targets. - public bool Equals(Expression x, Expression y) + /// Structurally compares two expressions. Primary entry point — no heap allocation for the comparer. + public static bool EqualsTo(Expression x, Expression y) { - SmallList xps = default, yps = default; - SmallList xls = default, yls = default; - return Eq(x, y, ref xps, ref yps, ref xls, ref yls); + var eq = default(ExpressionEqualityComparer); + return eq.Eq(x, y); } - /// Returns a hash based on NodeType and Type of the expression. - /// Consistent with structural equality: expressions that compare equal will have the same hash. - public int GetHashCode(Expression obj) => - obj == null ? 0 : HashCombiner.Combine((int)obj.NodeType, obj.Type?.GetHashCode() ?? 0); + /// IEqualityComparer implementation — delegates to the static to ensure a fresh context per call. + bool IEqualityComparer.Equals(Expression x, Expression y) => EqualsTo(x, y); - private static bool Eq(Expression x, Expression y, - ref SmallList xps, ref SmallList yps, - ref SmallList xls, ref SmallList yls) + /// Returns a hash based on NodeType and Type of the expression. + int IEqualityComparer.GetHashCode(Expression obj) + { + if (obj == null) return 0; + var h1 = (int)obj.NodeType; + var h2 = obj.Type?.GetHashCode() ?? 0; + return h1 == 0 ? h2 : unchecked((h1 << 5) + h1 ^ h2); + } + + /// Structurally compares two expressions, using the current parameter/label context for identity mapping. + /// For top-level comparisons, prefer the static . + public bool Eq(Expression x, Expression y) { if (ReferenceEquals(x, y)) return true; if (x == null | y == null) return false; @@ -10035,9 +10041,9 @@ private static bool Eq(Expression x, Expression y, { var px = (ParameterExpression)x; var py = (ParameterExpression)y; - for (var i = 0; i < xps.Count; i++) - if (ReferenceEquals(xps.Items[i], px)) - return ReferenceEquals(yps.Items[i], py); + for (var i = 0; i < _xps.Count; i++) + if (ReferenceEquals(_xps.Items[i], px)) + return ReferenceEquals(_yps.Items[i], py); // unmapped — compare structurally (Type already checked) return px.IsByRef == py.IsByRef && px.Name == py.Name; } @@ -10053,17 +10059,28 @@ private static bool Eq(Expression x, Expression y, { var lx = (LambdaExpression)x; var ly = (LambdaExpression)y; +#if LIGHT_EXPRESSION var pc = lx.ParameterCount; if (pc != ly.ParameterCount) return false; - var sc = xps.Count; + var sc = _xps.Count; + for (var i = 0; i < pc; i++) + { + _xps.AddDefaultAndGetRef() = lx.GetParameter(i); + _yps.AddDefaultAndGetRef() = ly.GetParameter(i); + } +#else + var pc = lx.Parameters.Count; + if (pc != ly.Parameters.Count) return false; + var sc = _xps.Count; for (var i = 0; i < pc; i++) { - xps.AddDefaultAndGetRef() = lx.GetParameter(i); - yps.AddDefaultAndGetRef() = ly.GetParameter(i); + _xps.AddDefaultAndGetRef() = lx.Parameters[i]; + _yps.AddDefaultAndGetRef() = ly.Parameters[i]; } - var eq = Eq(lx.Body, ly.Body, ref xps, ref yps, ref xls, ref yls); - xps.Count = sc; - yps.Count = sc; +#endif + var eq = Eq(lx.Body, ly.Body); + _xps.Count = sc; + _yps.Count = sc; return eq; } @@ -10080,8 +10097,7 @@ private static bool Eq(Expression x, Expression y, { var ux = (UnaryExpression)x; var uy = (UnaryExpression)y; - return ux.Method == uy.Method && - Eq(ux.Operand, uy.Operand, ref xps, ref yps, ref xls, ref yls); + return ux.Method == uy.Method && Eq(ux.Operand, uy.Operand); } case ExpressionType.Add: case ExpressionType.AddChecked: @@ -10108,9 +10124,9 @@ private static bool Eq(Expression x, Expression y, var bx = (BinaryExpression)x; var by = (BinaryExpression)y; return bx.Method == by.Method && - Eq(bx.Conversion, by.Conversion, ref xps, ref yps, ref xls, ref yls) && - Eq(bx.Left, by.Left, ref xps, ref yps, ref xls, ref yls) && - Eq(bx.Right, by.Right, ref xps, ref yps, ref xls, ref yls); + Eq(bx.Conversion, by.Conversion) && + Eq(bx.Left, by.Left) && + Eq(bx.Right, by.Right); } case ExpressionType.Call: @@ -10118,24 +10134,22 @@ private static bool Eq(Expression x, Expression y, var mx = (MethodCallExpression)x; var my = (MethodCallExpression)y; return mx.Method == my.Method && - Eq(mx.Object, my.Object, ref xps, ref yps, ref xls, ref yls) && - EqArgs(mx, my, ref xps, ref yps, ref xls, ref yls); + Eq(mx.Object, my.Object) && + EqArgs(mx, my); } case ExpressionType.MemberAccess: { var fx = (MemberExpression)x; var fy = (MemberExpression)y; - return fx.Member == fy.Member && - Eq(fx.Expression, fy.Expression, ref xps, ref yps, ref xls, ref yls); + return fx.Member == fy.Member && Eq(fx.Expression, fy.Expression); } case ExpressionType.New: { var nx = (NewExpression)x; var ny = (NewExpression)y; - return nx.Constructor == ny.Constructor && - EqArgs(nx, ny, ref xps, ref yps, ref xls, ref yls); + return nx.Constructor == ny.Constructor && EqArgs(nx, ny); } case ExpressionType.NewArrayInit: @@ -10143,16 +10157,22 @@ private static bool Eq(Expression x, Expression y, { var nx = (NewArrayExpression)x; var ny = (NewArrayExpression)y; - return EqArgs(nx, ny, ref xps, ref yps, ref xls, ref yls); +#if LIGHT_EXPRESSION + return EqArgs(nx, ny); +#else + var ec = nx.Expressions.Count; + if (ec != ny.Expressions.Count) return false; + for (var i = 0; i < ec; i++) + if (!Eq(nx.Expressions[i], ny.Expressions[i])) return false; + return true; +#endif } case ExpressionType.Conditional: { var cx = (ConditionalExpression)x; var cy = (ConditionalExpression)y; - return Eq(cx.Test, cy.Test, ref xps, ref yps, ref xls, ref yls) && - Eq(cx.IfTrue, cy.IfTrue, ref xps, ref yps, ref xls, ref yls) && - Eq(cx.IfFalse, cy.IfFalse, ref xps, ref yps, ref xls, ref yls); + return Eq(cx.Test, cy.Test) && Eq(cx.IfTrue, cy.IfTrue) && Eq(cx.IfFalse, cy.IfFalse); } case ExpressionType.Block: @@ -10163,18 +10183,17 @@ private static bool Eq(Expression x, Expression y, if (vc != by.Variables.Count) return false; var ec = bx.Expressions.Count; if (ec != by.Expressions.Count) return false; - var sc = xps.Count; + var sc = _xps.Count; for (var i = 0; i < vc; i++) { - xps.AddDefaultAndGetRef() = bx.Variables[i]; - yps.AddDefaultAndGetRef() = by.Variables[i]; + _xps.AddDefaultAndGetRef() = bx.Variables[i]; + _yps.AddDefaultAndGetRef() = by.Variables[i]; } var eq = true; for (var i = 0; i < ec && eq; i++) - eq = Eq(bx.Expressions.GetSurePresentRef(i), by.Expressions.GetSurePresentRef(i), - ref xps, ref yps, ref xls, ref yls); - xps.Count = sc; - yps.Count = sc; + eq = Eq(bx.Expressions[i], by.Expressions[i]); + _xps.Count = sc; + _yps.Count = sc; return eq; } @@ -10184,9 +10203,13 @@ private static bool Eq(Expression x, Expression y, var my = (MemberInitExpression)y; var bc = mx.Bindings.Count; if (bc != my.Bindings.Count) return false; - if (!Eq(mx.Expression, my.Expression, ref xps, ref yps, ref xls, ref yls)) return false; +#if LIGHT_EXPRESSION + if (!Eq(mx.Expression, my.Expression)) return false; +#else + if (!Eq(mx.NewExpression, my.NewExpression)) return false; +#endif for (var i = 0; i < bc; i++) - if (!EqBinding(mx.Bindings[i], my.Bindings[i], ref xps, ref yps, ref xls, ref yls)) return false; + if (!EqBinding(mx.Bindings[i], my.Bindings[i])) return false; return true; } @@ -10196,9 +10219,9 @@ private static bool Eq(Expression x, Expression y, var ly = (ListInitExpression)y; var ic = lx.Initializers.Count; if (ic != ly.Initializers.Count) return false; - if (!Eq(lx.NewExpression, ly.NewExpression, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(lx.NewExpression, ly.NewExpression)) return false; for (var i = 0; i < ic; i++) - if (!EqElementInit(lx.Initializers[i], ly.Initializers[i], ref xps, ref yps, ref xls, ref yls)) return false; + if (!EqElementInit(lx.Initializers[i], ly.Initializers[i])) return false; return true; } @@ -10207,25 +10230,21 @@ private static bool Eq(Expression x, Expression y, { var tx = (TypeBinaryExpression)x; var ty = (TypeBinaryExpression)y; - return tx.TypeOperand == ty.TypeOperand && - Eq(tx.Expression, ty.Expression, ref xps, ref yps, ref xls, ref yls); + return tx.TypeOperand == ty.TypeOperand && Eq(tx.Expression, ty.Expression); } case ExpressionType.Invoke: { var ix = (InvocationExpression)x; var iy = (InvocationExpression)y; - return Eq(ix.Expression, iy.Expression, ref xps, ref yps, ref xls, ref yls) && - EqArgs(ix, iy, ref xps, ref yps, ref xls, ref yls); + return Eq(ix.Expression, iy.Expression) && EqArgs(ix, iy); } case ExpressionType.Index: { var ix = (IndexExpression)x; var iy = (IndexExpression)y; - return ix.Indexer == iy.Indexer && - Eq(ix.Object, iy.Object, ref xps, ref yps, ref xls, ref yls) && - EqArgs(ix, iy, ref xps, ref yps, ref xls, ref yls); + return ix.Indexer == iy.Indexer && Eq(ix.Object, iy.Object) && EqArgs(ix, iy); } case ExpressionType.Default: @@ -10235,35 +10254,32 @@ private static bool Eq(Expression x, Expression y, { var lx = (LabelExpression)x; var ly = (LabelExpression)y; - return EqLabel(lx.Target, ly.Target, ref xls, ref yls) && - Eq(lx.DefaultValue, ly.DefaultValue, ref xps, ref yps, ref xls, ref yls); + return EqLabel(lx.Target, ly.Target) && Eq(lx.DefaultValue, ly.DefaultValue); } case ExpressionType.Goto: { var gx = (GotoExpression)x; var gy = (GotoExpression)y; - return gx.Kind == gy.Kind && - EqLabel(gx.Target, gy.Target, ref xls, ref yls) && - Eq(gx.Value, gy.Value, ref xps, ref yps, ref xls, ref yls); + return gx.Kind == gy.Kind && EqLabel(gx.Target, gy.Target) && Eq(gx.Value, gy.Value); } case ExpressionType.Loop: { var lx = (LoopExpression)x; var ly = (LoopExpression)y; - return EqLabel(lx.BreakLabel, ly.BreakLabel, ref xls, ref yls) && - EqLabel(lx.ContinueLabel, ly.ContinueLabel, ref xls, ref yls) && - Eq(lx.Body, ly.Body, ref xps, ref yps, ref xls, ref yls); + return EqLabel(lx.BreakLabel, ly.BreakLabel) && + EqLabel(lx.ContinueLabel, ly.ContinueLabel) && + Eq(lx.Body, ly.Body); } case ExpressionType.Try: { var tx = (TryExpression)x; var ty = (TryExpression)y; - if (!Eq(tx.Body, ty.Body, ref xps, ref yps, ref xls, ref yls)) return false; - if (!Eq(tx.Finally, ty.Finally, ref xps, ref yps, ref xls, ref yls)) return false; - if (!Eq(tx.Fault, ty.Fault, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(tx.Body, ty.Body)) return false; + if (!Eq(tx.Finally, ty.Finally)) return false; + if (!Eq(tx.Fault, ty.Fault)) return false; var hc = tx.Handlers.Count; if (hc != ty.Handlers.Count) return false; for (var i = 0; i < hc; i++) @@ -10271,18 +10287,17 @@ private static bool Eq(Expression x, Expression y, var hx = tx.Handlers[i]; var hy = ty.Handlers[i]; if (hx.Test != hy.Test) return false; - var sc = xps.Count; + var sc = _xps.Count; if (hx.Variable != null | hy.Variable != null) { if (hx.Variable == null | hy.Variable == null) return false; if (hx.Variable.Type != hy.Variable.Type) return false; - xps.AddDefaultAndGetRef() = hx.Variable; - yps.AddDefaultAndGetRef() = hy.Variable; + _xps.AddDefaultAndGetRef() = hx.Variable; + _yps.AddDefaultAndGetRef() = hy.Variable; } - var ceq = Eq(hx.Body, hy.Body, ref xps, ref yps, ref xls, ref yls) && - Eq(hx.Filter, hy.Filter, ref xps, ref yps, ref xls, ref yls); - xps.Count = sc; - yps.Count = sc; + var ceq = Eq(hx.Body, hy.Body) && Eq(hx.Filter, hy.Filter); + _xps.Count = sc; + _yps.Count = sc; if (!ceq) return false; } return true; @@ -10293,19 +10308,19 @@ private static bool Eq(Expression x, Expression y, var sx = (SwitchExpression)x; var sy = (SwitchExpression)y; if (sx.Comparison != sy.Comparison) return false; - if (!Eq(sx.SwitchValue, sy.SwitchValue, ref xps, ref yps, ref xls, ref yls)) return false; - if (!Eq(sx.DefaultBody, sy.DefaultBody, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(sx.SwitchValue, sy.SwitchValue)) return false; + if (!Eq(sx.DefaultBody, sy.DefaultBody)) return false; var cc = sx.Cases.Count; if (cc != sy.Cases.Count) return false; for (var i = 0; i < cc; i++) { var cx = sx.Cases[i]; var cy = sy.Cases[i]; - if (!Eq(cx.Body, cy.Body, ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(cx.Body, cy.Body)) return false; var tc = cx.TestValues.Count; if (tc != cy.TestValues.Count) return false; for (var j = 0; j < tc; j++) - if (!Eq(cx.TestValues[j], cy.TestValues[j], ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(cx.TestValues[j], cy.TestValues[j])) return false; } return true; } @@ -10317,7 +10332,7 @@ private static bool Eq(Expression x, Expression y, var vc = rx.Variables.Count; if (vc != ry.Variables.Count) return false; for (var i = 0; i < vc; i++) - if (!Eq(rx.Variables[i], ry.Variables[i], ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(rx.Variables[i], ry.Variables[i])) return false; return true; } @@ -10336,54 +10351,48 @@ private static bool Eq(Expression x, Expression y, } } - private static bool EqLabel(LabelTarget x, LabelTarget y, - ref SmallList xls, ref SmallList yls) + private bool EqLabel(LabelTarget x, LabelTarget y) { if (ReferenceEquals(x, y)) return true; if (x == null | y == null) return false; if (x.Type != y.Type) return false; - for (var i = 0; i < xls.Count; i++) - if (ReferenceEquals(xls.Items[i], x)) - return ReferenceEquals(yls.Items[i], y); + for (var i = 0; i < _xls.Count; i++) + if (ReferenceEquals(_xls.Items[i], x)) + return ReferenceEquals(_yls.Items[i], y); // Register the pair and compare by name - xls.AddDefaultAndGetRef() = x; - yls.AddDefaultAndGetRef() = y; + _xls.AddDefaultAndGetRef() = x; + _yls.AddDefaultAndGetRef() = y; return x.Name == y.Name; } - private static bool EqArgs(IArgumentProvider x, IArgumentProvider y, - ref SmallList xps, ref SmallList yps, - ref SmallList xls, ref SmallList yls) + private bool EqArgs(IArgumentProvider x, IArgumentProvider y) { var c = x.ArgumentCount; if (c != y.ArgumentCount) return false; for (var i = 0; i < c; i++) - if (!Eq(x.GetArgument(i), y.GetArgument(i), ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(x.GetArgument(i), y.GetArgument(i))) return false; return true; } - private static bool EqElementInit(ElementInit x, ElementInit y, - ref SmallList xps, ref SmallList yps, - ref SmallList xls, ref SmallList yls) + private bool EqElementInit(ElementInit x, ElementInit y) { if (x.AddMethod != y.AddMethod) return false; - var ac = x.ArgumentCount; - if (ac != y.ArgumentCount) return false; + var ax = (IArgumentProvider)x; + var ay = (IArgumentProvider)y; + var ac = ax.ArgumentCount; + if (ac != ay.ArgumentCount) return false; for (var i = 0; i < ac; i++) - if (!Eq(x.GetArgument(i), y.GetArgument(i), ref xps, ref yps, ref xls, ref yls)) return false; + if (!Eq(ax.GetArgument(i), ay.GetArgument(i))) return false; return true; } - private static bool EqBinding(MemberBinding x, MemberBinding y, - ref SmallList xps, ref SmallList yps, - ref SmallList xls, ref SmallList yls) + private bool EqBinding(MemberBinding x, MemberBinding y) { if (x.BindingType != y.BindingType | x.Member != y.Member) return false; switch (x.BindingType) { case MemberBindingType.Assignment: - return Eq(((MemberAssignment)x).Expression, ((MemberAssignment)y).Expression, - ref xps, ref yps, ref xls, ref yls); + return Eq(((MemberAssignment)x).Expression, ((MemberAssignment)y).Expression); case MemberBindingType.MemberBinding: { var mb = (MemberMemberBinding)x; @@ -10391,7 +10400,7 @@ private static bool EqBinding(MemberBinding x, MemberBinding y, var bc = mb.Bindings.Count; if (bc != mbOther.Bindings.Count) return false; for (var i = 0; i < bc; i++) - if (!EqBinding(mb.Bindings[i], mbOther.Bindings[i], ref xps, ref yps, ref xls, ref yls)) return false; + if (!EqBinding(mb.Bindings[i], mbOther.Bindings[i])) return false; return true; } case MemberBindingType.ListBinding: @@ -10401,7 +10410,7 @@ private static bool EqBinding(MemberBinding x, MemberBinding y, var ic = lb.Initializers.Count; if (ic != lbOther.Initializers.Count) return false; for (var i = 0; i < ic; i++) - if (!EqElementInit(lb.Initializers[i], lbOther.Initializers[i], ref xps, ref yps, ref xls, ref yls)) return false; + if (!EqElementInit(lb.Initializers[i], lbOther.Initializers[i])) return false; return true; } default: return false; @@ -10409,14 +10418,13 @@ private static bool EqBinding(MemberBinding x, MemberBinding y, } } - /// Extension method convenience wrapper for structural equality via . + /// Extension method for structural equality via . public static class ExpressionEqualityComparerExtensions { - /// Structurally compares two expressions using . + /// Structurally compares two expressions. Calls the static directly for best performance. public static bool EqualsTo(this Expression x, Expression y) => - ExpressionEqualityComparer.Default.Equals(x, y); + ExpressionEqualityComparer.EqualsTo(x, y); } -#endif /// Converts the expression into the valid C# code representation [RequiresUnreferencedCode(Trimming.Message)] diff --git a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs index c02e4084..db37bdc3 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Reflection; - #if LIGHT_EXPRESSION using static FastExpressionCompiler.LightExpression.Expression; using FastExpressionCompiler.LightExpression; @@ -12,8 +11,6 @@ namespace FastExpressionCompiler.LightExpression.IssueTests; namespace FastExpressionCompiler.IssueTests; #endif - -#if LIGHT_EXPRESSION public struct Issue431_Add_structural_equality_comparison_to_LightExpression : ITestX { public static readonly ConstructorInfo CtorOfA = typeof(A).GetTypeInfo().DeclaredConstructors.First(); @@ -35,7 +32,9 @@ public void Run(TestRun t) Eq_try_catch(t); Eq_loop_with_labels(t); Eq_switch(t); +#if LIGHT_EXPRESSION Eq_complex_lambda_round_trip(t); +#endif NotEq_different_constants(t); NotEq_different_types(t); NotEq_different_parameters(t); @@ -163,19 +162,21 @@ public void Eq_switch(TestContext t) t.IsTrue(e1.EqualsTo(e2)); } +#if LIGHT_EXPRESSION public void Eq_complex_lambda_round_trip(TestContext t) { var expr = Lambda>( MemberInit( New(CtorOfA, New(CtorOfB)), Bind(PropAProp, New(CtorOfB))), - ParameterOf("p")); + Parameter(typeof(object[]), "p")); var sysExpr = expr.ToLambdaExpression(); var restoredExpr = sysExpr.ToLightExpression>(); t.IsTrue(expr.EqualsTo(restoredExpr)); } +#endif public void NotEq_different_constants(TestContext t) { @@ -209,4 +210,3 @@ public class A public class B { } } -#endif diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index c7124d5b..d06a8dda 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -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()); From 50e5f623f8d35762152090aa2e65519e3451e2dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:34:29 +0000 Subject: [PATCH 5/7] Address review: SmallList8, proper recursive GetHashCode, IEqualityComparer, param/variable fail-fast, code improvements Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/1fc579e7-522b-4451-bbdf-34f552a1de4b Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 252 ++++++++++++++++-- 1 file changed, 223 insertions(+), 29 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index af8a8623..26e9487f 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10003,11 +10003,13 @@ internal static StringBuilder CreateExpressionString(this Expression e, StringBu /// Provides structural equality comparison for expression trees (both LightExpression and System.Linq.Expressions). /// Use the static method as the primary entry point — it creates a temporary struct on the stack with no heap allocation. /// Parameters are matched by their position within their enclosing lambda or block, and label targets by identity pairing. - /// No heap allocations for expressions with up to 4 lambda parameters or label targets. - public struct ExpressionEqualityComparer : IEqualityComparer + /// Up to 8 lambda parameters and label targets are stored on the stack; larger numbers spill to a heap array. + public struct ExpressionEqualityComparer : IEqualityComparer, IEqualityComparer { - private SmallList _xps, _yps; - private SmallList _xls, _yls; + // SmallList, NoArrayPool> stores the first 8 items inline in the struct (on the stack), + // which covers the vast majority of real-world lambdas without any heap allocation. + private SmallList, NoArrayPool> _xps, _yps; + private SmallList, NoArrayPool> _xls, _yls; /// Structurally compares two expressions. Primary entry point — no heap allocation for the comparer. public static bool EqualsTo(Expression x, Expression y) @@ -10016,18 +10018,201 @@ public static bool EqualsTo(Expression x, Expression y) return eq.Eq(x, y); } - /// IEqualityComparer implementation — delegates to the static to ensure a fresh context per call. - bool IEqualityComparer.Equals(Expression x, Expression y) => EqualsTo(x, y); + /// Computes a content-addressable hash for the expression tree. + /// Bound lambda and block parameters are hashed by their position index so that structurally + /// equal lambdas with differently-named parameters produce the same hash. + public static int GetHashCode(Expression expr) + { + var ctx = default(ExpressionEqualityComparer); + return ctx.Hash(expr); + } + + private static int Combine(int h1, int h2) => + h1 == 0 ? h2 : unchecked((h1 << 5) + h1 ^ h2); - /// Returns a hash based on NodeType and Type of the expression. - int IEqualityComparer.GetHashCode(Expression obj) + private int Hash(Expression expr) { - if (obj == null) return 0; - var h1 = (int)obj.NodeType; - var h2 = obj.Type?.GetHashCode() ?? 0; - return h1 == 0 ? h2 : unchecked((h1 << 5) + h1 ^ h2); + if (expr == null) return 0; + var h = Combine((int)expr.NodeType, expr.Type?.GetHashCode() ?? 0); + switch (expr.NodeType) + { + case ExpressionType.Parameter: + { + // Bound parameters are hashed by their position for structural consistency + // so that Lambda(x=>x+1) and Lambda(y=>y+1) produce the same hash. + for (var i = 0; i < _xps.Count; i++) + if (ReferenceEquals(_xps.GetSurePresentRef(i), expr)) + return Combine(h, i); + // Free/standalone parameter: use name as discriminator. + return Combine(h, ((ParameterExpression)expr).Name?.GetHashCode() ?? 0); + } + case ExpressionType.Constant: + return Combine(h, ((ConstantExpression)expr).Value?.GetHashCode() ?? 0); + case ExpressionType.Lambda: + { + var l = (LambdaExpression)expr; + var sc = _xps.Count; +#if LIGHT_EXPRESSION + var pc = l.ParameterCount; + for (var i = 0; i < pc; i++) _xps.AddDefaultAndGetRef() = l.GetParameter(i); +#else + var pc = l.Parameters.Count; + for (var i = 0; i < pc; i++) _xps.AddDefaultAndGetRef() = l.Parameters[i]; +#endif + h = Combine(h, Hash(l.Body)); + _xps.Count = sc; + return h; + } + case ExpressionType.Negate: case ExpressionType.NegateChecked: + case ExpressionType.UnaryPlus: case ExpressionType.Not: + case ExpressionType.ArrayLength: case ExpressionType.TypeAs: + case ExpressionType.Convert: case ExpressionType.ConvertChecked: + case ExpressionType.Quote: case ExpressionType.Throw: + case ExpressionType.OnesComplement: case ExpressionType.IsTrue: case ExpressionType.IsFalse: + case ExpressionType.Increment: case ExpressionType.Decrement: + case ExpressionType.PreIncrementAssign: case ExpressionType.PostIncrementAssign: + case ExpressionType.PreDecrementAssign: case ExpressionType.PostDecrementAssign: + case ExpressionType.Unbox: + { + var u = (UnaryExpression)expr; + return Combine(h, Combine(u.Method?.GetHashCode() ?? 0, Hash(u.Operand))); + } + case ExpressionType.Add: case ExpressionType.AddChecked: + case ExpressionType.Subtract: case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: case ExpressionType.Modulo: + case ExpressionType.Power: case ExpressionType.And: + case ExpressionType.Or: case ExpressionType.ExclusiveOr: + case ExpressionType.LeftShift: case ExpressionType.RightShift: + case ExpressionType.AndAlso: case ExpressionType.OrElse: + case ExpressionType.Equal: case ExpressionType.NotEqual: + case ExpressionType.LessThan: case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Coalesce: case ExpressionType.ArrayIndex: + case ExpressionType.Assign: + case ExpressionType.AddAssign: case ExpressionType.AddAssignChecked: + case ExpressionType.SubtractAssign: case ExpressionType.SubtractAssignChecked: + case ExpressionType.MultiplyAssign: case ExpressionType.MultiplyAssignChecked: + case ExpressionType.DivideAssign: case ExpressionType.ModuloAssign: + case ExpressionType.PowerAssign: case ExpressionType.AndAssign: + case ExpressionType.OrAssign: case ExpressionType.ExclusiveOrAssign: + case ExpressionType.LeftShiftAssign: case ExpressionType.RightShiftAssign: + { + var b = (BinaryExpression)expr; + return Combine(h, Combine(b.Method?.GetHashCode() ?? 0, Combine(Hash(b.Left), Hash(b.Right)))); + } + case ExpressionType.Call: + { + var m = (MethodCallExpression)expr; + h = Combine(h, Combine(m.Method.GetHashCode(), Hash(m.Object))); + var args = (IArgumentProvider)m; + for (var i = 0; i < args.ArgumentCount; i++) h = Combine(h, Hash(args.GetArgument(i))); + return h; + } + case ExpressionType.MemberAccess: + { + var m = (MemberExpression)expr; + return Combine(h, Combine(m.Member.GetHashCode(), Hash(m.Expression))); + } + case ExpressionType.New: + { + var n = (NewExpression)expr; + h = Combine(h, n.Constructor?.GetHashCode() ?? 0); + var args = (IArgumentProvider)n; + for (var i = 0; i < args.ArgumentCount; i++) h = Combine(h, Hash(args.GetArgument(i))); + return h; + } + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + { +#if LIGHT_EXPRESSION + var args = (IArgumentProvider)expr; + for (var i = 0; i < args.ArgumentCount; i++) h = Combine(h, Hash(args.GetArgument(i))); +#else + var na = (NewArrayExpression)expr; + for (var i = 0; i < na.Expressions.Count; i++) h = Combine(h, Hash(na.Expressions[i])); +#endif + return h; + } + case ExpressionType.Conditional: + { + var c = (ConditionalExpression)expr; + return Combine(h, Combine(Hash(c.Test), Combine(Hash(c.IfTrue), Hash(c.IfFalse)))); + } + case ExpressionType.Block: + { + var b = (BlockExpression)expr; + var sc = _xps.Count; + var vc = b.Variables.Count; + for (var i = 0; i < vc; i++) _xps.AddDefaultAndGetRef() = b.Variables[i]; + for (var i = 0; i < b.Expressions.Count; i++) h = Combine(h, Hash(b.Expressions[i])); + _xps.Count = sc; + return h; + } + case ExpressionType.MemberInit: + { + var mi = (MemberInitExpression)expr; +#if LIGHT_EXPRESSION + h = Combine(h, Hash(mi.Expression)); +#else + h = Combine(h, Hash(mi.NewExpression)); +#endif + for (var i = 0; i < mi.Bindings.Count; i++) h = Combine(h, mi.Bindings[i].Member.GetHashCode()); + return h; + } + case ExpressionType.ListInit: + { + var li = (ListInitExpression)expr; + h = Combine(h, Hash(li.NewExpression)); + for (var i = 0; i < li.Initializers.Count; i++) h = Combine(h, li.Initializers[i].AddMethod.GetHashCode()); + return h; + } + case ExpressionType.TypeIs: + case ExpressionType.TypeEqual: + { + var tb = (TypeBinaryExpression)expr; + return Combine(h, Combine(tb.TypeOperand.GetHashCode(), Hash(tb.Expression))); + } + case ExpressionType.Invoke: + { + var inv = (InvocationExpression)expr; + h = Combine(h, Hash(inv.Expression)); + var args = (IArgumentProvider)inv; + for (var i = 0; i < args.ArgumentCount; i++) h = Combine(h, Hash(args.GetArgument(i))); + return h; + } + case ExpressionType.Goto: + { + var g = (GotoExpression)expr; + return Combine(h, Combine((int)g.Kind, Combine(g.Target.Name?.GetHashCode() ?? 0, Hash(g.Value)))); + } + case ExpressionType.Label: + { + var l = (LabelExpression)expr; + return Combine(h, Combine(l.Target.Name?.GetHashCode() ?? 0, Hash(l.DefaultValue))); + } + case ExpressionType.Loop: + { + var l = (LoopExpression)expr; + return Combine(h, Hash(l.Body)); + } + default: + return h; + } } + /// IEqualityComparer<Expression> implementation — delegates to the static for a fresh context per call. + bool IEqualityComparer.Equals(Expression x, Expression y) => EqualsTo(x, y); + + /// IEqualityComparer<Expression> implementation — delegates to the static . + int IEqualityComparer.GetHashCode(Expression obj) => GetHashCode(obj); + + /// Non-generic IEqualityComparer implementation — delegates to the static methods for use with legacy BCL APIs. + bool IEqualityComparer.Equals(object x, object y) => EqualsTo(x as Expression, y as Expression); + + /// Non-generic IEqualityComparer implementation — delegates to the static . + int IEqualityComparer.GetHashCode(object obj) => GetHashCode(obj as Expression); + /// Structurally compares two expressions, using the current parameter/label context for identity mapping. /// For top-level comparisons, prefer the static . public bool Eq(Expression x, Expression y) @@ -10039,21 +10224,21 @@ public bool Eq(Expression x, Expression y) { case ExpressionType.Parameter: { + // Check if this parameter is bound inside an enclosing lambda or block by looking it + // up in the collected pairs. For example, in Lambda(x => x + 1, x), when we reach `x` + // inside the body we find it at position 0 in _xps; we then check that the corresponding + // param from the other expression is also at position 0 in _yps. + for (var i = 0; i < _xps.Count; i++) + if (ReferenceEquals(_xps.GetSurePresentRef(i), x)) + return ReferenceEquals(_yps.GetSurePresentRef(i), y); + // Single parameter expression, or parameter not in any parameter list of enclosing lambda or block. var px = (ParameterExpression)x; var py = (ParameterExpression)y; - for (var i = 0; i < _xps.Count; i++) - if (ReferenceEquals(_xps.Items[i], px)) - return ReferenceEquals(_yps.Items[i], py); - // unmapped — compare structurally (Type already checked) return px.IsByRef == py.IsByRef && px.Name == py.Name; } case ExpressionType.Constant: - { - var cx = (ConstantExpression)x; - var cy = (ConstantExpression)y; - return Equals(cx.Value, cy.Value); - } + return Equals(((ConstantExpression)x).Value, ((ConstantExpression)y).Value); case ExpressionType.Lambda: { @@ -10065,8 +10250,11 @@ public bool Eq(Expression x, Expression y) var sc = _xps.Count; for (var i = 0; i < pc; i++) { - _xps.AddDefaultAndGetRef() = lx.GetParameter(i); - _yps.AddDefaultAndGetRef() = ly.GetParameter(i); + var lpx = lx.GetParameter(i); + var lpy = ly.GetParameter(i); + if (lpx.Type != lpy.Type | lpx.IsByRef != lpy.IsByRef) return false; + _xps.AddDefaultAndGetRef() = lpx; + _yps.AddDefaultAndGetRef() = lpy; } #else var pc = lx.Parameters.Count; @@ -10074,8 +10262,11 @@ public bool Eq(Expression x, Expression y) var sc = _xps.Count; for (var i = 0; i < pc; i++) { - _xps.AddDefaultAndGetRef() = lx.Parameters[i]; - _yps.AddDefaultAndGetRef() = ly.Parameters[i]; + var lpx = lx.Parameters[i]; + var lpy = ly.Parameters[i]; + if (lpx.Type != lpy.Type | lpx.IsByRef != lpy.IsByRef) return false; + _xps.AddDefaultAndGetRef() = lpx; + _yps.AddDefaultAndGetRef() = lpy; } #endif var eq = Eq(lx.Body, ly.Body); @@ -10186,8 +10377,11 @@ public bool Eq(Expression x, Expression y) var sc = _xps.Count; for (var i = 0; i < vc; i++) { - _xps.AddDefaultAndGetRef() = bx.Variables[i]; - _yps.AddDefaultAndGetRef() = by.Variables[i]; + var bvx = bx.Variables[i]; + var bvy = by.Variables[i]; + if (bvx.Type != bvy.Type | bvx.IsByRef != bvy.IsByRef) return false; + _xps.AddDefaultAndGetRef() = bvx; + _yps.AddDefaultAndGetRef() = bvy; } var eq = true; for (var i = 0; i < ec && eq; i++) @@ -10357,8 +10551,8 @@ private bool EqLabel(LabelTarget x, LabelTarget y) if (x == null | y == null) return false; if (x.Type != y.Type) return false; for (var i = 0; i < _xls.Count; i++) - if (ReferenceEquals(_xls.Items[i], x)) - return ReferenceEquals(_yls.Items[i], y); + if (ReferenceEquals(_xls.GetSurePresentRef(i), x)) + return ReferenceEquals(_yls.GetSurePresentRef(i), y); // Register the pair and compare by name _xls.AddDefaultAndGetRef() = x; _yls.AddDefaultAndGetRef() = y; From 8c0d5c418b3ce4829f669cb6f9b8802ed48fef15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:37:15 +0000 Subject: [PATCH 6/7] Fix comment wording in Parameter case per code review Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/1fc579e7-522b-4451-bbdf-34f552a1de4b Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FastExpressionCompiler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 26e9487f..6b9cad3f 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10225,13 +10225,13 @@ public bool Eq(Expression x, Expression y) case ExpressionType.Parameter: { // Check if this parameter is bound inside an enclosing lambda or block by looking it - // up in the collected pairs. For example, in Lambda(x => x + 1, x), when we reach `x` - // inside the body we find it at position 0 in _xps; we then check that the corresponding - // param from the other expression is also at position 0 in _yps. + // up in the collected pairs. E.g. in Lambda>(body: Add(p, one), parameters: p), + // when we reach `p` inside the body we find it at index 0 in _xps, and then verify + // the corresponding param from the other expression is also at index 0 in _yps. for (var i = 0; i < _xps.Count; i++) if (ReferenceEquals(_xps.GetSurePresentRef(i), x)) return ReferenceEquals(_yps.GetSurePresentRef(i), y); - // Single parameter expression, or parameter not in any parameter list of enclosing lambda or block. + // Unbound/standalone parameter, not found in any enclosing lambda or block parameter list. var px = (ParameterExpression)x; var py = (ParameterExpression)y; return px.IsByRef == py.IsByRef && px.Name == py.Name; From 666b029fe87edd29ce63878413a521432ec43966 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:43:52 +0000 Subject: [PATCH 7/7] Improve Combine to Boost hash_combine, add AggressiveInlining, expression-bodied one-liners, hash tests Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/18ec5ad3-5605-4dfb-b8f3-2e945f46fefc Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 30 +++++---- ..._equality_comparison_to_LightExpression.cs | 64 +++++++++++++++++++ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 6b9cad3f..1eea4a49 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -10012,23 +10012,25 @@ public struct ExpressionEqualityComparer : IEqualityComparer, IEqual private SmallList, NoArrayPool> _xls, _yls; /// Structurally compares two expressions. Primary entry point — no heap allocation for the comparer. - public static bool EqualsTo(Expression x, Expression y) - { - var eq = default(ExpressionEqualityComparer); - return eq.Eq(x, y); - } + [MethodImpl((MethodImplOptions)256)] + public static bool EqualsTo(Expression x, Expression y) => + new ExpressionEqualityComparer().Eq(x, y); /// Computes a content-addressable hash for the expression tree. /// Bound lambda and block parameters are hashed by their position index so that structurally /// equal lambdas with differently-named parameters produce the same hash. - public static int GetHashCode(Expression expr) - { - var ctx = default(ExpressionEqualityComparer); - return ctx.Hash(expr); - } - + [MethodImpl((MethodImplOptions)256)] + public static int GetHashCode(Expression expr) => + new ExpressionEqualityComparer().Hash(expr); + + // Boost hash_combine formula: h1 ^= h2 + 0x9e3779b9 + (h1<<6) + (h1>>2) + // The golden-ratio constant 0x9e3779b9 breaks up symmetry and spreads bits across the + // full integer range. The shifts give good avalanche with no conditional branch. + // This outperforms the simpler djb2 (33*h1^h2) and is compatible with all target + // frameworks (unlike System.HashCode which requires .NET Standard 2.1+). + [MethodImpl((MethodImplOptions)256)] private static int Combine(int h1, int h2) => - h1 == 0 ? h2 : unchecked((h1 << 5) + h1 ^ h2); + unchecked(h1 ^ (h2 + (int)0x9e3779b9 + (h1 << 6) + (h1 >> 2))); private int Hash(Expression expr) { @@ -10202,15 +10204,19 @@ private int Hash(Expression expr) } /// IEqualityComparer<Expression> implementation — delegates to the static for a fresh context per call. + [MethodImpl((MethodImplOptions)256)] bool IEqualityComparer.Equals(Expression x, Expression y) => EqualsTo(x, y); /// IEqualityComparer<Expression> implementation — delegates to the static . + [MethodImpl((MethodImplOptions)256)] int IEqualityComparer.GetHashCode(Expression obj) => GetHashCode(obj); /// Non-generic IEqualityComparer implementation — delegates to the static methods for use with legacy BCL APIs. + [MethodImpl((MethodImplOptions)256)] bool IEqualityComparer.Equals(object x, object y) => EqualsTo(x as Expression, y as Expression); /// Non-generic IEqualityComparer implementation — delegates to the static . + [MethodImpl((MethodImplOptions)256)] int IEqualityComparer.GetHashCode(object obj) => GetHashCode(obj as Expression); /// Structurally compares two expressions, using the current parameter/label context for identity mapping. diff --git a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs index db37bdc3..beb170fc 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue431_Add_structural_equality_comparison_to_LightExpression.cs @@ -1,13 +1,16 @@ 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 @@ -35,6 +38,9 @@ public void Run(TestRun 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); @@ -178,6 +184,64 @@ public void Eq_complex_lambda_round_trip(TestContext t) } #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>(Add(p1, Constant(1)), p1); + var e2 = Lambda>(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>(Add(p1, Constant(1)), p1); + dict[e1] = "found"; + + var p2 = Parameter(typeof(int), "y"); // different identity/name + var e2 = Lambda>(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>(Add(p1, Constant(1)), p1); + var h1 = ExpressionEqualityComparer.GetHashCode(e1); + + var p2 = Parameter(typeof(int), "y"); + var e2 = Lambda>(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>(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)));